# Sandbox recipients and bounces

Resend's test addresses bounce, complain or deliver at send time, and a provider route bounces a sent email to any address.

Resend's [test addresses](https://resend.com/docs/dashboard/emails/send-test-emails) decide what happens to an email after `email.sent`, and `POST /_bounce` makes a bounce happen to any address, so you can test a suppression handler end to end. Reload the scenario so the ids below start at 1, then send to each outcome and read the egress log:

```bash
curl -s -X POST "$RYSTIC_URL/_rystic/scenario" -d '{"name":"design_studio"}'
curl -s "$RYSTIC_URL/emails" -H 'Authorization: Bearer re_placeholder' -H 'Content-Type: application/json' -d '{"from":"Acme <onboarding@resend.dev>","to":["bounced+rystic@resend.dev"],"subject":"One","html":"<p>x</p>"}'
# -> {"id":"28c9b732-cf71-466d-8000-000000000001"}
curl -s "$RYSTIC_URL/emails/28c9b732-cf71-466d-8000-000000000001" -H 'Authorization: Bearer re_placeholder' | jq -c '{last_event, bounce: .bounce.subType}'
# -> {"last_event":"bounced","bounce":"General"}
curl -s "$RYSTIC_URL/emails" -H 'Authorization: Bearer re_placeholder' -H 'Content-Type: application/json' -d '{"from":"Acme <onboarding@resend.dev>","to":["complained@resend.dev"],"subject":"Two","html":"<p>x</p>"}'
# -> {"id":"28c9b732-cf71-466d-8000-000000000002"}
curl -s "$RYSTIC_URL/_rystic/egress" | jq -c '[.events[].type]'
# -> ["smtp-delivery/bounced+rystic@resend.dev","email.sent","email.bounced","smtp-delivery/complained@resend.dev","email.sent","email.delivered","email.complained"]
```

### Which address does what

The first `to` recipient of an immediate send (no `scheduled_at`) decides, with or without a `+label`:

| First `to` recipient | Webhook events, in order | `last_event` |
|---|---|---|
| `bounced@resend.dev`, `bounced+<label>@resend.dev` | `email.sent`, `email.bounced` | `bounced` |
| `complained@resend.dev`, `complained+<label>@resend.dev` | `email.sent`, `email.delivered`, `email.complained` | `complained` |
| any other address, `delivered@resend.dev` included | `email.sent` | `queued` |

**Differs from Resend:** an email to `delivered@resend.dev` stays `queued`, and its only webhook event is `email.sent`. Resend moves it to `delivered` within seconds and sends `email.delivered`.

On `email.delivered`, `email.bounced` and `email.complained`, `data.to` is that first recipient alone. `email.bounced` adds `data.bounce` as Resend sends it:

| `data.bounce` field | Value |
|---|---|
| `type` | `Permanent` |
| `subType` | `General` |
| `message` | Resend's hard-bounce message |
| `diagnosticCode` | one entry, the SMTP 550 unknown-user response naming `bounce@simulator.amazonses.com`, with the recipient's `+label` after `bounce` |

`GET /emails/{email_id}` on a bounced email returns the same `bounce` object. The simulator records the outcome at send time.

**Differs from Resend:** Resend reaches the outcome within a few seconds of the send, and `last_event` reads `queued` or `sent` until then.

### Bounce an email on any address

A bounce on a real address is Resend's delivery pipeline acting, not a call your program makes, so a simulator route stands in for it. Send an email to an ordinary address, then bounce it:

```bash
curl -s "$RYSTIC_URL/emails" -H 'Authorization: Bearer re_placeholder' -H 'Content-Type: application/json' -d '{"from":"Acme <onboarding@resend.dev>","to":["jo@example.com"],"subject":"Three","html":"<p>x</p>"}'
# -> {"id":"28c9b732-cf71-466d-8000-000000000003"}
curl -s "$RYSTIC_URL/_bounce" -H 'Content-Type: application/json' -d '{"email_id":"28c9b732-cf71-466d-8000-000000000003","subType":"NoEmail"}' | jq -c '{last_event, bounce: .bounce.subType}'
# -> {"last_event":"bounced","bounce":"NoEmail"}
curl -s "$RYSTIC_URL/_bounce" -H 'Content-Type: application/json' -d '{"email_id":"28c9b732-cf71-466d-8000-000000000003"}' | jq -c '{name, statusCode}'
# -> {"name":"invalid_state","statusCode":409}
```

`POST /_bounce` bounces an email that has gone out, one whose `last_event` is `queued` or `sent`, whatever its recipient. The email then reads `bounced`, with its `bounce` object. `email.bounced` goes to the egress log and to every webhook subscribed to it, with `data.to` the first recipient alone.

| Body field | |
|---|---|
| `email_id` | required: the email to bounce |
| `subType` | optional: `General` (the default), `NoEmail`, `Suppressed` or `OnAccountSuppressionList` |
| `message` | optional: the bounce's `message` |
| `diagnosticCode` | optional: the bounce's `diagnosticCode` entries |

| You send | `POST /_bounce` returns |
|---|---|
| no `email_id`, or another `subType` | 422 `validation_error` |
| an unknown email | 404 `not_found` |
| an email that is scheduled, canceled or already settled | 409 `invalid_state` |

It is a control route like `/_rystic/*`: open by default, and once the simulator boots with `RYSTIC_ADMIN_TOKEN` set, a request needs that value in an `X-Rystic-Admin-Token` header.

A scenario timeline schedules it as a call to the simulator's own route:

```json
{"id": "bounce", "at": "+2h", "call": {"method": "POST", "path": "/_bounce", "body": {"email_id": "<id>", "subType": "NoEmail"}}}
```

### Several sandbox recipients

For an email sent to several addresses, the first `to` recipient decides the outcome. The simulator keeps every recipient on the record and on `email.sent`.

**Differs from Resend:** for an email sent to several sandbox addresses, Resend kept only the first `to` recipient on the stored email and on every event.

Whether Resend does the same to other multi-recipient sends is unconfirmed: it has not been checked against Resend.

---

Source: https://www.rystic.ai/docs/simulators/resend/features/sandbox-recipients · Markdown: https://www.rystic.ai/docs/simulators/resend/features/sandbox-recipients.md
