Idempotency
https://terrads.com/api/v1Updated September 10, 2026Creating 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 /runsreads the header. The gate endpoints (answer,approve-copy,cancel) are safe to retry on their own: a repeat is a409(not_awaitingornot_cancelable), never a second action.
What happens when a key is seen again
| Situation | Response |
|---|---|
| Same key, same body, the run exists | 200 with the original runId, its current status, and the creditCost that was charged. Nothing is charged again. |
| Same key, different body | 422 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 progress | 409 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 seen | The run is created: 201. |
"Same body" means the same JSON content — key order and whitespace do not matter.
A retry loop that is safe
- Generate the key and persist it.
POST /runswith the key. On a network error or timeout, go to step 2 with the same key and the same body.- On
201or200, record therunIdand start polling. - On
409 idempotency_in_flight, wait and go to step 2. - On
409 idempotency_key_spent, generate a new key and go to step 2 — the earlier attempt is fully refunded. - On any
4xxother 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.