Media billing, recovery, and errors
Understand USD quotes and charges, shared key limits, unknown task states, and API errors.
Quotes, wallet funds, and key limits
First call POST /v1/media/quote. Check currency: "USD", policy_version: "usd-wallet-v1", and the exact string quoted_usd, then pass that amount as max_cost_usd. The quote is for the entire task. A catalog unit price only explains the rate; videos may be billed per second.
The account's USD wallet pays the actual charge. The key's spending limits provide additional budget controls. LLM, image, video, and music requests share the same key's total and time-window limits. Unsettled tasks also reserve available budget. Even if the key has no total limit, actual usage still has to be paid from the account wallet.
A task's billing includes quoted_usd, held_usd, charged_usd, released_usd, and state. A hold is not an actual charge, and a release is not a new top-up. Reconcile the final cost using charged_usd and the actual console bill.
| State | Next step |
|---|---|
held | Budget is reserved. Continue querying the same task. |
settling | Settlement is in progress. Continue querying; do not create a replacement task. |
charged | Cost settlement is complete; the final charge may be zero. Download once generation has also succeeded. |
released | The hold was released. Check generation status and billing, then stop this example. |
released_unknown | The wallet hold may be released, but the upstream result and final cost remain unknown. Keep the task ID and account for the outstanding budget obligation; query or contact support. |
Generation success and settlement charged are separate conditions. Generation success alone does not establish that the full workflow is complete. Charges confirmed later may appear after an unknown state.
A successfully settled zero-cost task still uses billing.state: "charged", with charged_usd equal to zero, held_usd equal to zero, and the entire reserved amount recorded in released_usd. Only a trusted completed result and verified zero-cost settlement acknowledgment establish this outcome. Missing or unknown usage is not evidence of a zero charge. The existing success-and-settlement checks remain applicable; no separate download state is required.
Query actual spending
GET /v1/usage returns raw JSON without the media APIs' data wrapper. Only relevant fields are shown below; the response may include additional fields.
Example for a key with a total limit:
{
"mode": "quota_limited",
"quota": {
"limit": 1,
"used": 0.1396,
"remaining": 0.8604
}
}
Example for a key without a total limit:
{
"mode": "unrestricted",
"usage": {
"total": {
"cost": 0.1,
"actual_cost": 0.15
}
}
}
For actual spending, read quota.used when present; for a key without a total limit, read usage.total.actual_cost. A key with only a time-window limit may also be quota_limited without quota; use actual_cost in that case. usage.total.cost is a reference amount before rate adjustments, not the actual charge. The legacy balance field is not the account's USD wallet balance. Check wallet funds in the console.
Save usage before and after the run, then compare the change with the sum of each task's charged_usd. Other concurrent LLM or media calls on the same key are included in the usage change. Isolate the run or reconcile by task ID; do not assume the entire change belongs to these tasks.
Idempotency and recovery after interruption
Save the request body and Idempotency-Key before creating a task. Save the task ID immediately after receiving HTTP 202. Replaying with the same key, the same idempotency key, and the identical request body should return the same ID. Changing the prompt, option, or max_cost_usd creates a different request body.
If you know the ID, resume with GET /v1/media/tasks/{task_id}; do not create again. If the POST result is unknown and you have no ID, keep the original request and idempotency key. Query this key's history with GET /v1/media/tasks?limit=20 and paginate with next_cursor, or contact support to confirm acceptance. Do not automatically retry an unknown POST, delete the state file, or guess by recreating with a new idempotency key. The standalone client persists sent before the network request and saves the ID immediately after receiving it. After an unknown result, restarting stops before any API call.
Rate limits and errors
Each key can make up to 120 media requests per minute, including catalog, quote, task query, and history requests. Creation POSTs are limited to 10 per minute, including idempotent replays. The maximum number of unfinished tasks is 10 per key and 30 per user. The client creates tasks sequentially, spacing ordinary requests by at least 0.7 seconds, creation and replay requests by at least 7 seconds, and polling by 5 seconds.
| HTTP | Common cause and action |
|---|---|
| 400 | Invalid request fields, option, or cost ceiling. Check the current quote and exact USD string. |
| 401 | The key is invalid, expired, or revoked. Check the original key without exposing it in logs. |
| 403 | The key type or restrictions do not meet requirements, or content moderation rejected the request. Check the unified key and input. |
| 404 | The task does not exist or belongs to another key. Use the key that created it. |
| 402 / 403 | Insufficient wallet funds or key allowance. Check actual console funds and key limits. |
| 409 | Idempotency body conflict or concurrency limit. Keep the original task; do not blindly recreate it. |
| 429 | Too many requests. Follow Retry-After and reduce frequency. Do not automatically resend a creation request whose result is unknown. |
| 503 | The service or settlement capability is unavailable. Save state; continue querying known IDs, and confirm an unknown POST's result first. |
Media errors return an HTTP status and {code,message,reason,data:null}. Keep the status code, task ID, and original idempotency key so support can investigate. Do not publish your key or private upstream details. Public downloads are completely separate requests: do not send a Bearer token or allow a downgrade to HTTP.
UniRoute