H Product Studio
Discuss a project
application maintenance · 26 September 2026 · 9 min

Stripe Payment Succeeded, but SaaS Access Is Wrong: A Billing Test Checklist

A practical acceptance checklist for Stripe subscriptions: payment, invoices, subscription status, app access, durable webhooks and reconciliation.

Author
Anna Hartung
  • stripe
  • saas
  • billing
  • webhooks
  • application-maintenance

A customer pays, sees your success page, and lands in a dashboard that still says "Free plan". Another customer's card failed weeks ago and they still have every paid feature. Neither bug appears in a happy-path demo. Both appear in support tickets and quiet revenue leakage.

The usual cause: the app treats "Stripe said it succeeded" as one fact, when it is four.

Four records that are easy to confuse

Payment. Money moved, or is moving, through a PaymentIntent. It can succeed, fail, wait for customer authentication, or stay in processing for days with bank debits.

Invoice. What is owed for a period. A card charge can pay it, but so can credit balance, a zero amount, or marking it paid outside Stripe.

Subscription. Stripe's record of the commercial relationship: status, subscription items and their billing periods, scheduled cancellation.

Entitlement. What your application lets this account do right now. It lives in your database. Stripe informs it, and offers an optional Entitlements feature, but the rules for trials, grace periods, downgrades and refunds are your business decisions.

No single Stripe event proves access. Derive access from current Stripe state plus your written policy, and treat events as signals to recompute it.

What Stripe's documentation says

A success page is not evidence of payment. Stripe's fulfillment guide warns that customers may pay and never load your landing page, and requires webhooks for subscriptions and delayed payment methods. It also notes fulfillment may run several times, even concurrently, for one Checkout Session.

invoice.paid does not mean a card was charged. The invoicing overview lists paid invoices with no successful PaymentIntent: trials and free subscriptions, amounts covered by credit balance or below the minimum charge, and invoices marked paid out of band.

Status alone does not prove collection. Stripe's subscription lifecycle documentation explains that some asynchronous payment methods can activate a subscription before payment settles; the subscription can remain active even if that payment later fails. Trial-end behavior and the result of exhausted retries also depend on your configuration. Check the relevant invoice and payment outcome, not just active.

Delivery is unordered and repeatable. The webhooks guide says events can arrive out of order and more than once, so a local sort of notifications is not a substitute for reconciliation. The subscription webhook guide also documents how a failed response to invoice.created can delay automatic invoice finalization.

Write the access policy first

A test needs an expected result, and "whatever Stripe does" is not one. Decide:

  • What a trial unlocks, and what happens if it ends without a payment method.
  • Grace period length and feature level while past_due.
  • Whether upgrades unlock immediately or after the prorated amount is paid.
  • Whether downgrades apply now or at period end.
  • Whether refunds and disputes affect access, and who decides.

The scenarios below use an example policy. Replace the expected access decisions with yours.

Twelve acceptance scenarios

1. First paid purchase

  • Money and subscription: Charge succeeds; active.
  • Expected access: Paid plan, granted once.
  • Relevant notifications: checkout.session.completed, invoice.paid.
  • Acceptance evidence: One entitlement; latest invoice paid.

2. 3DS required, abandoned

  • Money and subscription: None; incomplete, then incomplete_expired.
  • Expected access: No paid access; "finish payment" prompt.
  • Relevant notifications: invoice.payment_action_required.
  • Acceptance evidence: Success-page visit grants nothing.

3. Delayed payment method

  • Money and subscription: Processing, then success or failure; Can already be active before settlement.
  • Expected access: Pending or explicitly provisional access; apply outcome policy.
  • Relevant notifications: checkout.session.async_payment_succeeded, checkout.session.async_payment_failed.
  • Acceptance evidence: active alone never proves collection.

4. Trial starts

  • Money and subscription: None ($0 invoice); trialing.
  • Expected access: Trial tier.
  • Relevant notifications: customer.subscription.created, invoice.paid.
  • Acceptance evidence: $0 invoice doesn't mark account as paying.

5. Trial ends

  • Money and subscription: First charge, or none; Depends on payment and missing-payment-method settings; may be active, past_due, paused or canceled.
  • Expected access: Paid, grace, or trial-ended screen.
  • Relevant notifications: customer.subscription.trial_will_end, customer.subscription.updated, customer.subscription.deleted.
  • Acceptance evidence: Test the actual configured outcome.

6. Renewal succeeds

  • Money and subscription: Charged; active, renewed item period.
  • Expected access: Extended.
  • Relevant notifications: invoice.paid.
  • Acceptance evidence: Relevant item period matches Stripe.

7. Renewal fails

  • Money and subscription: None yet; past_due.
  • Expected access: Access plus banner; grace deadline stored.
  • Relevant notifications: invoice.payment_failed.
  • Acceptance evidence: Not cut at first failure.

8. Retries exhausted

  • Money and subscription: None; canceled, unpaid or still past_due, depending on settings.
  • Expected access: Revoke or restrict under the explicit terminal/grace policy.
  • Relevant notifications: customer.subscription.deleted, customer.subscription.updated.
  • Acceptance evidence: A retained past_due status cannot silently grant access forever.

9. Mid-cycle upgrade

  • Money and subscription: Per proration settings; may bill next cycle; active, new items.
  • Expected access: Higher tier now or after payment.
  • Relevant notifications: customer.subscription.updated.
  • Acceptance evidence: Tier read from subscription items.

