Skip to main content
Every Migration API endpoint shares one error model, one idempotency model, and one rate limit.

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-readable error string:
Most errors carry only that string: there’s no field pointer or request-id header today, so branch on the HTTP status and surface 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.
There is an additional per-IP ceiling that absorbs bursts before authentication. It is deliberately not published: it sits well above these per-account limits and you will not meet it through normal use. Over any limit you get a 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:
Wait for one to finish, or cancel one, then retry.

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:
Reads are unaffected, so an account between plans can still retrieve what it already has.

Idempotency

Migration creation is idempotent on the external_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, and canceled as 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.

Data region

Migrations are created in your account’s region. If you need data to land in a specific region for residency reasons, talk to us at roupen@vern.so — additional regions are enabled per account.

Next