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_KEYheader)? - Does project44 sign webhook payloads? (No: use the shared header value)
- How do I handle
shipments.v1.event.addedorshipments.v1.state.updatedevents? - 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 toX-Api-Keyand readPROJECT44_WEBHOOK_HEADER). Look it up case-insensitively. PROJECT44_WEBHOOK_SECRETis the exact, whole header value, including anyBearerprefix 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;POSTis the default), so the examples accept all four and tolerate an empty body. Hookdeck's project44 source pre-selects onlyPOST: 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.
eventType | Sent When |
|---|---|
shipments.v1.shipment.created | A new shipment is created (once) |
shipments.v1.shipment.deleted | A shipment is deleted |
shipments.v1.event.added | An event is added to a shipment |
shipments.v1.event.updated | An existing shipment event is updated |
shipments.v1.event.removed | A shipment event is removed |
shipments.v1.state.added | A state is added to a shipment |
shipments.v1.state.updated | An existing state is updated |
shipments.v1.state.removed | A state is removed from a shipment |
shipments.v1.exception.addedOrUpdated | An 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
- references/overview.md - What project44 webhooks are, events and payload shape
- references/setup.md - Configure the webhook with an
API_KEYheader - references/verification.md - Header check details, gotchas, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests