Skip to main content

What this lets you do

Prove that a webhook request really came from Traceten and has not been tampered with or replayed, before your code acts on it. Anyone who discovers your endpoint URL can POST to it; the signature is what makes a delivery trustworthy.

The scheme

Every delivery carries two headers: The signature is computed over the timestamp and the raw request body, joined by a single .:
  • secret is the endpoint’s signing secret, shown once at registration (setup guide, step 3). Use it as a raw UTF-8 string; do not hex- or base64-decode it.
  • timestamp is the exact string value of the X-Traceten-Signature-Timestamp header.
  • raw_body is the exact bytes of the request body, untouched.
To verify: recompute the digest from the received timestamp and raw body, compare it to the header value in constant time, and reject the request if the timestamp is more than 300 seconds from your current time. Signing the timestamp is what defeats replay: an attacker who captures a valid delivery cannot re-send it later, because the aged timestamp fails the freshness check and any altered timestamp breaks the digest.
If you use a Traceten server SDK, prefer its built-in verifier over hand-rolling the scheme: verifyWebhook() in Node and Python, and VerifyWebhook in Go. Each does constant-time comparison and replay-window enforcement for you and returns a typed event. The snippets below are the reference implementation of the same scheme, for when you are not using an SDK.

Verify the raw body, not a re-serialized one

The single most common verification bug: a framework parses the JSON, your code re-serializes it, and the bytes no longer match what Traceten signed (key order, whitespace, and unicode escaping all differ). Always hash the raw bytes as received.
  • Express: use express.raw({ type: "application/json" }) on the webhook route, not express.json().
  • Fastify: use a content-type parser with { parseAs: "buffer" } for the webhook route.
  • Flask: use request.get_data(), not request.get_json().
  • FastAPI / Starlette: use await request.body(), not a Pydantic body parameter.
  • Go net/http: read r.Body with io.ReadAll before any JSON decoding.
Parse the JSON only after the signature checks out.

Node

Wired into an Express route (note express.raw):

Python

Wired into a Flask route (note request.get_data()):

Go

Wired into a net/http handler (note io.ReadAll before any decoding):

Conformance vector

Traceten publishes one fixed (secret, timestamp, body) → signature vector. Run your implementation against it before going live: if your code reproduces this signature, it is byte-identical to Traceten’s signer. The vector’s timestamp is intentionally old, so pass its own value as “now” (or an infinite tolerance) when testing:
For Go, override the clock by comparing against the vector’s timestamp directly, or temporarily inject now; the digest itself must come out to the expected value:

Troubleshooting

Every signature fails. You are almost certainly hashing a re-serialized body. Log the exact bytes you hash and compare them, character by character, with the delivery log’s payload in Settings → Webhooks. Also confirm you prepend the timestamp and the . separator. The conformance vector passes but real deliveries fail. Your secret is wrong or stale. Secrets are per-endpoint, and rotating invalidates the old one immediately. Fails only sometimes. Check server clock skew. The timestamp check compares against your clock; more than 300 seconds of skew rejects genuine deliveries. Sync with NTP rather than widening the tolerance. Signature header is missing. The request did not come from Traceten’s delivery path. Reject it.

Next