---
title: "Drafts and publishing"
description: "Keep changes to a form as a draft, test them in sandbox mode, then publish them or throw them away."
---

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

# Drafts and publishing

A form can keep unpublished changes. Live submissions keep using the published version while you edit and test a draft in [sandbox mode](https://docs.getquickforms.com/sandbox/). When the draft is right, publish it to make it live, or discard it to keep the live version as it is.

## Statuses

Every form has a `status`:

| Status | Meaning |
| --- | --- |
| `draft` | The form has never been published. It only takes sandbox submissions. |
| `changes_pending` | The form is live and has a saved draft that is not live yet. |
| `published` | The form is live as saved, with no pending changes. |

The [forms endpoints](https://docs.getquickforms.com/api/#forms) and the MCP form tools return `status`, `published_at` (when the form was last published, or `null` if it never has been) and `draft` (the pending changes, or `null`).

## What goes through a draft

These attributes are versioned. With `draft`, changes to them are kept aside until you publish:

- `name`
- `description`
- `mode`
- `schema`
- `settings`
- `webhook_url`
- `mail_provider_id`

Everything else applies immediately. In particular `is_active`, the switch that turns a form on or off, takes effect straight away even when you save it with `draft: true`. So does `turnstile_secret`, the form's [Turnstile](https://docs.getquickforms.com/spam-protection/#cloudflare-turnstile) secret key. The `settings.turnstile` switch and site key are part of `settings`, so they go through the draft.

## Save changes as a draft

Pass `"draft": true` when you update a published form. The changes are stored as the form's draft and the live version is untouched.

- Drafts build on each other. Each draft save starts from the current draft, not from the live version.
- A draft that ends up identical to the live version is dropped, and the form goes back to `published`.
- Without `draft`, changes go live immediately, as before. If there's a pending draft, a save without `draft` publishes the draft together with your new changes.

The example below uses illustrative values.

### REST API

```bash
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 '{"draft": true, "settings": {"success_message": "Thanks, we will reply within a day."}}'
```

### MCP

```json
{
  "tool": "update-form",
  "arguments": {
    "form_id": "01jc8xk2m5t9qw3e7r4y6u8v0p",
    "draft": true,
    "settings": { "success_message": "Thanks, we will reply within a day." }
  }
}
```

### 200 response

```json
{
  "data": {
    "id": "01jc8xk2m5t9qw3e7r4y6u8v0p",
    "name": "Contact",
    "status": "changes_pending",
    "published_at": "2026-11-01T09:00:00.000000Z",
    "draft": {
      "name": "Contact",
      "settings": { "success_message": "Thanks, we will reply within a day.", "...": "..." },
      "...": "..."
    },
    "settings": { "success_message": "Thanks! Your submission has been received.", "...": "..." },
    "...": "..."
  }
}
```

The top-level attributes in the response are the live version. `draft` holds every versioned attribute as it will be once published.

In the dashboard, use **Save**. The form editor and the form page show the form with its draft applied.

## Test a draft

Sandbox submissions run against the draft, so you can try changes before they go live. Live submissions always get the published version.

| How | What runs against the draft |
| --- | --- |
| Submit endpoint | A submission with the form's sandbox key in the `X-Quickforms-Sandbox` header or a `_sandbox` field. See [sandbox mode](https://docs.getquickforms.com/sandbox/). |
| Embed widget | `data-sandbox="qf_sandbox_..."` on the container renders the draft and sends sandbox submissions. The [iframe](https://docs.getquickforms.com/embed/#iframe) does the same with `?sandbox=qf_sandbox_...`. |
| Public schema | `GET /f/{form}/schema.json?sandbox=qf_sandbox_...` returns the draft with `Cache-Control: no-store`. Without the key it returns the published schema. |
| Dashboard | Test submissions from **Send a test** on the form page. |
| REST API | [`POST /api/v1/forms/{form}/test-submissions`](https://docs.getquickforms.com/api/#post-apiv1formsformtest-submissions). |
| MCP | `send-test-submission`. |

Forms in sandbox mode, and every form when `QUICKFORMS_FORCE_SANDBOX=true`, run all their submissions against the draft.

```bash
curl https://YOUR-QUICKFORMS-HOST/f/FORM_ID/schema.json?sandbox=qf_sandbox_... \
  -H "Accept: application/json"
```

## Publish or discard

Publishing makes the draft live, clears it and sets `published_at`. Publishing a form that has never been live puts it live. Discarding throws the draft away and leaves the live version as it is.

| Where | Publish | Discard |
| --- | --- | --- |
| Dashboard | **Publish** | **Discard changes** |
| REST API | [`POST /api/v1/forms/{form}/publish`](https://docs.getquickforms.com/api/#post-apiv1formsformpublish) | [`DELETE /api/v1/forms/{form}/draft`](https://docs.getquickforms.com/api/#delete-apiv1formsformdraft) |
| MCP | `publish-form` with `form_id` | `publish-form` with `form_id` and `discard: true` |

Both REST endpoints return the form. The examples below use illustrative values.

### REST API

```bash
curl -X POST https://YOUR-QUICKFORMS-HOST/api/v1/forms/FORM_ID/publish \
  -H "Authorization: Bearer $QUICKFORMS_TOKEN" \
  -H "Accept: application/json"

curl -X DELETE https://YOUR-QUICKFORMS-HOST/api/v1/forms/FORM_ID/draft \
  -H "Authorization: Bearer $QUICKFORMS_TOKEN" \
  -H "Accept: application/json"
```

### MCP

```json
{ "tool": "publish-form", "arguments": { "form_id": "01jc8xk2m5t9qw3e7r4y6u8v0p" } }
```

```json
{ "tool": "publish-form", "arguments": { "form_id": "01jc8xk2m5t9qw3e7r4y6u8v0p", "discard": true } }
```

### 200 response

```json
{
  "data": {
    "id": "01jc8xk2m5t9qw3e7r4y6u8v0p",
    "name": "Contact",
    "status": "published",
    "published_at": "2026-11-06T10:30:00.000000Z",
    "draft": null,
    "settings": { "success_message": "Thanks, we will reply within a day.", "...": "..." },
    "...": "..."
  }
}
```

## Create an unpublished form

Pass `"draft": true` when you create a form, through `POST /api/v1/forms` or `create-form`, and the form starts with status `draft`. Until it is published:

- Live submissions to the [submit endpoint](https://docs.getquickforms.com/submit/) get a `404` with `"This form has not been published yet."`.
- `GET /f/{form}/schema.json` returns the same `404`, so the embed widget can't load it without a sandbox key.
- Sandbox submissions are accepted, so you can test it end to end.

Updating a form that has never been published with `draft: true` saves the changes and keeps it unpublished. Updating it without `draft` publishes it. Without `draft`, a new form is published as soon as it is created, as before.

The example below uses illustrative values.

### REST API

```bash
curl -X POST https://YOUR-QUICKFORMS-HOST/api/v1/forms \
  -H "Authorization: Bearer $QUICKFORMS_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"template": "contact", "name": "Website contact", "draft": true}'
```

### MCP

```json
{
  "tool": "create-form",
  "arguments": { "template": "contact", "name": "Website contact", "draft": true }
}
```

Then send it a test submission and publish it when it works.

> **Note**
>
> The AI form builder and the templates also talk about a "draft": that is a suggested form that has not been saved yet. A form saved with `draft: true` is a different thing, a saved form that is not live yet.

Source: https://docs.getquickforms.com/drafts-and-publishing/index.mdx
