All articles

Stripe payments: three webhook pitfalls I didn't see coming

Originally published in French.

Payments aren't "integrating Stripe." They're everything that happens afterwards.

I built Navoy's payment service: the Stripe API, webhooks, reconciliation. Here are the three things I hadn't anticipated.

1. The same event arrives more than once.

A webhook isn't a notification: it's a delivery promise, kept as many times as it takes. Without idempotency, an order gets created twice and a customer gets charged twice. The rule: every event carries an identifier, and processing must be safe to replay with no side effects.

2. Arrival order isn't guaranteed.

I've seen a "payment confirmed" arrive after the "refund." If your logic assumes a sequence, it will eventually be wrong. You don't rebuild state from the order of the messages; you rebuild it from what each event asserts.

3. A webhook always gets lost eventually.

Network, deployment, timeout: one day, the event doesn't arrive. That's where reconciliation becomes essential — a job that compares your state with the provider's and catches up on the differences. It isn't over-engineering; it's the only way to sleep at night.

The nuance: on a product with three transactions a day, reconciliation can wait. On a product that really takes money, it's what saves you from having to explain to a customer where their money went.

Payments are still the only place in the code where a bug is counted in euros, not in tickets.

Do you handle webhooks in production? What surprised you?