Confirm what actually succeeded.
Check the correct Stripe account and live/test mode. Verify the relevant payment or invoice state server-side. For delayed payment methods, completing checkout may happen before funds are confirmed.
A successful browser redirect does not establish paid access. Stripe documents Checkout fulfillment and asynchronous payments; choose events and entitlement policy for your billing model.
Follow the event into your app.
Inspect delivery attempts for your registered endpoint. A failed response suggests a delivery or handler problem; a successful response only means your endpoint acknowledged the event.
Verify signatures using the raw request body. Durably accept work before acknowledging it, make processing idempotent and handle retries and out-of-order events. Follow Stripe’s webhook guidance.
Check which account was updated.
Trace a stable internal account ID from checkout creation to the entitlement record. A billing email may differ from the sign-in email, so matching only on email can update the wrong account or none at all.
Check the purchased price/product mapping, the database write result and whether subsequent reads use that same entitlement record.
Try the protected feature as that customer.
Inspect the server-side access decision and the account’s current entitlements. A stale session, cached account response or client-side plan badge can disagree with the authoritative record.
Verify the protected action, not just the badge. Do not fix this by trusting a success query parameter or allowing the browser to grant paid access.
The webhook returned 200. Why is access still locked?
A 200 response confirms receipt, not the entitlement update. Follow that event ID into your queue and worker. Check for skipped event types, swallowed exceptions, rolled-back writes and a job marked complete before the write finished.
Record the processing result separately from receipt. Before replaying an event, make sure processing is idempotent so a retry cannot grant duplicate credits or repeat another paid action.
The subscription is active. Why does the app still show Free?
Compare the Stripe customer and subscription IDs with the signed-in account, purchased product and server-side entitlement. Then test the protected action. If access works but the badge says Free, investigate the display or cache; if access is denied, investigate provisioning and the access check.
Choose a deliberate access policy for trials, renewals, payment delays and cancellations. A subscription status alone does not establish that your app saved the right entitlement. See Stripe’s subscription events and access guidance.
Reproduce the gap without taking a payment.
Our small local example deliberately accepts a payment result but skips the access update. Nothing throws; the customer still gets denied. The corrected path verifies the outcome.
curl -O https://app.braith.dev/examples/payment-access.mjs
node payment-access.mjsThe example uses an in-memory account and makes no network requests or charges. Read the full source.
Monitor the result after fixing the cause.
Use a shared task ID for verified payment, entitlement and feature-access evidence. Allow for expected delays. Missing browser evidence alone is inconclusive.
Get payment, retry and account-mismatch test cases before releasing your fix. Braith can monitor this payment-to-access task alongside your existing payment records and error tracker.