In short
A dependable webhook system signs every request so receivers can verify authenticity, retries with exponential backoff on failure, includes a unique event identifier so duplicates can be detected, and gives developers a log of what was sent with the ability to replay. Ordering cannot be guaranteed across retries, so events must carry enough information to be processed out of sequence.
What a receiver needs from you
Design from the other side. A developer receiving your webhooks needs to answer:
Is this genuinely from you? Anyone can POST to a public endpoint.
Have I already handled this? Retries mean the same event arrives more than once.
What order did these happen in? Delivery order is not event order.
What did I miss? After an outage, they need to catch up.
Why did this fail? Without visibility, every problem becomes a support ticket to you.
Every design decision below serves one of those.
Sign every request
Unsigned webhooks mean anyone who learns the endpoint can send fabricated events. For anything driving state changes — payments, provisioning, access — that is a serious vulnerability.
Sign the payload with a shared secret, typically HMAC, and send the signature in a header. Document how to verify it, with an example in the languages your customers use.
Include a timestamp in the signed content and document a tolerance window, so an intercepted request cannot be replayed indefinitely.
Support secret rotation by allowing two active secrets during a changeover. Without it, rotating a secret means an outage for every customer.
Retry properly
Endpoints will be down. Retrying is the whole point of a webhook system over a plain notification.
Retry on failure with exponential backoff, over a window long enough to survive a real outage — hours rather than minutes.
Define what counts as success. A 2xx response. Anything else retries. Document this, because receivers frequently return 200 for errors and then wonder why nothing retried.
Stop eventually, and tell the customer. An endpoint failing for a day should generate a notification, not silence.
Disable persistently failing endpoints rather than retrying forever, with a clear route to re-enable. Otherwise a customer who decommissioned a server costs you delivery capacity indefinitely.
Accept that order is not guaranteed
This is the constraint people design around badly.
Once retries exist, ordering cannot be guaranteed. An event that failed and succeeded on the third attempt arrives after events generated later.
So make events self-contained. Each should carry enough state to be processed independently, plus a timestamp and a sequence or version number so a receiver can detect that they are holding a newer state and ignore an older event.
Do not require receivers to process events in order. If ordering genuinely matters, that is a different integration pattern — a polled API or a stream — and saying so is more honest than implying guarantees you cannot keep.
Make duplicates detectable
Retries mean the same event arrives more than once, by design.
Give every event a unique identifier, stable across retries. That is what lets a receiver record what it has processed and skip repeats — the idempotency pattern from the receiver's side.
Document that duplicates will happen. Receivers who assume exactly-once delivery will build something that breaks, and they will report it as your bug.
Payload design
Include the event type and a version. Consumers need to route on type and to handle format changes.
Include enough data to act on, so common cases do not require a follow-up API call. But include an identifier too, so a receiver can fetch current state when they need it — particularly useful given events may be stale by the time they are processed.
Do not include sensitive data unnecessarily. The payload travels to a third-party endpoint and will be logged there.
Version by adding, never by removing or repurposing. A receiver written a year ago must keep working.
Give developers visibility
The difference between a webhook system that generates support tickets and one that does not.
A delivery log — what was sent, when, the response code, how many attempts. Most webhook support questions are answered by the customer looking at this themselves.
Replay. Let them resend a specific event or a range after fixing a bug on their side. Without it, every failure becomes an email to you.
A test event, so they can verify an endpoint during setup rather than by waiting for something to happen.
Clear failure reasons — timeout, connection refused, non-2xx, signature mismatch on their side.
Operational details
Deliver asynchronously, from a queue. A slow receiver must never affect the action that generated the event.
Set a short timeout, and document it, so receivers know to acknowledge quickly and process afterwards.
Let customers subscribe to specific event types rather than receiving everything.
Publish your egress IP addresses if you can, since some customers need to allowlist them.
Rate limit per endpoint so a burst does not overwhelm a receiver — a courtesy that prevents your integration being blamed for their outage.
The documentation that matters
A list of event types with example payloads. Verification code in the main languages, copy-pasteable. An explicit statement about duplicates and ordering. The retry schedule. The timeout. How to rotate a secret.
That page prevents more support load than any feature, because the questions it answers are the ones every integrator has.
If you are exposing webhooks to customers and want the contract reviewed before people build against it, book a call.
Common questions
Should webhooks be signed?
Yes. An unsigned endpoint means anyone who learns the URL can send fabricated events, which is serious for anything driving state changes like payments or provisioning. Sign the payload with a shared secret, include a timestamp in the signed content, and support two active secrets so rotation is not an outage.
Can webhook delivery order be guaranteed?
No, not once retries exist — an event that succeeds on its third attempt arrives after events generated later. Make each event self-contained with a timestamp and version so receivers can detect they hold newer state, and never require processing in order.
How should webhook retries work?
Exponential backoff over hours rather than minutes, so a real outage is survivable. Define success explicitly as a 2xx response and document it, notify the customer when an endpoint has been failing, and disable persistently dead endpoints with a clear route to re-enable them.
How do receivers handle duplicate webhook events?
By recording a unique event identifier that stays stable across retries and skipping repeats. You must provide that identifier and document that duplicates will occur, because receivers who assume exactly-once delivery will build something that breaks and report it as your bug.
What tooling should a webhook system provide?
A delivery log showing what was sent, when, the response and attempt count; the ability to replay a specific event or range after the receiver fixes a bug; a test event for verifying an endpoint during setup; and clear failure reasons. Most webhook support questions are answered by customers looking at this themselves.
