A rejected webhook looks like nothing happened
A signature check that fails returns an error and stops there. The payment already happened — only the announcement of it was lost, and nothing retries a message that was refused as invalid.
Independent reconciliation for Stripe webhooks
Every payment recorded once, and only once. What matters is the payment that was never recorded at all — and the fact that nothing else reported it. Inventing a row to make the books look complete is the one thing this was built to prevent.
The blind spot
Most billing monitoring can only report what arrived. The whole failure mode here is a payment that never arrived at all — which every ordinary log, alert and dashboard is structurally unable to see.
A signature check that fails returns an error and stops there. The payment already happened — only the announcement of it was lost, and nothing retries a message that was refused as invalid.
Every log and every dashboard can be genuinely healthy while a customer has paid for something the records say they never bought. Green is not the same claim as correct.
The only way to find a payment that was never recorded is to ask the sender what it actually sent and compare. Almost nothing does this, because there is no error to prompt it.
How it works
Recording and reconciling are kept apart on purpose. The write path never asks Stripe anything; the reconciliation job never writes to the ledger.
A verified webhook is written to the ledger. The database's own key decides what counts as a duplicate — the code never has to guess.
AWS Lambda
DynamoDB
A webhook that fails its signature check is rejected outright. That is correct behaviour for a forged request — and the exact reason a genuine but misconfigured one can go missing quietly.
Stripe Webhooks
On a schedule, a job asks Stripe what it actually sent in a window — the only way to find something that never triggered an error here.
src/reconcile.py
Anything Stripe has that the ledger doesn't is reported as missing. It is never written in after the fact to make the books look complete.
pytest + moto
The incident
A real failure from this system's own first hour. Every line of output below is verbatim.
The cause was mundane: a signing secret applied a few minutes after the webhook was switched on. Ordinary setup, ordinary timing, one lost payment.
This ran against Stripe in test mode. The failure works exactly the same way in production — the difference is whose money it is.
Fetched from the real Lambda on load. A failure here must not look like a healthy empty state, so it is reported.
What you get
Written to be read by whoever is deciding whether the books can be trusted, not only by whoever built the pipeline.
Every verified payment recorded once, deduplicated by the database's own key rather than application logic that could drift.
Answers: what did we actually receive?
A scheduled check that asks Stripe directly, rather than trusting that silence means everything arrived.
Answers: does our record match theirs?
A missing payment is reported by name, not silently written in to make the total look complete.
Answers: what did we miss, specifically?
Recorded and duplicate-suppressed counts, fetched live rather than quoted from a report that could be stale.
Answers: what is happening right now?
This ran against Stripe in test mode. The reconciliation works exactly the same way in production — the difference is whose money it is.