Skip to content

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:

FieldWhat it is
availableSpendable purchased, subscription and adjustment credits.
promoSpendable promo credits.
reservedCredits held by open reservations and leases.
balanceAll unspent credits: available + promo + reserved.
upcomingExpiriesThe next grants to expire, soonest first, with what is left of each.
autoReloadThe 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 (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: 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:

GrantWhen it expires unused
purchase, subscription, refund_reversalBreakage: revenue from credits the customer paid for and didn't use (or held, see below).
promo, adjustmentThe 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 (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 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.