If your Stripe webhook returns 200 OK but subscription access does not update, the bug is usually not in Stripe. The bug is in the bridge between Stripe events and your app database.
This is common in AI-built SaaS apps made with tools like Cursor, Lovable, Bolt, Replit, and v0. The checkout flow works. The customer pays. Stripe marks the webhook as delivered. But your app never updates the entitlement record that actually controls paid access.
Fast check: open your webhook handler and find the code path for invoice.payment_failed, customer.subscription.updated, and customer.subscription.deleted. If those branches only log the event and return success, your billing state can drift silently.
Why 200 OK is not enough
Stripe documents that a webhook endpoint should return a successful 2xx response quickly, especially before slow or complex work that could time out. That advice is correct for delivery reliability, but it creates a trap: your endpoint can acknowledge the event while the business logic behind it does nothing useful.
Stripe also shows event delivery status in the dashboard. A 200 means the endpoint received and acknowledged the request. It does not mean your code changed the subscription table, revoked access, refreshed the user session, or handled a failed payment correctly.
The silent billing bug pattern
A 2026 Indie Hackers post described three Cursor-built SaaS products with the same issue: the webhook caught Stripe events, returned 200, and exited without revoking access, updating subscriptions, or touching the database.
A DEV Community write-up summarized the detection problem well: founders often see normal MRR in Stripe, but they do not automatically see the intersection of failed Stripe payments and users who still have active access in the app database.
That is why this bug is dangerous. It does not always break checkout. It does not always throw an error. It leaks revenue and trust quietly.
What to check first
1. Confirm the webhook is hitting production
In Stripe Workbench, open your webhook endpoint and check recent deliveries. Make sure the URL points to your production domain, not a preview URL, local tunnel, staging app, or deleted deployment.
2. Verify the signature with the raw request body
Stripe requires the raw request body for signature verification. If your framework parses or modifies the body before verification, signature checks can fail or get bypassed incorrectly. In Node/Express, Stripe's example uses raw body middleware for the webhook route.
3. Trace the database write
Do not stop at "the webhook fired." Follow the event all the way to the row that gates paid access. Find the exact table and field your app uses for access, such as subscription_status, plan, role, has_paid_access, current_period_end, or an entitlements table.
4. Check failed payment behavior
Stripe sends invoice.payment_failed when an invoice payment fails. Your app should decide what happens next: notify the customer, mark the subscription as past due, start a grace period, or eventually gate access based on your billing policy.
5. Check cancellation behavior
If customer.subscription.deleted does not update your database, canceled users can keep Pro access. Stripe's subscription docs state that access should be revoked when a subscription becomes canceled or unpaid.
6. Add idempotency for webhook events
Stripe can deliver duplicate events. Store processed Stripe event IDs and skip already processed events. Stripe recommends guarding against duplicate event receipts by logging processed event IDs, and its guide to processing undelivered events uses explicit processing markers for this reason.
7. Do not depend on event order
Stripe does not guarantee event delivery order. If your code assumes customer.subscription.created always arrives before invoice.paid, it can break in production. For critical paths, retrieve the latest subscription, invoice, or customer object from Stripe before writing final state.
Minimum events a subscription SaaS should handle
checkout.session.completedfor first checkout completion.invoice.paidor equivalent successful recurring payment event.invoice.payment_failedfor failed renewal attempts.customer.subscription.updatedfor status, plan, trial, or period changes.customer.subscription.deletedfor cancellation and ended subscriptions.
What good looks like
A safer Stripe-to-access flow has one source of truth for product access. Usually this is a backend table keyed by your internal user or account ID and linked to the Stripe customer and subscription IDs.
- The webhook verifies Stripe signatures.
- The webhook stores or checks the Stripe event ID before processing.
- The handler maps
stripe_customer_idorsubscription_idto the correct internal account. - Subscription lifecycle events update the same access source of truth.
- The frontend reads access from the backend, not from a checkout redirect.
- A reconciliation job compares Stripe subscription state with database access state.
- Alerts fire when Stripe and your database disagree.
Quick test checklist
- Make a test checkout and confirm the correct user gets access.
- Trigger or simulate a failed invoice and confirm the database state changes.
- Cancel a subscription and confirm access is removed at the right time.
- Send the same event twice and confirm it is not processed twice.
- Change the plan and confirm the app shows the new entitlement.
- Compare active Stripe subscriptions against active paid users in your database.
When to ask for help
Ask for a Stripe webhook audit if payments are live, users have paid access, or you cannot clearly explain which database row controls access. This is especially important for AI-built apps where the visible checkout path was generated quickly and the failure paths were never tested.
Sources used
- Stripe docs: receive webhook events
- Stripe docs: subscription webhook events
- Stripe docs: process undelivered webhook events
- Indie Hackers: silent billing bug in Cursor-built SaaS
- DEV Community: silent billing bug in AI-generated SaaS
Want a senior developer to check this before launch?
I review Stripe webhooks, Supabase access, exposed keys, rate limits, and silent workflow failures in AI-built SaaS apps.
Get a Launch Audit See sample report