Skip to main content
This guide shows how to configure a production webhook consumer for Terminal49 tracking updates. For background on why Terminal49 recommends webhooks instead of polling, see Why Webhooks. For event names and payload details, use the Event Catalog and Webhook Payloads references.

Prerequisites

You need:
  • A Terminal49 API key from the developer portal.
  • A public HTTPS endpoint that accepts POST requests.
  • A list of events your integration should receive.
  • A place to store the webhook secret securely.
Use the List Webhook Events endpoint to fetch the event categories available to your account. Use the Trigger Webhook endpoint to send a sample event payload to an HTTPS URL while you test your handler.

Create the webhook

You can create a webhook from the dashboard or API. To use the dashboard, open Developer Webhooks and click Create Webhook Endpoint. To use the API:
curl -X POST "https://api.terminal49.com/v2/webhooks" \
  -H "Authorization: Token YOUR_API_KEY" \
  -H "Content-Type: application/vnd.api+json" \
  -d '{
    "data": {
      "type": "webhook",
      "attributes": {
        "url": "https://example.com/webhooks/terminal49",
        "active": true,
        "events": [
          "tracking_request.succeeded",
          "tracking_request.failed",
          "container.updated"
        ]
      }
    }
  }'
Subscribe only to the events your integration handles. Use the Event Catalog for the canonical event list.

Store the webhook secret

The webhook response includes a secret attribute. Store it in your secrets manager. Terminal49 signs every webhook delivery with an HMAC SHA-256 digest of the raw request body. The digest is sent in the X-T49-Webhook-Signature header.

Verify the signature

Verify the signature before parsing or trusting the JSON body.
def valid_signature?(raw_body, signature, secret)
  digest = OpenSSL::HMAC.hexdigest("SHA256", secret, raw_body)
  Rack::Utils.secure_compare(digest, signature.to_s)
end

Allowlist Terminal49 webhook IPs

Fetch the current source IP list from the List Webhook IPs endpoint and allowlist those addresses in your firewall or application. Cache the list and refresh it periodically. Do not rely on a hard-coded list in application code.

Accept the notification

Your endpoint should return 200, 201, 202, or 204 after it safely accepts the event. Any other response, including a timeout, triggers retries. Terminal49 retries failed deliveries up to 12 times before marking the notification as failed.
app.post("/webhooks/terminal49", express.raw({ type: "*/*" }), async (req, res) => {
  const signature = req.header("X-T49-Webhook-Signature");

  if (!validSignature(req.body, signature, process.env.T49_WEBHOOK_SECRET)) {
    return res.sendStatus(401);
  }

  const payload = JSON.parse(req.body.toString("utf8"));
  await enqueueWebhook(payload);

  res.sendStatus(202);
});

Handle duplicate deliveries

Retries can deliver the same notification more than once. Use data.id as the idempotency key for your processing log.
async function processWebhook(payload) {
  const notificationId = payload.data.id;

  if (await alreadyProcessed(notificationId)) {
    return;
  }

  await handleEvent(payload);
  await markProcessed(notificationId);
}

Monitor failed notifications

Use the Webhook Notifications API to review recent deliveries and spot the ones marked delivery_status: failed:
curl -s "https://api.terminal49.com/v2/webhook_notifications" \
  -H "Authorization: Token YOUR_API_KEY" \
  -H "Content-Type: application/vnd.api+json"
Missed notifications cannot be replayed. After you fix the consumer, use Trigger a Webhook to re-test your handler with a sample payload, then re-fetch the affected shipments and containers from the REST API to backfill any missed state changes. For more help, see How to Troubleshoot Missing Webhook Notifications.