Skip to main content

Documentation Index

Fetch the complete documentation index at: https://terminal49-codex-docs-audit-diataxis-skill.mintlify.app/llms.txt

Use this file to discover all available pages before exploring further.

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.
  • A public HTTPS endpoint that accepts POST requests.
  • A list of events your integration should receive.
  • A place to store the webhook secret securely.

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. Ruby:
def valid_signature?(raw_body, signature, secret)
  digest = OpenSSL::HMAC.hexdigest("SHA256", secret, raw_body)
  Rack::Utils.secure_compare(digest, signature.to_s)
end
Node.js:
import crypto from "crypto";

function validSignature(rawBody, signature, secret) {
  const digest = crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");

  const received = Buffer.from(signature || "", "hex");
  const expected = Buffer.from(digest, "hex");

  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}
Python:
import hashlib
import hmac

def valid_signature(raw_body: bytes, signature: str, secret: str) -> bool:
    digest = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(signature or "", digest)
Go:
import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
)

func validSignature(rawBody []byte, signature string, secret string) bool {
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write(rawBody)
	expected := hex.EncodeToString(mac.Sum(nil))
	return hmac.Equal([]byte(signature), []byte(expected))
}

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: "application/json" }), 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 failed deliveries:
curl -s "https://api.terminal49.com/v2/webhook_notifications?filter[delivery_status]=failed" \
  -H "Authorization: Token YOUR_API_KEY" \
  -H "Content-Type: application/vnd.api+json"
Use Trigger a Webhook to replay notifications after you fix the consumer.