Signature & security
Every delivery is signed. Always verify it: an endpoint that accepts any POST is an open door for anyone who discovers your address.
Delivery headers
Section titled “Delivery headers”| Header | Value |
|---|---|
Content-Type |
application/json |
X-AiTrack-Signature |
sha256=<hex HMAC-SHA256 of the body, with the endpoint's signing secret> |
X-AiTrack-Signature-Version |
the endpoint’s signature scheme version (default v1) |
X-AiTrack-Timestamp |
epoch milliseconds of the delivery attempt |
X-AiTrack-Delivery-Id |
unique id of this delivery |
X-AiTrack-Delivery-Attempt |
attempt number (1, 2, 3…) |
Idempotency-Key |
<endpoint id>:<event id>, stable across retries of the same event |
How the signature is computed
Section titled “How the signature is computed”The server computes HMAC_SHA256(signing_secret, JSON.stringify(payload)) and sends it as sha256=<hex> in the X-AiTrack-Signature header. To verify it, recompute the same HMAC over the raw request body (before any JSON.parse) using your endpoint’s signing secret, and compare it in constant time (never with a plain string ===: a naive comparison can leak the signature one byte at a time via a timing attack).
- Read the request body as raw bytes/string, not as an already-deserialized object — many frameworks parse JSON before you can intercept the original body: configure your middleware to give you the raw body on this route.
- Compute the HMAC-SHA256 of the raw body with your signing secret.
- Compare the result against
X-AiTrack-Signature(stripped of thesha256=prefix) using a constant-time comparison. - If it doesn’t match, respond
401and discard the request — don’t process it.
import { createHmac, timingSafeEqual } from 'node:crypto';
function verifyAitrackSignature(rawBody, signatureHeader, secret) { const expected = 'sha256=' + createHmac('sha256', secret) .update(rawBody) // the RAW body, before JSON.parse .digest('hex');
const received = signatureHeader || ''; const ok = received.length === expected.length && timingSafeEqual(Buffer.from(received), Buffer.from(expected));
return ok;}
// Express example: expose the raw body on this specific route.app.post( '/webhooks/aitrack', express.raw({ type: 'application/json' }), (req, res) => { const ok = verifyAitrackSignature( req.body, // Buffer, thanks to express.raw() req.headers['x-aitrack-signature'], process.env.AITRACK_WEBHOOK_SECRET, ); if (!ok) return res.status(401).end();
const event = JSON.parse(req.body); // idempotency: discard if you've already seen event.id res.status(200).end(); },);import hashlibimport hmacimport os
def verify_aitrack_signature(raw_body: bytes, signature_header: str, secret: str) -> bool: expected = "sha256=" + hmac.new( secret.encode("utf-8"), raw_body, hashlib.sha256 ).hexdigest() return hmac.compare_digest(signature_header or "", expected)
# Flask example: request.get_data() gives the raw body before parsing.@app.route("/webhooks/aitrack", methods=["POST"])def aitrack_webhook(): raw_body = request.get_data() signature = request.headers.get("X-AiTrack-Signature", "") secret = os.environ["AITRACK_WEBHOOK_SECRET"]
if not verify_aitrack_signature(raw_body, signature, secret): return "", 401
event = request.get_json() # idempotency: discard if you've already seen event["id"] return "", 200# Useful for a quick terminal check against a saved payload file: recompute# the HMAC and eyeball it against the received X-AiTrack-Signature header# (fine for manual debugging only, never for a production endpoint).openssl dgst -sha256 -hmac "$AITRACK_WEBHOOK_SECRET" payload.jsonIdempotency
Section titled “Idempotency”Idempotency-Key (and the id inside the payload) are stable per event: if the same id arrives twice — a retry after a transient timeout on your side — discard it instead of reprocessing it. See Delivery & retries for the full policy.
Testing an endpoint
Section titled “Testing an endpoint”Use POST /api/webhooks/:id/test: it sends a synthetic type: "ping" event only to the specified endpoint, signed just like a real delivery — so you can verify your implementation without waiting for a real event.