A webhook endpoint is a URL on the public internet that accepts POST requests and acts on them. If it does not verify who sent the request, anyone who discovers the URL can tell your system whatever they like — that a payment cleared, that a subscription upgraded, that an account was verified.
Signature verification is what makes the endpoint trustworthy, and it is skipped surprisingly often because the integration works without it.
What changed in 2026
- Signing became near-universal among providers. Sending signed webhooks is now standard practice for services of any size.
- Verification helpers spread. Provider SDKs shipping verification functions removed most of the implementation risk.
- Raw-body guidance became prominent. The framework-middleware failure got documented well enough to become common knowledge.
- Asynchronous processing became the recommended default. Acknowledge fast, process out of band.
Verify before anything else
The order matters. Verification must happen before you parse, before you act, and before you log the contents anywhere.
The mechanics are HMAC signing: the provider computes a signature over the request content using a shared secret, you recompute and compare — see HMAC request signing for the underlying scheme.
The failure specific to webhooks is the raw body problem. Most frameworks parse a JSON body into an object before your handler runs. Signatures cover exact bytes, so re-serialising that object produces different bytes — different key order, different whitespace, different number formatting — and verification fails.
It fails intermittently, depending on payload contents, which makes it maddening to debug. The fix is to capture the raw body before parsing middleware touches it, usually by configuring the framework to preserve it on the webhook route specifically.
Use timing-safe comparison, and reject requests whose timestamp is outside a short window so a captured request cannot be replayed indefinitely.
Acknowledge fast, process later
The common architectural mistake: doing the work inside the webhook handler.
Providers expect a prompt response and treat a slow one as a failure. Slow handlers cause timeouts, timeouts cause retries, and retries cause duplicate processing of work that was actually succeeding — a failure mode that gets worse under load, exactly when handlers are slowest.
The pattern that works: verify the signature, persist the raw event, return 200, and process asynchronously.
| Step |
Where |
| Verify signature |
In the handler |
| Persist the event |
In the handler |
| Return 200 |
Immediately |
| Parse and act |
Background worker |
| Retry on failure |
Your own retry logic |
That decouples your processing reliability from the provider's timeout. A processing failure becomes your retry rather than theirs, which you control.
Duplicates are normal
Webhook delivery is at-least-once. Providers retry on any response they consider a failure, including timeouts where you actually succeeded. Network conditions produce duplicates independently.
So duplicate delivery is not an error condition — it is expected behaviour, and handlers must be idempotent by design.
The standard approach uses the event identifier the provider supplies: record processed identifiers, and skip anything already seen. That turns a duplicate into a no-op — see idempotency explained.
Ordering is the related trap: webhooks may arrive out of order, so an event describing an older state can arrive after a newer one. Check the event's timestamp or version against what you have recorded rather than applying blindly, or a delayed retry will overwrite fresher data.
Do not trust the payload as truth
A verified webhook proves the provider sent it. It does not mean the payload is a complete or current picture.
For anything consequential — a payment amount, a subscription state — the safer pattern is to treat the webhook as a notification that something changed and then fetch the current state from the provider's API. That eliminates ordering problems and stale payloads in one step.
It costs an API call per event, and for financial or access-control decisions that is cheap insurance against acting on an out-of-order or partial payload.
Common mistakes
- No verification. The endpoint accepts input from anyone.
- Verifying re-serialised body. Intermittent failures.
- Processing synchronously. Timeouts and duplicate deliveries.
- Assuming exactly-once delivery. Retries are normal.
- Applying events without checking order. Stale data overwrites fresh.
- Returning a non-2xx on a business-logic failure. Triggers provider retries for something retrying will not fix.
- Logging payloads before verification. Unverified attacker-controlled content in your logs.
FAQ
What status should I return on a bad signature?
A 4xx — it is a client error and should not be retried. Returning 5xx makes the provider retry something that will never succeed.
What if processing fails after I returned 200?
Your problem to retry, which is the point of persisting the event first. Do not rely on the provider redelivering something you acknowledged.
Should I allow-list source IP addresses?
A useful additional layer where the provider publishes ranges, and not a substitute for signatures — addresses change and can be spoofed at some layers.
How do I test webhook handling?
Most providers offer test events and a way to replay them. Verify that your handler is genuinely idempotent by delivering the same event twice.
Where to go next
For the signing scheme, read HMAC request signing. For duplicate handling, idempotency explained, and for the delivery guarantees involved, exactly-once delivery.