Agent skill
Terminal49 Webhooks Skill
Receive and verify Terminal49 container and shipment tracking webhooks. Use when setting up a Terminal49 webhook endpoint, verifying the X-T49-Webhook-Signature HMAC-SHA256 header, debugging 401s, or handling events like tracking_request.succeeded, container.transport.vessel_arrived and container.updated.
Install this skill
npx skills add hookdeck/webhook-skills --skill terminal49-webhooks
Terminal49 is a container and shipment tracking API. When a tracking request resolves, a container hits a transport milestone, an ETA changes or a container record updates, Terminal49 POSTs a JSON:API webhook_notification document to your webhook URL. Every delivery is signed: the X-T49-Webhook-Signature header carries the lowercase hex HMAC-SHA256 of the raw request body, keyed with the secret returned when you created the webhook.
When to Use This Skill
- How do I receive Terminal49 webhooks?
- How do I verify the
X-T49-Webhook-Signatureheader? - Why does my Terminal49 webhook endpoint return 401?
- How do I handle
tracking_orrequest.succeeded container.transport.vessel_events?arrived - How do I deduplicate Terminal49 notifications (
data.id) and recover missed ones?
Verification (core)
const crypto = require('crypto');
function verifyTerminal49Signature(rawBody, signature, secret) {
// Validate the format first: Buffer.from(x, 'hex') silently drops bad characters.
if (!secret || typeof signature !== 'string' || !/^[0-9a-fA-F]{64}$/.test(signature)) return false;
const expected = crypto.createHmac('sha256', Buffer.from(secret, 'utf8')).update(rawBody).digest();
const received = Buffer.from(signature.toLowerCase(), 'hex'); // hex is case-insensitive
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}
- Header:
X-T49-Webhook-Signature: <64 hex chars>, no prefix. Compare the whole value as one digest. - Signed content is the raw body bytes only: no timestamp, id or header is signed, so there is no replay window to check (don't invent one). The key is the secret's UTF-8 bytes as-is, not hex- or base64-decoded.
- Read the body raw (
express.raw({ type: '*/*' }),await request.arrayBuffer(),await request.body()) and parse JSON only after the check. NeverJSON.parse/stringifyfirst. - Missing, empty, malformed or mismatched signature:
401. - Python:
hmac.compare_digeston bytes (bytes.fromhexafterre.fullmatch(r"[0-9a-fA-F]{64}", sig)).
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Event Types
Dispatch on data.attributes.event. data.type is always webhook_notification; data.relationships.reference_ names the cause (transport_event, container_updated_event, ...) and included carries the related shipment, container and events. No header names the event.
data.attributes.event | Sent When |
|---|---|
tracking_ | A tracking request was resolved to a shipment and containers |
tracking_request.failed | A tracking request could not be resolved |
container.transport.vessel_ | A container's vessel arrived at port |
container.transport.vessel_ | A container's vessel departed |
container.transport.available | A container is available for pickup |
container.updated | A container's attributes changed |
shipment.estimated.arrival | A shipment's estimated arrival changed |
The catalog has dozens more (container.transport.* milestones, container.pickup_, tracking_, tracking_, ...); see references/overview.md. Handle the ones you need and log the rest.
Delivery and Retries
- Return
200,201,202or204once you've safely accepted the event. Any other status, or a timeout, is retried up to 12 times, then the notification is markedfailed. - Retries can repeat a notification: use
data.idas the idempotency key. - Missed notifications cannot be replayed. Re-fetch the affected shipments and containers from the REST API to backfill.
Environment Variables
TERMINAL49_WEBHOOK_SECRET=your_webhook_secret # the `secret` attribute returned when the webhook is created
Local Development
Terminal49 can't reach localhost. Tunnel with the Hookdeck CLI (no account required):
npx hookdeck-cli listen 3000 terminal49 --path /webhooks/terminal49
Use the printed URL as the webhook URL in the Terminal49 dashboard.
Resources
- references/overview.md - What Terminal49 webhooks are, events and payload shape
- references/setup.md - Create the webhook and get its secret
- references/verification.md - Signature check details, gotchas, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests