Creating exports over the API, rate limits and pagination
The export job lifecycle over HTTP, how credits are reserved and settled, the per-plan rate limits, and the pagination shape every list endpoint uses.
Creating an export
POST /api/v1/exports
{
"name": "Accountants — Oslo",
"query": { …the same body you sent to /search… },
"format": "csv", // csv | xlsx | json | parquet
"columns": ["name", "email", "phone"],
"select_all_matching": true
}
// -> 202
{ "id": "exp_9f2c…", "status": "queued",
"estimated_rows": 638, "credits_reserved": 638 }
columns is optional and must be a subset of
the 34 canonical group names; omit it for all of them. Note that a
group is not a column — eight of them expand to more than one, and a full selection is
64 columns from 34 names. The file is always written in canonical order
regardless of the order you list them in.
format must be one your plan includes: csv and xlsx
everywhere, json from Growth, parquet from Scale. Asking for one you
do not have returns 403 forbidden rather than silently downgrading you.
select_all_matching has to be set deliberately
There is no ambiguity in the API — the field is explicit. The UI equivalent is the trap described in select on page vs select all matching.
Polling it
GET /api/v1/exports/exp_9f2c…
{ "id": "exp_9f2c…", "status": "running",
"progress": { "done": 412, "total": 638 } }
Statuses are queued → running → complete |
incomplete | failed. incomplete is a normal
outcome, not an error — partial rows are written and the job can resume. Full
explanation in what happens after you press Export.
Poll about once a second for small jobs and back off for large ones.
The rest of the surface
| Endpoint | Does |
|---|---|
GET /api/v1/exports | List: id, name, rows, status, format, credits_used, created_at, downloaded_at |
GET /api/v1/exports/:id | One export, with progress |
GET /api/v1/exports/:id/download | 302 to a signed URL, or streams the file. Free, repeatable. |
POST /api/v1/exports/:id/resume | Re-queues an incomplete job. Bills only newly written rows. |
DELETE /api/v1/exports/:id | Removes the export and its file. Does not refund settled credits. |
Credits, precisely
- Reserved on enqueue, for
estimated_rows. - Settled on completion against rows actually written; the difference is refunded.
- Released in full on
failed. - Resume bills only what it newly writes.
- Re-download costs nothing, ever.
Export creation is idempotent per request, so a retried POST after a timeout
cannot double-charge you. See how lead credits work.
Rate limits
| Plan | Requests per second |
|---|---|
| Starter | No API access |
| Growth | 10 |
| Scale | 50 |
| Enterprise | 200 |
Ten a second sounds tight and is not, because the expensive operation is not the request. A search is one call that returns a count over the entire match set, and an export is one call that runs for minutes on our side. The only thing that genuinely presses against the limit is a tight polling loop, which is why the advice above is to back off rather than to poll faster.
Exceeding it returns 429 with rate_limited. Back off and retry; a
plain exponential backoff starting at one second is more than enough. Your current usage is
published in the response headers rather than discovered by being cut off, so read it there.
Pagination
Every list endpoint takes ?page=1&per_page=50 (max 200) and answers with
the same envelope:
{ "data": […], "page": 1, "per_page": 50, "total": 1234 }
Conventions worth knowing
- Money is in minor units (cents).
- Timestamps are ISO-8601 UTC with a trailing
Z. - Errors always use the
{ error: { code, message, details } }shape.
Prefer natural language to HTTP? The MCP server exposes the same operations as agent tools.
Updated on: