> ## Documentation Index
> Fetch the complete documentation index at: https://docs.integrateledger.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Billing lifecycle

> How a customer moves from attach to paid credits, and what happens at each renewal, upgrade and cancellation.

Ledger runs billing through your Stripe account and treats Stripe's signed webhooks as the only proof of payment. Your backend asks for a plan or pack with [`attach`](https://docs.integrateledger.com/api-reference/billing/attach); Ledger grants credits when Stripe says the money arrived.

## From attach to credits

```mermaid theme={"system"}
%%{init: {"theme": "base", "themeVariables": {"fontFamily": "IBM Plex Sans, sans-serif", "fontSize": "14px", "actorBkg": "#f7f6f3", "actorBorder": "#c4bfb5", "actorTextColor": "#1a1816", "signalColor": "#3d3934", "signalTextColor": "#1a1816", "noteBkgColor": "#eff2fd", "noteBorderColor": "#bfcbf7", "noteTextColor": "#1d36a0", "activationBkgColor": "#dfe5fb", "activationBorderColor": "#5f7be6"}}}%%
sequenceDiagram
    participant App as Your backend
    participant L as Ledger
    participant S as Stripe
    actor C as Customer
    App->>L: POST /v1/billing/attach
    L->>S: Create Checkout Session
    L-->>App: checkoutUrl (status pending_checkout)
    App->>C: Redirect to checkoutUrl
    C->>S: Pays on Checkout
    S-->>C: Redirect to your success URL
    S->>L: invoice.paid (signed webhook)
    Note over L: Plan active, credits granted
    App->>L: GET /v1/customers
    L-->>App: availableCredits
```

The redirect back to your success URL happens at about the same time as the webhook, but it proves nothing: anyone can open that URL. Read the customer from your backend, or refresh your billing page, once the webhook has landed.

## What attach does

| You attach | Customer state | Result |
| - | - | - |
| A free plan | Any | Active at once. `{ subscriptionId, status: "active" }` |
| A paid plan | No saved card | Pending. `{ checkoutUrl, subscriptionId }` |
| A paid plan | Has a saved card in Stripe | Charged straight away. `{ subscriptionId, status: "active" }`, or `402 payment_failed` if declined |
| A credit pack | Any | Pending. `{ checkoutUrl, addOnPurchaseId }` |

A *saved card* is the default payment method on the customer's Stripe customer. When Ledger charges it, the response arrives after Stripe has confirmed the invoice, so the credits are already there. A declined card creates no subscription, and the customer can attach again. A card that needs 3-D Secure isn't charged off-session: attach returns a `checkoutUrl` for the same subscription, and the customer authenticates on Checkout.

Attaching the same plan or pack again while its Checkout is open returns the same `checkoutUrl`, so retries are safe. Once Stripe expires an unpaid Checkout (after 24 hours), the next attach opens a new one.

## Subscription statuses

