Back to journal
SaaS8 min readJuly 24, 2026

Stripe Webhooks in Production: The Checklist That Prevents Revenue Leaks

A practical Stripe webhook production checklist covering signature verification, idempotency, event ordering, retries, reconciliation, alerting, refunds, and safe replay tools.

#Stripe#webhooks#payments#SaaS engineering#reliability#backend development
Stripe Webhooks in Production: The Checklist That Prevents Revenue Leaks

Short answer

A production-ready Stripe webhook handler must verify signatures, acknowledge events quickly, process each event idempotently, tolerate retries and out-of-order delivery, reconcile against Stripe, and provide safe observability and replay controls. Treat webhooks as an unreliable event stream—not as a single guaranteed notification—and your billing system becomes substantially easier to trust and operate.

Stripe webhooks often sit on the critical path between a successful payment and the access, provisioning, or accounting action that follows. A fragile handler can create duplicate subscriptions, grant access after a failed payment, miss refunds, or leave your internal records disagreeing with Stripe.

This checklist focuses on the operational details that matter once your application has real customers and real revenue.

1. Verify every webhook signature

Never trust an incoming request because it came from a URL that is difficult to guess. The endpoint must verify the Stripe-Signature header against the raw request body and the webhook endpoint secret.

Signature verification has two common failure modes:

  • The framework parses or modifies the request body before verification.
  • The application uses the wrong endpoint secret, especially when test and live environments are mixed.

Configure the route to preserve the raw payload. Pass that exact payload, the signature header, and the correct secret to Stripe's official verification utility. Do not reconstruct JSON and then verify the reconstructed object.

Keep test and live webhook endpoints separate where possible. Store secrets in a managed secret store or environment configuration, rotate them deliberately, and make the active environment visible in operational logs without printing the secret itself.

A valid signature proves that the payload was signed with the endpoint secret. It does not make the event safe to process twice, guarantee event ordering, or prove that your application has completed the associated business action. Those are separate responsibilities.

2. Acknowledge quickly, process safely

Your webhook route should do only the work required to accept the event and enqueue it for processing. Return a successful response promptly after signature validation and durable capture of the event.

A typical flow is:

  1. Read the raw request body.
  2. Verify the signature.
  3. Persist the event ID and payload, or place it into a durable queue.
  4. Return a 2xx response.
  5. Process the event asynchronously.

Avoid making a slow third-party API call, sending email, provisioning infrastructure, or running a large database transaction before acknowledging the request. Long-running handlers increase timeout risk and make retries harder to reason about.

The exact architecture can vary. A small SaaS may persist events in a database-backed inbox table. A larger system may use a queue or streaming platform. The important property is durability: once you acknowledge the request, the event must not exist only in application memory.

3. Make processing idempotent

Stripe may deliver the same event more than once. Your handler must produce the same business result whether an event is processed once, twice, or several times.

Use the Stripe event ID as a deduplication key. A common pattern is an inbox table with a unique constraint on stripe_event_id:

FieldPurpose
stripe_event_idPrevents the same event from being accepted twice
event_typeSupports routing and operational searches
account_idIdentifies the relevant customer or connected account
payloadPreserves the original event for inspection and replay
statusTracks pending, processing, succeeded, or failed work
attempt_countShows retry behavior
processed_atRecords successful completion
last_errorCaptures the latest failure context

The unique constraint must be enforced by the database, not only by an application-level lookup. Two workers can check for an event at the same time and both conclude that it is new unless the database arbitrates the race.

Deduplicating event IDs is necessary but not always sufficient. Your business operation should also be safe to repeat. For example, granting a feature should be an upsert or a state transition, not an unconditional insert. Sending a receipt should use a separate notification key so a retry does not send duplicate messages.

Use transactions around related state changes where practical. If a payment event updates an invoice and unlocks access, define what must be atomic and what can happen through a follow-up job.

4. Do not assume event ordering

Webhook events can arrive out of order. A subscription update may be delivered before your system has processed the subscription creation event. A payment failure may arrive after a later successful payment, depending on timing and retry behavior.

Design handlers around current resource state rather than assumptions about event sequence. When an event matters, retrieve the relevant Stripe object if your workflow requires authoritative current data. For example, a customer.subscription.updated event can trigger a fetch of the subscription before applying access rules.

You should still record every event, even when a later event appears to supersede it. The event history is useful for debugging, reconciliation, and identifying gaps in your state machine.

Define explicit transition rules. A subscription should not move from active to canceled merely because an old event was processed late. Include timestamps, version indicators, or resource state checks where the domain requires protection against stale updates.

5. Treat retries as normal

A failed response, timeout, infrastructure outage, or temporary database issue can cause Stripe to retry delivery. Your system should make retries boring.

Classify failures into two broad groups:

  • Transient failures: database unavailability, queue outage, rate limiting, or temporary upstream errors. These should remain retryable.
  • Permanent failures: invalid internal data, unsupported event types that require attention, or a bug that will fail identically on every attempt. These should be visible and routed to a dead-letter or review workflow.

