# Sandbox mode

Use Sandbox mode to test your email content, delivery handling, and webhook integration without sending email to recipients.

Sandbox mode applies to a project. It is not a route type. Transactional and Broadcast routes keep their normal settings. Inbound routes continue to receive email as usual.

:::warning
Sandbox messages do not go to recipient mail servers. Webhook deliveries are real HTTPS requests. They can start work in your application. Use a test endpoint, or check the `sandbox` field before your webhook handler starts a customer action.
:::

## What Sandbox mode changes

| Area | Sandbox behavior |
| --- | --- |
| Outbound delivery | Lettermint records a simulated result and does not connect to a recipient mail server. |
| Sender domains | You can use an unverified or unknown From domain. A verified team domain the project may use is linked to the message, the same as Live. A domain restricted from the project is rejected, the same as Live. |
| Message processing | Authentication, address, content, attachment, size, recipient, schedule, and idempotency checks still apply. |
| Routes | Transactional and Broadcast routes can send Sandbox messages. Inbound routes are not affected. |
| Events and webhooks | Lettermint creates message history and sends signed webhook requests with the normal event names. |
| Billing and statistics | Sandbox messages do not use your email quota and do not affect billable usage, delivery statistics, reputation, warmup, or Live sending limits. |
| Suppressions | Sandbox messages ignore Live suppression lists and do not add, change, or remove suppression records. |
| Project limits | A Sandbox project counts toward your project limit in the same way as a Live project. |
| Retention | Lettermint keeps Sandbox messages, source, and events under the normal retention rules. |

Lettermint still prepares the message source, plain-text part, attachments, preview, tracking links, and hosted unsubscribe links. A simulation does not fetch a customer link or send an automatic reply email.

The dashboard and API show `delivery_mode` and `sandbox_result` for each message. A project can contain both Live and Sandbox history after a mode change. You can filter the message list with `filter[delivery_mode]=live` or `filter[delivery_mode]=sandbox`.

## Create a Sandbox project

To create a Sandbox project in the dashboard:

1. Open **Projects** and select **Create project**.
2. Select **Sandbox** as the delivery mode.
3. Select **Transactional**, **Broadcast**, or **Both** as the email type.
4. Enter the project name, select the SMTP setting, and create the project.

The Team API also accepts `delivery_mode` when you create a project:

```json
{
  "name": "CI email tests",
  "delivery_mode": "sandbox",
  "initial_routes": "both",
  "smtp_enabled": true
}
```

The allowed values are `live` and `sandbox`. If you omit `delivery_mode`, Lettermint creates a Live project.

## Change the delivery mode

Open the project, go to **Settings**, and use the **Delivery mode** switch. Select **Save changes**, then confirm the mode change.

The new mode applies only to messages that Lettermint accepts after the change.

| Message state | Result after a mode change |
| --- | --- |
| Accepted in Live mode | The message stays Live and can still be delivered. |
| Accepted in Sandbox mode | The message stays Sandbox and remains simulated. |
| Scheduled in Live mode | The message uses Live delivery when its schedule releases it. |
| Scheduled in Sandbox mode | The message runs its saved simulation when its schedule releases it. |

The same rule applies to queued messages, retries, and recovery. The message keeps the mode and result that Lettermint saved at acceptance.

You can also send `delivery_mode` in a Team API project update request:

```json
{
  "delivery_mode": "live"
}
```

:::warning
Your Project API tokens and SMTP credentials stay active when you change the mode. After you switch a project to Live, each new accepted message can go to its real recipients. Verify your sender domains and test configuration before you confirm the change.
:::

Live messages must use a verified From domain. Sandbox messages can use an unverified From domain because no recipient mail server receives the message.

## Select a result with the Sending API

Add `sandbox_result` to a request to `POST /v1/send`. The field works only when the saved project mode is Sandbox.

```json
{
  "from": "Build checks <ci@unverified.example>",
  "to": ["recipient@example.com"],
  "subject": "Test hard bounce handling",
  "text": "This message will not be delivered.",
  "sandbox_result": "hard_bounced"
}
```

The accepted response of a Sandbox send includes `sandbox: true` and the message-level `sandbox_result`:

```json
{
  "message_id": "019b9d66-9ec8-73ba-a58f-b4f1f6fc5982",
  "status": "pending",
  "sandbox": true,
  "sandbox_result": "hard_bounced"
}
```

The response of a Live send does not include `sandbox` or `sandbox_result`. The same applies to each item in a batch response.

If you omit `sandbox_result`, Lettermint uses `delivered`. Lettermint rejects `sandbox_result` when the project is Live.

### Batch requests

