What a webhook is
A webhook is a reversed API call: instead of clients polling you for changes, *you* POST a JSON payload to a URL the consumer registered whenever an event happens. Stripe, GitHub, and Shopify all work this way. Good payload design makes integrations painless; bad design makes them fragile.
Anatomy of a good payload
{
"id": "evt_1NXyZA",
"type": "order.shipped",
"created": 1716268800,
"api_version": "2026-01-01",
"data": {
"object": {
"id": "ord_7Tn",
"status": "shipped",
"tracking_number": "DLVY99887"
}
}
}Five things make this robust:
- `id` — a unique event ID for deduplication and idempotency.
- `type` — a dot-namespaced event name (
order.shipped) consumers switch on. - `created` — when the event occurred, so consumers can order or discard stale events.
- `api_version` — which payload schema this follows, so you can evolve safely.
- `data.object` — the resource that changed, nested so you can add sibling fields later.
Make delivery verifiable
Anyone who learns the URL can POST fake events. Sign every payload so the consumer can verify it came from you. The standard pattern is an HMAC signature over the raw body, sent in a header:
X-Signature: t=1716268800,v1=5257a8...The consumer recomputes HMAC-SHA256(secret, timestamp + "." + rawBody) and compares. Include the timestamp in the signed content to prevent replay attacks, and reject events older than a few minutes.
Design for reliability
Networks fail. Consumers go down. Design for it:
- At-least-once delivery. Assume events may arrive more than once; consumers must dedupe on
id. - Retries with backoff. Retry failed deliveries on an increasing schedule, then give up and surface the failure.
- Fast acknowledgement. Consumers should return
2xximmediately and process asynchronously — never do slow work before acking, or you will time out and trigger retries.
Versioning the payload
Never break existing consumers. Safe evolution:
- Add fields freely — consumers ignore unknown keys.
- Never remove or repurpose a field within a version.
- Carry `api_version` so you can introduce a new shape while old consumers keep getting the old one.
A consumer's checklist
1. Verify the signature. Reject if invalid.
2. Reject if the timestamp is too old (replay protection).
3. Dedupe on event id. Skip if already processed.
4. Return 2xx immediately.
5. Process the event asynchronously.