---
title: "Tokens, quotas, LMC and PAYG"
url: https://lumean.app/en/developers/docs/guide/billing
locale: en
description: "A token is not a ruble and not LMC. What is charged to the subscription quota, what to the balance, and how to get through the 402 payg_topup_required fork without paying twice."
updated: 2026-09-04
sources: [post-orders, post-orders-order-retry, get-subscriptions, get-usage, get-usage-service]
errors: ["402", payg-topup-required, quote-mismatch, insufficient-balance, insufficient-allowance]
spec_version: 0.0.1
---

# Tokens, quotas, LMC and PAYG

Billing in the public API involves three different quantities, and they are regularly confused with one another.

| Quantity | What it is | Where to see it |
| --- | --- | --- |
| **Token** | an accounting unit of a service's work volume. Not money and not LMC | `total_tokens`, `tokens_spent` in the order; `GET /usage` |
| **Subscription quota** | the plan's token allowance for the billing period. Spent first, free of charge | `GET /subscriptions` → `items[].tokens_remaining` |
| **LMC** | the internal currency. Only what the quota didn't cover is paid from it | `shortfall_lmc` in the `402` response; `price_formatted` in the order |

The charge order is always the same and can't be configured: **subscription quota first, LMC only for the
remainder**. If the quota is sufficient, the balance isn't touched at all, and the `confirm_payg_topup` flag
changes nothing.

> [!WARNING]
> The public API doesn't show your LMC balance: `GET /wallets` and `GET /ledger-transactions`
> return `404` in the standard configuration. The public view of your state is `GET /usage` and
> `GET /subscriptions`, and you learn the top-up amount from the `402` body, not from the balance in advance.

## The 402 fork: the server asks for consent

If the order costs more than the tokens left in the quota, and the service allows pay-as-you-go
(`Service.payg_access` ≠ `disabled`), the server **doesn't create the order and charges nothing**; instead it
returns `402` with the top-up price. It's not a rejection but a question.

```text
Развилка PAYG: доплата по факту вместо отказа

  POST /orders
       │
       ↓
  402  reason: payg_topup_required
       тело: shortfall_lmc, quote_token
       quote_token живёт около 60 секунд
       │
       ↓
  повтор ТОГО ЖЕ тела + confirm_payg_topup: true + quote_token
       │
       ├── уложились в ~60 с ──→ 201 Created — заказ создан, добор списан
       │
       └── опоздали ──────────→ 409 reason: quote_mismatch
                                 котировка протухла или цена изменилась:
                                 повторить с шага 402 и взять новый quote_token
```

The `402` body:

| Field | Meaning |
| --- | --- |
| `reason` | always `payg_topup_required` — branch on it, not on `message` |
| `shortfall_tokens` | how many tokens the quota didn't cover — exactly these are paid from LMC |
| `shortfall_lmc` | the top-up price in LMC, ready to show to the user |
| `shortfall_lmc_minor` | the same amount in minor units — for exact calculations |
| `quote_token` | a signed quote; send the string back unchanged |
| `expires_at` | the price is locked until this moment (the token lives ~60 seconds) |

If you agree, repeat **the exact same** request with two added fields:

```bash
# 1. Regular attempt → HTTP 402 with a quote
curl -s -X POST "$BASE/orders" -H "X-API-KEY: $KEY" \
  -H "Content-Type: application/json" \
  -d '{ "template_id": "<UUID>", "input_text": "…" }'

# 2. Consent: the same body + confirmation and the same quote_token → HTTP 201
curl -s -X POST "$BASE/orders" -H "X-API-KEY: $KEY" \
  -H "Content-Type: application/json" \
  -d '{ "template_id": "<UUID>", "input_text": "…",
        "confirm_payg_topup": true, "quote_token": "q_9f8b2c1a4d3e" }'
```

If you don't agree, simply don't repeat the request: the order wasn't created, nothing was charged, there's nothing to cancel.

### 402 · `payg_topup_required` — payg_topup_required — PAYG top-up required

| Что произошло | Что делать |
| --- | --- |
| The order was not created and no money was charged — instead, the server sent the price of the top-up. Body: `reason: "payg_topup_required"`, `shortfall_tokens`, `shortfall_lmc`, `shortfall_lmc_minor`, `quote_token`, `expires_at`. | Show the user `shortfall_lmc` and get explicit consent — an automatic retry here means silently charging money. The quote is short-lived (`expires_at`, on the order of a minute); if it has expired, a repeat request WITHOUT confirmation returns a fresh 402 with a new `quote_token`. |

`Retry-After`: нет. Разбор: https://lumean.app/en/developers/docs/errors/payg-topup-required

### Four confirmation traps

1. **The flag alone charges nothing.** `confirm_payg_topup: true` without a `quote_token` — or with
   one that has expired, been modified, or was issued for a different order — returns `402` again with a new quote.
2. **Consent is an upper bound, not a fixed amount.** If the top-up has become cheaper by the time of the charge,
   the actual price is charged. If it has become more expensive, you get `409` `quote_mismatch`
   with the fields `actual_lmc_minor` and `quoted_lmc_minor`.
3. **Repeating without confirmation is safe and is a standard step:** it issues a fresh `402`.
   That's exactly how you refresh a stale or more expensive price — re-sending the old `quote_token`
   is pointless.
