Pulse
7 7IT Solutions
Payments

A Stripe Integration That Survives the Edge Cases

Lior Aharonov Lior Aharonov 7 min read Updated 2026-06-22

Taking a payment with Stripe looks deceptively simple. The demo is a button, a card field, and a success page, and money moves. Then real customers arrive, and the edges show up: a browser that closes before the success page loads, a card that needs an extra authentication step, a retry that almost charges twice, a dispute that needs answering, a report that does not match Stripe's. None of these appear in the tutorial, and all of them appear in production.

This guide is about those edges. It covers the patterns that make a Stripe integration trustworthy with real money, the most important being a single mindset shift: the browser is not the source of truth, the webhook is. The examples target a Node or Vercel app, and the principles carry to Revolut and other processors.

The mistake: trusting the redirect, not the webhook

The most common payment bug is fulfilling the order on the client. The customer pays, the page redirects to a success URL, and your code marks the order paid and ships it. The problem is that the redirect is not reliable: the customer can close the tab the instant the payment goes through, their connection can drop, or someone can simply visit the success URL directly. Tie fulfillment to that moment and you will both miss real orders and ship fake ones.

Fulfillment must be driven by a verified webhook from Stripe, which is the authoritative signal that the money actually moved.

// the only reliable trigger for fulfillment is the verified webhook
export async function POST(req: Request) {
  const event = stripe.webhooks.constructEvent(
    await req.text(),
    req.headers.get("stripe-signature")!,
    process.env.STRIPE_WHSEC!,
  );
  if (event.type === "payment_intent.succeeded") {
    await fulfillOnce(event.data.object.id);   // idempotent: safe if it fires twice
  }
  return new Response("ok");
}

Webhooks are the source of truth

Because fulfillment hangs off webhooks, treat that endpoint with the discipline any webhook deserves: verify the signature on the raw body, acknowledge fast, and make every handler idempotent so a redelivered event cannot fulfill an order twice. Stripe sends the same event more than once by design, so dedupe on the event id and key your fulfillment on the payment so it runs exactly once. There is a full breakdown of durable webhook handling in the related reading below, and all of it applies here.

Idempotency on the way in, too

The same duplicate-protection matters when you call Stripe. Network blips and retries can cause your "create a charge" request to be sent twice, and without protection that means two charges. Stripe supports an idempotency key on write requests precisely for this: send the same key on a retry and Stripe returns the original result instead of creating a second charge.

// retrying this request will not create a second charge
await stripe.paymentIntents.create(
  { amount, currency: "usd", customer },
  { idempotencyKey: `pi_${orderId}` },
);

Strong Customer Authentication and async results

Payments are not always synchronous. Many cards, and regulations like SCA in some markets, require an extra authentication step (3D Secure), so a payment can come back needing further action rather than instantly succeeding. The PaymentIntents flow is built for this: it can return a state that asks the customer to authenticate, and only later resolves to succeeded. Build for that path instead of assuming a charge clears in one call, and let the final webhook, not the initial response, tell you the money is really in.

Refunds, disputes, and failed payments

Money also flows backward, and a complete integration handles it. Listen for refund events and keep your records in step. Handle disputes, where a customer's bank claws back a charge, by capturing the webhook, flagging the order, and submitting evidence within the window. And expect failed and retried payments, especially for subscriptions, where a card declines and recovers later. Each of these is just another event type on the same webhook, so the same idempotent, verified handling covers them.

Reconcile with Stripe

Even with solid webhooks, build a periodic reconciliation pass. Treat Stripe as the financial ledger and, on a schedule, compare its record of payments, refunds, and payouts against your own database, then resolve any differences. This catches the rare missed event, confirms your books match the processor's, and is your recovery plan after any incident. With money involved, "we think it matched" is not good enough, reconciliation makes it certain.

Test the edges, not just the happy path

Stripe gives you the tools to rehearse the hard cases, so use them. Work in test mode with the test cards that simulate declines, authentication challenges, and disputes, and use the Stripe CLI to forward and replay webhooks against your local environment so you can prove your handler is idempotent and your fulfillment fires correctly. Testing only a successful Visa charge is how the edges reach production unnoticed.

