Exports
Ask for every response as one file, and collect it when it is ready.
The read API pages through responses 25 or 100 at a time, which is the right shape for syncing and the wrong shape for "give me everything as a CSV". Exports are the second shape.
They are asynchronous on purpose. A worker has a wall-clock budget and you have a timeout, so above a few thousand rows the only honest answer is a receipt and a link that appears shortly.
Request one
POST /v1/forms/{formId}/exports{
"format": "csv",
"status": ["completed"],
"created_after": 1759276800000
}{
"id": "exp_4b1e9c07a2d5f83b6c14",
"object": "export",
"status": "queued",
"download_url": null
}202, immediately — nothing has been read yet. Needs response:export.
| Field | Default | |
|---|---|---|
format | csv | csv or json |
status | ["completed"] | any statuses, or ["all"] |
source | all | chat, api or embed |
mode | live | live, test or all |
created_after / created_before | — | epoch milliseconds |
Send an Idempotency-Key and a retried request returns the same export rather
than queueing a second run of the same query.
Collect it
GET /v1/exports/{exportId}{
"id": "exp_4b1e9c07a2d5f83b6c14",
"status": "ready",
"row_count": 4812,
"bytes": 918273,
"download_url": "https://api.chatform.in/d/export/exp_4b1e...?exp=1764...&sig=...",
"download_expires_at": 1764000600000,
"expires_at": 1764086400000
}Poll until status is ready. A few seconds is plenty for most forms; there is
no webhook for this yet. A failed export carries the reason in error.
GET /v1/exports lists recent ones, newest first, optionally filtered by
form_id.
download_url carries no API key — the signature is the credential, and it
expires in ten minutes. Read the export again to mint a fresh one rather than
storing the URL. The reasoning is the same as for files.
What is in the file
CSV gets one column per answerable question, named by the question with its ref
in brackets — Email? (q_email) — so a column can be matched back to the
document without the file being an export of our primary keys.
response_id,status,source,started_at,completed_at,"Email? (q_email)","Rate us (q_rating)"
sbm_...,completed,chat,2026-09-01T10:14:22.000Z,2026-09-01T10:16:05.000Z,ada@example.com,5Values are the labels a person would recognise, not internal ids: an option
comes out as its text, a matrix as readable pairs. An unanswered cell is empty
rather than (skipped) — a spreadsheet already has a way to say nothing is
there.
format: "json" gives JSON Lines instead — one response object per line, with
answers keyed by ref and values in their canonical form. That is the one to
use when something downstream is going to parse it.
Partial responses
Exporting what you finished collecting is never behind the paywall. Asking for
unfinished responses (in_progress, abandoned, or all) needs a plan that
includes partial responses, and an over-limit request is refused with a 402
rather than quietly narrowed to completed. An export that silently contains
something other than what you asked for is worse than one that says no.
disqualified is a status of its own, for respondents a screen-out ending
turned away. It is final, like completed, and an export includes it only when
you name it or ask for all. Like the unfinished ones, it needs a plan that
includes partial responses.
Finished and unfinished responses are not the same kind of row, so the
dashboard's Download keeps them apart: it hands you an .xlsx with a
Completed tab and an Unfinished tab over the same columns. Its .csv
route — which has no tabs, and which the spreadsheet feed still uses — puts
every completed response first and the unfinished ones after instead. The API's
csv is one flat table with a status column, unchanged.
Retention
An export is a full copy of respondent data sitting in a bucket, so it is not kept indefinitely: the object and its record are deleted 24 hours after the request. Re-request rather than re-download later — the query is stored on the export record, so asking again means the same thing.
The ceiling on a single export is 100,000 responses. Above that, filter by date range, or page the read API.