Prepaid AI credits and holds
This recipe continues Bill AI tokens. There, usage bills on the invoice at the end of the period. Here, the customer pays first: they buy credit, your app checks the credit before a model call, and the usage that the credit covers costs them nothing more on the invoice.
How credit works
A credit grant gives one customer an amount of credit. The credit is money, or an amount of your own credit unit, for example 1 credit = USD 0.01.
| Promotional | Paid | |
|---|---|---|
| The customer pays | Nothing | An invoice for the credit |
| Starts | At once | When the invoice is paid |
| Cost basis | 0 | The net price on the invoice |
| Applies | Before tax | Before tax if taxed at purchase, after tax if taxed at use |
| Ends early | Void | A credit note on its invoice |
When an invoice is finalised, grants pay its usage lines in this order: restored credit first,
then the lowest priority, then the soonest expiry, promotional before paid, then the oldest.
Usage that no grant covers bills as usual.
Create a credit unit
Skip this step to sell credit in money. A unit lets you show "1,000 credits" instead of
"USD 10.00". minor_per_unit is in minor units, so "1" is one cent:
curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/credit_units \
-H "Authorization: Bearer $TMBILLING_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"id": "credit",
"name": "credit",
"plural_name": "credits",
"currency": "USD",
"minor_per_unit": "1",
"decimals": 0
}'
The first grant in the unit fixes currency, minor_per_unit and decimals. After that you can
change only the names. In the dashboard: Settings → Credit units.
Sell credit packs
A credit pack is a one-time price with grants_credit. The buyer pays through checkout, a payment
link, the portal's Buy credits list or an invoice that you create. Each pack line on a paid
invoice gives one paid grant:
curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/prices \
-H "Authorization: Bearer $TMBILLING_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"product": "ai-credits",
"name": "1,000 credits",
"type": "one_time",
"model": "flat",
"currency": "USD",
"amount_minor": 900,
"grants_credit": {
"kind": "pack",
"unit": "credit",
"amount": 1000,
"category": "paid",
"expires_after": { "days": 365 },
"tax_treatment": "taxed_at_purchase"
}
}'
This pack sells 1,000 credits (face value USD 10.00) for USD 9.00. The cost basis is USD 9.00, and USD 1.00 is a discount that the ledger records when the credit is used.
tax_treatment decides when tax is charged:
taxed_at_purchase: the pack invoice is taxed. Usage that the credit pays has no tax.taxed_at_use: the pack invoice has no tax. Usage is taxed, and the credit pays the tax too. Only for credit in money.
To set a default for all packs, set credit_config.default_tax_treatment on the business.
Include credits in a plan
On a recurring price, grants_credit with kind: "per_cycle" gives promotional credit each
billing period. rollover decides what happens to credit that is not used:
"grants_credit": {
"kind": "per_cycle",
"unit": "credit",
"amount": 5000,
"rollover": { "mode": "carry", "percent": 50, "cap": 2000, "expire_after_cycles": 1 }
}
At each renewal, half of the unused credit (at most 2,000) moves to a new grant that lasts one
more period. Carried credit does not move a second time. "mode": "none" lets unused credit
expire, and "expire_after_cycles": null keeps carried credit until it is used.
Give credit by hand
Promotional credit, for example a welcome bonus:
curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/customers/$CUS/credit_grants \
-H "Authorization: Bearer $TMBILLING_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "category": "promotional", "unit": "credit", "amount": 500, "expires_at": "2027-01-01T00:00:00Z" }'
Paid credit always has an invoice. Send category: "paid", the net price_minor and a
tax_treatment. We create the invoice, and the grant starts when the invoice is paid. If you
already have the money, add mark_paid and the grant is active in the response:
{
"category": "paid",
"unit": "credit",
"amount": 1000,
"price_minor": 900,
"tax_treatment": "taxed_at_purchase",
"mark_paid": { "method": "bank_transfer", "reference": "Transfer 0042" }
}
A paid grant needs an API key with credits:manage and invoices:manage.
Check before each model call
Ask the access check with the meter and an estimate of the cost in minor units. It writes nothing:
curl "$TMBILLING_URL/v1/businesses/$BIZ/customers/$CUS/access?meter=ai_tokens&estimate=25" \
-H "Authorization: Bearer $TMBILLING_API_KEY"
{
"object": "access_check",
"allowed": true,
"code": "ok",
"currency": "USD",
"available_minor": 1240,
"available_units": [{ "unit": "credit", "units": 1240 }],
"balance": {
"grants_minor": 1500,
"unbilled_usage_minor": 210,
"open_holds_minor": 50,
"overdraft_limit_minor": 0
},
"on_exhausted": { "policy": "block", "source": "business_default" }
}
The answer is always 200. Read allowed, and code when it is false:
code | Means |
|---|---|
ok | Go ahead. |
no_subscription | No live subscription bills this meter. |
not_entitled | The plan does not include the feature, or its limit is used up. |
spend_limit_reached | A spend limit stops more usage. limits names it and when it resets. See Spend limits and automatic top-up. |
in_flight_limit | Open holds already use the permitted part of the balance. |
insufficient_credit | The customer's policy blocks, and the credit is used up. |
If your app knows the customer only by a usage key, call
GET /v1/businesses/$BIZ/access?customer_key=workspace-4821&meter=ai_tokens.
95% of access checks answer in less than 100 ms, measured at our server. Network time is not included.
When the access check blocks
Blocking is off until you turn it on. With it off, insufficient_credit never occurs and all
usage bills on the invoice. Turn it on with credit_config.blocking_enabled on the business
(in the dashboard: Settings → Credits). Then the first of these sets the policy:
- an override on the customer (
credit_settings.on_exhausted), until itsoverride_expires_at; - a live grant for the meter with
on_exhausted: "block"; - the
on_exhaustedof the plan price'sgrants_credit; - the business default,
credit_config.on_exhausted_default.
An overdraft limit lets the balance go below zero before the block, for example USD 2.00 for a trusted customer.
Hold, then capture or release
Many calls can run at the same time. A check alone lets two calls see the same credit. A hold reserves the estimate first:
curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/customers/$CUS/credit_holds \
-H "Authorization: Bearer $TMBILLING_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "amount_minor": 25, "meter": "ai_tokens", "expires_in": 120, "scope": { "workspace": "workspace-4821" } }'
A refused hold answers 402 with code: "in_flight_limit" or "insufficient_credit". Do not
make the model call.
After the call, capture the hold with the usage event. We record the event and close the hold in one step, so the balance never counts both or neither:
curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/credit_holds/$HOLD/capture \
-H "Authorization: Bearer $TMBILLING_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"usage_event": {
"key": "msg_XXXXXXXXXXXX01:output",
"customer": "workspace-4821",
"event_name": "ai_tokens",
"properties": { "model": "small-v1", "token_type": "output", "tokens": 1200 }
}
}'
If the call fails, release the hold with POST …/credit_holds/$HOLD/release. A hold that you do
not capture or release expires after expires_in seconds (default 60, at most 86,400).
Usage above the hold is accepted, because the tokens were used. If it takes the balance past the
overdraft limit, the hold has overdraft: true and the rest bills on the next invoice.
credit_hold_created, credit_hold_captured, …) go only to webhook endpoints that
list them by name. An endpoint that receives all events does not get them, so a busy app does
not send one webhook for each model call.See every movement
GET /v1/businesses/$BIZ/customers/$CUS/credit_transactions lists each grant, draw, expiry, void,
restore and rollover, newest first. Filter by grant, kind, category, unit, invoice and
created[gte] / created[lt]. On an invoice, grant_credits_pre_tax_minor and
grant_credits_post_tax_minor show what the grants paid. In the dashboard, open the customer and
select View credits.
Next
To limit spend, warn the customer before a limit, and buy credit for them before it runs out, see Spend limits and automatic top-up.
Bill AI tokens
Charge for AI usage per model and token type: one meter, a rate card or the model price list with a markup, your cost and margin, and an SDK helper that reports each model response once.
Spend limits and top-up
Keep AI usage spend in check: hard and soft spend limits per customer, subscription or user, limit tiers, alerts with a state per threshold, credit runway, automatic top-up, early usage invoices, a rate-change preview and a usage widget for your app.