A Stripe integration checklist

  • Fulfill on the verified webhook, never on the client redirect or success page.
  • Verify the signature on the raw body, acknowledge fast, and make handlers idempotent.
  • Send an idempotency key on write calls so retries never double-charge.
  • Handle the authentication (3D Secure) path, the final webhook confirms success.
  • Process refund, dispute, and failed-payment events, and answer disputes in time.
  • Reconcile against Stripe on a schedule and treat it as the ledger.
  • Test declines, authentication, and disputes with test cards and the Stripe CLI.

FAQ

Should I fulfill the order on the success page or the webhook?

On the webhook, always. The redirect to a success page is unreliable, the customer can close the tab before it loads, lose connection, or visit the URL directly, so fulfilling there both misses real orders and ships fake ones. A verified payment_intent.succeeded webhook is the authoritative signal that the money actually moved, so trigger fulfillment from it and keep that handler idempotent so a redelivered event cannot fulfill twice.

How do I stop Stripe from charging a customer twice?

Guard both directions. When you create a charge, send an idempotency key so a retried request returns the original result instead of a second charge. When you process webhooks, dedupe on the event id and key fulfillment on the payment so a redelivered event has no extra effect. Duplicates are normal in distributed systems, so the fix is making both your calls to Stripe and your handling of its events safe to repeat.

What is Strong Customer Authentication and why does it matter?

SCA is a requirement in some markets for an extra authentication step on card payments, commonly 3D Secure, and it means a payment is not always instant. With PaymentIntents the charge can return a state that asks the customer to authenticate before it resolves to succeeded. If your code assumes every charge clears synchronously, those payments break, so build for the authentication path and let the final webhook confirm the money is really in.

How do I handle refunds and disputes in code?

They arrive as webhook events, so handle them the same way as payments. On a refund event, update your records to match. On a dispute, where the customer's bank reverses a charge, capture the event, flag the order, and submit your evidence within Stripe's window. Keeping these flows in sync through verified, idempotent webhook handling means your system reflects reality even when money moves backward.

Do I really need to reconcile with Stripe if my webhooks work?

Yes. Webhooks keep you in sync in real time, but they can still be missed during an outage or a bad deploy, and with money you want certainty, not probability. A scheduled reconciliation pass that compares Stripe's record of payments, refunds, and payouts against your own catches anything that slipped through, confirms your books match the processor, and gives you a clean recovery path after an incident.

If you are building checkout, subscriptions, or payouts and want them to hold up once real money is moving, tell me what you are charging for and I will give you a straight technical read on the integration.

Want a hand applying this?

Tell me where your business is stuck and I will give you a straight, useful read, no pitch.

Go deeper

Payments

Embedding Stripe in a Headless WooCommerce and Next.js Store: What Actually Matters

A practical, field-tested guide to wiring Stripe into a headless WooCommerce and Next.js storefront, covering keys and authorization, the UI layer, tokens and PaymentMethods, the backend PaymentIntent, authorize-then-capture, and the webhook details that quietly break in production.

Read →
Payments

Revolut for WooCommerce: Why I Built My Own Integration

The off-the-shelf Revolut gateway got a WooCommerce store most of the way there, but the wallets were unreliable, the checkout jumped, and the card fields were so bare they cost customer trust. Here is why I rebuilt the integration around Revolut's secure card field, and how owning the UI fixed it without touching PCI scope.

Read →
Payments

Revolut Merchant API on Vercel: Wallets, 3D Secure, and the Sync vs Async Trap

A field-tested guide to the Revolut Merchant API on Vercel, covering the order-first flow, Apple Pay and Google Pay, how 3D Secure is handled, the sync versus async reality that catches teams out, and the webhook signature details that must be exactly right on a serverless function.

Read →
Automation

API Integrations: Why Connecting Your Stack Beats Copy-Paste

Why manually moving data between your business tools is costing more than you think, and how API integrations make your software work as one system.

Read →
Shopify

The Customer Portal Shopify Doesn't Give You (Reorders, Invoices, Approvals)

Fast reorders, invoice and PO access, saved carts, buyer approvals, and rich order history are where Shopify's native accounts stop short. Here is what a real customer portal looks like, often headless, and how we build it in phases you can trust.

Read →
eCommerce

When You Need a Custom WooCommerce Plugin (and When You Don't)

How to tell whether your WooCommerce store needs a custom plugin or whether an existing one will do, and how custom code can cut plugin bloat.

Read →