4. **`POST /orders/{order}/retry` has the same flow** — an order retry goes through the same billing
   and can likewise answer `402` and `409`.

### 409 · `quote_mismatch` — quote_mismatch — top-up quote is stale

| Что произошло | Что делать |
| --- | --- |
| The confirmed top-up did not go through: the order was not created and nothing was charged. Body: `reason: "quote_mismatch"`, `actual_lmc_minor` (what it costs now), `quoted_lmc_minor` (what it was in the quote). | The difference `actual_lmc_minor − quoted_lmc_minor` is the price increase; it is in minor units, and what you should show the user is `shortfall_lmc` from the next quote. The old `quote_token` is no longer valid: submitting it again guarantees the same 409. |

`Retry-After`: нет. Разбор: https://lumean.app/en/developers/docs/errors/quote-mismatch

## Three different branches under code 402

These get confused most often: the code is the same, the `reason` differs, and the remedies are incompatible.

| `reason` | What happened | What to do |
| --- | --- | --- |
| `payg_topup_required` | the quota ran short, a top-up is allowed | confirm the top-up with the quote |
| `insufficient_balance` | the LMC balance lacks funds for the order's reserve | top up the wallet or reduce the order volume |
| `insufficient_allowance` | the quota ran short, and PAYG is disabled for the service | there's nothing to top up with: wait for the new period, change the plan or cut the text |

You can tell `insufficient_allowance` apart from a top-up offer by the body's contents: the
`shortfall_*`, `quote_token` and `expires_at` fields are **absent**. A `confirm_payg_topup` sent
along with such a request is simply ignored by the server.

Two more domain rejections live nearby, but with code `403`: `no_subscription` and
`entitlement_missing` — there's no subscription, or no required plan feature, at all. And `limit_exceeded`
with code `429` isn't about money but a subscription usage limit; see
[Limits and the 429 code](/developers/docs/guide/limits).

## Minor units: why `shortfall_lmc_minor` can't be shown as is

All monetary fields in the API are integers in the asset's minor units. The real value is calculated
as `minor / 10 ** precision`. In the spec example, `shortfall_lmc_minor: 360000000` corresponds to
`shortfall_lmc: 3.6`, which means the divisor here is `10^8`, not `100`.

> [!CAUTION]
> `precision` is not two and is not a constant — it lives in the data: on an order in
> `currency_asset.precision`, on a service rate in the `lumc_precision` field. A formula dividing by 100
> gives an amount that is off by a factor of a million, and showing `360000000` to the user as rubles is even worse.
> For display, use the ready-made `shortfall_lmc` and `price_formatted`; the minor-unit fields are only for
> arithmetic.

## Where to check the remainder before ordering

### GET /subscriptions

Get subscriptions of API key owner

Справочник: https://lumean.app/en/developers/docs/api/get-subscriptions

Only live subscriptions are returned (status `active`, not expired). The remainder is in
`items[].token_allowance`, `tokens_used`, `tokens_remaining`. There are no monetary fields here, by
design.

### GET /usage

Get Usage Limits

Справочник: https://lumean.app/en/developers/docs/api/get-usage

`GET /usage` lists limits: `limit_type`, `limit_value`, `used`, `remaining`,
`period_start` / `period_end` / `resets_in`, plus `token_unlimited` and `is_exceeded`. It also shows
which services are available to you at all — the set is defined by the subscription's entitlements. For a single service, use
[`GET /usage/{service}`](/developers/docs/api/get-usage-service), where `{service}` is the
`service_code` from the `GET /usage` output (`elevenlabs`, `heygen`, `lumimg`, `sfx`, `music`,
`voice_search`), not an arbitrary string.

> [!WARNING]
> The LLM reference (§7.7) calls `{service}` the service's integer `id`. That's wrong:
> in the spec the parameter is a string, and the path accepts the service **code** — the same one found in the template's
> `service_key`. A numeric `id` gives `404`.

You can estimate the top-up in advance from the service rate: `shortfall_tokens × lumcoin_per_token`
(the `lumcoin_per_token` field is also in LMC minor units).

## What's visible after the fact, in the order itself

- `total_tokens` — the full cost in tokens, including retries and regenerations;
- `tokens_spent` — actually spent (`retry` repeats aren't charged again);
- `tokens_from_quota` and `tokens_from_payg` — the split into the free and paid portions;
- `price_units` — the locked PAYG amount in minor units, `price_formatted` — the same amount
  for display.

> [!WARNING]
> A zero portion arrives as `null`, not `0`. For an order fully covered by the quota,
> `tokens_from_payg` is `null`; for pure PAYG, `tokens_from_quota` is `null`.
> The check `if (order.tokens_from_payg > 0)` won't behave on `null` the way you expect.

**Правильно**

Show the user `shortfall_lmc`, wait for explicit consent, and repeat the request with the
`quote_token` from that same response. If the quote has expired, request a new one by repeating without confirmation.

**Неправильно**

Automatically retry a `402` with `confirm_payg_topup: true`: that's silently charging money
behind the user's back, and the first price discrepancy will turn the loop into a stream of `409`s.

Topping up the balance and buying a plan aren't available with an API key — the public surface doesn't process
payments. That's done in the dashboard: [wallet](/wallet), [plans](/shop),
[account limits](/limits).

---

- This page in the browser: https://lumean.app/en/developers/docs/guide/billing
- Full machine-readable reference: https://api.lumean.app/docs/llm.md
