← Back to articles

June 3, 2026

9 min read

Integrating Stripe in Production: Webhooks, Idempotency, and Payment Flows That Do Not Fail

How to build a Stripe payment system that handles retries, network failures, and duplicate events without compromising consistency.

Leer en espanol

Webhooks are the core of the payment system, not a detail

A common mistake when integrating Stripe is treating webhooks as a nice-to-have. In practice, they are the primary channel through which your system receives payment confirmations, failures, refunds, and state changes. Relying only on the API response when creating a PaymentIntent leaves the system blind to what Stripe confirms asynchronously.

Stripe can retry a webhook up to 64 times over 72 hours if your endpoint does not respond with a 2xx. That means the handler must be idempotent: processing the same event twice should produce no duplicate effects. Without that design from the beginning, debugging in production becomes expensive.

Verify the webhook signature before processing any event

Stripe signs every webhook with a secret you generate in the dashboard. Verifying that signature is the first step in the handler: if it is invalid, the request comes from an external source and must be rejected with a 400 before executing any business logic.

The verification uses the timestamp included in the header to prevent replay attacks. If the event is more than five minutes old, Stripe considers it expired. The official SDK handles all of this, but you must pass the raw body (unparsed) and the full header — not the already-deserialized JSON object.

Webhook handler with signature verification and immediate response.

import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST(req: Request) {
  const body = await req.text();
  const signature = req.headers.get('stripe-signature') ?? '';

  let event: Stripe.Event;

  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!,
    );
  } catch {
    return new Response('Invalid signature', { status: 400 });
  }

  // Respond 200 immediately; process in background
  processWebhookEvent(event).catch(console.error);

  return new Response('ok', { status: 200 });
}

Idempotency: the guarantee against double processing

Stripe can deliver the same event more than once. The `event.id` field is the unique identifier per event. Before processing, check whether that ID has already been handled and save it to the database upon completion. Without this check, a Stripe retry can credit a payment twice or send two confirmation emails.

The standard pattern is a `processed_webhook_events` table with `event.id` as the primary key. When an event arrives, attempt to insert that ID. If the insert fails due to a duplicate, the event was already processed — respond 200 and do nothing else. If the insert succeeds, run the business logic inside the same database transaction.

  • Save event.id with a unique index before processing.
  • Use a database transaction to atomize the insert and the side effect.
  • Never assume the same event type arrives only once.

Payment flows that tolerate network failures and retries

The safest flow for a one-time payment is: create the PaymentIntent on the server with an `idempotency_key` derived from the order ID, return the `client_secret` to the frontend, confirm the payment from the client using Stripe.js, and listen for `payment_intent.succeeded` in the webhook to update the order status.

If the user closes the browser after confirming but before the frontend processes the response, the webhook ensures the order is still updated. If the payment fails and the user retries, the same `idempotency_key` prevents creating a duplicate PaymentIntent. The two mechanisms are complementary, not alternatives.

  • Derive the idempotency_key from the order ID, not the session ID.
  • Update order status only from the webhook, never from the frontend alone.
  • Test the retry flow with Stripe test cards (4000002500003155).

Checklist before going live with Stripe

Before activating live mode, a short checklist prevents 90% of production problems: confirm the webhook secret is the production one (not the test one), verify the endpoint responds in under 30 seconds (or offloads processing to a background job), review that all relevant events are subscribed in the dashboard, and enable Stripe alerts for failed deliveries.

If the project has subscriptions, consider enabling the Stripe Billing customer portal. It reduces support overhead because users can update payment methods, cancel, and download invoices without involving the team.

More articles

Back to articles