Add `sandbox_result` to each item in a `POST /v1/send/batch` request. Use separate items when you need different results.

```json
[
  {
    "from": "Build checks <ci@unverified.example>",
    "to": ["delivery-case@example.com"],
    "subject": "Test delivery handling",
    "text": "Simulate delivery.",
    "sandbox_result": "delivered"
  },
  {
    "from": "Build checks <ci@unverified.example>",
    "to": ["complaint-case@example.com"],
    "subject": "Test complaint handling",
    "text": "Simulate a complaint.",
    "sandbox_result": "spam_complaint"
  }
]
```

One `sandbox_result` applies to all To, Cc, and Bcc recipients in that message.

See the [Sending API reference](/api-reference/sending) for the complete single-send and batch request schemas.

## Select a result with SMTP

Add `X-Lettermint-Sandbox-Result` to an SMTP message:

```text
X-Lettermint-Sandbox-Result: soft_bounced
```

Omit the header to simulate `delivered`. The header supports the same values as the Sending API.

Lettermint rejects this header when the saved project mode is Live. It also rejects duplicate headers and unsupported values. Lettermint removes the control header from the stored customer headers.

See the [SMTP guide](/guides/send-email-with-smtp) for connection settings and other control headers.

## Per-recipient results with test addresses

A recipient at a Lettermint testing domain can select its own simulated result, the same way [Lettermint test addresses](/platform/emails/sending-test-emails) work in a Live project. The result comes from the local part before `+`, so `hardbounce+signup@lettermint.dev` simulates a hard bounce for that recipient. This overrides the message-level `sandbox_result` for that recipient only. Other recipients on the same message keep the message-level result.

This works for both `lettermint.dev` and the compatible `testing.lettermint.co` domain. It works over the Sending API and SMTP, and is case-insensitive. Lettermint keeps the `+label` in the recipient address in events and webhooks, for example `hardbounce+signup@lettermint.dev`.

Lettermint accepts these local parts:

| Local part | Result |
| --- | --- |
| `ok` | `delivered` |
| `softbounce` | `soft_bounced` |
| `hardbounce` | `hard_bounced` |
| `spamcomplaint` | `spam_complaint` |
| `dsn` | `hard_bounced` |
| Any `sandbox_result` value, with or without underscores (for example `hard_bounced` or `hardbounced`) | That result |

A recipient at a testing domain with a local part that does not match one of these is rejected when Lettermint accepts the message.

Precedence, from highest to lowest:

| Precedence | Source |
| --- | --- |
| 1 | The recipient's test address |
| 2 | The message-level `sandbox_result` |
| 3 | `delivered` (default) |

### Example: API

```json
{
  "from": "Build checks <ci@unverified.example>",
  "to": ["hardbounce+signup@lettermint.dev", "recipient@example.com"],
  "subject": "Test per-recipient results",
  "text": "One recipient hard bounces. The other uses the message-level result.",
  "sandbox_result": "delivered"
}
```

`hardbounce+signup@lettermint.dev` simulates `hard_bounced`. `recipient@example.com` simulates `delivered`, the message-level result.

### Example: SMTP

```text
X-Lettermint-Sandbox-Result: delivered
To: hardbounce+signup@lettermint.dev, recipient@example.com
```

Same result: the first recipient hard bounces, the second follows the message-level `delivered` result.

The dashboard and API also show `sandbox_result` on each message recipient, alongside the message-level `delivery_mode` and `sandbox_result` on the message itself.

## Supported results

Every accepted Sandbox message starts with `message.created`. Lettermint then records the selected sequence for each recipient.

| `sandbox_result` | Event sequence |
| --- | --- |
| `delivered` | `message.sent`, then `message.delivered` |
| `hard_bounced` | `message.sent`, then `message.hard_bounced` |
| `soft_bounced` | `message.sent`, then `message.soft_bounced` |
| `deferred` | `message.sent`, `message.soft_bounced`, then `message.delivered` after five seconds |
| `failed` | `message.sent`, `message.soft_bounced`, then `message.failed` |
| `suppressed` | `message.suppressed` |
| `spam_complaint` | `message.sent`, `message.delivered`, then `message.spam_complaint` |
| `auto_replied` | `message.sent`, `message.delivered`, then `message.auto_replied` |
| `opened` | `message.sent`, `message.delivered`, then `message.opened` |
| `clicked` | `message.sent`, `message.delivered`, `message.opened`, then `message.clicked` |
| `unsubscribed` | `message.sent`, `message.delivered`, then `message.unsubscribed` |

Lettermint records each simulation step once. A retry does not create a second copy of a completed event.

