# Wallets and credits

> Prepaid credits live in wallets, one per customer and currency. Credits come from top-ups, credit packs, subscriptions and grants; usage reserves them and captures what was used. Unused credits expire; low-balance events and auto-reload keep wallets funded.

A **wallet** holds a customer's prepaid credits in one currency. Customers buy credits with top-ups and credit packs, get them with a subscription, or receive them from you as a grant. Your integration spends them as usage happens: it **reserves** an amount before the work, then **captures** what was used. This is prepaid usage billing; usage billed in arrears on the renewal invoice works through metered prices instead, and a customer can have both on different meters.

A customer has one wallet per currency in each brand and mode. It opens at the first top-up, credit pack, subscription credit or grant, or with `POST /api/v1/wallets`. Every amount is in micros of the wallet's currency, so a charge of €0.0004 is exact.

## Balances

`GET /api/v1/wallets/{id}` returns:

| Field              | What it is                                                                          |
| ------------------ | ----------------------------------------------------------------------------------- |
| `available`        | Spendable purchased, subscription and adjustment credits.                           |
| `promo`            | Spendable promo credits.                                                            |
| `reserved`         | Credits held by open reservations and leases.                                       |
| `balance`          | All unspent credits: `available + promo + reserved`.                                |
| `upcomingExpiries` | The next grants to expire, soonest first, with what is left of each.                |
| `autoReload`       | The auto-reload settings, the number of failed reloads and whether a reload is due. |

The balances come straight from the ledger: each wallet has three ledger accounts, and every change to them is a posted ledger transaction. `GET /api/v1/wallets/{id}/transactions` lists them with the signed change of each balance.

## Credit grants

Every credit belongs to a **credit grant**, which records where it came from (`purchase`, `subscription`, `promo`, `adjustment` or `refund_reversal`), how much is left and when it expires. `GET /api/v1/wallets/{id}/grants` lists them.

Credits are spent in this order:

1. promo credits, soonest expiry first;
2. subscription credits, soonest expiry first;
3. purchased, adjustment and refund-reversal credits, soonest expiry first, then oldest.

You grant credits without a payment with `POST /api/v1/wallets/{id}/grants`:

- `promo`: promotional credits, spent first and never refunded;
- `adjustment`: a goodwill credit, spent with purchased credits.

A grant needs a `reason` and can have an `expiresAt`. In the ledger, credits granted without a payment are a cost (`promo_expense`); unused ones that expire reverse it. Granting credits is a financial operation: people need a recent sign-in, and your organization's approval policy can require a second person above an amount. Owners, admins and support can grant credits.

## Buying credits: top-ups and credit packs

Customers buy credits through checkout with a **credit-pack price**: a fixed pack (for example €25 of credits), or a custom amount between the price's minimum and maximum.

- A **top-up** session (`mode: "topup"`) adds credits to an existing customer's wallet. It sells one credit pack and needs `customer`; the customer pays on the hosted page with their own email. Once the customer has paid you before, the sale is taxed as an existing customer's purchase, which continues where a registration later blocks new sales.
- A **payment** session can also sell credit packs, next to one-time prices, to new or returning buyers.

```http
POST /api/v1/checkout/sessions
{
  "mode": "topup",
  "customer": "cus_…",
  "lineItems": [{ "lookupKey": "credits_custom", "customAmount": { "amount": "40000000", "currency": "EUR" } }],
  "successUrl": "https://example.com/wallet"
}
```

When the payment succeeds, the wallet gets a `purchase` grant and the customer an invoice. The credits are the amount before tax by default; a brand with the `gross` credit basis gives the full amount paid and bears the tax itself. A pack's bonus credits arrive as a `promo` grant, and a pack with `expiresAfterDays` sets when both expire. The hosted page shows the credits before the customer pays.

## Subscription credits

A recurring price can include credits (`recurring.includedCredits`: an amount and an optional `expiresAfterDays`). They go into the customer's wallet as a `subscription` grant for every period the subscription is billed for:

- when the subscription starts active (a free plan, an invoice sent for payment, nothing due, or a paid checkout), or when its first payment activates it;
- when a trial ends: the trial itself gets none;
- at every renewal while it is active or past due. A paused or unpaid period gets none.

Each item gets the price's amount once per period, whatever its quantity; with items on different intervals, each gets its credits at its own period start. A shorter first period before a later billing cycle anchor gets the same share as its charge. The credits expire `expiresAfterDays` after the period starts, or at the end of the period. A renewal that runs twice grants once.

In the ledger, a period's fee pays for its credits: they move from the product's revenue into the wallet, become usage revenue as they are spent and breakage when they expire.

## Reservations

For usage that happens a few times a minute or less, reserve the amount first:

```http
POST /api/v1/wallets/wal_…/reservations
Idempotency-Key: job-8841
{ "amount": { "amount": "3000000", "currency": "EUR" }, "ttlSeconds": 300 }
```

The amount is taken in spend order and held in `reserved`, so nothing else can spend it. If the wallet's spendable credits are less than the amount, the answer is **402** with `insufficient_funds`. A reservation lasts `ttlSeconds`, at most an hour. Then:

