• 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": { } } }
  • code is stable and is what your code should branch on.
  • message is a human-readable sentence. It may change; do not parse it.
  • details is 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

CodeStatusWhat it means, and what to do
invalid_api_key401No usable bearer token. Check the header and the key; a revoked key answers the same way.
past_due402The subscription's last payment failed. Update the card in the app; keys resume without changes.
insufficient_credits402The run costs more than the team's balance. Top up or lower the run's size. Nothing was charged.
free_quota_exhausted402A team without a plan has used its free video plans. Subscribe to make more.
no_subscription403The team has no active plan.
upgrade_required403The plan does not include API access.
insufficient_scope403The key's scopes do not cover this endpoint. Not returned for keys created today, which carry every scope.
not_found404The run or brand does not exist, or belongs to another team — the API does not distinguish.
invalid_input400The body or query failed validation. Fix and resend.
invalid_product400productAssetId is not a product your team can use.
invalid_reference400A reference in references is not an asset your team can use.
invalid_angle400angleId is not an angle of the given brand.
invalid_audience400audienceId is not an audience of the given brand.
invalid_model400modelSlug (or a video brief's stillModel) is not a model currently available for runs. Read GET /models.
unsupported_kind400approve-copy was called on a video run, which has no copy review.
over_budget400approve-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_missing400approve-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_shape400approve-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_unavailable400approve-plan: an AI motion choice that is not offered right now. Nothing was charged.
clip_voice400A voice line on a video plan made from the person's own clip — its scenes keep their own words.
clip_last_scene400A 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_say400A 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_still400reroll: the id is part of the run but is not a still.
avatar_required400POST /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_unavailable503Not 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_length400approve-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_answers400The answer request left an open question unanswered. Answer all of them in one request.
idempotency_key_required400POST /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_brand400Not returned today: brandId is required by validation, so a missing brand is invalid_input first. Listed because the shared pipeline can name it.
wrong_gate409The run is paused, but on the other gate. Read pendingInput.gate and use the matching endpoint.
not_awaiting409The run is not paused for input right now. Poll and read pendingInput.
copy_changed409The 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_changed409A 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_ready409A 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_limit409plan/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_flight409reroll: 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_ready409approve-stills: a still is still being made or checked. Wait until pendingInput.ready equals pendingInput.stills.
not_cancelable409The run has already finished, failed or been canceled.
quote_changed409The 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_flight409A request with this key is still being processed. Wait, then resend the same request.
idempotency_key_spent409An earlier attempt with this key was charged and refunded. Create the run again with a new key.
idempotency_key_reused422This key was already used with a different body. Use a new key.
too_many_active_runs429The 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_limited429Too many requests in the window. Honour Retry-After. See rate limits.
limit_reached429Not returned today: the API creates no brands or other counted objects. Listed because the shared pipeline can name it.
plan_cap_reached429The team has made as many video plans as fair use allows for now. Try again later; nothing was created.
model_concurrency_cap429Too many runs on this model are in flight for the team. Wait or choose another model.
too_many_pending_generations429Too much unfinished work is queued for the team. Wait for it to drain.
create_failed500The run was accepted but could not be started. It was canceled and refunded; create it again with a new key.
cost_breakdown_unavailable500The run could not be priced. Retry later; nothing was charged.
approve_failed500approve-plan was charged but the run could not start: it was canceled and every credit given back. Create the run again.
retry_failed500plan/retry could not start the new plan; the variation is still waiting at the plan gate. Retry shortly.
llm_unavailable503On 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_unavailable503A video run cannot be priced right now; nothing was charged. Retry in a few minutes.
internal_error500Something failed on our side. Retry with backoff; if it persists, contact support with the time and the endpoint.

Retrying

  • 5xx and 429 rate_limited: retry with backoff (Retry-After on 429).
  • 409 idempotency_in_flight: retry the identical request shortly.
  • 4xx otherwise: the request or the account needs changing first. Repeating it unchanged returns the same answer.
  • Retrying POST /runs with the same Idempotency-Key can never charge you twice — that is what the header is for. It can, however, answer 409 idempotency_key_spent, which asks for a new key; the two 429s 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.