Agent skill

project44 Webhooks Skill

Receive and authenticate project44 webhooks. Use when setting up a project44 webhook endpoint for shipment tracking, checking the API_KEY authentication header, debugging 401s, or handling granular events like shipments.v1.event.added, shipments.v1.state.updated and shipments.v1.shipment.created.

Install this skill

npx skills add hookdeck/webhook-skills --skill project44-webhooks


project44 (supply chain visibility) pushes shipment updates to an HTTPS endpoint you host. The webhook is configured through the API with PUT /api/v4/webhooks and an authentication method of API_KEY, BASIC, OAUTH2, MTLS or AD_OAUTH2_CERT. This skill covers API_KEY: project44 adds a header you name, with a value you choose, to every request.

project44 webhooks are not signed. There is no HMAC and no timestamp. Your endpoint authenticates a request by comparing that header with the secret you stored.

When to Use This Skill

  • How do I receive project44 webhooks?
  • How do I authenticate project44 webhook requests (API_KEY header)?
  • Does project44 sign webhook payloads? (No: use the shared header value)
  • How do I handle shipments.v1.event.added or shipments.v1.state.updated events?
  • Why does my project44 webhook endpoint return 401, and why did project44 stop retrying?

Verification (core)

const crypto = require('crypto');

// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
  const ha = crypto.createHash('sha256').update(String(a)).digest();
  const hb = crypto.createHash('sha256').update(String(b)).digest();
  return crypto.timingSafeEqual(ha, hb);
}

function verifyProject44Request(headers, env = process.env) {
  const secret = env.PROJECT44_WEBHOOK_SECRET;
  if (!secret) return false;                                   // never accept when unconfigured
  const name = (env.PROJECT44_WEBHOOK_HEADER || 'X-Api-Key').toLowerCase();
  const received = headers[name];                              // Node lowercases header names
  return Boolean(received) && safeEqual(received, secret);     // WHOLE value, no Bearer parsing
}
  • The header name is whatever you set as api_key_name (the examples default to X-Api-Key and read PROJECT44_WEBHOOK_HEADER). Look it up case-insensitively.
  • PROJECT44_WEBHOOK_SECRET is the exact, whole header value, including any Bearer prefix you typed. Compare the whole value; don't strip or parse a prefix.
  • Missing header or mismatch: 401. Authenticate before parsing the body.
  • A static shared credential authenticates the sender, not the body, and gives no replay protection (no signature, no timestamp). Use HTTPS only, make the handler idempotent (use eventId), and rotate the credential if it leaks.
  • The webhook method is configurable (GET, POST, PUT, PATCH; POST is the default), so the examples accept all four and tolerate an empty body. Hookdeck's project44 source pre-selects only POST: allow any other method you use on the source.
  • Read the body raw (express.raw({ type: '*/*' })) so any content type works, and parse JSON only after the credential check.

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Event Types

Granular webhooks carry an eventType field (the same value you choose as webhookConfiguration.webhookEventType when configuring the webhook). Other fields: eventId, createdDateTime, dataType, context and data.

eventTypeSent When
shipments.v1.shipment.createdA new shipment is created (once)
shipments.v1.shipment.deletedA shipment is deleted
shipments.v1.event.addedAn event is added to a shipment
shipments.v1.event.updatedAn existing shipment event is updated
shipments.v1.event.removedA shipment event is removed
shipments.v1.state.addedA state is added to a shipment
shipments.v1.state.updatedAn existing state is updated
shipments.v1.state.removedA state is removed from a shipment
shipments.v1.exception.addedOrUpdatedAn exception is added or updated

Non-granular (standard) shipment webhooks use a different payload with no eventType; the examples acknowledge them as unhandled.

Response Codes Matter

project44 treats 2xx as delivered. It retries only 408, 429, 502, 503, 504 and connection failures (queued up to 48 hours, in order, so a retriable failure blocks later messages). 401, other 4xx, 3xx and 500 are not retried: the message is dropped. So the examples return 503 (not 500) when your own handler fails, and a wrong secret loses messages. Respond within a few seconds; the timeout is 20 seconds.

Environment Variables

PROJECT44_WEBHOOK_HEADER=X-Api-Key        # the api_key_name you configured (default X-Api-Key)
PROJECT44_WEBHOOK_SECRET=your-secret      # the exact api key value, including any "Bearer " prefix

Local Development

project44 can't reach localhost. Tunnel with the Hookdeck CLI (no account required):

npx hookdeck-cli listen 3000 project44 --path /webhooks/project44

Use the printed URL as the webhook url in the project44 configuration.

Resources


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 5, 2026

View on GitHub →