Status codes
Run-level failures are different from request errors: a
POST /runs request can
return 201 (accepted) and then resolve to a failed run. Read the run’s
error field for the reason. See Poll a run.
Error body
Every error response is a JSON object with a single human-readableerror
string:
error for debugging.
The one exception is the concurrent-run limit, which adds a
machine-readable code of run_concurrency_limit alongside the runs that
triggered it.
Rate limits
Limits are counted per account, not per API key or per IP — keys belonging to the same account share one budget, and using a key from several machines doesn’t multiply it. Each endpoint belongs to one of four classes, sized to what it costs us to serve:
Two actions are capped more tightly still, because each one provisions something
new:
Starting a run with
kind of update, clarify or execute is not capped at
one per hour — those continue a run you’ve already started rather than beginning
new work — but they do count against the Agent class above.
The read budget assumes the documented polling cadence of
~10 seconds across up to
3 concurrent runs, which is 18 requests per minute, leaving
room for previews and logs. If you poll faster you will hit the limit without
learning anything sooner — use the
thread stream for live progress instead.
429:
429 responses carry headers you should prefer over guessing:
Space your run polling out so a fleet of concurrent migrations doesn’t burn the
read budget on status checks — or switch to the
thread stream, which doesn’t spend a read per
update.
Concurrent runs
An account may have at most 3 runs in progress at a time, across every migration. This is separate from the per-migration rule — one run at a time per migration — and applies on top of it. Only runs that are actually working count:queued, running, and blocked
(waiting on your answer). A run sitting in awaiting_approval is waiting on you,
not consuming capacity, so it doesn’t count against the limit.
Exceeding it returns 429 with a machine-readable code and the runs responsible:
File limits
A migration run is bounded by how many files it consumes, not by how many you upload over its lifetime:
Only PDF and Word documents count toward the first limit — they go through
document parsing, which is the expensive path. Spreadsheets and CSVs
(
.xlsx, .xls, .csv) do not count toward it, only toward the 400.
The per-request limit applies to a single
POST /migrations/{id}/files call. It is not a
ceiling on the migration — request URLs in batches of 25 and upload as many
times as you need, up to the 400 total.
Over any of these you get a 400 naming which limit was hit and what the run
would have totalled.
Plans
The endpoints that create work —POST /migrations,
starting a run and
starting an export — require an active
subscription, and return 403 without one:
Idempotency
Migration creation is idempotent on theexternal_id you choose. Re-sending
POST /migrations with the same external_id
(and the same source) returns the existing migration — with a 200 instead
of a 201 — rather than creating a second one. This makes create retries safe
and doubles as recovery: if you lose a migration ID, re-POST with the same
external_id to get it back.
Runs are not keyed by an idempotency token. Instead, only one run runs at a
time per migration — starting a second while one is in flight returns 409 with
the active run_id, which is how you recover a run you lost track of. See
Start a run. Across migrations, an account is also
capped at 3 runs in progress.
Polling conventions
- Poll runs on an interval of ≈2s, backing off as the run ages.
- Treat
awaiting_approval,completed,failed, andcanceledas terminal for a given run; stop polling. - Make your handling idempotent — the same terminal state can be read more than once.
- Prefer the thread stream (SSE) over tight polling when you want live progress.