[List customer subscriptions](https://docs.integrateledger.com/api-reference/customers/subscriptions.md) returns each subscription's `status`:

| Status | Meaning |
| - | - |
| `pending_checkout` | Checkout is open; nothing is granted yet. |
| `active` | The plan is running and its credits are available. |
| `canceled` | The subscription ended, or a paid plan replaced this free plan. |
| `incomplete_expired` | Checkout expired or failed before payment. |
| `past_due`, `unpaid`, `paused` | Stripe is waiting on a payment. The plan and its credits stay, but it gets no overage and can't be upgraded or downgraded; the next renewal's credits come once that invoice is paid. `GET /v1/customers` shows it as `currentPlan.status`. |
| `incomplete`, `trialing` | Mirrored from Stripe for a subscription whose first payment needs attention. |

## Renewals

Stripe renews paid plans every month. Ledger grants the new period's credits when Stripe creates the renewal invoice, about an hour before it charges the card, so a customer isn't stuck at zero while the payment goes through. If Stripe voids that invoice, the credits it granted end. Free plans renew in Ledger, at the same time of day each month from when they started.

The previous period's allowance expires at the period's end, whether or not it was spent.

## Plan changes

An [upgrade](https://docs.integrateledger.com/configure-pricing/upgrades-and-downgrades.md#upgrade) charges the full new price now and starts a new period; the old plan's unspent credits stay until the old period would have ended, and are spent first. A [downgrade](https://docs.integrateledger.com/configure-pricing/upgrades-and-downgrades.md#downgrade) starts the cheaper plan at the next renewal by default, or now with prorated credits. [Cancelling at period end](https://docs.integrateledger.com/configure-pricing/upgrades-and-downgrades.md#cancel-at-period-end) keeps the plan and its credits until the paid period ends. Ledger doesn't refund a cancellation; cancelling immediately is done in Stripe, and Ledger follows Stripe's `customer.subscription.deleted` event.

## Refunds and disputes

Refund and dispute payments in Stripe. Ledger follows Stripe's signed events, and changes credits only for a credit pack refunded in full or lost to a dispute.

| What happens in Stripe | What Ledger does |
| - | - |
| A credit pack is refunded in full | The pack's unused credits expire. Credits already spent stay spent, so the balance never goes negative. |
| A credit pack is refunded in part | Recorded on the customer, no change to credits. Once later refunds make it a full refund, the unused credits expire. |
| A subscription invoice is refunded, with or without tax | Recorded on the customer. The plan's credits stay; remove credits with a [balance adjustment](https://docs.integrateledger.com/customers/balance-adjustments.md), or cancel the subscription, if you need to. |
| A payment is disputed | Recorded and the customer is marked. `GET /v1/customers` returns `dispute`, the most serious one: `open` while Stripe is waiting on it, then `won` or `lost`. Credits stay while it's open, and when it's won. |
| A dispute over a credit pack is lost | The money went back to the cardholder, as for a full refund: the pack's unused credits expire. A lost dispute over a subscription invoice keeps the plan's credits. |
| A renewal payment fails | Recorded on the subscription until its invoice is paid or voided. Credits already granted stay, and Stripe's `past_due` or `unpaid` status shows on the plan. |

The dashboard's customer page lists the customer's refunds and disputes, with what each did to credits. Canceling a subscription in Stripe ends the plan as [cancellation](https://docs.integrateledger.com/configure-pricing/upgrades-and-downgrades.md#cancel-at-period-end) does.

## Credit packs

A pack's credits are granted when Stripe's signed `checkout.session.completed` event confirms the payment. Stripe also sends the customer an invoice for the pack, with a line such as "Top-up – 500 credits". The invoice never grants the credits a second time.

## When something goes wrong

| Symptom | Cause | What to do |
| - | - | - |
| Status stays `pending_checkout` after paying | The webhook hasn't reached Ledger | Check **Settings › Stripe** shows the webhook as **Receiving**. Ledger waits for the signed event. |
| `502 stripe_webhook_unhealthy` from attach | The endpoint is missing, disabled or misconfigured in Stripe | Choose **Check again** in **Settings › Stripe**, or reconnect Stripe. |
| `409 subscription_exists` | The customer already has an active or pending plan | [Upgrade or downgrade](https://docs.integrateledger.com/configure-pricing/upgrades-and-downgrades.md) a paid plan, or wait for the pending Checkout to finish or expire. |
| `409 purchase_pending` | A different pack's Checkout is open | Let it finish or expire, then attach again. |
| The customer paid but got no credits | Ledger refused the invoice (for example, it had a coupon) | Grant the credits with a [balance adjustment](https://docs.integrateledger.com/customers/balance-adjustments.md) that names the invoice, or refund it in Stripe. |

[Errors](https://docs.integrateledger.com/errors.md) lists every code.

## Next steps

> [card] [Display billing](https://docs.integrateledger.com/getting-started/display-billing.md)
>
> Put the lifecycle to work in your app.

> [card] [Stripe](https://docs.integrateledger.com/stripe.md)
>
> The Stripe account, endpoint and events Ledger uses.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.