Billing
Understand SpicyAPI’s pay-as-you-go USD billing: request quotes, holds, final charges, model-specific refusal terms, adding funds, spend caps and invoices.
Cash, not credits
Prices are quoted in US dollars: "$0.008 per image", "$0.042 per output second". Prepaid funds, promotional grants and credit facilities are denominated in dollars, but a credit limit is not a cash 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.
How paying works, in short
- Add money first. A new account starts with a balance of $0 and there is no free tier. A task you send before adding funds is refused with
40201and costs nothing. - When a task is accepted, its estimated price is set aside. This is called a hold. The money is still yours, but it can't pay for another task at the same time.
- If the task succeeds, you pay for what was actually used — never more than the hold. Anything left over goes back to your balance.
- Failed or expired tasks normally release the full hold. Some content refusals are charged at the accepted quote only when the model page explicitly discloses this exception. Check
settledandcostfor the final charge. - Spend caps that you set on each API key stop a runaway script from emptying your balance.
The rest of this page explains each step in detail.
Adding funds
Add funds under Billing in the console: by card, by crypto, or by manual transfer when that option is shown. You can also turn on auto top-up so long jobs don't stop when the balance runs low. Paid balance does not expire; promotional credit expires on the date shown in Billing.
For the step-by-step walkthrough — payment options, when the money arrives and how auto top-up works — see Account setup. Top-ups are generally not refundable as cash; the refund policy lists the exceptions, such as a duplicate charge.
Three phases
Every task goes through the same three billing steps. Nothing here needs action from you; it is described so you can read your balance history with confidence.
holdsettlereleaseHold
At creation we hold funds for the estimated usage. The amount is exactly the estimatedCost returned by createTask.
A hold reserves the funding sources used by that task; it is not a final charge. Reserved prepaid funds, eligible grants or approved credit cannot fund another task at the same time. It shows up as held in GET /api/v1/chat/credit and as "On hold" in the console.
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.
Settle or release
succeeded→ settled against actual usage, capped at the accepted hold. Unused funds are released to their original funding sources; no later amount is collected above that hold.failed/expired→ normally fully released. A model page may explicitly disclose that certaincontent_rejectedfailures are charged at the accepted quote. 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 completes settlement or release according to the recorded outcome.
Settlement and release share one deduplication key, so a task's billing lifecycle terminates exactly once — retries cannot post it twice.
Read the settlement, not just the task state
Failed or expired tasks normally release the full hold. Some content refusals are charged at the accepted quote only when the model page explicitly discloses this exception. Check settled and cost for the final charge. A charged refusal remains state: "failed" with errorCode: "content_rejected"; once settled, settled is true and cost is positive. Other failures and expired tasks release the full hold automatically.
Checking your balance
The quickest way is the Billing page in the console: it shows your available balance, the amount currently on hold and every balance movement. From code, call:
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 balance = body.data;
console.log(balance.funding ?? balance);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"]}')
balance = body["data"]
print(balance.get("funding", balance)){
"code": 200,
"msg": "success",
"data": {
"available": "128.42",
"held": "0.36",
"total": "128.78"
}
}| Field | Meaning |
|---|---|
available | Net wallet amount; may be negative after approved credit usage and is not a standalone admission check |
held | Locked by tasks in flight |
total | The sum of the two |
When funding eligible for the selected model and time cannot cover the estimate, createTask returns 40201. Credit expiry, exhausted limits and a payment recovery shortfall can also affect admission. Do not replace server checks with a client-side test of the sign of available.
You don't need to check the balance before every generation: task acceptance already enforces available balance and spend caps.
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.
// Keep the exact decimal string for display
const usd = 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()Spend caps
A spend cap is a ceiling you put on how much money can be spent. Up to six limits apply to a task; whichever is reached first rejects the task with 40202, before anything is created or held:
| Limit | Who sets it | msg when reached |
|---|---|---|
| Key lifetime cap | You, on the API keys page. The ceiling for this key's whole life | This key has reached its total spend cap |
| Key monthly cap | You, on the API keys page. The budget for this calendar month | This key has reached its monthly budget |
| Key daily cap | You, on the API keys page. New keys have no cap unless you set one | This key has reached its daily spend cap |
| Team daily limit | Team owners, admins and budget managers, on the Team page. Applies only to keys that belong to a team | Workspace daily budget has been reached |
| Team monthly limit | Same as above | Workspace monthly budget has been reached |
| Platform daily cap | Us. This is our own circuit breaker; you can't raise it | Platform quota for today is exhausted, please try again later |
Daily caps restart at 00:00 UTC; monthly caps follow the UTC calendar month and restart on the 1st. A lifetime cap never resets. If the platform cap is reached, retry after 00:00 UTC, or contact support if it keeps happening.
An unsettled hold counts toward every cap — lifetime, monthly and daily — until the task settles, no matter which day or month it falls in: the money is not spent yet, but it cannot be spent twice either. Team limits are described in Console and teams.
A blank cap field does not mean the same thing everywhere
- Creating or editing a key: a blank cap means no limit, and so does
0. New keys have no cap unless you set one. Clearing a cap on an existing key removes it; to keep a cap, leave a number in the field. - Team spend limits: blank keeps the current value;
0means no limit.
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, with plain labels such as Top-up, On hold, Charged and Returned. When you download that list with Export CSV, the kind column holds one of these stable codes:
| Kind | Meaning |
|---|---|
topup | Money you paid, added to your balance |
bonus | Extra amount from a top-up offer, booked separately from what you paid |
trial | Credit from a trial campaign, separate from top-ups and bonuses |
hold | Estimated amount reserved when a task is accepted; not a charge |
settle | The final task charge, including a content refusal billed under the model’s terms; any unused part of the hold goes back |
refund | The full hold returned because the task failed or timed out |
adjust | A manual correction by our team; always carries an operator identity and a reason |
chargeback | Balance taken back after a payment dispute |
forfeit | Remaining balance given up when you closed your account |
grant_expire | Unused promotional funds removed after expiry |
grant_revoke | Unused promotional funds withdrawn |
The ledger is append-only. Nothing already written is ever rewritten — a returned hold is a new 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 what you paid because a payment refund (money sent back to your card) covers only what you paid; a bonus has no cash value. This is unrelated to the refund entries above, which return a failed task's hold to your balance.
These balance movements are kept for 10 years, separately from task content, so they stay available for reconciliation and disputes long after a task's prompts and results are gone. See Data retention.
Invoices and usage statements
Two documents can be issued yourself from Billing → Invoices in the console, both downloadable as PDF:
| Document | What it covers | Is it a payment document? |
|---|---|---|
| Payment invoice | One top-up you have already paid, for the amount charged | Yes. This is the one to file for reimbursement |
| Usage statement | What you actually spent in one closed month | No. That money was paid when you topped up, so filing both would claim the same money twice |
Enter your details once at the top of Billing → Invoices. A name (your company or your own), address line 1, city and country are required; a tax number (VAT, GST and the like) is optional. They are printed on the document exactly as entered — you supply them and we verify none of it, the tax number included. Only Latin, Greek and Cyrillic characters can be printed, so write a Japanese or Korean company name in its registered romanized form. A document already issued keeps the details that applied when it was created; later edits reach only new ones.
- Each paid top-up and each closed month can be documented once. If a document already exists, download it instead of issuing another.
- To fix wrong details, choose Reissue: the old document is marked voided and a replacement with a new number covers the same item. Use Void without replacing only when a document covers the wrong top-up or month.
- Account statements, further down the same page, are a different thing: a fixed monthly snapshot of every balance movement, for your own bookkeeping. They are not a receipt and not a tax document.
Tax is shown as zero: we are a US supplier, we collect and remit no VAT, GST or sales tax on your behalf, and every document carries a reverse-charge notice, so any tax due where you are stays yours to account for. An issued invoice is never edited — it is voided and reissued, with the reason and the replacement recorded.
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. A few endpoints round that length up to a fixed block — see Length billed in whole blocks | Read the current quantityField and schema; the standard duration field is duration_seconds. Do not assume a missing duration selects the shortest tier. |
per_request | Per call, regardless of parameters | — |
per_1k_tokens | Per token. The API unit is per_1k_tokens; the website and price.perMillionTokens in the catalog show the same prices per million tokens. Input, cached input, cache writes and output can each have their own price | — |
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 model can have different prices for different tiers. Resolution, duration, audio options and reference media may affect the quote, depending on its schema. pricing[].variant identifies a price-card tier; there is no variant field in the request body. Do not assume that label can always be sent as resolution, or calculate every model as starting price × duration. Send the complete input to jobs/quote for the exact estimate and charge cap.
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. The charge is capped at the amount held when the task was accepted: unused funds are released and no later amount is collected above the hold.
Length billed in whole blocks
Most per_second models bill the exact length they produce. A handful bill in fixed blocks instead: the measured length is rounded up to the next whole block before it is priced. Where the block is 8 seconds, a 6-second clip and an 8-second clip cost the same, and a 9-second clip costs two blocks. This is not how every per-second model works — it applies only to the endpoints that declare it, and each of those says so on its own model page.
They have something else in common, and it is what makes the rounding visible in a quote: they take their length from a video you supply rather than from a duration_seconds value you choose, so nobody knows how long the result will be until the clip has been read. Their quantityField is empty, and the figure you are quoted before submitting is therefore the worst case — the longest clip the endpoint accepts:
{
"estimatedCost": "0.96",
"maxCharge": "0.96",
"quantity": "32",
"unit": "per_second"
}The amounts are illustrative; quantity here is a ceiling in seconds, not a measurement. The quoted amount is the amount held; the final charge is computed from the rounded duration and never exceeds the hold. The remainder is released when the task settles, exactly like the three phases above.
You will see a hold that is larger than the final charge
This is the expected behaviour on these endpoints, not an overcharge. Between acceptance and settlement your balance shows the ceiling on hold, because until the clip has been read nobody knows how long the result will be. When the task settles you are charged for the real length rounded up to the block, and the difference returns to your balance on its own. The ledger records both movements, so hold and settle can always be read back against each other.
Two things follow, and both are worth planning for:
- Budget against the quote, not against your clip. Until the task settles, the money set aside is the ceiling, and it counts against your spend caps like any other hold.
- A short clip still costs a whole block. Trimming from 9 seconds to 8 can halve the price of an 8-second-block endpoint; trimming from 8 to 5 changes nothing at all.
Exact units and prices are on the pricing page and each model's own page.
Customer discounts and promotions
Offers can apply to everyone or to one account, across all models or selected models. Ongoing offers run until disabled; timed offers apply only between their actual start and end. Account-specific prices apply when that account signs in or quotes with its API key, and are never published as another customer's price.
Offers do not stack. The lowest price you qualify for applies automatically. For a standard $10 charge, a 10% public offer and a 20% account discount produce an $8 price, not a compounded $7.20 price. When an offer ends or is disabled, new quotes use the best remaining eligible offer, or the standard price if none remains.
Homepage, catalog and model-page offers help you compare options. Obtain the actual estimate from jobs/quote with your identity, model and complete inputs. A countdown does not reserve a price: refresh the quote after an offer, input or price changes. Accepted tasks retain their frozen price even if the offer ends during execution, and the final charge cannot exceed the amount held at acceptance.
Offers apply automatically to API usage charges, not to the amount you top up, and do not provide top-up cashback. Daily or monthly budget examples extrapolate the current price; a timed offer may end before that budget period does, so check its end time as well.
Promotional funds and credit facilities
GET /api/v1/chat/credit retains available, held and total, and can include a funding breakdown. Older responses may omit funding; missing data does not mean zero. Use client.getBalance() in the SDK or spicyapi balance --json in the CLI.
funding.prepaidAvailableUsdcontains prepaid or legacy unrestricted available funds;grantAvailableUsdtotals currently active grants. Each entry ingrantsreports granted, available, held and spent amounts, status, start, expiry andmodelSlugs. An empty model list means all models; otherwise the grant pays only for the listed models. The list contains up to 100 grants, prioritizing grants with remaining or held balances and then ordering by newest first.grantsHasMoresignals additional records. Do not substitute the visible list subtotal for the account total.funding.creditdescribes an approved pay-later facility.limitUsdis its ceiling,availableUsdis unused eligible credit,usedUsdis settled outstanding principal,heldUsdis reserved for accepted tasks, andexpiresAtis its expiry. The credit limit is not a top-up, grant or cash balance. Expiry, disabling or exceeding the limit restricts new requests; existing debt remains payable.- Promotional funds cannot be withdrawn. Discounts set the request price; grants and credit determine how that price is funded. A grant does not lower the model price card. Its model scope and validity apply, and unused amounts can expire or be revoked. Failed tasks and unused reservations release funds to their original source; this does not reactivate an expired or revoked grant.
- A positive
funding.cashShortfallUsdis a shortfall from external payment recovery. Grants and credit cannot cover it; new calls remain blocked until it is covered. A negativeavailablecan instead reflect approved credit usage. Do not infer rejection from its sign or add it directly to the credit limit. Admission checks funding sources, model eligibility, validity and spend caps on the server.
Amounts remain exact USD decimal strings, and dates use RFC3339. Label the timezone when displaying dates. The console billing page shows funding and grant details; changes take effect only after manual administrator approval. To ask about grants or credit facilities, open a conversation under Support in the console or email support@spicyapi.ai. The public API does not offer writes that increase your own allowance or issue grants.
Current API key usage
For a chart of spend per day and per model across your whole account (or a team), use Billing → Usage in the console. The API endpoint below reports on the key that calls it only:
curl 'https://api.spicyapi.ai/api/v1/usage?from=2026-09-01&to=2026-09-06' \
-H "Authorization: Bearer $SPICY_API_KEY"Dates use UTC [from,to), so this example includes September 1–5. Omit dates for the last seven
days including today; the maximum range is 92 days. Results belong only to the current API key.
There is no parameter for another key or an account-wide report; use chat/credit for wallet balance.
data.totalCalls counts all tasks; data.totalSpend includes settled charges only. days and
models group the same tasks by creation date and model, with exact USD decimal strings. Pending
holds are excluded, and late settlement can update earlier dates. Hidden task history remains
included; dates without calls are omitted. Do not add the two groupings together.
Use client.getUsage({ from, to }) in the SDK or spicyapi usage --from 2026-09-01 --to 2026-09-06
in the CLI. This endpoint has its own, tighter rate limit — see Rate limits.

