Zum Inhalt springen
TrackJet

Webhooks: signatures, retries, dead letters

HMAC verification done right, the retry ladder, and test-firing your endpoint.

Diese Seite ist derzeit nur auf Englisch verfügbar.

Register

`` curl -X POST "https://trackjet.world/api/v1/webhooks" \ -H "Authorization: Bearer $TRACKJET_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"url":"https://you.example/hook","events":"shipment.status_changed"}' ``

The response includes a one-time secret — store it now; it signs every delivery to that endpoint. URLs must be public HTTPS (private/internal targets are rejected at registration — SSRF guard). The key you register with needs the integration.manage scope; a read-only key is refused with insufficient_scope.

What every delivery carries

| Header | Meaning | | --- | --- | | X-TrackJet-Signature | sha256=<hmac> over timestamp + "." + raw_body | | X-TrackJet-Timestamp | Unix seconds, the value inside the signature | | X-TrackJet-Delivery | Event id — stable across all retries of the same event | | X-TrackJet-Event | Event name, e.g. shipment.status_changed |

Verify signatures (do not skip this)

`` signed = header["X-TrackJet-Timestamp"] + "." + raw_body expected = "sha256=" + hmac_sha256(signed, your_secret) if not constant_time_equals(expected, header["X-TrackJet-Signature"]): reject(401) if abs(now - int(header["X-TrackJet-Timestamp"])) > 300: reject(401) # your tolerance ``

Compute over the raw bytes before any JSON parsing, and compare in constant time. The timestamp is inside the signature, so it cannot be edited to refresh a captured delivery — which is what lets you reject one that is too old.

Dedupe on the delivery id

X-TrackJet-Delivery identifies the event, not the attempt: all retries of one event carry the same id, and a redrive out of the dead-letter queue carries it too.

`` if seen_before(header["X-TrackJet-Delivery"]): return 200 # already handled ``

Return 200 for a duplicate rather than reprocessing it. Without this, an endpoint that creates a record per delivery creates up to six for a single status change — three inline attempts plus three dead-letter redrives.

Delivery semantics

  • Retries: failed deliveries retry with exponential backoff; persistent failures park in a dead-letter queue that is redriven on a schedule with the same signed bytes, timestamp and delivery id — your signature check still passes, and your dedupe still recognises the event.
  • Replay window: because a redrive can arrive long after the original, keep your timestamp tolerance generous (5 minutes is typical for real-time; the dedupe key is what protects you, the timestamp only bounds a captured replay).
  • Auto-disable: an endpoint that keeps failing is disabled with its failure count, instead of being hammered forever.
  • Ordering: deliveries are near-real-time but not guaranteed ordered — treat the payload's occurred_at as the truth, not arrival order.

Test-fire before you ship

`` curl -X POST "https://trackjet.world/api/v1/webhooks/<id>/test" \ -H "Authorization: Bearer $TRACKJET_API_KEY" ``

Sends one clearly-marked sandbox.test event through the real pipeline — same signature construction, same headers, same retry path — so you can verify your receiver end-to-end without waiting for a real status change.