10. Downgrade

  • Money and subscription: Credit may go to balance; New items or scheduled change.
  • Expected access: Lower tier now or at period end.
  • Relevant notifications: customer.subscription.updated, subscription_schedule.*.
  • Acceptance evidence: Limits enforced at right moment.

11. Cancel at period end

  • Money and subscription: No new charge; active with cancel_at_period_end, later deleted.
  • Expected access: Full access until period end.
  • Relevant notifications: customer.subscription.updated, then customer.subscription.deleted.
  • Acceptance evidence: No early revocation.

12. Refund vs cancellation

  • Money and subscription: Returned, or not; A refund doesn't cancel; cancelling doesn't refund by default.
  • Expected access: Per policy, applied deliberately.
  • Relevant notifications: charge.refunded, customer.subscription.deleted.
  • Acceptance evidence: Refund never changes access silently.

Webhooks: accept durably, then act

  1. Verify on the server. Use Stripe's library with the raw body, the Stripe-Signature header and that endpoint's secret. Keep the library's timestamp checks. Verify the Stripe account and test/live mode, then map the customer to the correct application tenant using a server-owned relationship, not a supplied email address. Verification failures get a 4xx and no side effects.
  2. Persist before acknowledging. Store the verified event durably, with a unique constraint on event ID, then return 2xx. If storage fails, return an error so Stripe retries. A fast response means storing now and processing later, not acknowledging and hoping.
  3. Process from a queue. A worker re-fetches the current subscription and invoice from Stripe and recomputes the entitlement under a versioned policy. Serialize work for the same subscription, or use locking and conditional writes, so a slower worker cannot overwrite a newer decision. Apply the entitlement and processing record atomically; a fresh fetch alone does not prevent concurrent-worker races.
  4. Deduplicate the business operation. Event-ID dedupe stops redelivery, but the same outcome can arrive via Checkout, invoice.paid, the success page and a manual resend. Key grants on what is granted, such as "subscription X, invoice Y".
  5. Make side effects idempotent. Emails, credits, seats and Stripe API writes need keys derived from the business operation, so a retried worker can't double-grant or double-bill.

Test in a sandbox, never with real money

  • Use a dedicated Stripe sandbox with its own keys and webhook secret, plus a separate app database with obviously fake users. Nothing in this setup should reach live keys or real customers.
  • Drive real test subscriptions. Stripe notes that CLI-triggered events carry fake data unrelated to your subscriptions.
  • Use Stripe's published test payment methods for successful collection, failing renewals and customer authentication. Never use real card details in the sandbox.
  • Use test clocks for trials, renewals and prorations. Check the documented restrictions for the scenario and payment method; a clock does not simulate every real-world collection or delivery behavior.
  • Break things on purpose: resend events, stop the worker and let events pile up, return 500 and watch retries.

A green run proves your logic against Stripe's state transitions. It doesn't prove production delivery timing or behaviour at renewal peaks.

Reconcile Stripe against your app

Events will eventually be missed: a bad deploy, a rotated secret, a stuck worker. A scheduled job using a read-only restricted key lists subscriptions and compares status, tier, relevant subscription-item billing periods and scheduled cancellation with your entitlements. Report four buckets: qualifying paid periods without access, unexplained access, wrong tier and wrong period. Compare against the policy: legitimate trials, grace periods, credits and approved out-of-band payments are not automatically errors. Start report-only. Any repair path needs separate, scoped write authority and an auditable policy decision; a read-only audit key must not silently become a correction credential.

Route ambiguous cases to a person: out-of-band payments, partial refunds, open disputes, customers with two subscriptions, or a success-page screenshot while Stripe shows incomplete. Give them an owner, a runbook, and an audit log of overrides with reason and expiry.

Scope

This is our recommended acceptance framework, built from Stripe's public documentation. It is not an official Stripe checklist, and it is not legal or tax advice on refunds or invoicing. For the wider production review, see our AI-built app production audit.

If your product is live and billing, webhooks and the surrounding code need to stay correct as Stripe, pricing and features change, that is the work of our application maintenance service.

Keep reading

More from the engineering stream.

  1. Post · 001
    25 Aug 2026

    Is Your AI-Built App Production-Ready? A 12-Gate Audit

    Audit an AI-built application against twelve evidence gates for ownership, authorization, data, dependencies, recovery and release—without mistaking a working demo or green scanner for production readiness.

    Read post
  2. Post · 002
    25 Aug 2026

    Your Developer Disappeared: The First 72 Hours

    A safe recovery runbook for a live product when its developer or agency is unreachable: preserve evidence, regain authorized control and avoid changes that turn an access problem into an outage.

    Read post
  3. Post · 003
    25 Aug 2026

    Technical Due Diligence for a Small SaaS: Evidence to Verify Before You Buy

    A practical evidence-led technical due diligence guide for small SaaS acquisitions—covering source, production, data, access, suppliers, AI risk and operating continuity.

    Read post
All posts →
Get started ·  011

Let’s build what’s next
and keep it moving.

From an inherited codebase or a new product idea to long-term delivery — we define, build, modernize and operate software your team can truly own.

Studio
H Product Studio
Takeovers · Modernization · Long-term development · New builds
Contact
hello@buildwithh.com
Delivery
Remote-first studio
Working across U.S. time zones