> ## 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.

# Display billing

> Build the billing page in your app, with plan, balance and history, and sell plans and credit packs through Stripe Checkout.

Most apps need one page where a customer sees what they're on and changes it. Load the data from your backend, render it, and send each button to one Ledger call.

Resolve the signed-in user's `customerId` on your server, never from a value the browser sends, so one customer can't read or change another's billing.

## Load the page

```typescript TypeScript theme={"system"}
const customerId = session.user.id;

const [customer, subscriptions, purchases, plans, packs] = await Promise.all([
	billing.getCustomer({ customerId }),
	billing.listCustomerSubscriptions({ customerId }),
	billing.listCustomerPurchases({ customerId }),
	billing.listPlans(),
	billing.listAddOns(),
]);
```

```python Python theme={"system"}
customer_id = session.user.id

customer = billing.get_customer(customer_id=customer_id)
subscriptions = billing.list_customer_subscriptions(customer_id=customer_id)
purchases = billing.list_customer_purchases(customer_id=customer_id)
plans = billing.list_plans()
packs = billing.list_add_ons()
```

| Show | From |
| - | - |
| Credits left | `customer.availableCredits` |
| Current plan and when it renews | `customer.currentPlan.name`, `.currentPeriodEnd` |
| "Confirming your payment" | `customer.currentPlan.status` is `pending_checkout` |
| Plan and pack history | `subscriptions.items`, `purchases.items` |
| What they can buy | `plans.items`, `packs.items` |

[Balances](https://docs.integrateledger.com/customers/balances.md) explains each field. Listing plans and packs needs a key with **Catalog** read access.

## Wire up the buttons

| The customer chooses | Call |
| - | - |
| A plan, with no paid plan today | `attach` with `planId`, then redirect to Checkout (below) |
| A credit pack | `attach` with `addOnId`, then redirect to Checkout (below) |
| A pricier or cheaper plan, on a paid plan today | [Upgrade or downgrade](https://docs.integrateledger.com/configure-pricing/upgrades-and-downgrades.md) |
| Cancel, or undo it | [Stop or resume renewal](https://docs.integrateledger.com/configure-pricing/upgrades-and-downgrades.md#cancel-at-period-end) |

## Sell through Checkout

**1. Set where Checkout returns**

In **Settings › Checkout**, set a **success URL** and a **cancel URL**: the pages in your app that Stripe sends the customer back to. They must be `https`; plain `http` is accepted only for `localhost` in Sandbox. Until you set them, Sandbox returns the customer to the Ledger dashboard. Live requires them.

**2. Attach and redirect**

```typescript TypeScript theme={"system"}
const result = await billing.attach({ customerId, planId: "pro" }); // or addOnId

if ("checkoutUrl" in result) {
	return res.redirect(303, result.checkoutUrl);
}
// status "active": Ledger charged the customer's saved card.
```

```python Python theme={"system"}
result = billing.attach(customer_id=customer_id, plan_id="pro")  # or add_on_id

if "checkoutUrl" in result:
    return redirect(result["checkoutUrl"], code=303)
# status "active": Ledger charged the customer's saved card.
```

A customer with a saved card is charged straight away and gets `status: "active"`, unless the card needs 3-D Secure, which returns a `checkoutUrl` instead. Calling `attach` again while a Checkout is open returns the same URL, so a double click is safe. A customer on a free plan keeps it until the paid plan's payment lands.

**3. Confirm from your backend**

When the customer lands on your success URL, load the page again from your backend. Ledger grants credits only when Stripe's signed webhook confirms the payment, usually within seconds; the redirect proves nothing. While `currentPlan.status` is `pending_checkout`, show "Confirming your payment" and reload in a moment.

### Test it

In Sandbox, pay with [Stripe test cards](https://docs.stripe.com/testing), any future expiry and any CVC:

| Card | Result |
| - | - |
| `4242 4242 4242 4242` | Succeeds. |
| `4000 0025 0000 3155` | Asks for authentication, then succeeds. |
| `4000 0000 0000 9995` | Declined. The plan stays `pending_checkout`. |

To see renewals without waiting a month, [simulate time](https://docs.integrateledger.com/customers/simulate-time.md).

### Errors from attach

| Status | `error` | What to do |
| - | - | - |
| `402` | `payment_failed` | The saved card was declined. Nothing was created; the customer can attach again. |
| `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) instead. |
| `409` | `purchase_pending` | A different pack's Checkout is open. Let it finish or expire. |
| `400` | `checkout_urls_not_configured` | Set the Checkout URLs in **Settings › Checkout**. |
| `502` | `stripe_webhook_unhealthy`, `stripe_checkout_failed` | Check **Settings › Stripe**, then retry the same attach; it can't open a second Checkout. |

[Billing lifecycle](https://docs.integrateledger.com/concepts/billing-lifecycle.md) shows what happens from attach to credits, and at each renewal.

> [!NOTE]
>
> **Other ways.** Without an SDK, use [`POST /v1/billing/attach`](https://docs.integrateledger.com/api-reference/billing/attach).


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