# Recipe: API credits

> Sell prepaid credits for an API or an AI feature and spend one per request. A short balance answers 402 with a top-up link.

Buyers prepay a pack of credits; every request spends some. Perly Pay is the ledger: spending is one atomic call that refuses an overdraft, and every buyer has their own top-up page. Nothing is ever debited on its own — when credits run out, the buyer tops up. The whole credits feature is in [Sell credits](https://pay.perly.io/docs/credits.md); this is the recipe around it.

> **Note:** With an assistant: the `sell-credits` skill of the Perly Pay plugin for Claude Code does this recipe in your project — the plugin and its example, `examples/api-credits` (MIT), are on their way. Until then, any assistant can read the same skill from `/.well-known/skills/perly-pay-sell-credits/SKILL.md`.

## How it works

1. **A credit pack** — A product and a price with `model: "credit_pack"`, `unit_amount` in cents and `credits`. Made once per mode.
2. **Your users are customers** — Made once with `external_id` (your user id); from then on `credits.*` take that id.
3. **Top up** — `credits.topUpUrl(userId)` is the buyer's own page: they pick a pack and pay. Each paid pack adds credits.
4. **Spend per request** — `credits.consume(userId, cost, { idempotencyKey })` first, then the work. A retry with the same key returns the first spend.
5. **Short?** — `insufficient_credits` (409) spends nothing: answer `402` with the top-up page, not a 500.

**Spend one request's credits**

```js
try {
  await pay.credits.consume(userId, cost, { product: PRODUCT, action: "api", idempotencyKey: `req-${userId}-${requestId}` });
} catch (e) {
  if (e.code !== "insufficient_credits") throw e;
  return reply(402, { top_up: await pay.credits.topUpUrl(userId, { product: PRODUCT }) });
}
// now do the work
```

## Run the example

1. **Install** — `npm install` in `examples/api-credits` (Node 20.12+).
2. **Key** — Copy `.env.example` to `.env` and put a test key (`sk_test_…`) in `PERLY_PAY_SECRET_KEY`.
3. **Set up** — `npm run setup` makes the product and price with your key and prints their ids for `.env`.
4. **Run** — `npm start` (or `npm run dev`), as its README says.
5. **Pay without a wallet** — Call `POST /v1/shout` (402), open the page `POST /top-up` gives, pick the pack, press **Simulate payment** on the invoice, call again: 200 and `X-Credits-Remaining: 499`.

## Pitfalls

- Spending without an idempotency key: a retried request is charged twice.
- Checking the balance, then spending, as two steps. `consume` alone refuses an overdraft; the balance is for display.
- Doing the work before spending. Spend first.
- Test ids with a live key. Products, prices and webhook endpoints belong to one mode: run the setup again with `sk_live_…` when you go live.
