Stripe took the money and the board never moved
A payment cleared and the position was never granted. One mismatched signing secret, and the refund safeguard structurally could not catch it.
26 August 2026 · 6 min read
If you searched forStripe webhook returns 400 Invalid signature and the payment never applies
What happened
A payment for the top spot on a leaderboard cleared. Stripe shows it as succeeded. The leaderboard never changed.
From inside the product everything looked fine. Every page rendered. No error appeared anywhere a person would see. The only trace was in Stripe: the confirmation event sat undelivered, retrying.
The symptom
Stripe's event delivery list showed repeated failures against our endpoint, every one a 400. Our own logs, if you knew where to look, had the matching line.
What the endpoint returned to Stripe
POST /api/stripe/webhook
400 {"error":"Invalid signature"}The cause
Stripe signs every event it sends with a secret unique to that endpoint. The receiving code verifies that signature before it trusts a single byte of the body — which is the correct design, because a forged event on a paid product hands out the thing money buys, for free.
The secret stored in our hosting environment was not the secret belonging to that endpoint. So every confirmation was refused as unsigned. Perfectly working code, correctly rejecting perfectly genuine messages.
Why the refund safeguard did not save us
The product already had a safeguard: if the spot is taken while somebody is paying, the money is refunded. It never fired.
That safeguard lives inside settlement. It runs after the confirmation is verified, decides the position cannot be granted, and refunds. Here the confirmation was never verified, so settlement never ran, so nothing decided anything. The money simply sat there.
This is the part worth taking away: a refund safeguard protects against a payment that cannot be honoured. It does not protect against a payment nobody ever hears about.
How to check yours in one command
The honest test is not whether the endpoint exists. It is whether an event has ever actually landed. Everything else is a guess.
Ask Stripe whether anything was ever delivered
const events = await stripe.events.list({ limit: 30 })
const completed = events.data.filter(e => e.type === "checkout.session.completed")
const stuck = completed.filter(e => e.pending_webhooks > 0)
// stuck.length === completed.length → signing-secret mismatch.
// completed.length === 0 → never proven. Not the same as fine.The order that prevents it
The webhook endpoint has to exist before a single payment is possible. Not once checkout works, not when you get around to testing.
We created ours a day after the checkout worked. Every payment made in that window had nowhere to deliver its confirmation, and all of them are still unsettled. Nobody noticed for a day, because there is nothing to notice — the product looks completely healthy while it silently takes payments for nothing.
Every one of these is running on the board. Components worth copying are in the kit.