Skip to content

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.

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

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).

  1. 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.
  2. Compute the HMAC-SHA256 of the raw body with your signing secret.
  3. Compare the result against X-AiTrack-Signature (stripped of the sha256= prefix) using a constant-time comparison.
  4. If it doesn’t match, respond 401 and 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();
},
);

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.

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.