• Base URL https://terrads.com/api/v1
  • Updated October 13, 2026

Creating a run charges credits. Networks fail, clients time out, and a retry without protection would create — and charge — a second run. So POST /runs requires an Idempotency-Key header, and the API guarantees that one key creates at most one run for your team.

curl -s -X POST "$BASE/runs" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: order-8812-hero-v1" \
  -H "Content-Type: application/json" \
  --data @run.json

A request without the header is a 400 with idempotency_key_required. Nothing is charged.

The key

  • Any string of 1 to 255 characters from letters, digits and _ . : -.
  • Choose it before sending the request and store it with the thing that caused the request (the order, the campaign row, the job). A random UUID works; a key derived from your own record id is better, because it survives your process restarting.
  • Keys are scoped to your team. Another team using the same string is unrelated to you.
  • Only POST /runs reads the header. The gate endpoints (answer, approve-copy, cancel) are safe to retry on their own: a repeat is a 409 (not_awaiting or not_cancelable), never a second action.

What happens when a key is seen again

SituationResponse
Same key, same body, the run exists200 with the original runId, its current status, and the creditCost that was charged. Nothing is charged again.
Same key, different body422 idempotency_key_reused. The key is bound to the first body; send the new body with a new key.
Same key, first request still in progress409 idempotency_in_flight. Wait a moment and retry the same request; you will get the replay above.
Same key, an earlier attempt was charged and then refunded (it could not start)409 idempotency_key_spent. Nothing is held; create the run again with a new key.
Key never seenThe run is created: 201.

"Same body" means the same JSON content — key order and whitespace do not matter.

A retry loop that is safe

  1. Generate the key and persist it.
  2. POST /runs with the key. On a network error or timeout, go to step 2 with the same key and the same body.
  3. On 201 or 200, record the runId and start polling.
  4. On 409 idempotency_in_flight, wait and go to step 2.
  5. On 409 idempotency_key_spent, generate a new key and go to step 2 — the earlier attempt is fully refunded.
  6. On any 4xx other than those (400, 402, 403, 422), fix the request or the account and go to step 2 with a new key. A refused create never charges, and the refusal is not a replay — but a new key keeps your records unambiguous.

Refusals that are not replays

Some refusals happen before anything is charged, and the key is genuinely untouched: validation errors (400), a plan or credit problem (402, 403), a price above your maxCredits (409 quote_changed). You may resend with the same key once the problem is fixed.

too_many_active_runs (429) — your team already has as many runs in progress as it may — is the exception. Depending on exactly when the run was refused, the key may be untouched or it may already be spent, and the API cannot tell you which; a same-key retry could answer 409 idempotency_key_spent. So the response carries details.retry: "new_key": when your active runs have dropped, create the run again with a new key. You are never charged twice either way.

Video runs

Creating a video run charges nothing, and the key works the same way: the same key and body replays the run (200), a different body under it is 422 idempotency_key_reused, and a request still being processed is 409 idempotency_in_flight. A replay reports the run as it is now — once its plan was approved, creditCost is what the approval charged. A video create that was accepted but could not start answers 500 create_failed, as an image create does; create it again with a new key.