The happy path is easy; the edge cases are the product
Charging a card once is simple. Running a subscription business is a long list of states that are not "paid and happy": the card that declines on renewal, the customer who upgrades mid cycle, the refund, the dispute, the trial that ends. The quality of a billing integration is entirely in how it handles those, and the place it handles them is the webhook.
Treat the webhook as the source of truth
Do not assume a charge succeeded because the checkout call returned. The authoritative signal is the event Stripe sends to your webhook, after the money actually moved. Provision access on invoice.paid, revoke it on customer.subscription.deleted, and warn on invoice.payment_failed. The user interface reflects subscription state; it does not decide it.
switch (event.type) {
case "invoice.paid":
await grantAccess(event.data.object.customer);
break;
case "invoice.payment_failed":
await flagPastDue(event.data.object.customer);
break;
case "customer.subscription.deleted":
await revokeAccess(event.data.object.customer);
break;
}
Verify and de duplicate the events
The webhook carries a signature; verify it against the raw body before trusting a single field (see the webhook verification guide). Stripe also retries, so process each event id once. A "payment failed" event handled twice is a customer locked out twice for one failure.
Failed renewals are a workflow, not an error
A card declining on renewal is normal, not exceptional. Stripe will retry on a schedule; your job is to keep access alive during the grace period, tell the customer, and only revoke when the retries are exhausted. Cutting someone off the instant the first renewal blips is how you lose a paying customer to a temporary bank hiccup.
Reconcile against your own ledger
Stripe knows what Stripe charged. Your database should know what you think you are owed. Reconciling the two on a schedule catches the gaps, a webhook you missed during a deploy, a subscription that drifted out of sync, before they become a support ticket or a revenue leak. When the product also takes mobile money, that reconciliation spans both rails (see the dual rail reconciliation guide).
