Simulators
Sandbox recipients and bounces
Resend’s test addresses 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:
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:
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:
{"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.