Blog
Notes on Stripe billing drift, webhook reliability, and reconciliation.
September 27, 2026
Stripe sent the same webhook event twice: causes and how to make handlers idempotent
Stripe guarantees at-least-once delivery, not exactly-once. Here is why the same event.id can arrive two or three times, and the idempotency pattern that stops it from granting access, sending email, or charging twice.
September 25, 2026
Stripe webhook signature verification failed: 6 causes and how to fix each
Getting "No signatures found matching the expected signature for payload"? The six real causes — parsed body, wrong whsec_ secret, test vs live mode, invisible characters, timestamp tolerance, and proxies — with working Next.js and Express fixes.
September 21, 2026
A customer canceled, and Stripe still says the subscription is active
When a customer cancels with cancel_at_period_end, the subscription stays active until the period runs out — and the event that finally ends it fires weeks after the click. Here is what each field means and where access logic goes wrong.
September 18, 2026
How to reconcile Stripe subscriptions with your database (a script you can run today)
A copy-paste Node script that lists every Stripe subscription, compares it to who your app grants access to, and prints three lists: access without payment, payment without access, and paying customers your database has never heard of.
September 16, 2026
The Stripe subscription lifecycle, mapped
Every Stripe subscription status, the event that leads to it, and the access-control decision most integrations get wrong at that exact step — an interactive map connecting incomplete, trialing, active, past_due, and canceled.
September 11, 2026
You refunded a customer three months ago. They still have access.
charge.refunded and charge.dispute.created don’t touch the subscription object at all — refunding a payment and revoking access are two separate actions, and most integrations only ever wired up one of them.
September 10, 2026
A customer downgraded three months ago and still has every Pro feature
customer.subscription.updated fires on every plan change, but most access logic never actually reads it — it just checks "is this subscription active," which stays true straight through an upgrade or downgrade.
September 8, 2026
How much is silent billing drift actually costing you?
A free interactive calculator: enter your subscription count, average value, and an estimated drift rate to see what silent Stripe billing drift could be costing you this year.
September 8, 2026
Your free trial ended three weeks ago. This user still has full access.
A trial that fails to convert doesn’t always look like a failure — a card decline enters a retry window, and a trial with no card at all just quietly expires with no event that’s easy to notice. Three ways a trial ends, and why checking status beats checking whether a subscription exists.
September 7, 2026
Why your Stripe MRR is wrong — and Stripe will never tell you
Annual plans spiking into one month, past-due subscriptions still counted as revenue, coupons distorting the number in either direction, FX rates shifting historical charts — five specific, checkable reasons your MRR dashboard doesn’t match what you actually collected.
September 5, 2026
Your app has two Stripe customers for the same user, and neither of them notices
A retried signup request, a guest checkout followed by account creation, a webhook that races your onboarding flow — any of these can leave one user with two Stripe customer objects, and your app quietly checking the wrong one.
September 3, 2026
A customer just paid you and is staring at a paywall
Checkout redirects the customer back before your webhook has necessarily arrived — and if you granted access purely on the redirect, or purely on a webhook that silently isn’t configured in live mode, the gap between "paid" and "unlocked" is where angry support tickets come from.
September 3, 2026
Ghost customers: when a Stripe subscription goes bad without ever firing subscription.deleted
If your access logic only listens for customer.subscription.deleted, a subscription can sit unpaid for weeks — full access, zero revenue — and you’ll never get the event that was supposed to tell you.
September 3, 2026
Your Stripe webhook returns 200. That doesn’t mean it worked.
A 200 response only tells Stripe the request was received — not that your handler actually did anything with it. Here’s the failure mode, why it survives for months, and how to catch it.