Per-request billing: request IDs and billed amounts
Every call has a request ID and one final billed amount. LLM requests return the ID in the x-apimodels-request-id response header and the amount in the response body under usage.cost (on streams, in the final chunk that carries usage); non-streaming requests also carry x-apimodels-cost. For image, video and audio tasks the ID is the task_id returned at creation, and the creation response tells you how much was put on hold. GET /v1/records/{id} with any of these IDs returns exactly what that request took from your balance. Built for customers who resell us with our written permission (Terms, section 6; write to business@apimodels.app) or need to reconcile each of their own charges against ours.
De un vistazo
- Endpoint
- GET /api/v1/records/{task_id}
- Autenticación
- Authorization: Bearer <API_KEY>
- Precios
- Free (the lookup is never charged)
- Response headers
- x-apimodels-request-id · x-apimodels-cost
- Response body
- usage.cost · reservedCredits
- Covers
- Every LLM, image, video and audio call
- Moneda
- USD
Where the request ID comes from
Every successful response from the LLM endpoints (/v1/chat/completions, /v1/messages, the native Gemini /v1beta, /v1/responses and /v1/completions) carries the x-apimodels-request-id header; its value is the task_id of that request. Non-streaming responses also carry x-apimodels-cost: the amount actually deducted, in USD, after every discount on your account, written only once the deduction has completed. Streaming responses carry the ID only, because the amount is known when the last chunk settles; look the record up with the ID.
HTTP/2 200
content-type: application/json
x-apimodels-request-id: cmumilhfd0013mg012q3qobr9
x-apimodels-cost: 0.0001
access-control-expose-headers: x-apimodels-request-id, x-apimodels-costusage.cost in the response body
OpenAI and Anthropic SDKs usually hide response headers, so the same amount is also written into the response body as usage.cost. In a non-streaming response usage.cost.charged is the amount deducted, the same number as x-apimodels-cost.
{
"id": "chatcmpl-…",
"choices": [ … ],
"usage": {
"prompt_tokens": 165,
"completion_tokens": 6,
"cost": { "currency": "USD", "amount": 0.0001, "discount": 1, "charged": 0.0001 }
}
}Streaming responses put usage.cost in the final chunk that carries usage: the usage chunk you get with stream_options.include_usage in the OpenAI format, message_delta in the Anthropic format (/v1/messages), and response.usage inside response.completed on /v1/responses. That chunk goes out before the deduction is written, so charged is the amount due under the same formula; only a request whose balance ran short ends up deducted for less, and the record lookup is authoritative. message_start carries input usage only and has no cost.
# OpenAI 格式(stream_options.include_usage = true):最后的用量帧
data: {"object":"chat.completion.chunk","choices":[],"usage":{"prompt_tokens":165,"completion_tokens":6,"cost":{"currency":"USD","amount":0.0001,"discount":1,"charged":0.0001}}}
data: [DONE]
# Anthropic 格式(/v1/messages):message_delta
event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"input_tokens":165,"output_tokens":6,"cost":{"currency":"USD","amount":0.0001,"discount":1,"charged":0.0001}}}| Field | Required | Type | Description |
|---|---|---|---|
| usage.cost.currency | — | string | Always "USD" |
| usage.cost.amount | — | number | What this request costs at list price, before any discount |
| usage.cost.discount | — | number | Your account discount as a multiplier: 1 = none, 0.95 = 5% off |
| usage.cost.charged | — | number | The charge after the discount. Non-streaming: the amount deducted; streaming: the amount due (see above) |
usage.cost is returned by /v1/chat/completions, /v1/messages, /v1/responses and /v1/completions; native Gemini /v1beta responses do not include it, so use the record lookup there.
One exception: when a non-streaming LLM request runs past about 95 seconds, the gateway sends the response headers early to keep the connection alive, so the two headers cannot carry values; the same two values are then added to the response body under an apimodels field. The OpenAI and Anthropic SDKs ignore the unknown field.
{
"id": "chatcmpl-…",
"choices": [ … ],
"usage": { … },
"apimodels": { "request_id": "cmumilhfd0013mg012q3qobr9", "cost": 0.0001 }
}For image, video and audio tasks the ID is the data.taskId returned at creation. The creation response also carries reservedCredits: the amount put on hold from your balance when the task was accepted, which is the most this task can cost. On completion it settles at the actual amount and the rest is released; a failed task releases all of it.
{
"code": 200,
"msg": "success",
"data": {
"taskId": "cmum4dq3z00dwlm01wugboo5z",
"state": "pending",
"model": "seedance-2.0",
"reservedCredits": 0.46,
"currency": "USD"
}
}Once the task finishes, both the GET ?task_id= result and the callback (callback_url) carry credits (the final amount deducted) and currency in data, equal to what the record lookup returns.
{
"code": 200,
"msg": "success",
"data": {
"taskId": "cmum4dq3z00dwlm01wugboo5z",
"state": "completed",
"credits": 0.338,
"currency": "USD",
"resultUrls": ["https://r2.apimodels.app/videos/cmum4dq3z00dwlm01wugboo5z.mp4"]
}
}When calling from a browser, both headers are exposed through Access-Control-Expose-Headers, so response.headers.get() can read them.
Look up a record
GET /api/v1/records/{task_id}
Authorization: Bearer <API_KEY>Example: call, then reconcile that one request
cURL
curl https://api.apimodels.app/v1/records/cmumilhfd0013mg012q3qobr9 \
-H "Authorization: Bearer $API_KEY"Python
import requests, os
# 1) make the call and keep the request id from the response headers
r = requests.post(
"https://api.apimodels.app/v1/chat/completions",
headers={"Authorization": f"Bearer {os.environ['API_KEY']}"},
json={"model": "claude-haiku-5-5", "messages": [{"role": "user", "content": "Hi"}]},
)
request_id = r.headers["x-apimodels-request-id"]
print("charged now:", r.headers.get("x-apimodels-cost")) # non-streaming only
# 2) any time later: the authoritative billed amount for that one request
rec = requests.get(
f"https://api.apimodels.app/v1/records/{request_id}",
headers={"Authorization": f"Bearer {os.environ['API_KEY']}"},
).json()["data"]
print(rec["settled"], rec["credits"], rec["currency"])Node.js
const headers = { Authorization: "Bearer " + process.env.API_KEY };
// 1) make the call and keep the request id from the response headers
const r = await fetch("https://api.apimodels.app/v1/chat/completions", {
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({ model: "claude-haiku-5-5", messages: [{ role: "user", content: "Hi" }] }),
});
const requestId = r.headers.get("x-apimodels-request-id");
console.log("charged now:", r.headers.get("x-apimodels-cost")); // non-streaming only
// 2) any time later: the authoritative billed amount for that one request
const rec = await fetch("https://api.apimodels.app/v1/records/" + requestId, { headers });
const { data } = await rec.json();
console.log(data.settled, data.credits, data.currency);Respuesta
{
"code": 200,
"msg": "success",
"data": {
"task_id": "cmumilhfd0013mg012q3qobr9",
"model": "claude-haiku-5-5",
"type": "LANGUAGE_MODEL",
"state": "completed",
"settled": true,
"credits": 0.0001,
"currency": "USD",
"usage": {
"input_tokens": 165,
"output_tokens": 6,
"cached_input_tokens": 149,
"cache_creation_tokens": 0
},
"created_at": 1790676571513,
"completed_at": 1790676572799
}
}| Field | Required | Type | Description |
|---|---|---|---|
| data.task_id | — | string | The request ID, identical to the header value or the taskId returned at creation |
| data.model | — | string | Public model name, e.g. claude-haiku-5-5 |
| data.type | — | string | LANGUAGE_MODEL / TEXT_TO_IMAGE / IMAGE_TO_VIDEO / … as on the call record |
| data.state | — | string | pending (still running) / completed / failed |
| data.settled | — | boolean | true once the amount is final; while false, credits is null — poll again later |
| data.credits | — | number | null | USD actually taken from the balance, after account discounts and per-model prices; 0 for failed requests |
| data.currency | — | string | Always "USD" |
| data.usage | — | object | LLM only: input_tokens, output_tokens, cached_input_tokens, cache_creation_tokens; reasoning tokens are inside output_tokens |
| data.created_at | — | number | Creation time, Unix milliseconds |
| data.completed_at | — | number | null | Completion time, Unix milliseconds; null until done |
What the amount means
- credits is what finally left the balance, not list price times usage: account discounts and negotiated per-model prices are already applied.
- Failed requests are always 0. Upstream errors, requests rejected by our validation, and timeouts are not charged.
- Streaming LLM calls settle on the last chunk; video models billed by output length settle once the clip exists. Until then settled is false.
- x-apimodels-cost, usage.cost.charged in a non-streaming response and the record’s credits are the same number; the record is the source of truth, the header and body just save you a lookup.
- usage.cost.charged is roughly amount × discount. For LLM calls both amount and charged are rounded to 4 decimal places with a $0.0001 minimum per request; on accounts with negotiated per-model prices, charged is the number to use.
- reservedCredits is a ceiling, not the bill: while held, that part of your balance cannot be used by other requests, and whatever is not charged returns to your available balance at settlement.
- Amounts are kept to 6 decimal places (one millionth of a dollar).
Errores
- 401 — Clave API ausente o no válida
- 404 — Record not found. A request ID that belongs to another account also returns 404; its existence is never confirmed
- 500 — Error interno del servidor
Preguntas frecuentes
How do I find out what one API call cost me?
Read x-apimodels-request-id from the response headers (LLM) or keep the task_id returned at creation (image, video, audio), then call GET https://api.apimodels.app/v1/records/{id} with your API key. data.credits is the USD amount actually deducted after all discounts; failed requests are 0. Non-streaming LLM responses also carry the amount directly in x-apimodels-cost.
Why is credits null and settled false?
The request has not finished settling. Streaming LLM calls settle on the last chunk, and some video models settle on the actual output length. Poll the record again after completion; once settled is true the amount is final and will not change.
Do streaming responses include the cost header?
No. Streaming responses carry only x-apimodels-request-id, because the amount is known when the stream ends. Query GET /v1/records/{id} after the final chunk. Non-streaming responses carry both headers; a non-streaming LLM request that runs past about 95 seconds gets the same two values in an apimodels field in the response body instead, because the headers were already sent to keep the connection alive.
Can I look up another account's request?
No. A request ID that belongs to a different account returns 404, exactly like a missing one; the API never confirms whether such an ID exists. The lookup itself is free and is not counted as a call.