Stripe webhooks connect an external payment state to your application. That makes the endpoint both operationally important and security-sensitive.
A production implementation must verify that the payload came from Stripe, remain safe when the same event arrives more than once, tolerate events arriving out of order and recover when downstream systems are unavailable.
This checklist focuses on those boundaries. It complements our broader Stripe integration guide.
1. Verify the signature against the raw body
Stripe signs each webhook payload. Your endpoint should pass the unmodified request body, the Stripe-Signature header and the endpoint's signing secret to the official library.
The essential shape in a Next.js route is:
const body = await request.text() const signature = request.headers.get('stripe-signature')
const event = stripe.webhooks.constructEvent( body, signature, process.env.STRIPE_WEBHOOK_SECRET )
Do not parse the request as JSON and then serialise it again before verification. Whitespace, encoding or key-order changes can make a genuine signature fail. Stripe's current signature troubleshooting guidance explicitly requires the body in the exact form Stripe sent.
Reject missing signatures and verification failures with a clear client error. Log a safe diagnostic, but do not log the full payload or secrets as a shortcut.
2. Use the correct secret for each endpoint and environment
The webhook signing secret is not an API key. Each registered endpoint has its own secret, and a Stripe CLI forwarding session uses a different temporary secret.
Keep test and live environments separate. Store secrets in the deployment platform's secret manager, scope access to the service that needs them and rotate a secret if it may have been exposed.
Common deployment mistakes include:
- using a CLI secret in production;
- copying the test endpoint secret to the live environment;
- configuring the same URL twice and reading the wrong endpoint record;
- placing the secret in a client-visible environment variable;
- updating the secret in one region or deployment but not another.
Record which Stripe account, mode, endpoint ID and application environment belong together. Avoid printing the secret during diagnosis.
3. Make replay safety a database property
Webhook delivery is at least once, not exactly once. Your handler must expect duplicates.
Create a durable processing record keyed by the Stripe event ID. Insert it with a unique constraint before applying a side effect. If the insert conflicts, return success for the already-accepted event rather than sending another email, adding another credit or creating another fulfilment job.
When separate event IDs could describe the same logical transition, add a domain-level guard as well. For example, a unique fulfilment record for an order is safer than trusting that only one event type can trigger fulfilment.
Stripe's webhook guidance recommends logging processed event IDs and, in some duplicate cases, considering the data object ID together with the event type.
Idempotency is not a boolean in application memory. It must survive restarts, concurrent deliveries and regional scaling.
4. Listen only for events you use
Configure the endpoint for the smallest useful event set and implement an explicit allowlist in the handler.
switch (event.type) { case 'checkout.session.completed': case 'invoice.paid': case 'customer.subscription.deleted': await enqueueStripeEvent(event) break default: break }
The correct list depends on your payment model. Do not copy event types from a tutorial without defining their business effect.
An allowlist reduces data exposure, log noise and accidental behaviour when a new Stripe product is enabled. Unknown events should be acknowledged safely unless your contract intentionally requires otherwise.
5. Return quickly and process asynchronously
Signature verification, minimum validation and durable queueing should happen in the request path. Slow email, CRM, fulfilment and analytics work should happen in a worker.
The request path should:
- Read the raw body.
- Verify the signature.
- Validate the allowed event type and expected mode or account.
- Store or enqueue the event durably.
- Return a successful response.
This limits timeouts and makes retry behaviour easier to reason about. Stripe automatically retries live deliveries for up to three days according to its current documentation, so an endpoint that repeatedly times out can create a growing duplicate workload.
The queue must itself be reliable. A successful HTTP response after writing only to process memory can permanently lose the event when the instance exits.
6. Do not depend on event order
Stripe does not guarantee delivery order. A later lifecycle event may arrive before an earlier one.
Design each handler around a state transition, not a presumed narrative. Where current Stripe state matters, retrieve the relevant object using the server API and reconcile it with your domain record. Avoid overwriting a terminal local state with stale data from an older event.
Use timestamps cautiously. They can help with diagnostics, but domain rules and authoritative object state should decide whether a transition is valid.
7. Validate account, mode and ownership
Signature verification proves the payload was signed with the endpoint secret; your application should still verify the context it expects.
For Stripe Connect or multi-account systems, confirm the connected account and application relationship before applying effects. Check whether the event is live or test mode. Confirm the referenced customer, order or subscription belongs to the tenant being updated.
Never trust client-supplied metadata as the sole authority for access or value. Use a server-created identifier and verify it against your database.
8. Treat metadata as routing context, not a complete ledger
Metadata is useful for stable internal references such as an order ID or tenant ID. Keep it small and avoid personal, confidential or regulated data.
When an event arrives:
- retrieve the internal record;
- verify its expected amount, currency and Stripe object relationship;
- apply a permitted state transition;
- retain an audit link to the Stripe event.
Do not fulfil purely because a browser returned to a success URL. The server-side event and authoritative payment state should drive the durable result.
9. Control API versions and schema changes
Record the Stripe API version used by the application and endpoint. Test version upgrades against stored representative events and a staging environment.
Avoid spreading raw Stripe event access throughout the codebase. Convert validated events into small internal commands or domain transitions. That boundary makes version changes, retries and tests easier to manage.
Generate types where helpful, but remember that static types cannot verify the origin, current state or business ownership of runtime data.
10. Build observability without leaking data
For every delivery, capture safe structured fields such as:
- event ID and type;
- Stripe object ID where appropriate;
- account and mode;
- receive and processing timestamps;
- queue message ID;
- processing status and attempt count;
- internal correlation ID;
- sanitised error category.
Create alerts for sustained signature failure, queue age, repeated processing failure and unusual event volume. Keep payload access restricted and use a documented retention period.
A useful operational view should answer: Was the event received? Was it verified? Was it queued? Which worker processed it? Which domain record changed? Can it be retried safely?
11. Provide controlled replay and reconciliation
Retries should reuse the same idempotent path. Do not tell an operator to delete the processing record and hope.
Build an authenticated administrative route or runbook that can:
- inspect a failed event without exposing unnecessary payload data;
- retry the internal job;
- retrieve current Stripe state;
- reconcile a domain record;
- record who performed the action and why.
Periodically reconcile critical payment and subscription state against Stripe, especially after an outage. Webhooks are the timely notification channel; reconciliation is the safety net.
12. Test failure, not only success
Before production, exercise:
- invalid and missing signatures;
- a modified body;
- duplicate concurrent delivery;
- out-of-order lifecycle events;
- queue unavailability;
- worker failure after a partial downstream action;
- a revoked or rotated secret;
- test-mode data at a live boundary;
- unknown event types;
- slow downstream services;
- manual replay after recovery.
Use Stripe's test tooling and fixtures, then verify the application database, queue and logs. A 200 response alone does not prove the business effect completed.
Production deployment checklist
Before switching traffic on:
- The live endpoint is HTTPS and registered in the correct Stripe account.
- The live signing secret is stored server-side and mapped to the right deployment.
- Signature verification uses the raw request body.
- Only required event types are enabled and allowed.
- Durable event and domain idempotency constraints exist.
- The request returns after durable acceptance, not slow side effects.
- Workers tolerate duplicates and out-of-order events.
- Logs and alerts avoid secrets and unnecessary personal data.
- Operators have a replay and reconciliation runbook.
- A complete live-mode, low-value journey has been verified safely where appropriate.
The bottom line
A secure Stripe webhook is a small ingestion boundary backed by durable state. Verify the exact bytes, minimise the accepted event set, acknowledge quickly and move business work into a replay-safe process.
Assume duplicates, disorder and downstream failure. Store evidence of every transition and give operators a controlled way to recover.
If you need a review of a production Stripe flow, talk to LogicLeap. We can trace the endpoint, data model, deployment secrets and recovery path as one system.


