Every event delivered, and only counted once.
A webhook is a promise to another server, and most implementations break it quietly: an unsigned payload anyone can forge, a retry that gets processed twice, an outage that drops the event for good. We sign every delivery, stamp it with an idempotency key, and retry with backoff until it lands or dead-letters, so your endpoint gets every event, once, and can prove it came from us.
Drive it: send an event, then take the endpoint down and watch the retries back off and dead-letter, bring it back and replay. Every attempt is signed and carries the same idempotency key.
same idempotency key on every retry · your endpoint processes it once
One event, delivered until it lands.
What you just operated is this. The naive version fires one POST and hopes. This one signs the payload, treats only a 2xx as delivered, and keeps retrying on a growing delay until your server takes it or it parks in a dead-letter queue you can replay from.
Each event gets an HMAC signature over its body and timestamp, plus a stable idempotency key.
A POST to your endpoint, with a tight timeout. A 2xx is the only thing that counts as success.
Anything else is retried on exponential backoff, so a brief outage never loses the event.
After the last attempt it parks in a DLQ with every attempt and the final reason.
One click resends a parked event once your endpoint is healthy. Nothing is lost.
On your side: verify, then dedupe.
Delivery is only half of it. Your endpoint recomputes the signature over the raw body with your secret and compares it in constant time, rejecting anything forged or stale. Then it checks the idempotency key, so a retry of an event you already handled is acknowledged, not run twice. We ship this handler with the build.
1// verify.ts — reject what we didn't sign, dedupe the rest2import { createHmac, timingSafeEqual } from "node:crypto";34export function verify(req, secret, seen) {5 const sig = req.headers["x-signature"];6 const ts = req.headers["x-webhook-timestamp"];7 if (now() - Number(ts) > 300) return reject("stale"); // replay window89 const expected = "sha256=" + createHmac("sha256", secret)10 .update(ts + "." + req.rawBody).digest("hex");11 if (!timingSafeEqual(buf(sig), buf(expected)))12 return reject("bad signature"); // constant-time1314 const id = req.headers["x-webhook-id"];15 if (seen.has(id)) return ack("duplicate"); // idempotent16 seen.add(id);17 return handle(req.body);18}The promises that make it dependable.
A webhook you can build a business process on has to be more than a POST. These are the guarantees we wire in, so the other side can trust every event.
Retried until delivered, and idempotency keys let your endpoint collapse the duplicates to a single effect.
HMAC over the raw body plus a timestamp, so payloads can't be forged and old ones can't be replayed.
Retries space out, 2s to minutes to hours, so a struggling endpoint isn't hammered while it recovers.
An endpoint that keeps 4xx-ing is paused and you're alerted, instead of piling up dead deliveries forever.
Nothing is silently dropped. Failed events wait with their history and replay when you're ready.
Every attempt, response code, and payload recorded, so a missed webhook is a query, not a mystery.
Firing a POST is a line of code. Delivering it, exactly once, is the system.
Almost every webhook bug is the same story: the one that fired twice and double-charged, the one that got lost when the endpoint blinked, the forged one nobody checked. None of that shows up in a demo on a good day. It shows up in production, on the bad one. So we build the signing, the idempotency, the backoff, and the dead-letter queue up front, because that boring machinery is the entire difference between a webhook and a promise you can actually keep.
Webhooks your integrations can build on.
Signed payloads, idempotency keys, retries with backoff, and a dead-letter queue with replay, so every event lands once and can be proven.