Cancelled customers who keep access: the Stripe webhook bug nobody reports
If you take subscriptions through Stripe, you keep two copies of "who is paying": Stripe's, and a column in your own database ( subscribed , is_pro , plan ...) that your app actually checks. A webhook keeps them in sync. When the webhook misses an event, the two copies disagree. It fails in two directions: Paid, no access. Someone pays and stays locked out. They email you within the hour.…
Subscriptions on Stripe require two copies of information - one from Stripe and another in your own database. A webhook ensures these copies stay in sync. However, if the webhook misses an event, the two copies can become out of sync. This can lead to two scenarios: someone pays but doesn't get access, or someone cancels but still retains access.
The latter is the issue at hand, as it's the problem that's often overlooked. The issue stems from three main reasons. Firstly, the webhook only handles the "checkout.session.completed" event, granting access without revoking it. Secondly, when a customer cancels at period end, Stripe sends a "customer.subscription.updated" event with "cancel_at_period_end" set to true.
This payment isn't revoked until the period ends, which can lead to paying customers being locked out early. Thirdly, if the checkout success page sets "subscribed" to true, anyone who opens that URL can access the service, regardless of payment status. This can't be rectified by a cancellation webhook. The webhook may also fail entirely due to several reasons.
It could be pointing to the wrong route after a deploy, have a signature verification failure, or throw an error in the handler. Once the issue is identified, it's crucial to replay the failed events from the dashboard immediately, as Stripe retries failed deliveries for about three days before giving up. If the endpoint was down over a long weekend, for example, it could lose events permanently.
Additionally, refunds and disputes don't automatically cancel the subscription. If access should be ended, the webhook should be programmed to handle "charge.refunded" and "charge.dispute.created" events. The first step is to identify the affected customers. In Stripe, filter "Billing → Subscriptions" by "Canceled" and cross-reference these customers in your access table.
Any customer still having access is affected. The same principle applies to checking active subscribers. To automate this process, you can create a tool like "Entitled" that regularly compares Stripe and your own database, listing any discrepancies and suggesting fixes. A free scan is available. Regularly checking your webhook's event list is also recommended.
Written by urgent.news from Dev.to's reporting — not their text. Machine-written — may contain errors; check the original before relying on it.