Webhooks
Webhooks notify your backend when a session reaches a terminal result. Treat a webhook as a signed notification, not as a replacement for the authenticated session result endpoint.
Each delivery includes webhook-signature: t=<timestamp>,v1=<hex-signature>, plus the compatibility headers x-timestamp and x-signature. The timestamp is Unix milliseconds. The signature is HMAC-SHA256 over the exact UTF-8 message ${timestamp}.${rawBody} using the endpoint signing secret; compare the expected hex digest in constant time.
Verify each delivery
- Verify the HMAC signature using the signing secret configured for the webhook endpoint and the exact raw request body.
- Validate the signed timestamp and reject events outside your replay window (the Gateway quickstart uses a five-minute tolerance).
- Compare signatures in constant time and verify the exact raw request body used by the signature scheme.
- Make processing idempotent using the stable
eventId. Retries and manual replay may deliver the same event more than once. - Acknowledge promptly, then queue slower work in your own system.
Node.js HMAC example
Read the request as a raw UTF-8 string or Buffer before any JSON middleware. Pass the webhook-signature header and your endpoint signing secret to this function. Invalid headers, timestamps and signatures throw before JSON is parsed.
import { createHmac, timingSafeEqual } from 'node:crypto';
const SIGNATURE_RE = /^t=([0-9]+),v1=([0-9a-f]{64})$/i;
const TOLERANCE_MS = 300_000;
function verifyDelivery(rawBody, signatureHeader, secret, nowMs = Date.now()) {
if (!secret) throw new Error('Webhook secret is required');
const parts = SIGNATURE_RE.exec(signatureHeader ?? '');
if (!parts) throw new Error('Invalid webhook signature header');
const timestamp = Number(parts[1]);
if (!Number.isSafeInteger(timestamp) || Math.abs(nowMs - timestamp) > TOLERANCE_MS) {
throw new Error('Webhook timestamp outside five-minute tolerance');
}
const expected = createHmac('sha256', secret)
.update(parts[1] + '.', 'utf8')
.update(rawBody).digest();
const supplied = Buffer.from(parts[2], 'hex');
if (expected.length !== supplied.length || !timingSafeEqual(expected, supplied)) {
throw new Error('Invalid webhook signature');
}
const text = Buffer.isBuffer(rawBody) ? rawBody.toString('utf8') : rawBody;
return JSON.parse(text);
}The two HMAC updates sign the timestamp prefix and original body in sequence, without allocating a combined payload buffer. Buffer input is decoded only after signature verification.
This verifies the signed body, not your business decision. Validate the event schema, then atomically deduplicate its signed eventId in durable storage before enqueueing work. Timestamp tolerance alone does not stop duplicate deliveries within that window. Return a successful acknowledgement for an already accepted event.
Prefer a maintained helper? The SDK exports verifyWebhook and framework middleware, including event validation and configurable timestamp tolerance.
Webhook payloads contain status and issuer metadata; read requested claims through the authenticated session API. Do not put credentials or personal data in logs. Rotate webhook secrets if they are exposed and update the endpoint configuration.
Configure your endpoint with PUT /v1/webhook-endpoint, then inspect failed deliveries. See Errors for retry handling.