- **capture** what was used with `POST /api/v1/reservations/{id}/capture`. By default the rest is released and the reservation closes; with `final: false` it stays open for more captures. `usage` lines say what the amount was spent on, one per meter: the revenue is booked per meter, and each line is stored as a usage event and counted in the [daily usage aggregates](https://ownrecord.io/docs/concepts/usage.md) (not on a meter a metered price bills in arrears). A capture can't take more than is still reserved, and can't happen after `expiresAt`;
- or **release** it with `POST /api/v1/reservations/{id}/release` when the work didn't happen;
- or let it expire: the `reservation_expiry` job releases it within a minute and sends `reservation.expired`.

Reserves and captures are idempotent. Send an `idempotencyKey` in the body (unique per wallet for reservations, per reservation for captures), or rely on the request's `Idempotency-Key`. Sending the same request again returns the same reservation or capture; the same key with a different request is refused.

For thousands of small charges a minute, use [leases](https://ownrecord.io/docs/concepts/leases.md): a larger block of credits your integration meters locally and settles in batches.

## Expiry and breakage

A grant can have an `expiresAt`: credit packs set it from the price's `expiresAfterDays`, and you can set it on a grant. Every hour, the `credit_expiry` job takes what is left of each grant past its `expiresAt` out of the wallet, and books it by where the credits came from:

| Grant                                         | When it expires unused                                                                    |
| --------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `purchase`, `subscription`, `refund_reversal` | Breakage: revenue from credits the customer paid for and didn't use (or held, see below). |
| `promo`, `adjustment`                         | The cost booked when you granted them (`promo_expense`) is reversed.                      |

Credits that an open reservation or lease holds don't expire while they're held: they're spent when the reservation is captured, or expire at the next run after it's released. An expiry that takes the balance below the low-balance threshold sends `wallet.low_balance`, like any other debit, and can start an auto-reload.

**When breakage becomes revenue** is your accountant's decision, set per brand in **Settings → Credits** or with `PATCH /api/v1/brands/{id}`:

- `policies.breakageRecognition: "at_expiry"` (the default): expired paid credits are revenue (`breakage`) when they expire;
- `"manual"`: they move to the **expired credits** liability instead, and stay there until your finance team recognises them with a ledger adjustment (debit `expired_credits:EUR`, credit `breakage:EUR`), or settles them another way, for example a refund on request or unclaimed-property rules.

The policy applies to credits that expire from then on.

## Low balance and depleted

Set `lowBalanceThreshold` with `PATCH /api/v1/wallets/{id}`. When a capture or an expiry takes the balance below it, Ownrecord sends **`wallet.low_balance`**, once per dip: not again until the balance is back at or above the threshold and falls below it again. When the balance reaches zero, it sends **`wallet.depleted`**. Reserving and releasing move credits inside the wallet, so they never trigger these events; a lease's settles count once the lease is captured, when it closes, renews or expires. Credits added send **`wallet.credited`**.

With `wallet.low_balance`, the customer gets an email in their language: their balance, the threshold, and a link to top up in the [customer portal](https://ownrecord.io/docs/concepts/customer-portal.md) (or that auto-reload tops it up). Customers also see their balances and history, top up and set auto-reload in the portal.

## Auto-reload

**Auto-reload** charges the customer's saved payment method off-session when a capture or an expiry takes the balance below `autoReload.below`, and buys `autoReload.amount` of credits. It needs a payment method the customer saved at a subscription or setup checkout. Set it with `PATCH /api/v1/wallets/{id}`:

```http
PATCH /api/v1/wallets/wal_…
{ "autoReload": { "enabled": true, "below": { "amount": "5000000", "currency": "EUR" },
  "amount": { "amount": "25000000", "currency": "EUR" }, "paymentMethodId": "pm_…" } }
```

A reload is a top-up of one of the brand's **credit-pack prices** in the wallet's currency: the pack whose amount is `autoReload.amount` (its bonus credits and expiry apply), or else a pack with a custom amount range that `autoReload.amount` fits; if several do, the oldest. It's taxed like any top-up, with a fresh tax quote from the customer's stored evidence and tax IDs, so `amount` is the price before tax for a tax-exclusive pack and includes tax for a tax-inclusive one. The customer gets credits at the brand's credit basis (the net amount by default) and the receipt by email, and you get `wallet.credited`, `payment.succeeded` and `invoice.issued`.

One reload runs at a time (`autoReload.pendingSince` is set while it does). A reload doesn't start, and the next fall below the threshold tries again, when:

- the wallet was frozen or closed, or auto-reload was turned off, since the balance fell;
- credits arrived meanwhile and the balance is back at or above `below`;
- the saved payment method was removed, or the customer was deleted;
- the brand sells no active credit pack in the wallet's currency that `amount` fits;
- the tax engine blocks the sale.

When the card is declined, the customer gets an email, and the failure is counted in `autoReload.failureCount`. After 2 failed reloads in a row, auto-reload turns itself off (`enabled: false`, `disabledAt`), and the email says so. A successful reload resets the count; so does turning auto-reload on again.

## Freezing and closing

`status: frozen` (with a `reason`) stops new reservations and leases at once, for example while a dispute is open, and sends **`wallet.frozen`**. Reservations made before the freeze still settle, credits still arrive and expire, and no auto-reload starts. `status: active` unfreezes it.

`status: closed` retires a wallet that is empty: no credits and no open reservations. A closed wallet takes no reservations or grants. A purchase in its currency reopens it, because credits that were paid for are never refused.

## In test mode

Test and live mode have separate wallets. Credits in test mode are never charged or billed to anyone.

A customer on a [test clock](https://ownrecord.io/docs/concepts/test-clocks.md) lives at the clock's time: their credits expire when an advance passes their `expiresAt` (the hourly job leaves them alone), and their auto-reloads are quoted, charged and recorded at the clock's time.
