---
name: stripe_billing_integration
description: >
  Implement Stripe subscription billing end to end: Checkout, the subscription
  lifecycle webhooks, dunning with Smart Retries, and time-travel testing with
  test clocks. Use when wiring paid tiers, handling failed renewals, setting
  up the Customer Portal, or debugging billing events.
---

# Stripe Billing Integration

## Purpose

Ship the production billing patterns Stripe integrations need but most tutorials skip: the full subscription lifecycle, webhook handling done safely, retry and dunning behavior that actually recovers failed payments, and a test strategy that covers trials, renewals, and proration without waiting 30 days.

## Workflow

1. **Pick the integration shape.** Checkout (Stripe-hosted) for the fastest reliable path; Payment Element when the checkout page must be custom. For self-service plan changes and card updates, use the Customer Portal, never build your own.
2. **Build the webhook handler first, fulfillment second.** Register an endpoint, verify signatures with the raw body, and route events before writing any access-grant logic. Fulfillment that exists before the handler exists is how double-grant and never-grant bugs ship.
3. **Map Stripe subscription status to access.** `trialing` and `active` mean provisioned. `past_due` means keep access during the grace window and run dunning, not a lockout. `canceled`, `unpaid`, and `incomplete_expired` mean revoke. `incomplete` means never provision.
4. **Subscribe to the minimum event set** that covers the lifecycle: `checkout.session.completed`, `customer.subscription.created`, `customer.subscription.updated`, `customer.subscription.deleted`, `invoice.paid`, `invoice.payment_failed`, `invoice.payment_action_required`, `customer.subscription.trial_will_end`. See `references/billing-webhooks.md` for the event matrix and the raw-body verification code.
5. **Make the handler idempotent and order-safe.** Dedup on `event.id` (Stripe retries non-2xx deliveries for up to about 3 days). Events can arrive out of order, so when two events disagree about a subscription, re-read its current state from the Stripe API instead of trusting whichever event arrived last.
6. **Configure Smart Retries and dunning.** Stripe's recommended default is 8 retries within 2 weeks; during retries the subscription sits in `past_due`, and when retries run out the subscription moves to the end state you configured (usually `unpaid` or `canceled`). Pair this with your own reminder emails that link the customer to the Customer Portal. Details and the decline-code taxonomy are in `references/dunning.md`.
7. **Test with test clocks, not wall-clock waits.** Create a clock, attach test customers to it, and advance it to hit trial end, renewal, failed renewal, and proration scenarios in minutes. Recipes are in `references/test-clocks.md`. For quick smoke tests without clocks, use the Stripe CLI: `stripe trigger invoice.payment_failed` and `stripe listen --forward-to http://localhost:3000/webhooks/stripe`.

## Output Contract

A Stripe billing integration that:
- Creates customers and subscriptions through Checkout or Payment Element with the subscription and price IDs stored locally
- Handles the 8 core lifecycle events with signature verification, idempotency, and a 2xx response fast enough that Stripe never needs to retry
- Keeps access in sync with subscription status per the mapping above, including a past-due grace window
- Recovers failed payments through Smart Retries plus customer outreach, not lockouts on first failure
- Has test-clock coverage for trial-to-active, successful renewal, failed renewal through dunning to the end state, and mid-cycle proration

## Operating Rules

- Verify every webhook signature with the raw, unparsed request body (`stripe.webhooks.constructEvent(rawBody, sig, endpointSecret)`). A body that was parsed and re-serialized will fail verification, because the bytes differ from what Stripe signed. On failure return HTTP 400, never 500.
- Never grant or revoke access from `success_url`. It is a display page, and users can visit it without paying. Webhooks are the only fulfillment source.
- The amount on `checkout.session.completed` is `amount_total` in cents. Not `amount`, not `total`.
- For mid-cycle upgrades use `proration_behavior: 'create_prorations'`. Never compute prorations by hand.
- On the subscription object, `default_payment_method` wins over the customer's default when retries charge. If you update one, make sure it is the one retries use.
- Retry logic follows the decline code: soft declines (`insufficient_funds`, `do_not_honor`, `processing_error`) are worth retrying, hard declines (`lost_card`, `stolen_card`, `card_not_supported`) are not, and `authentication_required` (3DS/SCA) never self-resolves, so it goes straight to customer action.
- Test keys are the only keys used outside production. Secret keys never touch client-side code or version control.
