---
title: "Sandbox mode"
description: "Validate and store submissions without sending any email or webhook."
---

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

# Sandbox mode

Sandbox submissions are validated and stored like any other, but no email or webhook is sent. You see what would have been sent instead.

Sandbox submissions run against the form's [draft](https://docs.getquickforms.com/drafts-and-publishing/) when it has one, so they are also how you test changes before you publish them. A form that has never been published accepts sandbox submissions only.

## Four ways in

| How | Scope |
| --- | --- |
| `X-Quickforms-Sandbox: qf_sandbox_...` header or a `_sandbox` field | One submission. The key must match the form. |
| `"mode": "sandbox"` | Every submission to that form. |
| `QUICKFORMS_FORCE_SANDBOX=true` | Every submission in the environment. Useful for local and preview environments. |
| Test submissions | Always sandbox: the dashboard test button, the API test-submissions endpoint and the MCP send-test-submission tool. |

Rotate a form's sandbox key with **Rotate** beside it under **Share**, then **API or endpoint**, on the form page, `POST /api/v1/forms/{form}/sandbox-key/rotate`, or the `rotate-sandbox-key` MCP tool. The old key stops working straight away.

## What you get back

Deliveries are created with status `simulated` and hold the full payload that would have been sent: the email with its recipients and subject, or the webhook request including the `Quickforms-Signature` header computed with your real secret. Use that to [test your verification code](https://docs.getquickforms.com/webhooks/#test-with-a-sandbox-submission), with the timestamp check relaxed.

### Send a test

```bash
curl -X POST https://YOUR-QUICKFORMS-HOST/api/v1/forms/FORM_ID/test-submissions \
  -H "Authorization: Bearer $QUICKFORMS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{"data": {"name": "Ada Lovelace", "email": "ada@example.com"}}'
```

### 201 response

```json
{
  "data": {
    "id": "01jca9qf3tz5b8m2k7w4r6x0hn",
    "form_id": "01jc8xk2m5t9qw3e7r4y6u8v0p",
    "sandbox": true,
    "spam": false,
    "failed_rules": false,
    "held": false,
    "classification": null,
    "reviewed_at": null,
    "confirmation_status": null,
    "confirmed_at": null,
    "source": "api",
    "data": { "name": "Ada Lovelace", "email": "ada@example.com" },
    "meta": [],
    "deliveries": [
      {
        "id": "01jca9qf4b...",
        "channel": "webhook",
        "status": "simulated",
        "target": "https://example.com/hooks",
        "attempts_count": 0,
        "payload": {
          "method": "POST",
          "headers": {
            "Content-Type": "application/json",
            "User-Agent": "quickforms-webhooks/1.0",
            "Quickforms-Event": "submission.created",
            "Quickforms-Submission": "01jca9qf3tz5b8m2k7w4r6x0hn",
            "Quickforms-Signature": "t=1762424640,v1=4b1e0c7d9a..."
          },
          "body": "{\"id\":\"evt_01jca9qf3tz5b8m2k7w4r6x0hn\",\"type\":\"submission.created\",...}"
        }
      }
    ],
    "created_at": "2026-11-06T10:24:00.000000Z"
  }
}
```

List only sandbox or only live traffic with `?sandbox=true` or `?sandbox=false` on the [submissions endpoint](https://docs.getquickforms.com/api/#submissions).

## Double opt-in

On a form with [double opt-in](https://docs.getquickforms.com/double-opt-in/), a sandbox submission gets a `simulated` confirmation email and nothing else at first. Its payload holds the confirmation link in `button.url`. Open that link to simulate the person confirming: the submission becomes `confirmed` and its notification email, autoresponder and webhook are created as `simulated` deliveries.

```json title="Sandbox submit response (illustrative)"
{
  "message": "Thanks! Your submission has been received.",
  "id": "01jca9qf3tz5b8m2k7w4r6x0hn",
  "sandbox": true,
  "spam": { "flagged": false, "spam": false, "failed_rules": false, "reasons": [] },
  "deliveries": [
    { "channel": "confirmation", "status": "simulated", "target": "ada@example.com" }
  ]
}
```

The full payload, including `button.url`, is on the delivery in the [REST API](https://docs.getquickforms.com/api/#get-apiv1submissionssubmission) and the `get-submission` MCP tool, until retention clears it (30 days by default).

## Testing the spam filter

Sandbox submissions skip the [spam filter](https://docs.getquickforms.com/spam-filter/), so tests don't use your classifier budget and aren't flagged. Sandbox callers of the submit endpoint still get a `spam` object in the response, with `flagged`, `spam`, `failed_rules` and `reasons`, but it reports that nothing was flagged. The [cheap filters](https://docs.getquickforms.com/spam-protection/#geo-filters), country and disposable email, still run.

Source: https://docs.getquickforms.com/sandbox/index.mdx
