Errors
Base URL
https://terrads.com/api/v1Updated September 10, 2026Every error, on every endpoint, is JSON in one shape:
{ "error": { "code": "insufficient_credits", "message": "…", "details": { } } }
codeis stable and is what your code should branch on.messageis a human-readable sentence. It may change; do not parse it.detailsis present only where noted below.
Messages never contain internal identifiers, stack traces or upstream error
text. Error responses are never cached (Cache-Control: no-store).
Codes
| Code | Status | What it means, and what to do |
|---|---|---|
invalid_api_key | 401 | No usable bearer token. Check the header and the key; a revoked key answers the same way. |
past_due | 402 | The subscription's last payment failed. Update the card in the app; keys resume without changes. |
insufficient_credits | 402 | The run costs more than the team's balance. Top up or lower the run's size. Nothing was charged. |
no_subscription | 403 | The team has no active plan. |
upgrade_required | 403 | The plan does not include API access. |
insufficient_scope | 403 | The key's scopes do not cover this endpoint. Not returned for keys created today, which carry every scope. |
not_found | 404 | The run or brand does not exist, or belongs to another team — the API does not distinguish. |
invalid_input | 400 | The body or query failed validation. Fix and resend. |
invalid_product | 400 | productAssetId is not a product your team can use. |
invalid_reference | 400 | A reference in references is not an asset your team can use. |
invalid_angle | 400 | angleId is not an angle of the given brand. |
invalid_audience | 400 | audienceId is not an audience of the given brand. |
invalid_model | 400 | modelSlug is not a model currently available for runs. Read GET /models. |
missing_answers | 400 | The answer request left an open question unanswered. Answer all of them in one request. |
idempotency_key_required | 400 | POST /runs was sent without an Idempotency-Key header, or with one that is empty, longer than 255 characters, or uses characters outside letters, digits and _ . : -. |
no_active_brand | 400 | Not returned today: brandId is required by validation, so a missing brand is invalid_input first. Listed because the shared pipeline can name it. |
wrong_gate | 409 | The run is paused, but on the other gate. Read pendingInput.gate and use the matching endpoint. |
not_awaiting | 409 | The run is not paused for input right now. Poll and read pendingInput. |
not_cancelable | 409 | The run has already finished, failed or been canceled. |
quote_changed | 409 | The current price is above your maxCredits. details.creditCost is the current price. Nothing was charged. |
idempotency_in_flight | 409 | A request with this key is still being processed. Wait, then resend the same request. |
idempotency_key_spent | 409 | An earlier attempt with this key was charged and refunded. Create the run again with a new key. |
idempotency_key_reused | 422 | This key was already used with a different body. Use a new key. |
too_many_active_runs | 429 | The team already has as many runs in progress as it may. details.retry is "new_key": wait for a run to finish, then create again with a new Idempotency-Key — see idempotency. |
rate_limited | 429 | Too many requests in the window. Honour Retry-After. See rate limits. |
limit_reached | 429 | Not returned today: the API creates no brands or other counted objects. Listed because the shared pipeline can name it. |
model_concurrency_cap | 429 | Too many runs on this model are in flight for the team. Wait or choose another model. |
daily_limit_reached | 429 | The team's fair-use credit ceiling for the rolling day was reached. Try later. |
too_many_pending_generations | 429 | Too much unfinished work is queued for the team. Wait for it to drain. |
create_failed | 500 | The run was accepted but could not be started. It was canceled and refunded; create it again with a new key. |
cost_breakdown_unavailable | 500 | The run could not be priced. Retry later; nothing was charged. |
internal_error | 500 | Something failed on our side. Retry with backoff; if it persists, contact support with the time and the endpoint. |
Retrying
5xxand429 rate_limited: retry with backoff (Retry-Afteron 429).409 idempotency_in_flight: retry the identical request shortly.4xxotherwise: the request or the account needs changing first. Repeating it unchanged returns the same answer.- Retrying
POST /runswith the sameIdempotency-Keycan never charge you twice — that is what the header is for. It can, however, answer409 idempotency_key_spent, which asks for a new key; the two429s above differ on this point, so read idempotency before you write the loop.
Charging happens only when a run is created (201). No code in the table
above leaves you charged for a run that did not start: a refusal before
acceptance charges nothing, and a run that was accepted but could not start
is canceled and refunded.