The `suppressed` result creates `message.suppressed` for the current message only. It does not create a suppression record. Simulated bounces, complaints, and unsubscribes also do not change suppression lists.

If you open a hosted unsubscribe link for a Sandbox message, Lettermint returns the normal success response and records one Sandbox unsubscribe event. It does not add the recipient to a suppression list.

## Test webhooks

Sandbox sends make real signed HTTP requests to webhooks that accept Sandbox events. Normal delivery retries and signature headers apply.

Outbound message payloads contain these top-level fields:

```json
{
  "id": "54d7e8c9-1195-4ba0-9d3f-b9af92305add",
  "event": "message.hard_bounced",
  "timestamp": "2026-09-21T12:00:00.000Z",
  "sandbox": true,
  "sandbox_result": "hard_bounced",
  "context": {
    "scope": "route",
    "team_id": "9f4e3d2c-1b0a-4987-9654-3210fedcba98",
    "project_id": "7d6c5b4a-3210-4987-9654-fedcba987654",
    "route_id": "1a2b3c4d-5678-4987-9654-abcdef123456"
  },
  "data": {
    "message_id": "019b9d66-9ec8-73ba-a58f-b4f1f6fc5982",
    "recipient": "recipient@example.com"
  }
}
```

Use `sandbox` for a simple branch in your webhook handler. Use `sandbox_result` when the handler must check the selected test case. For a Live message, `sandbox` is `false` and `sandbox_result` is `null`. On a recipient event, `sandbox_result` is that recipient's resolved result, which can differ from the message-level result when the recipient used a test address. On `message.created`, it is the message-level result.

Each webhook has a delivery mode filter:

| Filter | Events sent to the webhook |
| --- | --- |
| Live | Live events only |
| Sandbox | Sandbox events only |
| Both | Live and Sandbox events |

New team, project, and route webhooks default to Live. Existing webhooks keep their current filter. Changing a project mode does not change this filter. Disabled webhooks stay disabled.

The **Test webhook** action sends one `webhook.test` request. Use a Sandbox send when you need to test a complete message event sequence, the delivery mode filter, and the Sandbox payload fields.

Always [verify the webhook signature](/platform/webhooks/signing). Store each webhook `id` that your application processes so a retry cannot run the same action twice.

## Use Sandbox mode in CI

Use a separate Sandbox project for CI, staging, and local development. This keeps the mode stable and keeps production credentials out of test jobs.

A practical test has these steps:

1. Send a message with the result that the test needs.
2. Store the returned `message_id`.
3. Receive and verify the signed webhook request.
4. Check `sandbox`, `sandbox_result`, and `data.message_id`.
5. Make the webhook handler idempotent before it changes application data.

Use separate batch items when one test run needs more than one result. Do not add `scheduled_at` unless the schedule itself is part of the test. The `deferred` result already waits five seconds before it records `message.delivered`.

## Scheduled Sandbox messages

A scheduled message saves its delivery mode and result when Lettermint accepts it. A later project mode change does not change the scheduled message.

The normal schedule limits and lifecycle actions still apply. You can reschedule or cancel a Sandbox message in the same way as a Live message. See [Scheduling](/platform/emails/scheduling) for the supported time formats and actions.

## Limits and safe use

Sandbox mode accepts up to 100 recipients per team per minute. The limit includes Sandbox messages from all projects in the team. Each message can have up to 50 recipients across To, Cc, and Bcc. The maximum message size is 25 MB, including attachments. See [Sending limits](/platform/emails/limitations) for the other request limits.

When a team exceeds the Sandbox rate limit, the Sending API returns `429 Too Many Requests` with a `Retry-After` header. A single request or batch with more Sandbox recipients than the per-minute limit can never be accepted, so it returns `422` instead; split it into smaller requests. An SMTP submission over the limit receives a temporary (4xx) failure. Retry after the wait time in `Retry-After`, or after your SMTP client's normal retry delay.

Use these rules in test systems:

- Check the project delivery mode before a test run.
- Do not use `sandbox_result` as a per-message safety control for a Live project. Lettermint rejects it.
- Treat webhook calls as real external actions, even when the email delivery is simulated.
- Use test data when message content or recipient addresses are sensitive. Lettermint stores the submitted content and addresses under the normal retention rules.
- Verify domains before you switch to Live.
- Keep Live and Sandbox project tokens in separate environment variables.

For a one-off delivery test in a Live project, you can also use [Lettermint test addresses](/platform/emails/sending-test-emails). Use Sandbox mode when you need arbitrary recipients, unverified sender domains, batch cases, per-recipient results in one message, or a complete project boundary for CI.
