Webhook Best Practices: A Checklist for Senders and Receivers (2026)
Webhook best practices for senders and receivers: Standard Webhooks signing, retries with backoff, endpoint isolation, 410 handling, replay; verify, ack fast, dedupe.
Webhook best practices come down to two short lists. Senders should sign every request with a per-endpoint secret (ideally the Standard Webhooks scheme), give each event a stable ID, retry with exponential backoff and Retry-After, isolate each endpoint, time out quickly, stop on 410 Gone and keep a delivery log they can replay. Receivers should verify the signature against the raw body, respond 2xx fast, queue the real work and deduplicate on the event ID.
Most webhook advice is written for one side. This checklist covers both, because the failures are shared: a sender with no backoff takes down a receiver that is already struggling, and a receiver that does slow work inline causes the timeouts that make the sender retry. If you send webhooks to your customers, the first half is your spec. If you consume them, the second half is.
The checklist at a glance
| Practice | Side | Why it matters |
|---|---|---|
| Sign with HMAC-SHA256 per endpoint (Standard Webhooks) | Sender | Receivers can prove the request is yours and untampered |
Stable webhook-id per event, fresh timestamp per attempt | Sender | Receivers dedupe retries; old captures can't be replayed |
Exponential backoff with a deadline, honour Retry-After | Sender | Survives outages without hammering a struggling endpoint |
| Per-endpoint queues, rate limits and concurrency caps | Sender | One slow customer never delays the others |
| 5-15 second timeouts, no redirects | Sender | Workers stay free; signatures and SSRF rules hold |
Stop on 410 Gone, auto-disable dead endpoints | Sender | No retry traffic to URLs that will never answer |
| Typed, documented, additive event types | Sender | Customers subscribe to what they need; nothing breaks on change |
| Small payloads, no secrets | Sender | Fewer timeouts, nothing sensitive sitting in logs |
| Block private and loopback destinations | Sender | Your webhook system is not an SSRF proxy |
| Log every attempt, support retry and replay | Sender | Support can answer "did you send it?" in seconds |
| Rotate secrets with an overlap window | Sender | Customers switch keys with zero dropped deliveries |
| Publish through an outbox with idempotency keys | Sender | Events survive crashes between commit and send |
| Verify the signature on the raw body, check the timestamp | Receiver | Rejects forged and replayed requests |
Respond 2xx within a few seconds, queue the work | Receiver | Avoids timeouts and duplicate deliveries |
| Deduplicate on the event ID with a unique constraint | Receiver | At-least-once delivery becomes effectively once |
| Don't assume ordering | Receiver | Retries and parallel lanes reorder events |
| Reconcile against the sender's API | Receiver | Catches anything that was never delivered |
Best practices for webhook senders
1. Sign every delivery, with a separate secret per endpoint
An unsigned webhook is an unauthenticated POST to a public URL: anyone who learns the URL can forge events. Sign each request with HMAC-SHA256 and give every endpoint its own secret, so one leaked key compromises one integration, not all of them.
Don't invent a scheme. Standard Webhooks is an open spec with libraries in most languages:
webhook-id: msg_2KWPBgLlAfxdpx2AI54pPJ85f4W
webhook-timestamp: 1761382800
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
The signature is base64(HMAC-SHA256(secret, "{webhook-id}.{webhook-timestamp}.{raw body}")), and the secret is a whsec_ prefixed base64 key. Customers who already verify another Standard Webhooks provider can reuse their code. You can generate test headers with the Standard Webhooks signature generator and check them with the verifier.
2. Give each event a stable ID and a fresh timestamp per attempt
The event ID (webhook-id) must be identical across every retry and every manual replay; it is what receivers deduplicate on. The timestamp should be the time of this attempt, re-signed each time, so receivers can reject anything older than a few minutes and a captured request can't be replayed next week.
3. Retry with exponential backoff, a deadline and Retry-After
Failures are normal: deploys, cold starts, a database failover. Retry any non-2xx response, timeout or connection error, with gaps that grow quickly and a hard stop:
| Attempt | Delay before it (Webhook Relay Outbound Gateway schedule) |
|---|---|
| 1 | none, sent immediately |
| 2 | 5 seconds |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 5 hours |
| 7 | 10 hours |
| 8 | 10 hours, and never later than 48 hours after publishing |
Stripe retries for up to three days; one to three days is the usual range. Add a little jitter so a recovering endpoint isn't hit by a synchronized wave. When a receiver answers 429 Too Many Requests or 503 Service Unavailable with a Retry-After header, wait at least that long (cap it, for example at one hour). For the receiving side of this story, see webhook retries and idempotency.
4. Isolate every endpoint
The classic homegrown bug: one global queue and a pool of workers. A customer whose endpoint takes 30 seconds to time out ties up workers, and every other customer's webhooks queue behind it. Give each endpoint its own lane: its own rate limit (for example 50 requests per second), its own concurrency cap and its own retry schedule. A slow or dead endpoint should only ever delay itself.
5. Use short timeouts and don't follow redirects
A 5-15 second timeout is plenty for a receiver that acknowledges and queues. Longer timeouts just let slow receivers consume your worker capacity. Let customers raise it per endpoint if they really need to (up to 60 seconds is a sensible ceiling).
Treat 3xx responses as failures. Following a redirect can send a signed payload to a host the customer never registered, and it is a classic way around SSRF checks.
6. Stop on 410 Gone and auto-disable dead endpoints
410 Gone is the receiver telling you to stop: disable the endpoint and don't retry. For endpoints that just keep failing, track health per endpoint (active → failing after a run of failures) and disable automatically after a long, consistent outage, such as 100 consecutive failures spanning at least 12 hours. Tell the customer when this happens; don't make them find out from missing data.
7. Design event types like an API
- Name them
resource.action(invoice.paid,subscription.canceled) and publish a catalog with an example payload for each. - Let endpoints subscribe to specific event types instead of receiving everything.
- Treat the payload as a public API: add fields freely, never remove or rename them without a new version or a new event type.
- Deprecate rather than delete, so nothing new is published to an event type customers still depend on.
8. Keep payloads small and free of secrets
Large bodies cause timeouts and fill logs. Set a size limit (256 KiB is generous) and for big objects send an ID plus a link the receiver can fetch with its API key (a "thin" payload). Never put passwords, tokens or full card numbers in a webhook: payloads end up in delivery logs, receiver logs and support tickets.
9. Block private and internal destinations
Customers type URLs into your product, and your servers then make requests to them. Without checks, http://169.254.169.254/ or https://10.0.0.5/admin turns your webhook system into an SSRF proxy into your own network. Require HTTPS, reject hosts that resolve to private, loopback or link-local addresses at registration, and check again when you connect, because DNS can change after registration.
10. Log every attempt and make replay a button
When a customer says "we never got the invoice.paid for inv_123," support needs to answer in seconds: when each attempt was made, what status and body came back, and whether it is still retrying. Store every attempt with its request headers and (truncated) response for at least 30 days. Then make recovery cheap:
- Retry one delivery to one endpoint.
- Recover everything that exhausted its retries since a point in time (the customer's outage started Tuesday 14:00).
- Replay missing messages that were skipped while an endpoint was paused.
Every copy must keep its original event ID so deduplicating receivers stay correct.
11. Rotate secrets with an overlap window
Customers will need to rotate keys after a leak or an employee leaving. If rotation swaps the key instantly, every delivery fails until they deploy the new secret. Instead, sign with both the new and old secrets for a window (24 hours works), sending two v1, signatures in the header. Standard Webhooks receivers accept any matching signature, so the switch is seamless.
12. Publish durably, with idempotency keys
The first place a webhook gets lost is inside your own service: the order commits, the process crashes, the send never happens. Write the event to an outbox table in the same database transaction as the business change, and publish from a worker. Make the publish call idempotent (an Idempotency-Key derived from the event), so a worker that retries after a timeout can't create a second message. The webhook system design post covers this architecture end to end.
Best practices for webhook receivers
1. Verify the signature against the raw body
Compute the HMAC over the exact bytes received, not a re-serialized JSON object, compare in constant time and reject timestamps more than about five minutes from now. Accept any of several signatures in the header so secret rotation doesn't break you. Provider-specific details and code are in how to verify a webhook signature; broader hardening is in webhook security.
2. Acknowledge fast, work later
Return a 2xx as soon as the event is durably stored, ideally in well under a second and always within the sender's timeout. Charging cards, sending email and calling other APIs belong in a background job. Slow handlers cause timeouts, and timeouts cause duplicates.
3. Deduplicate on the event ID
Delivery is at-least-once everywhere. Insert the event ID into a table with a unique constraint before acting, in the same transaction as the side effect; if the insert conflicts, return 200 and do nothing.
4. Don't depend on ordering
Retries, parallel delivery lanes and replays all reorder events. Use timestamps or version numbers in the payload to ignore stale updates, or treat the webhook as a signal and fetch the current state from the sender's API.
5. Use status codes deliberately
2xx means done. 401 for a failed signature. 429 or 503 with Retry-After when you need the sender to back off. 410 only when you want the sender to stop delivering to that URL permanently. Avoid returning 500 for events you deliberately ignore; that just schedules retries.
6. Reconcile periodically
Even with retries, an endpoint can be down longer than the sender's retry window. A nightly job that lists recent objects from the sender's API and compares them with what you processed catches the gaps.
Or don't build the sender side
The receiver checklist is a few dozen lines of code. The sender checklist is a system: a durable queue, a retry scheduler, a signer, per-endpoint rate limiting, a health state machine, SSRF protection, a delivery log with retention and a recovery pipeline, operated for years.
If you would rather not own that, Webhook Relay's Outbound Gateway is webhooks as a service built to this checklist: you publish an event once through the REST API, the TypeScript or Go SDK or MCP, and it delivers Standard Webhooks signed requests to each customer's endpoints with the retry schedule above, per-endpoint lanes (50 requests per second by default), 410 handling, optional auto-disable, 24-hour secret rotation overlap and 35 days of per-attempt history with retry, recover and replay. It starts at $9.99/month on the Basic plan with no per-message fees. It does not have an embeddable customer portal, FIFO ordering or non-HTTP destinations, so check those against your requirements; the webhooks as a service comparison covers the alternatives.
To try it, follow send webhooks from Node.js / TypeScript or from a Go service, or start with the Outbound Gateway docs.
