# Test email delivery (mail sandbox)

The mail sandbox lets you exercise your entire email integration (webhook handlers, suppression logic, retry behaviour) with real API calls that never deliver anywhere. Send to a magic address at `messagebird.dev` and the outcome is determined by the address itself: `bounce@messagebird.dev` always bounces, `delivered@messagebird.dev` always delivers.

What makes this different from validation-only sandbox modes: a sandbox send runs through the **real production pipeline**. You get the same `202` response, the same per-recipient [event](/docs/guides/email/events) sequence, and the same [webhook](/docs/guides/webhooks) deliveries as a production send: identical event types and payload shapes, with no "this is a test" flag. The message is never handed to the delivery infrastructure, so it never reaches a real inbox. That means zero reputation risk: simulated bounces and complaints don't touch your sending reputation, and they don't write to your [suppression list](/docs/guides/email/suppressions), so the same bounce address is reusable across every test run.

The sandbox needs no setup: no toggle, no test mode, no special API key. It triggers purely on the recipient address, on the normal send endpoints (`POST /v1/email/messages` and `POST /v1/email/batches`).

## Magic addresses

All addresses are on `@messagebird.dev`. The local-part selects the outcome:

| Address                   | Simulated outcome                                                                   | Webhook sequence                                                                    | Notes                                                                                                               |
| ------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `delivered@`              | The receiving mail server accepts the message                                       | `email.accepted` → `email.processed` → `email.delivered`                            | The happy path                                                                                                      |
| `bounce@` / `hardbounce@` | Hard bounce: SMTP `550`, `5.1.1 Unknown User`, bounce class 10                      | `email.accepted` → `email.processed` → `email.bounced` with `bounce_type: "hard"`   | No suppression-list write, so the address stays reusable                                                            |
| `softbounce@`             | Soft bounce: SMTP `451`, `4.3.0 Temporary failure, please retry`, class 20          | `email.accepted` → `email.processed` → `email.bounced` with `bounce_type: "soft"`   | Soft bounces never suppress, real or simulated                                                                      |
| `deferred@` / `delay@`    | Deferral: SMTP `451`, `4.2.1 Mailbox temporarily unavailable, will retry`, class 21 | `email.accepted` → `email.processed` → `email.deferred`                             | The simulated deferral is terminal: no retry follows, so the recipient stays `deferred`                             |
| `complaint@` / `spam@`    | The recipient reports the message as spam (`feedback_type: "abuse"`)                | `email.accepted` → `email.processed` → `email.complained`                           | No suppression write; reusable                                                                                      |
| `suppressed@`             | The recipient is treated as already on your suppression list                        | `email.accepted` → `email.rejected` with `rejection_reason: "recipient_suppressed"` | Short-circuits during processing exactly like a real suppressed recipient: no `email.processed`, no delivery events |
| `reject@`                 | The delivery pipeline rejects the transmission                                      | `email.accepted` → `email.rejected` with `rejection_reason: "transmission_failed"`  | No `email.processed` or delivery events follow                                                                      |

A send can mix sandbox and real recipients. Each recipient gets its own lifecycle: real recipients deliver normally, sandbox recipients are simulated.

The same events also land on the message's timeline in the [email log](/docs/guides/email/email-log) and in the [events API](/docs/api/reference/list-email-message-events), so you can drive the sandbox without a webhook endpoint and read the outcomes back.

## Addressing rules