Do not acknowledge an event simply to stop retries when your application has not durably captured it. If processing is asynchronous, acknowledge after durable enqueue or persistence—not after a best-effort in-memory handoff.

Track processing latency and attempt counts. A growing retry backlog is an operational incident even if the public webhook endpoint continues returning 2xx responses.

6. Build reconciliation into the system

Webhooks are useful notifications, but they should not be your only source of truth. A reconciliation job compares your internal payment and subscription records with Stripe's current resources and identifies differences.

Run reconciliation for areas such as:

  • Payments marked pending internally but succeeded in Stripe.
  • Charges or PaymentIntents present in Stripe but missing internally.
  • Refunds recorded in Stripe but absent from your application.
  • Subscriptions whose status or cancellation fields disagree.
  • Invoices that remain open internally after a final Stripe state.

Make the job scoped and repeatable. It might process accounts updated in the last day, inspect records with unusual states, or run a broader periodic comparison. Store discrepancy results with enough context for an operator to understand what happened.

Reconciliation is especially important after deployments, credential changes, queue outages, or migrations. It turns silent event loss into a detectable data-quality problem.

7. Model refunds and disputes explicitly

A successful payment is not the end of the payment lifecycle. Refunds, partial refunds, disputes, and chargebacks can change what your customer is entitled to receive and what your business needs to record.

Handle refund events as first-class state changes. Store the Stripe refund ID, payment reference, amount, currency, reason where available, and status. Partial refunds should not overwrite the original payment amount or be treated as a full reversal.

Decide what a refund means for access. The correct behavior depends on your product and terms, but it should be an explicit policy implemented in code. Avoid coupling access removal directly to a single event without considering pending or partial states.

Disputes often need a separate operational workflow, including evidence collection and notifications. Even if the first version only records the event and creates an internal task, do not discard it as an unsupported payment detail.

8. Add alerting that reflects business impact

Logging every event is not the same as observing the system. Create alerts around failures that affect revenue, access, or data integrity.

Useful signals include:

  • Signature verification failures above a normal baseline.
  • Events stuck in processing beyond an agreed threshold.
  • A growing dead-letter queue.
  • Repeated failures for one event type or account.
  • Reconciliation discrepancies.
  • Refund or dispute events that cannot be mapped to internal records.
  • Large gaps between Stripe resource activity and internal records.

Include event ID, event type, object ID, environment, account identifier, attempt number, and a correlation ID in structured logs. Redact payment details and personal data that operators do not need.

Alert on actionable conditions, not every isolated failure. A single transient error may be expected; a backlog that continues growing is not.

9. Provide safe replay tools

A replay tool is essential when a handler bug, deployment issue, or downstream outage affects production events. It should be designed as an operational control, not a shortcut around normal processing.

A safe replay workflow should:

  • Restrict access to authorized operators.
  • Preserve the original event payload and ID.
  • Show the previous attempts and errors.
  • Allow replay by event ID or a tightly scoped time range.
  • Use the same idempotent processing path as normal delivery.
  • Require confirmation for broad replays.
  • Record who initiated the replay and when.
  • Support dry runs for changes with significant side effects.

Avoid editing the original event to make it easier to process. If a repair requires a manual data correction, record that correction separately and keep the original evidence intact.

Production checklist

  • [ ] Verify the signature using the raw request body.
  • [ ] Use the correct endpoint secret for the environment.
  • [ ] Keep test and live configuration separate.
  • [ ] Persist or enqueue events durably before returning 2xx.
  • [ ] Enforce a unique constraint on Stripe event IDs.
  • [ ] Make business operations idempotent, not only event ingestion.
  • [ ] Handle duplicate and out-of-order events safely.
  • [ ] Separate transient failures from permanent failures.
  • [ ] Track attempts, latency, status, and last error.
  • [ ] Run periodic reconciliation against Stripe resources.
  • [ ] Model refunds, partial refunds, and disputes explicitly.
  • [ ] Add alerts for backlog, failed processing, and discrepancies.
  • [ ] Redact sensitive data from logs.
  • [ ] Provide authenticated, auditable replay controls.
  • [ ] Test retries, duplicates, stale events, malformed payloads, and recovery paths.

Practical test cases

Before launch, test more than the happy path. Send the same signed event twice and confirm that the second attempt creates no duplicate business record. Deliver a cancellation event before an update and verify that stale data cannot restore access incorrectly.

Simulate a database outage after the request arrives. Confirm that the endpoint does not acknowledge an event that was never durably captured. Then restore the dependency and verify that queued work resumes.

Test a partial refund and a refund retry. Confirm that the total refunded amount is correct and that customer access follows the documented product policy. Finally, test replay against a previously successful event and confirm that the replay is safe and auditable.

Final recommendation

A Stripe webhook endpoint is small code with large consequences. Keep the ingress path fast, preserve evidence, make processing repeatable, and build reconciliation and replay into the operating model from the start. If your payment flow needs a broader reliability review, see our web development services or services. For help evaluating a production implementation, contact CodeAustral.

If the note connects to your work

If the project needs a clearer technical read, send a brief.

Send a brief