Errors
- Base URL
https://terrads.com/api/v1 - Updated October 22, 2026
Every 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. |
free_quota_exhausted | 402 | A team without a plan has used its free video plans. Subscribe to make more. |
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 (or a video brief's stillModel) is not a model currently available for runs. Read GET /models. |
unsupported_kind | 400 | approve-copy was called on a video run, which has no copy review. |
over_budget | 400 | approve-plan: a voice line is longer than the AI motion scene it plays over. details.variation and details.scene name it; shorten it with POST /runs/{runId}/plan, or send none for that variation's motion. Nothing was charged. |
describe_missing | 400 | approve-plan: a scene the run would make with AI (AI motion, or an AI picture) has nothing in its description yet — a scene added in the app starts empty. details.variation and details.scene name it; describe it in the app, or send none for that variation's motion. Nothing was charged. |
photo_shape | 400 | approve-plan: an AI motion scene starts from the person's photo, and the photo is too wide or too tall for the ad's shape. details.variation and details.scene name the first such scene; pick another photo in the app, or send a preset for that variation's motion (a preset starts its scenes from an AI picture). Nothing was charged. |
motion_unavailable | 400 | approve-plan: an AI motion choice that is not offered right now. Nothing was charged. |
clip_voice | 400 | A voice line on a video plan made from the person's own clip — its scenes keep their own words. |
clip_last_scene | 400 | A change that would remove the last scene cut from the person's clip. Not returned by the API's word edits today; listed because the shared pipeline can name it. |
dont_say | 400 | A scene's own description of how its voice line is read uses one of the brand's don't-say words. Not returned by the API's word edits today; listed because the shared pipeline can name it. |
not_a_still | 400 | reroll: the id is part of the run but is not a still. |
avatar_required | 400 | POST /runs: a video brief named the presenter style, which needs a character to present it. Characters are picked in the app only; nothing was made. |
avatar_unavailable | 503 | Not returned by the API today: a brief naming the presenter style is avatar_required first. Listed because the shared pipeline names it when that style can't be made right now. |
presenter_length | 400 | approve-plan: the presenting character's line takes under 5 s or 15 s or more to say. details.variation and details.scene name it, and details.issue says which: short or long. Change the line with POST /runs/{runId}/plan. Nothing was charged. |
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. |
copy_changed | 409 | The copy drafts were rewritten after the read you sent basedOnSeq from. details carries the current drafts, the concepts that changed and the basedOnSeq to send next. |
plan_changed | 409 | A video run's plans changed after the read you sent basedOnSeq from. details.basedOnSeq is the one to send; read the run again first. Nothing was charged. |
plan_not_ready | 409 | A video run's plan is not complete: a variation could not be planned (retry it with POST /runs/{runId}/plan/retry), or its plan is not there yet. |
plan_retry_limit | 409 | plan/retry: this run has used its five free retries and the variation still could not be planned. Create a new run, with a changed brief. |
retry_in_flight | 409 | reroll: that still is already being made again, or your expectedAttempt is not its attempt any more. List the stills: an attempt one higher means your re-roll happened. Nothing more was charged. |
stills_not_ready | 409 | approve-stills: a still is still being made or checked. Wait until pendingInput.ready equals pendingInput.stills. |
not_cancelable | 409 | The run has already finished, failed or been canceled. |
quote_changed | 409 | The current price is above your maxCredits — on create, on approve-plan or on reroll. 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. On create, details.retry is "new_key": wait for a run to finish, then create again with a new Idempotency-Key — see idempotency. On approve-plan, wait for a run to finish and approve again; nothing was charged. |
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. |
plan_cap_reached | 429 | The team has made as many video plans as fair use allows for now. Try again later; nothing was created. |
model_concurrency_cap | 429 | Too many runs on this model are in flight for the team. Wait or choose another model. |
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. |
approve_failed | 500 | approve-plan was charged but the run could not start: it was canceled and every credit given back. Create the run again. |
retry_failed | 500 | plan/retry could not start the new plan; the variation is still waiting at the plan gate. Retry shortly. |
llm_unavailable | 503 | On create, an image run cannot be started or a video run planned right now; on plan/retry, the variation cannot be planned again right now (the retry is not counted). Nothing was created or charged. Retry in a few minutes. |
cost_table_unavailable | 503 | A video run cannot be priced right now; nothing was charged. Retry in a few minutes. |
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.
An image run is charged when it is created (201); a video run when its plan
is approved (approve-plan), and again for each still you re-roll. No code in
the table above leaves you charged for work that did not start: a refusal
charges nothing, and a run that was accepted or approved but could not start
is canceled and refunded.