Verifying a webhook delivery
Everything you need to verify a delivery, with nothing left to infer.
Headers
POST /your/endpoint HTTP/1.1
X-Forumulate-Event: page.published
X-Forumulate-Delivery: 9f2c… ← the idempotency key to dedupe on
X-Forumulate-Timestamp: 1787025242
X-Forumulate-Signature: v1,sha256=9f86…, sha256=5b41…The signature
v1 is HMAC-SHA256 over {timestamp}.{raw body}, hex, using your endpoint's secret:
const base = `${req.headers['x-forumulate-timestamp']}.${rawBody}`;
const expected = 'v1,sha256=' + crypto.createHmac('sha256', secret).update(base).digest('hex');
const ok = req.headers['x-forumulate-signature'].split(', ').some((s) =>
crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)),
);Compare in constant time, and reject a delivery whose timestamp is more than a few minutes old — that is what stops a captured delivery being replayed later.
The header may carry several signatures, comma-separated. Accept the delivery if any of them matches: that is how a secret rotates without downtime, and how the older sha256= signature (over the body alone) is still honoured for one release.
Rotating a secret
POST /api/orgs/{orgId}/webhooks/{webhookId}/rotate-secret
{ "overlapHours": 24 }Both secrets sign every delivery until the overlap ends, so you can swap your copy whenever you like rather than at the same instant we do.
Ordering and repeats
Deliveries are at-least-once and not ordered. Every payload carries a sequence that increases per organization: keep the highest one you have seen and discard anything lower, or you will eventually overwrite a newer version with an older one. Dedupe repeats on X-Forumulate-Delivery.
Events
page.created, page.published, page.deleted, change_request.merged, version.created, locale.created, topic.created, topic.published, post.created, post.flagged, topic.solved, user.joined, badge.granted, group.member_added, and the synthetic ping.