TimeToCargo

Webhooks

Receive tracking events and verify incoming requests

Webhooks notify you when tracking updates become available in TimeToCargo. The service sends a POST request containing ordinary JSON to your callback_url.

Create a webhook with POST /v2/webhooks or in your account.

Event types

GET /v2/webhook-event-types returns the current event types and their descriptions. Use it when configuring your subscription. Examples include ETA changes (tracking.eta_update), movement history updates (tracking.history_update), and delivery (tracking.delivered).

If you omit event_types when creating a webhook, your callback_url receives all public event types listed by GET /v2/webhook-event-types.

tracking.test is a separate service event for testing delivery. You cannot subscribe to it. It is sent only when you call POST /v2/webhooks/{id}/test, with an empty data object and resource: null.

Webhook secret

When you create a webhook, the API returns a secret for verifying event signatures. Store the secret from the creation response. The API returns secret only when a webhook is created or updated with regenerate_secret: true. The same secret verifies all events for that webhook until it is replaced.

If you lose the secret, regenerate it with PATCH /v2/webhooks/{id} and regenerate_secret: true, then update your handler.

Delivery rules

  • Acknowledge delivery with a 2xx HTTP response. If processing takes time, save the event to a durable queue, respond, and process it separately after sending the response.
  • TimeToCargo waits up to 10 seconds for a response.
  • Delivery is retried after 408, 409, 425, 429, 5xx, timeouts, and DNS or network errors. There are at most five attempts in total.
  • After 429, the Retry-After header is respected if present.
  • Delivery is not retried after 400, 401, 403, 404, or 422.

Event and delivery IDs

x-timetocargo-event-id identifies the event and matches payload.id. Use it to distinguish events and prevent duplicate processing, making your handler idempotent.

x-timetocargo-delivery-id identifies a delivery attempt. Use it to distinguish attempts in your request logs. The same event may arrive more than once with the same event ID but different delivery IDs.

If an event has already been processed successfully, respond to repeated delivery with a 2xx status. Coordinate recording the event ID with processing so a failure does not lose the event or repeat the operation.

Signature verification

Signature verification is optional, but strongly recommended. It confirms that the event came from TimeToCargo and that its body has not changed. The request body is ordinary JSON. The signature does not encrypt or encode it.

Use your webhook’s secret. The x-timetocargo-signature header contains an HMAC SHA-256 signature of timestamp + '.' + rawBody, prefixed with sha256=. The timestamp is in x-timetocargo-timestamp, and rawBody is the unmodified request body.

The examples below verify the signature and enforce a five-minute timestamp tolerance. Install express for Node.js or fastapi for Python. Replace the queue function with your own implementation. The example only prints the event.

// Verify the event timestamp and signature using the webhook secret.
import crypto from "node:crypto";
import express from "express";

const app = express();
const secret = process.env.TIMETOCARGO_WEBHOOK_SECRET;
const MAX_SKEW_SECONDS = 5 * 60;

if (!secret) {
    throw new Error("TIMETOCARGO_WEBHOOK_SECRET is not set");
}

function queueWebhook(payload: unknown, eventId: string): void {
    // Persist the event to your queue here; logging is only a placeholder.
    console.log("accepted", eventId, payload);
}

app.post("/webhooks/timetocargo", express.raw({ type: "application/json" }), (req, res) => {
    const timestamp = req.header("x-timetocargo-timestamp");
    const signature = req.header("x-timetocargo-signature");
    const eventId = req.header("x-timetocargo-event-id");

    if (!timestamp || !signature || !eventId) {
        return res.sendStatus(401);
    }

    const age = Math.abs(Date.now() / 1000 - Number(timestamp));
    if (!Number.isFinite(age) || age > MAX_SKEW_SECONDS) {
        return res.sendStatus(401);
    }

    // Sign the original body bytes without reserializing JSON.
    const expected =
        "sha256=" + crypto.createHmac("sha256", secret).update(timestamp).update(".").update(req.body).digest("hex");

    const signatureBuffer = Buffer.from(signature);
    const expectedBuffer = Buffer.from(expected);
    if (signatureBuffer.length !== expectedBuffer.length || !crypto.timingSafeEqual(signatureBuffer, expectedBuffer)) {
        return res.sendStatus(401);
    }

    queueWebhook(JSON.parse(req.body.toString("utf8")), eventId);
    return res.sendStatus(200);
});

Compute the signature from the original request body. Parsing and reserializing JSON can change its bytes and invalidate the signature.

Last updated on

On this page