---
title: "Partial submissions"
description: "Save progress while people fill a form in, see how many finish, and find the field where the rest stop."
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.getquickforms.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Partial submissions

With partial submissions on, the [embed widget](https://docs.getquickforms.com/embed/) and the [hosted form page](https://docs.getquickforms.com/hosted-forms/) 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.

> **Note**
>
> This saves what people type before they submit, including answers to forms they never send. It is off by default.

## Turning it on

In the dashboard, open the form and switch on **Partial submissions**. Or set `settings.partials` through the [API](https://docs.getquickforms.com/api/#patch-apiv1formsform) or [MCP](https://docs.getquickforms.com/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](#retention). |

```bash title="Turn on partials"
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](#completion). 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](https://docs.getquickforms.com/personal-links/) token, as an alternative to the `X-Quickforms-Link` header. |

Send `X-Quickforms-Sandbox` with the form's sandbox key to save [sandbox](https://docs.getquickforms.com/sandbox/) progress against the draft. Sandbox partials are kept out of the live funnel.

### Request

```bash
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"}'
```

### 202 response

```json
{ "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 a `429` and existing ones keep saving.
- `last_field` is 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.

```json title="JSON submission with _session"
{
  "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_progress` partials `abandoned` once their last save is older than the form's `abandon_after_minutes`.
- Deletes `in_progress` and `abandoned` partials whose last save is older than the form's `retention_days`.
- Deletes `completed` partials once they were completed more than `retention_days` ago.

## 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. |

### Request

```bash
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"
```

### 200 response

```json
{
  "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](https://docs.getquickforms.com/api/#authentication). 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. |

### Request

```bash
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"
```

### 200 response

```json
{
  "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](https://docs.getquickforms.com/personal-links/) 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`.

```bash title="Delete a partial"
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](https://docs.getquickforms.com/mcp/) 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`.

Source: https://docs.getquickforms.com/partial-submissions/index.mdx
