spicyapiDocs
Main content

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

  1. 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 40201 and costs nothing.
  2. 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.
  3. If the task succeeds, you pay for what was actually used — never more than the hold. Anything left over goes back to your balance.
  4. 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.
  5. 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.

hold
settlerelease
Funds are held when the task is accepted, then resolved exactly once: settled against actual usage, or released in full.

Hold

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 certain content_rejected failures are charged at the accepted quote. Historical canceled records 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/credit
curl
curl https://api.spicyapi.ai/api/v1/chat/credit \
  -H "Authorization: Bearer $SPICY_API_KEY"
JavaScript
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);
Python
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"
  }
}
FieldMeaning
availableNet wallet amount; may be negative after approved credit usage and is not a standalone admission check
heldLocked by tasks in flight
totalThe 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:

LimitWho sets itmsg when reached
Key lifetime capYou, on the API keys page. The ceiling for this key's whole lifeThis key has reached its total spend cap
Key monthly capYou, on the API keys page. The budget for this calendar monthThis key has reached its monthly budget
Key daily capYou, on the API keys page. New keys have no cap unless you set oneThis key has reached its daily spend cap
Team daily limitTeam owners, admins and budget managers, on the Team page. Applies only to keys that belong to a teamWorkspace daily budget has been reached
Team monthly limitSame as aboveWorkspace monthly budget has been reached
Platform daily capUs. This is our own circuit breaker; you can't raise itPlatform 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; 0 means 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:

KindMeaning
topupMoney you paid, added to your balance
bonusExtra amount from a top-up offer, booked separately from what you paid
trialCredit from a trial campaign, separate from top-ups and bonuses
holdEstimated amount reserved when a task is accepted; not a charge
settleThe final task charge, including a content refusal billed under the model’s terms; any unused part of the hold goes back
refundThe full hold returned because the task failed or timed out
adjustA manual correction by our team; always carries an operator identity and a reason
chargebackBalance taken back after a payment dispute
forfeitRemaining balance given up when you closed your account
grant_expireUnused promotional funds removed after expiry
grant_revokeUnused 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:

DocumentWhat it coversIs it a payment document?
Payment invoiceOne top-up you have already paid, for the amount chargedYes. This is the one to file for reimbursement
Usage statementWhat you actually spent in one closed monthNo. 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.

UnitMeaningWhich input field supplies the quantity
per_imagePer generated imagewhichever field the model declares; defaults to 1
per_secondPer second of output. A few endpoints round that length up to a fixed block — see Length billed in whole blocksRead the current quantityField and schema; the standard duration field is duration_seconds. Do not assume a missing duration selects the shortest tier.
per_requestPer call, regardless of parameters—
per_1k_tokensPer 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.prepaidAvailableUsd contains prepaid or legacy unrestricted available funds; grantAvailableUsd totals currently active grants. Each entry in grants reports granted, available, held and spent amounts, status, start, expiry and modelSlugs. 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. grantsHasMore signals additional records. Do not substitute the visible list subtotal for the account total.
  • funding.credit describes an approved pay-later facility. limitUsd is its ceiling, availableUsd is unused eligible credit, usedUsd is settled outstanding principal, heldUsd is reserved for accepted tasks, and expiresAt is 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.cashShortfallUsd is a shortfall from external payment recovery. Grants and credit cannot cover it; new calls remain blocked until it is covered. A negative available can 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.

On this page