Verifying a webhook delivery

Embed
Preview of the card
<a href="https://forumulate.com/reference/webhooks-verifying"><img src="https://forumulate.com/reference/_api/public/orgs/046a9bc0-d530-43ae-9bae-af7945008cda/spaces/8042f004-e3d9-445f-b7a4-b6c1f86836e6/pages/62334e58-b38d-4908-b93e-ce2c3ab12589/social-card.svg?v=3e7ae33eec8b" alt="Verifying a webhook delivery" width="600" height="315"></a>

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.

The Forumulate teamUpdated