The response lifecycle
Open a response, answer into it, complete it — and why partial answers are first-class.
A response is created the moment someone starts, not when they finish. Every answer is stored as it arrives. That is what makes a half-finished response visible in your dashboard, and it is why this API has a lifecycle rather than a single submit endpoint.
A single "post the whole form" endpoint would create finished records with no per-question history — so the drop-off funnel, the per-question summaries and every partial count would mean something different for API traffic than for conversations. The convenience path exists, but it runs through the same three steps internally.
Open
POST /v1/forms/{formId}/responses{
"hiddenFields": { "utm_source": "docs" },
"expiresIn": 86400
}Returns the response, with next pointing at the first question. This counts as
a start, exactly as opening a conversation does.
expiresIn is how long an unfinished response stays open before it is marked
abandoned. Default is 24 hours; the range is five minutes to thirty days.
Answer
POST /v1/responses/{id}/answersOne at a time:
{ "ref": "q_email", "value": "maya@northwind.co" }Or several:
{
"answers": [
{ "ref": "q_email", "value": "maya@northwind.co" },
{ "ref": "q_role", "value": "Founder" }
]
}A batch is all-or-nothing. If one answer is rejected, none are written — a partial write would leave you unable to tell what landed.
The reply carries the updated response including the next question, so a client never has to ask separately.
Answers are normalised
What you send is not always what gets stored. " Maya@Northwind.CO " becomes
maya@northwind.co; "Founder" becomes the option's id; "98123 45678" on a
phone block with an Indian country hint becomes +919812345678. The block
reference documents this per type.
Order is enforced
By default you can only answer the question the form is waiting on, or re-answer
one already behind it. Answering ahead returns
block_not_reachable.
That is deliberate: accepting a jump to question nine would record eight
abandonments at questions nobody was ever asked. If you are importing data that
was collected elsewhere, open the response with "mode": "free" and the check is
relaxed — the answers are still validated, and the response records that its flow
was not enforced.
A question hidden by its own visibility rule is refused with
block_not_visible, whatever the mode.
Complete
POST /v1/responses/{id}/completeRefuses with incomplete if a required question that was actually asked is
unanswered, and names them:
{
"error": {
"code": "incomplete",
"message": "Required questions are unanswered",
"issues": [{ "ref": "q_role", "code": "required", "message": "\"Role?\" is required" }]
}
}Required questions inside a branch nobody took are not asked for — that is what makes conditional logic usable.
The reply carries the resolved ending, including its redirect and call to action if the form has them.
Abandon
POST /v1/responses/{id}/abandonThere is deliberately no "complete anyway" flag. "I want the data but they did not finish" already has a name and a status, and letting completion mean two things would make your completion rate unreadable. Abandoned responses keep every answer that was given.
A response abandoned with no answers is deleted instead, and so is one that
expires empty: a conversation that ends before its first answer leaves no row
either, and an empty response is not a partial one. The reply still describes
it, and reading that id afterwards returns 404.
Reading responses back
GET /v1/forms/{formId}/responses?limit=50&include=answersCursor-paginated. Filters: status, created_after, created_before,
updated_since, ending_ref, source, q (full-text over answers), and
mode for test data.
A response is in_progress, abandoned, completed or disqualified. The
last is as final as completed: the respondent reached a screen-out ending and
the form turned them away. Pass several statuses comma-separated, or all.
Omitting status means completed only. Asking for in_progress, abandoned,
disqualified or all needs a plan that includes partial responses; if yours
does not, you get a 402 rather than a quietly narrowed result set.
For everything at once rather than a page at a time, ask for an export.
Editing
DELETE /v1/responses/{id}/answers/{ref}Retracts one answer and moves the flow back to it. Later answers are kept — one of them may still be valid, and re-asking everything is a worse experience than re-asking what actually changed.
Idempotency
Creating and completing accept an Idempotency-Key header. A network timeout
tells you nothing about whether the write landed, and this is what makes the
retry safe.