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

# Credit packs

> Sell a fixed number of credits as a one-time purchase that never expires.

A credit pack is a one-time purchase: a fixed `creditAmount` for a `priceCents`. Customers buy one when they run out before their plan renews, or instead of a plan. Pack credits never expire, and Ledger spends them after the plan allowance.

In the API and the JSON file, packs are *add-ons* with `type: "credit_pack"` (`/v1/add-ons`, `addOnId`).

## Define a pack

```ts ledger.config.ts theme={"system"}
creditPacks: [
	creditPack({ id: "credits_500", name: "Top-up", creditAmount: 500, priceCents: 1000 }),
],
```

```json ledger.catalog.json theme={"system"}
"addOns": [
  { "id": "credits_500", "name": "Top-up", "type": "credit_pack", "creditAmount": 500, "priceCents": 1000 }
]
```

Publish with `npx integrateledger-cli push`. Packs aren't synced to Stripe ahead of time; Ledger prices each Checkout when it opens.

## Sell it

Offer the pack when [`check`](https://docs.integrateledger.com/getting-started/gating) returns `allowed: false` or `track` returns `409 insufficient_credits`. The customer doesn't need a plan. Attach it with `addOnId` and redirect to Checkout:

```typescript TypeScript theme={"system"}
const purchase = await billing.attach({ customerId, addOnId: "credits_500" });
if ("checkoutUrl" in purchase) {
	return res.redirect(303, purchase.checkoutUrl);
}
```

```python Python theme={"system"}
purchase = billing.attach(customer_id=customer_id, add_on_id="credits_500")
return redirect(purchase["checkoutUrl"], code=303)
```

The response is `{ checkoutUrl, addOnPurchaseId }`. Ledger grants the credits when Stripe's signed `checkout.session.completed` event confirms the payment; Stripe also sends the customer an invoice with a line such as "Top-up – 500 credits". [Balances](https://docs.integrateledger.com/customers/balances.md#purchases) shows the purchase history. Setting up Checkout redirects and testing are covered in [Display billing](https://docs.integrateledger.com/getting-started/display-billing.md#sell-through-checkout).

## Rules

* **One open pack Checkout at a time.** Attaching a different pack while one is open returns `409 purchase_pending`. Attaching the same pack returns the same URL. Stripe expires an unpaid Checkout after 24 hours.
* **Prices lock while a Checkout is open.** Changing the pack's `creditAmount` or `priceCents` returns `409 purchase_pending` until it completes or expires. Renaming is fine.
* **Archiving stops new sales.** Removing a pack from the file archives it; a Checkout already open can still complete.
* **Refunds happen in Stripe.** A full refund expires the pack's unused credits; credits already spent stay spent, so the balance never goes negative. A partial refund changes no credits. A lost dispute expires the unused credits as a full refund does. See [Refunds and disputes](https://docs.integrateledger.com/concepts/billing-lifecycle.md#refunds-and-disputes).

> [!NOTE]
>
> **Other ways.** Packs can also be managed in the dashboard under **Add-ons**, or with the [add-on endpoints](https://docs.integrateledger.com/api-reference/add-ons/create.md).


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