Separate receipt from completion
- 1 · Verify
Authenticate the original request
Validate the signature using the raw request body and the correct endpoint secret before trusting an event.[1]
- 2 · Accept
Persist before acknowledging
Make a verified event durable, then return promptly. A successful response should mean your system has accepted responsibility, not that every downstream action has already finished.
- 3 · Apply
Guard the business transition
Process asynchronously, account for duplicates and out-of-order delivery, and keep enough state to retry failures safely.[1]
Delivery is not exactly-once execution
Webhooks replace repeated polling with notifications, but they do not remove network uncertainty. Stripe documents retries, possible duplicate deliveries and no guaranteed event order. Design against that contract rather than against the clean sequence in a demonstration.[1]
An illustrative failure is simple: a worker sends a fulfilment request, then crashes before recording completion. Retrying the event may send it again. Refusing every retry may instead lose unfinished work. The useful goal is a controlled, repeatable business outcome, not a promise that the handler will run only once.[1]
Validate before the event becomes trusted work
For Stripe, use the official library’s signature verification with the raw body, signature header and endpoint signing secret. A JSON parser that changes the original bytes before verification can break the check. Test the framework’s actual request path, not only a helper fed a hand-built object.[1]
After authentication, validate the event type, expected account context and fields needed by the operation. Separate test and live configuration. Do not accept a caller-supplied customer or tenant identifier as authority to update another account. Keep secrets and unnecessary customer payloads out of logs; event identifiers and bounded error codes are usually a better operational starting point.[1]
Acknowledge durable acceptance, not an in-memory promise
Stripe recommends returning a successful response quickly and processing complex work asynchronously. Our implementation recommendation is to commit a verified event to durable storage before that response. If persistence fails, return a failure so the sender can retry. A background task held only in process memory can disappear after a successful acknowledgement.[1]
A small database-backed inbox can be enough. Enforce a unique key for the provider, relevant account context and event ID in the database, rather than doing a separate check followed by insert. Keep received, processing, completed and failed states distinguishable. Claim work atomically and provide a bounded way to recover a worker that dies while holding a claim.[1]
Event deduplication and business idempotency are different
An event ID catches repeat delivery of the same event. It does not automatically stop two distinct events from requesting the same fulfilment. Stripe also describes duplicate Event objects and points to object ID plus event type for identifying those cases. Choose the final business guard from the operation’s meaning: for example, fulfil one paid order once.[1][2]
Where the effect is a local database change, commit the guarded state transition and completion marker together. For an external action, use that service’s idempotency mechanism where available and retain the stable operation key. Stripe’s API idempotency keys protect outgoing requests within its documented contract; they do not deduplicate your incoming webhook handler. Do not use personal information as the key or assume provider keys last forever.[1][2]
Treat an old event as evidence, not necessarily current state
Do not overwrite a current subscription or order with an older snapshot just because it arrived later. Stripe does not guarantee ordering and warns against using the event’s seconds-resolution created field as an ordering or deduplication rule. Model allowed transitions and retrieve the authoritative resource when current state is needed.[1]
Reconciliation is still required. Periodically compare important local outcomes with the provider, detect missing work and route ambiguous cases for review. Retain an audit trail linking the event, resource, attempted operation and final outcome. A replay should enter the same guarded path as normal delivery, not bypass safeguards through an administrative shortcut.[1]
Test the gaps between the steps
Exercise an invalid signature, duplicate concurrent deliveries, an unavailable database, a worker crash after an external side effect, a late event and a manual replay. Check the final business records and external effect count—not just the HTTP status. Use sandboxes and fixtures; do not generate real payments to test retry handling.[1]
Monitor the age of the oldest unfinished event, exhausted retries and reconciliation differences. Set a bounded retry policy and a review queue for persistent failures. A green endpoint with a growing backlog is an intake success and a processing failure, so alert on both separately.[1]
- Invalid requests never enter trusted processing.
- Acknowledged events survive a process restart.
- Concurrent duplicates produce one guarded outcome.
- Failed work remains visible and safely replayable.
