Billing
Priced in cash, held then settled, always released on failure — and the three spend caps.
Cash, not credits
Prices are quoted in US dollars: "$0.008 per image", "$0.042 per output second". Your wallet balance is a dollar balance.
Avoiding credits is deliberate. A credit system inserts a conversion layer between "top up X, get Y bonus" and "what is a credit worth", which makes real unit cost hard to compute. The price you see is the price you pay.
Over the wire, money is a string — without the dollar sign
"$0.008" is how this page writes prices for humans. The API returns a decimal string with no currency symbol: estimatedCost, cost and all three balance fields look like "0.008" — no $, no separators, always USD.
// Nothing to strip first
const usd = Number(task.cost);usd = float(task["cost"])Strings rather than JSON numbers, deliberately: a JSON number lands as a float64 in most languages, and 0.008 has no exact binary representation. If the figure is going into your books, parse it as a decimal:
from decimal import Decimal
usd = Decimal(task["cost"]) # not float()Billing units
A model has exactly one billing unit.
| Unit | Meaning | Which input field supplies the quantity |
|---|---|---|
per_image | Per generated image | whichever field the model declares; defaults to 1 |
per_second | Per second of output | whichever field the model declares (duration on seedance); defaults to the shortest tier |
per_request | Per call, regardless of parameters | — |
per_1k_tokens | Per thousand tokens, priced separately for input and output | — |
You do not have to guess the name: quantityField on the catalog endpoint reports it per model, and an empty string means one unit per call.
A single model can have different rates per tier, selected by input.resolution — 720p and 1080p need not cost the same. On the catalog endpoint that is pricing[].variant; there is no variant field in the request body, so send the resolution of the tier you want.
Always read quantity and price from the live catalog
Do not hard-code a quantity field for an entire model category. An empty quantityField means one billing unit per call; otherwise read the estimate from the named input field. Model tiers, defaults and billing behavior can change with the catalog. Final settlement follows the provider receipt, but the charge is capped at the amount held when the task was accepted: unused funds are released and the platform absorbs any provider overage.
Exact units and prices are on the pricing page and each model's own page.
Three phases
Hold
At creation we hold funds for the estimated usage. The amount is exactly the estimatedCost returned by createTask.
The held amount leaves your available balance but is not yet revenue — it is still your money, temporarily unusable for other tasks. It shows up as held in GET /api/v1/chat/credit.
The hold and the task row are written in one database transaction. There is no intermediate state where a task exists without a hold, or the reverse.
The hold is a reservation, not a charge.
Settle or release
succeeded→ settled against actual usage, capped at the accepted hold. Unused funds return to your available balance; if the provider reports more than the hold, the platform absorbs the overage and never collects it from you.failed/expired→ fully released. Historicalcanceledrecords are also fully released, but accepted tasks cannot currently be canceled.
Once settlement completes, recordInfo reports settled: true and cost is final. While settled is false, cost can still move.
Sweeper
If our process dies mid-flight, a sweeper finds holds attached to already-terminal tasks and releases them.
Settlement and release share one deduplication key, so a task's billing lifecycle terminates exactly once — retries cannot post it twice.
Failed tasks are not billed — a commitment, not best effort
Every terminal state other than succeeded releases the hold. You do not need to do anything, and you do not need to come to us to reconcile.
Checking your balance
GET /api/v1/chat/creditcurl https://api.spicyapi.ai/api/v1/chat/credit \
-H "Authorization: Bearer $SPICY_API_KEY"const res = await fetch('https://api.spicyapi.ai/api/v1/chat/credit', {
headers: { Authorization: `Bearer ${process.env.SPICY_API_KEY}` },
});
const body = await res.json();
if (body.code !== 200) throw new Error(`${body.code} ${body.msg}`);
const available = Number(body.data.available);
if (available < 5) console.warn('balance running low');import os, requests
res = requests.get(
"https://api.spicyapi.ai/api/v1/chat/credit",
headers={"Authorization": f"Bearer {os.environ['SPICY_API_KEY']}"},
)
body = res.json()
if body["code"] != 200:
raise RuntimeError(f'{body["code"]} {body["msg"]}')
available = float(body["data"]["available"])
if available < 5:
print("balance running low"){
"code": 200,
"msg": "success",
"data": {
"available": "128.42",
"held": "0.36",
"total": "128.78"
}
}| Field | Meaning |
|---|---|
available | Immediately usable for new tasks |
held | Locked by tasks in flight |
total | The sum of the two |
When the balance cannot cover the estimate, createTask returns 40201.
Spend caps
Three layers. Any one of them hitting its ceiling rejects the task with 40202:
| Layer | Who sets it | Message on hit |
|---|---|---|
| Key lifetime cap | You, in the console | The key's lifetime spend cap was reached |
| Key daily cap | You, in the console. New keys ship with a low one | The key's daily spend cap was reached |
| Platform daily cap | Us. This is our own circuit breaker | Platform allowance for today is exhausted; retry later |
Daily caps roll over at UTC midnight.
A daily cap of 0 means unlimited, not zero
Leaving the field blank when creating a key applies the system default; entering 0 explicitly means you want no limit. Those are different intentions, and we will not overwrite the explicit one with a default.
Caps exist to protect you. A runaway loop burning through a large amount in a few hours is a routine incident in this industry, and it almost always happens on an account with no cap set.
The ledger
The console's billing page lists every balance movement. The kinds are:
| Kind | Meaning |
|---|---|
topup | Funds added |
bonus | Promotional credit from a top-up tier, booked separately from the principal |
hold | Estimated funds reserved at acceptance; not a charge |
settle | Settlement against actual usage after success |
refund | Release for a non-successful terminal state |
adjust | Manual correction; always carries an operator identity and a reason |
chargeback | Balance clawed back after a payment dispute |
The ledger is append-only. Nothing already written is ever rewritten — a refund is a new negative entry, not an erasure of the original. That means any balance can be recomputed from the beginning, which turns "the numbers do not add up" into a question with a provable answer.
Bonus credit is booked apart from the principal because refunds return the principal only: the bonus is our marketing cost, not your money.
The console billing page keeps balance movements such as top-ups, holds, settlements, refunds and manual adjustments available for reconciliation and disputes. Those records are outside the 30-day cleanup window for task input and output; we do not publish one fixed retention term for every ledger entry. See Data retention.

