Webhooks
Verify webhook signatures
Authenticate Trace webhook requests using the raw body and HMAC-SHA256.
Trace signs the exact request body with the secret issued for that subscription.
| Header | Purpose |
|---|---|
sterling-webhook-id | Stable delivery ID for idempotency. |
sterling-webhook-timestamp | Unix timestamp used in the signed message. |
sterling-webhook-signature | v1= followed by the lowercase hexadecimal digest. |
1signed_payload = sterling-webhook-timestamp + "." + raw_body
2expected = HMAC-SHA256(webhook_secret, signed_payload)
3received = sterling-webhook-signature.removeprefix("v1=")1import { createHmac, timingSafeEqual } from "node:crypto";
2
3const signed = `${timestamp}.${rawBody}`;
4const expected = createHmac("sha256", process.env.TRACE_WEBHOOK_SECRET)
5 .update(signed)
6 .digest("hex");
7const received = signature.replace(/^v1=/, "");
8
9const valid = expected.length === received.length && timingSafeEqual(
10 Buffer.from(expected, "hex"),
11 Buffer.from(received, "hex"),
12);1import hashlib
2import hmac
3import os
4
5signed = f"{timestamp}.{raw_body}".encode()
6expected = hmac.new(
7 os.environ["TRACE_WEBHOOK_SECRET"].encode(),
8 signed,
9 hashlib.sha256,
10).hexdigest()
11valid = hmac.compare_digest(expected, signature.removeprefix("v1="))Verification order
- Read the raw request bytes before parsing JSON.
- Reject a timestamp outside your replay window; five minutes is a practical default.
- Join the timestamp, a period, and the raw body.
- Compute HMAC-SHA256 with the subscription secret.
- Compare the received and expected hex digests with a timing-safe function.
- Record
sterling-webhook-id, then acknowledge duplicates with2xxwithout repeating their side effects.
Parsing and re-serializing JSON changes the signed bytes and causes a valid request to fail verification.