What breaks in payments after the checkout works
You tested the checkout yourself, with your own card, by hand, and it worked. That is usually why it is the one part that is fine. Everything else in payments happens without you and without a single click: renewals, failed charges, cancellations, refunds, disputes. Stripe tells your application about those events through a webhook, and if nothing is listening, your application never learns about any of them.
The gap does not show up straight away, because the first month looks correct. People pay, access is granted. The drift accumulates later: one card fails to renew, one subscription ends, one customer takes their money back. In your database all three are still paying customers.
The mechanism is worth being precise about. A payment is not one event, it is a chain stretched over time. A successful checkout only tells you the user reached the end of the form. After that comes the charge, then monthly renewals, and any of them can fail. Stripe sends each such event to your endpoint, waits for a response, and retries for a while if none comes. Then it stops trying.
First check: whether an endpoint exists at all. Open the developers section of your Stripe dashboard and find the list of webhook endpoints. If it is empty, your application knows about nothing except what happens in the user's browser. If an endpoint is there, look at its delivery errors: 4xx and 5xx responses mean events were being sent and your server was not accepting them.
Second check: test mode against live mode. In Stripe these are two separate universes with separate keys and separate webhook lists. A very common story is an endpoint configured in test mode, all green, with nothing at all configured in live. Flip the mode switch in the dashboard and look at the list again.
Third check: reconcile the two lists. Export the active subscriptions from Stripe and compare them against the users your database marks as paying. Both sides of the mismatch matter. The people who pay and have no access will write to you. The people who have access and do not pay never will.
Fourth check: what happens when a charge fails. Stripe publishes test cards that attach normally and then get declined when money is actually taken. Run that scenario in test mode and watch what your app does. In most vibe-coded apps it does nothing at all: the user keeps full access, and you find out from a report at the end of the quarter.
Fifth check: cancellations and refunds. Cancel a test subscription from the Stripe dashboard directly, not through your own interface. Access should disappear. If it does not, your app is treating its own database as the source of truth, and the source of truth is always Stripe.
Separately, confirm that your endpoint verifies the signature on incoming requests. A webhook without signature verification is a form through which a stranger can tell your application about a payment that never happened. And confirm that receiving the same event twice does not grant access twice: Stripe may deliver an event more than once, and that is normal behaviour rather than a fault.
The repair runs in the opposite order to the way this broke. First an endpoint that accepts events and answers quickly. Then signature verification and protection against repeats. Then handling for failed charges and cancellations. Reconciling the accumulated drift comes last, because until the rest is in place it is pointless: it will come apart again within a week.
Working through the old drift is manual, one person at a time. Some of them left long ago, some are paying for something they do not have and need an email from you. That part is not engineering work, and there is usually more of it than there is of the fix.