Public beta
Webhook signatures
Every webhook delivery is signed with HMAC-SHA256 using the secret returned when you registered the endpoint. Always verify the signature before trusting the payload.
Headers
X-Zippex-Signature— hex HMAC-SHA256 of`${timestamp}.${rawBody}`using your endpoint secret as the key.X-Zippex-Timestamp— ISO-8601 timestamp the dispatcher used when computing the signature.X-Zippex-Event-Id— unique event ID for de-duplication.
Verification algorithm
- Read
X-Zippex-Timestampand the raw request body. - Compute HMAC-SHA256 over
`${timestamp}.${rawBody}`using the endpoint secret as the key. - Hex-encode the digest and constant-time compare with
X-Zippex-Signature. - Reject deliveries whose timestamp is more than 5 minutes old to defeat replay attacks.
import { createHmac, timingSafeEqual } from 'node:crypto';
const TOLERANCE_SECONDS = 300;
export function verify(req, rawBody, secret) {
const signature = req.headers['x-zippex-signature'];
const timestamp = req.headers['x-zippex-timestamp'];
if (!signature || !timestamp) throw new Error('Missing signature headers');
const sentAt = new Date(timestamp).getTime();
if (Number.isNaN(sentAt) || Math.abs(Date.now() - sentAt) > TOLERANCE_SECONDS * 1000) {
throw new Error('Stale signature');
}
const expected = createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const a = Buffer.from(signature, 'hex');
const b = Buffer.from(expected, 'hex');
if (a.length !== b.length || !timingSafeEqual(a, b)) {
throw new Error('Bad signature');
}
}Use the raw body
Verify the signature against the unparsed request body. Re-serializing a parsed JSON object can change byte ordering or whitespace and break verification.
Retries
Failed deliveries (timeout or 5xx) are retried at 1 second, 30 seconds, and 5 minutes. Each retry carries the same
X-Zippex-Event-Id so you can dedupe.