Skip to main content

Stripe

You can't code-review a webhook retry

My coding agent wrote a Stripe webhook handler that passed review and CI — and it still would have credited a customer twice on the first retried event. Reading code can't catch this class of bug. Running the failure can.

FetchSandbox EngineeringFetchSandbox Engineering

My coding agent wrote a Stripe webhook handler for me a few weeks ago. Signature verification, event type check, fulfillment call, clean 200 at the end. The diff read like something I would have written on a good day. I approved it in about ninety seconds.

Here is the shape of it:

app.post("/webhooks/stripe", async (req, res) => {
  const event = verifySignature(req);
  if (event.type === "charge.succeeded") {
    await creditCustomer(event.data.object);
  }
  res.sendStatus(200);
});

Every line of that is correct. And the first time Stripe delivers the same event twice — which it is allowed to do, and eventually will — that handler credits the customer twice. Or creates the order twice. Or bills twice, if the thing on the other end of that call creates an invoice.

Why review can't catch it

The bug is not on any line, so no amount of reading finds it. Each statement is defensible on its own. What is wrong is an assumption living between the lines: that every event arrives exactly once. Stripe promises at-least-once delivery. Retries, duplicates, and out-of-order arrival are documented, normal behavior.

Code review evaluates the diff against the traffic you can imagine. I imagined one clean charge.succeeded. So did the agent, because the quickstart code it learned from imagined one too.

CI passed for the same reason

My tests replayed the events I thought of. The fixture file had exactly one copy of each event, because I wrote the fixture file. Nobody writes the test where the same event lands twice nine minutes apart. The suite was my assumptions, checking my assumptions.

I spent three years on the developer platform of a very large payments company, and this exact bug got past good reviewers more times than I can count. It was never a talent problem. Reading is the wrong tool for this class of bug.

The review that actually works

What caught it was running the failure. Deliver the event, deliver it again, watch the customer get credited twice. Add the dedupe on the event ID, run it again, watch it hold. Ten minutes, and the review went from "looks right" to "proved right."

One catch: running the retry only helps if the check asserts the right thing. A verifier that merely confirms the count stopped growing will happily green a "fix" that grants nothing at all — I learned that one the hard way.

That loop — reproduce the failure, fix it, rerun, keep the receipt — is what FetchSandbox runs from your IDE over MCP. The sandbox runs the real charge lifecycle, fires the webhook events, and covers the failure scenarios your fixture file doesn't have. Your agent proves the handler before customers do it for you.

Receipts

Here's a real Stripe charge lifecycle run against the FetchSandbox sandbox, captured 2026-07-15:

POST /v1/charges                          → 200  id: CHG-0000-9C7S-0000  status: pending
POST /v1/charges/CHG-0000-9C7S-0000/capture → 200  status: succeeded
GET  /v1/charges/CHG-0000-9C7S-0000       → 200  status: succeeded
✓ 2/2 webhook events verified: charge.created, charge.succeeded

Full timeline: fetchsandbox.com/runs/fbc5c62531?flow=run_0ea5df17-ff47-47e4-9924-ccbf8423a9e6

The charge ID is the whole story. When charge.succeeded arrives a second time, CHG-0000-9C7S-0000 is the only thing telling your handler it already did this work.

If you'd rather judge this on real code than my word, our open-source playground has a webhook-dedupe bug planted in a Stripe app, waiting to be caught: github.com/fetchsandbox/playground.