- **Detection is by local-part only, and only on the `messagebird.dev` domain.** `bounce@yourdomain.com` is a normal address.
- **Only the local-parts in the [magic addresses table](#magic-addresses) are magic.** Any other address at `messagebird.dev` is a normal recipient: on a send from the shared onboarding domain it goes through the same verified-member check as any other address, and is rejected unless it belongs to a verified member of your workspace.
- **Matching is case-insensitive**: `Bounce@messagebird.dev` and `bounce@messagebird.dev` behave identically.
- **`+label` subaddressing is stripped before matching**: `bounce+signup-flow@messagebird.dev` still bounces. Use labels to correlate test cases; the full address, label included, appears in your events and webhooks, so each test run can tag its own recipients.

## Walkthrough: simulate a bounce, end to end

You don't need a verified sending domain: send from Bird's shared onboarding domain, `onboarding@messagebird.dev`, the same one used in [Send your first email](/docs/get-started/send-your-first-email). The recognized sandbox addresses are exempt from the onboarding domain's verified-member restriction (they still count toward its [daily quota](/docs/get-started/send-your-first-email#about-the-onboarding-domain)), so the whole test is self-contained on `messagebird.dev`: one Bird-owned domain on both ends.

Make sure you have a webhook endpoint subscribed to email events (see [Webhooks](/docs/guides/webhooks)), then send:

```bash
curl -X POST https://us1.platform.bird.com/v1/email/messages \
  -H "Authorization: Bearer $BIRD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "onboarding@messagebird.dev",
    "to": ["bounce+signup-flow@messagebird.dev"],
    "subject": "Sandbox bounce test",
    "html": "<p>This message will hard-bounce.</p>",
    "tags": [{ "name": "flow", "value": "signup" }],
    "metadata": { "test_run": "docs-capture-1" }
  }'
```

If your key starts with `bk_eu1_`, call `https://eu1.platform.bird.com` instead.

The API responds `202 Accepted` with an `em_*` message ID, indistinguishable from a production send. That is the point: the code path you're testing is your real one. Your webhook endpoint then receives `email.accepted`, `email.processed`, and finally `email.bounced`. Every event echoes the send's `tags` and `metadata` (null when the send carried none), and the `email.bounced` payload carries the full bounce classification:

```json
{
  "type": "email.bounced",
  "timestamp": "2026-07-23T14:51:00.362Z",
  "data": {
    "email_id": "em_01ky7qanhrejer0bn34v38hrxh",
    "recipient_id": "er_01ky7qanhrejds4decpk83q5qq",
    "workspace_id": "ws_01ky7m21hjffh9s8kq76gyb1xf",
    "recipient": "bounce+signup-flow@messagebird.dev",
    "recipient_role": "to",
    "bounce_type": "hard",
    "bounce_class": 10,
    "bounce_code": "550",
    "bounce_description": "5.1.1 Unknown User",
    "sending_ip": null,
    "tags": [{ "name": "flow", "value": "signup" }],
    "metadata": { "test_run": "docs-capture-1" }
  }
}
```

Field meanings live in the [events reference](/docs/guides/email/events). No simulator flag appears anywhere in the payload: the event type and shape are exactly what a real hard bounce produces. The only thing that identifies it as simulated is the recipient address itself, so if your handler needs to special-case test traffic, key off the `messagebird.dev` recipient domain.

To verify the other half of bounce handling (that an already-suppressed recipient is never sent to), repeat the send with `suppressed@messagebird.dev` and assert you receive `email.accepted` followed by `email.rejected` with `rejection_reason: "recipient_suppressed"`, and no `email.processed` or delivery events. This mirrors a real suppressed recipient exactly: the message is accepted, then short-circuited during processing before any send.

## What the sandbox does and doesn't do

- **No suppression-list writes.** Simulated hard bounces and complaints do not add the recipient to your [suppression list](/docs/guides/email/suppressions); that is what keeps the addresses reusable. Your webhook still fires (`email.bounced`, `email.complained`), so your own suppression logic is fully exercised. To test the already-suppressed rejection path, use the dedicated `suppressed@` address.
- **No real delivery, ever.** Sandbox recipients are intercepted before the message reaches the delivery infrastructure. Nothing is transmitted, no inbox is involved, and your sending reputation is untouched.
- **Request validation still applies.** A sandbox send uses the normal endpoints, so schema checks, size caps, and header rules reject an invalid request as usual. What the sandbox skips is everything past the handoff: rendering and delivery behaviour beyond that point aren't exercised.
- **Opens and clicks aren't simulated yet.** No recipient address can trigger an engagement event, so `email.opened` and `email.clicked` can't be exercised through the sandbox today.
- **Stats include sandbox traffic.** Sandbox sends count toward your workspace's aggregate stats and bounce and complaint rates. Heavy sandbox bouncing skews your dashboards, not your reputation.

## Next steps

- [Events](/docs/guides/email/events): the full event vocabulary and per-recipient lifecycle
- [Webhooks](/docs/guides/webhooks): subscribing, signature verification, and retries
- [Send your first email](/docs/get-started/send-your-first-email): the onboarding-domain quickstart this walkthrough builds on
- [Suppressions](/docs/guides/email/suppressions): how the real suppression list works