With partial submissions on, the embed widget and the hosted form page save what people have typed so far. quickforms uses those saves to build a completion funnel: how many people started, how many finished, how many gave up, and the field they were on when they stopped.
Partials are never validated against the schema and never delivered. No emails, webhooks or destinations are sent for them.
Turning it on
In the dashboard, open the form and switch on Partial submissions. Or set settings.partials through the API or MCP:
| Key | Default | Description |
|---|---|---|
partials.enabled |
false |
Save progress from the embed and hosted page. |
partials.abandon_after_minutes |
30 |
How long a partial can go without a save before it is marked abandoned. 5 to 1440. |
partials.retention_days |
30 |
How long partials are kept. 1 to 90. See retention. |
curl -X PATCH https://YOUR-QUICKFORMS-HOST/api/v1/forms/FORM_ID \
-H "Authorization: Bearer $QUICKFORMS_TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"settings": {"partials": {"enabled": true}}}'The form’s public GET /f/{form}/schema.json includes "partials": true or false, which is how the embed and hosted page know whether to save progress.
How progress is saved
The embed and hosted page give each visitor an anonymous session ID per form, kept in the browser’s sessionStorage (so it lasts for the tab). No cookies are set.
- Embed widget: saves about 1.5 seconds after the person stops typing. Control fields (names starting with
_) and the Turnstile token are left out. - Hosted page: saves about 1.5 seconds after typing stops, and straight away when moving between steps. Files are never sent with progress.
Each save posts the answers so far and the last field the person touched. On submit, the same session ID is sent as the _session control field so the partial can be marked completed. After a successful submit the session is cleared, so the next fill starts a new partial.
POST /f/{form}/progress
You can also save progress from your own form. No authentication is needed: it is a public endpoint like POST /f/{form}.
| Field | Required | Description |
|---|---|---|
session |
Yes | Your anonymous ID for this visitor and form. 8 to 64 characters of A-Z, a-z, 0-9, _ and -. |
data |
Yes | An object of answers so far. Can be empty. |
last_field |
No | The name of the field the person last touched, up to 64 characters. |
link |
No | A personal link token, as an alternative to the X-Quickforms-Link header. |
Send X-Quickforms-Sandbox with the form’s sandbox key to save sandbox progress against the draft. Sandbox partials are kept out of the live funnel.
curl -X POST https://YOUR-QUICKFORMS-HOST/f/FORM_ID/progress \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"session": "3f9c2a7e5b1d4c8a", "data": {"name": "Ada Lovelace", "email": "ada@exa"}, "last_field": "email"}'{ "id": "01jcb6t2w8n4k7q9r3s5v1x0yz", "status": "in_progress" }What is kept:
- Only the form’s own fields. Unknown keys and file fields are dropped.
- Strings are trimmed and capped at 5000 characters. Lists keep up to 100 plain values, each capped at 500 characters. Empty values are dropped.
- One save can hold up to 64 KB. A larger one gets a
422. - A form can start 1000 new sessions an hour by default, set with
QUICKFORMS_PARTIALS_NEW_SESSIONS_PER_HOUR. Past that, new sessions get a429and existing ones keep saving. last_fieldis only updated when it names a field on the form.- The visitor’s user agent and referer are stored in the partial’s
meta.
Each save replaces the partial’s answers for that session. A save to an abandoned partial puts it back to in_progress.
| Status | When |
|---|---|
202 |
Saved. Returns the partial’s id and status. |
204 |
Ignored: partials are off, the form is inactive or unpublished (and no sandbox key was sent), or a honeypot field in data is filled. |
422 |
session is missing or invalid, or data is missing. |
429 |
More than 120 progress or view requests a minute from one IP. |
Completion
When a submission to POST /f/{form} includes _session, the partial with that session ID is marked completed, with completed_at and the new submission_id. _session is a control field, so it is never stored in the submission’s data.
Once a partial is completed it stays that way. Late saves for the same session are ignored.
{
"name": "Ada Lovelace",
"email": "ada@example.com",
"message": "Hello",
"_session": "3f9c2a7e5b1d4c8a"
}Statuses
| Status | Meaning |
|---|---|
in_progress |
Saved recently and not submitted yet. |
abandoned |
No save for abandon_after_minutes. abandoned_at is set. |
completed |
The real submission arrived with the same _session. |
Retention
A scheduled job (quickforms:abandon-partials) runs every five minutes. It:
- Marks
in_progresspartialsabandonedonce their last save is older than the form’sabandon_after_minutes. - Deletes
in_progressandabandonedpartials whose last save is older than the form’sretention_days. - Deletes
completedpartials once they were completed more thanretention_daysago.
Funnel
GET /api/v1/forms/{form}/funnel
Started, completed and abandoned counts for partials created in the window, plus where people stop: the last_field of abandoned partials, most common first.
| Query | Description |
|---|---|
days |
1 to 365, default 30. |
sandbox |
true for sandbox partials. Live partials by default. |
curl -G https://YOUR-QUICKFORMS-HOST/api/v1/forms/FORM_ID/funnel \
-H "Authorization: Bearer $QUICKFORMS_TOKEN" \
-H "Accept: application/json" \
--data-urlencode "days=30"{
"data": {
"started": 4,
"completed": 1,
"abandoned": 3,
"in_progress": 0,
"completion_rate": 0.25,
"drop_off": [
{ "field": "name", "label": "Name", "abandoned": 2 },
{ "field": "email", "label": "Email", "abandoned": 1 }
]
}
}started is completed + abandoned + in_progress. completion_rate is completed / started, rounded to three decimals, or null when nothing has started. label is the field’s label from the schema, or its name if it has none.
Partials API
All partials endpoints use a team API key. Anything outside the API key’s team returns 404.
GET /api/v1/forms/{form}/partials
A form’s partials, most recently active first.
| Query | Description |
|---|---|
status |
in_progress, abandoned or completed. Omit for all. |
sandbox |
true or false. Omit for both. |
per_page |
1 to 100, default 25. |
page |
Page number. |
curl -G https://YOUR-QUICKFORMS-HOST/api/v1/forms/FORM_ID/partials \
-H "Authorization: Bearer $QUICKFORMS_TOKEN" \
-H "Accept: application/json" \
--data-urlencode "status=abandoned"{
"data": [
{
"id": "01jcb6t2w8n4k7q9r3s5v1x0yz",
"form_id": "01jc8xk2m5t9qw3e7r4y6u8v0p",
"status": "abandoned",
"sandbox": false,
"data": { "name": "Ada Lovelace", "email": "ada@exa" },
"fields_filled": 2,
"last_field": "email",
"link_id": null,
"submission_id": null,
"meta": { "user_agent": "Mozilla/5.0 ...", "referer": "https://yoursite.com/contact" },
"started_at": "2026-11-06T10:20:00.000000Z",
"last_activity_at": "2026-11-06T10:21:30.000000Z",
"abandoned_at": "2026-11-06T10:55:00.000000Z",
"completed_at": null
}
],
"links": { "first": "...?page=1", "last": "...?page=1", "prev": null, "next": null },
"meta": { "current_page": 1, "from": 1, "last_page": 1, "per_page": 25, "to": 1, "total": 1 }
}| Field | Description |
|---|---|
data |
The answers saved so far. |
fields_filled |
How many fields have a value. |
last_field |
The last field the person touched. |
link_id |
The personal link the partial came through, otherwise null. |
submission_id |
The submission that completed it, otherwise null. |
started_at |
When the first save arrived. |
last_activity_at |
When the latest save arrived. |
GET /api/v1/partials/{partial}
One partial, in the same shape, wrapped in data.
DELETE /api/v1/partials/{partial}
Deletes a partial. Returns 204.
curl -X DELETE https://YOUR-QUICKFORMS-HOST/api/v1/partials/PARTIAL_ID \
-H "Authorization: Bearer $QUICKFORMS_TOKEN" \
-H "Accept: application/json"Dashboard
The form page shows a Funnel panel for the last 30 days of live visitors: started, completed, abandoned and in progress, the completion rate, where people stop, and the recent partials, filterable by status. It shows while partials are on, or while there are partials from when they were.
MCP
The MCP server has a read-only list-partial-submissions tool. It takes form_id, and optionally status (in_progress, abandoned or completed), sandbox and days (the funnel window, 1 to 365, default 30). It returns the funnel in the shape above plus up to 50 of the most recently active partials in data.
Turn partials on with settings.partials (enabled, abandon_after_minutes, retention_days) on create-form or update-form.