Webhooks flip the API relationship: instead of a developer pulling data on their schedule, your system pushes events onto theirs, and that push must survive network failures, duplicate sends, out-of-order arrival, and forged requests. Getting webhook design right means treating retries, signatures, idempotency, and schema versioning as product decisions, not backend implementation details.
Quick Answer: Webhook reliability is a contract, not a feature. Define retry policy, signature verification, idempotency keys, and event versioning explicitly, publish them, and hold to them — because every developer downstream is building distributed-systems logic on top of whatever you promise, whether you document it or not.
Why Webhooks Are a Different Product Than Your REST API
A request-response API puts the developer in control of timing, retries, and error handling. A webhook inverts that: you decide when to send, how many times to retry, and what happens if their endpoint is down for maintenance. That inversion means your team now owns failure modes that used to belong entirely to the consumer.
This isn't a minor UX wrinkle. It's the difference between a client library and a distributed system. When your platform pushes an event, the receiving server might be redeploying, rate-limited, or behind a load balancer that occasionally 504s. Your webhook sender has to decide, on their behalf, whether that's worth retrying and how aggressively.
The devtools literature on this is consistent: a good developer time-to-first-call experience for pull-based APIs is about reducing friction to the first successful request. Webhooks reverse the burden — the "first call" moment now happens on the consumer's server, and if your delivery semantics are vague, that first call becomes their first debugging session instead of their first success.
The Control Inversion, Concretely
In a polling model, a broken client just tries again later — nobody else notices. In a push model, a broken receiver generates retries, dead-letter events, and support tickets on your side. You've become responsible for:
- Delivery guarantees — at-least-once, at-most-once, or (rare, expensive) exactly-once
- Ordering guarantees — whether event #2 can ever arrive before event #1
- Security verification — proving the payload actually came from you
- Duplicate handling — helping consumers detect and discard repeat sends
- Schema evolution — changing payloads without breaking every integration overnight
Each of these is a product decision with tradeoffs, not a solved engineering problem you can defer. Teams that treat webhooks as "just a POST request with a callback URL" tend to relearn these lessons in production, one incident review at a time.
Retry Policy: The Decision That Defines Your Reliability Story
A retry policy is a promise about what happens when the consumer's endpoint fails, and it must specify retry count, backoff schedule, total retry window, and what "failure" even means — a non-2xx response, a timeout, or both. Vague retry behavior is the single most common source of webhook-related support tickets.
Start by defining what counts as a failed delivery. Most mature webhook systems treat any non-2xx HTTP status, connection timeout, or DNS failure as a retriable failure, while explicitly not retrying on 4xx client errors that indicate a permanent problem (like an invalid endpoint URL) versus a transient one (like a 429 rate limit, which should back off and retry).
Backoff Strategy Comparison
| Strategy | Behavior | Best for | Tradeoff |
|---|---|---|---|
| Fixed interval | Retry every N seconds | Low-volume, internal integrations | Can thunder-herd a struggling receiver |
| Exponential backoff | Delay doubles each attempt (1s, 2s, 4s, 8s...) | Most production webhook systems | Slower recovery detection on transient blips |
| Exponential with jitter | Backoff plus randomized offset | High-volume, multi-tenant platforms | Slightly more complex to implement and document |
| Capped exponential | Backoff grows then plateaus (e.g., max 1 hour) | Long-tail retries over 24+ hours | Requires a defined total retry window |
Stripe, GitHub, and Shopify — three of the most widely integrated webhook senders in the industry — all converge on exponential backoff with jitter and a retry window measured in hours to days, not minutes. That convergence is itself a signal: this isn't an arbitrary choice, it's what distributed-systems research on retry storms (a body of work associated with Google's SRE practices) recommends to avoid synchronized retry spikes overwhelming a recovering endpoint.
Your retry policy decision checklist:
- What HTTP responses count as retriable vs. terminal?
- What's the backoff curve, and is jitter included?
- What's the total retry window before you give up (hours? days?)?
- Do you expose retry status to the consumer via a dashboard or API?
- What happens after final failure — dead-letter queue, email alert, nothing?
The answer to #5 matters more than teams expect. A webhook that silently stops retrying after 3 attempts, with no notification, erodes trust fast — the consumer only discovers the gap when a customer complains that their data is stale.
Signature Verification: Proving the Payload Is Really From You
Every webhook payload should be signed with an HMAC signature so the receiving server can verify authenticity before trusting the data, and this is a security decision your product must make explicit, documented, and easy to implement correctly. An unsigned webhook is an open invitation for anyone who guesses the endpoint URL to inject fake events.
The standard pattern: compute an HMAC-SHA256 hash of the raw request body using a shared secret, send it in a header (commonly X-Webhook-Signature or similar), and instruct consumers to recompute the hash on their end and compare. This is the same cryptographic pattern used across nearly every major webhook provider, and for good reason — it's well-understood, fast to compute, and doesn't require asymmetric key management.
Common Signature Implementation Mistakes
- Comparing signatures with
==instead of a constant-time comparison — opens a timing-attack side channel, however small - Signing the parsed JSON instead of the raw body — any whitespace or key-order difference breaks verification on the consumer's side
- Omitting a timestamp in the signed payload — allows replay attacks where an old, valid request is resent later
- Not rotating or versioning secrets — makes secret rotation a breaking change instead of a graceful transition
A signature scheme without a timestamp check is not a security feature — it's a checkbox. Include a timestamp in the signed string and reject requests outside a tolerance window (commonly 5 minutes) to close the replay gap.
Document the exact signing algorithm, header name, and a copy-pasteable code sample in at least two languages. This is squarely a docs-as-product problem: the gap between "we have signature verification" and "developers actually verify signatures correctly" is almost entirely a documentation and example-quality gap, not a cryptography gap.
Idempotency Keys: Handling the Duplicate You Will Inevitably Send
Idempotency keys let a consumer safely process the same event twice without double-charging a customer or duplicating a database row, and you should include a stable, unique event ID in every payload specifically for this purpose. At-least-once delivery — the only realistic guarantee for most webhook systems — means duplicates are not an edge case; they are a certainty over enough volume.
Exactly-once delivery sounds appealing but is prohibitively expensive to guarantee end-to-end across an unreliable network, per the foundational distributed-systems reasoning behind the two-generals problem. Nearly every production webhook system — Stripe included, by its own public documentation — settles on at-least-once delivery plus idempotency keys as the practical substitute for exactly-once semantics.
What Goes Into an Idempotent Event Design
- A unique
event_id(UUID or similar) that stays identical across every retry of the same logical event - Clear documentation instructing consumers to store processed
event_ids and skip duplicates - A reasonable dedup window recommendation (e.g., "store IDs for at least 24 hours")
- Idempotency behavior called out explicitly in your API reference, not just implied
Skipping this is how "the payment webhook fired twice" incidents happen — not because your delivery logic is broken, but because it's working exactly as at-least-once semantics promise, and nobody told the consumer to expect it.
Event Schema Versioning: Change Without Breaking Every Integration at Once
Webhook payloads need an explicit version field and a deprecation policy, because unlike a REST endpoint a consumer can call on demand, a webhook payload arrives on your schedule and an unannounced schema change breaks their parser mid-flight with no warning. Treat every event type's schema as a public contract with its own versioning lifecycle.
The safest approach is additive-only changes within a version (new optional fields never break existing parsers) plus a new version number for anything that renames, removes, or changes the type of an existing field. Many teams version at the event-type level (invoice.paid.v2) rather than globally, since a global version bump forces every integration to re-certify even when only one event type actually changed.
Schema Change Categories
| Change type | Safe within current version? | Example |
|---|---|---|
| Add optional field | Yes | New metadata.region key |
| Add new event type | Yes | New subscription.paused event |
| Rename existing field | No — new version required | user_id → account_id |
| Change field type | No — new version required | amount from integer to string |
| Remove field | No — new version required | Dropping legacy_status |
Give consumers a realistic overlap window — commonly 6-12 months — where both the old and new version are delivered side by side, and communicate the deprecation timeline the same way you'd handle any breaking API change: changelog, email, and a sunset date, not a silent cutover.
The Webhook Reliability Checklist and Example Payload
A webhook offering is production-ready when retry behavior, signature verification, idempotency, and versioning are all documented in one place a developer can read before writing their first line of receiver code. Scattering these details across support tickets and Slack threads is how integrations quietly diverge from your intended contract.
Webhook reliability checklist:
- Delivery guarantee stated explicitly (at-least-once is the realistic default)
- Retry count, backoff curve, and total retry window documented
- Terminal vs. retriable failure conditions defined
- HMAC signature scheme documented with header name and code samples
- Timestamp included in signed payload with a replay-tolerance window
- Unique
event_idpresent on every event for idempotency - Recommended dedup storage window stated (e.g., 24+ hours)
- Schema version field on every event type
- Deprecation policy and overlap window published for breaking changes
- A dashboard or endpoint where consumers can inspect delivery/retry history
Example Event Payload With a Delivery-Guarantee Contract
{
"event_id": "evt_9f2a1c3d4e5f",
"event_type": "invoice.paid.v2",
"created_at": "2026-07-10T14:32:00Z",
"delivery_attempt": 1,
"data": {
"invoice_id": "inv_88213",
"amount": 4200,
"currency": "usd",
"customer_id": "cus_5a1b2c"
}
}
Delivery-Guarantee-Contract:
guarantee: at-least-once
retry_schedule: exponential backoff with jitter, starting at 5s
max_retry_window: 24 hours
terminal_after: 24h with no successful 2xx response
idempotency: dedup on `event_id`, recommended storage window 72h
signature_header: X-Webhook-Signature (HMAC-SHA256 over raw body + timestamp)
replay_tolerance: 5 minutes
Publishing this contract alongside the payload sample turns an implicit assumption into a checkable spec — the same instinct behind treating documentation as a product surface rather than an afterthought bolted on after the API ships.
Where Prodinja Fits Into This
Defining a webhook contract usually lives in scattered docs, Slack threads, and tribal knowledge until an integration breaks and someone has to reconstruct it from memory. Prodinja's API Designing tool lets you define the webhook payload contract and example events directly — endpoint shape, sample payloads, and the delivery-guarantee details above — so consumers know exactly what shape to expect and how to verify it, in one artifact instead of several.
That kind of explicit, example-backed contract is also what makes a webhook offering usable by bottom-up, developer-led adoption — a developer evaluating your API on their own time needs the contract to be self-service, not something they have to ask a solutions engineer to clarify.
Key Takeaways
- Webhooks invert control, making delivery, ordering, and security failure modes your product's responsibility instead of the consumer's.
- Retry policy is a documented contract, not an implementation detail — specify what counts as failure, the backoff curve, and the total retry window.
- HMAC signature verification with a timestamp is the baseline security bar; skipping the timestamp leaves a replay-attack gap.
- At-least-once delivery plus idempotency keys is the realistic, industry-standard substitute for the much costlier exactly-once guarantee.
- Schema versioning must be additive-first, with breaking changes tied to a new version number and a published deprecation window.
- A reliability checklist and example payload with a stated delivery-guarantee contract turns implicit assumptions into something developers can verify before they integrate.
Frequently Asked Questions
What is the best retry policy for webhooks?
Exponential backoff with jitter, over a retry window of roughly 24 hours, is the industry-standard baseline used by Stripe, GitHub, and Shopify. Pair it with a clear terminal-failure action, like a dead-letter queue or alert, so consumers aren't left guessing when retries stop.
How do I verify a webhook signature is authentic?
Compute an HMAC-SHA256 hash of the raw request body plus a timestamp using your shared secret, send it in a header, and have the consumer recompute and compare it with a constant-time comparison function. Reject requests where the timestamp falls outside a tolerance window, commonly 5 minutes, to prevent replay attacks.
Do webhooks guarantee exactly-once delivery?
No — nearly all production webhook systems guarantee at-least-once delivery because exactly-once is prohibitively expensive across unreliable networks. The practical fix is including a unique event_id in every payload so consumers can deduplicate on their end.
How should I version webhook event schemas?
Make additive changes, like new optional fields, freely within the current version, and bump to a new version number only for renames, type changes, or removals. Version at the event-type level rather than globally, and give consumers a 6-12 month overlap window before sunsetting the old version.
What should a webhook payload include besides the event data?
At minimum: a unique event_id, an explicit schema version, a timestamp, and the delivery attempt number, alongside the event-specific data. These fields are what let consumers implement idempotency, verify signatures, and handle schema changes without breaking on the next payload you send.