spicyapiDocs
Main content

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.

UnitMeaningWhich input field supplies the quantity
per_imagePer generated imagewhichever field the model declares; defaults to 1
per_secondPer second of outputwhichever field the model declares (duration on seedance); defaults to the shortest tier
per_requestPer call, regardless of parameters
per_1k_tokensPer 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 / expiredfully released. 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 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/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 available = Number(body.data.available);
if (available < 5) console.warn('balance running low');
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"]}')

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"
  }
}
FieldMeaning
availableImmediately usable for new tasks
heldLocked by tasks in flight
totalThe 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:

LayerWho sets itMessage on hit
Key lifetime capYou, in the consoleThe key's lifetime spend cap was reached
Key daily capYou, in the console. New keys ship with a low oneThe key's daily spend cap was reached
Platform daily capUs. This is our own circuit breakerPlatform 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:

KindMeaning
topupFunds added
bonusPromotional credit from a top-up tier, booked separately from the principal
holdEstimated funds reserved at acceptance; not a charge
settleSettlement against actual usage after success
refundRelease for a non-successful terminal state
adjustManual correction; always carries an operator identity and a reason
chargebackBalance 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.

On this page