Core concepts
Sandbox & test recipients
Two lanes: rehearse your integration for free, then prove real delivery safely.
Testing email splits cleanly in two. The sandbox proves your integration — that you're calling the API correctly and your templates render. Test recipients prove the outside world — that mail actually goes out, gets signed, delivers, bounces and comes back through your webhooks.
The sandbox: a free rehearsal
A rm_test_… key runs every request through the full pipeline — validation, suppression, rendering, events — but never delivers and never bills. It's a separate workspace with its own data, so nothing you do there can touch production. Open it in the dashboard from Developers → Testing — it's deliberately kept out of the workspace picker, since it isn't a product you run.
- Sandbox messages are stored with their full rendered content — read them back over the API, or in the dashboard under Testing.
- Webhooks still fire for sandbox sends, so you can verify your handlers.
- Sandbox sends are free forever and don't count toward any quota.
Test recipients: the real path, safely
Every address at test.rootmail.dev is a scenario. Mail sent to one takes the real send path — your DKIM key, your sending provider, your webhooks — but is delivered to the provider's mailbox simulator. No person receives it, and it's excluded from your sender reputation, so you can force a hard bounce as often as you like at no cost.
Scenarios
delivered@test.rootmail.devdelivered | A clean delivery, with a real message.delivered event. |
bounced@test.rootmail.devbounced | A permanent rejection. The address is auto-suppressed, exactly as a real hard bounce would be. |
complained@test.rootmail.devcomplained | The recipient reports spam — proves complaint handling and auto-suppression. |
suppressed@test.rootmail.devsuppressed | Already on the provider's suppression list; the send is refused before it goes anywhere. |
away@test.rootmail.devdelivered | Delivers, then returns an out-of-office auto-reply — useful for reply handling. |
// Prove your bounce handling, end to end.
const { data } = await mail.testing.list();
const bounce = data.find((t) => t.slug === "bounced")!;
await mail.messages.create({
to: bounce.email, // bounced@test.rootmail.dev
subject: "Bounce me",
html: "<p>This should never arrive.</p>",
});
// → message.sent, then message.bounced on your webhook,
// and the address lands on your suppression list.
// Bounce tests suppress the address. Clear them to run again:
await mail.testing.reset();/v1/test-recipients{
"object": "list",
"domain": "test.rootmail.dev",
"data": [
{
"object": "test_recipient",
"slug": "bounced",
"email": "bounced@test.rootmail.dev",
"label": "Hard bounce",
"description": "The address rejects permanently…",
"outcome": "bounced"
}
]
}/v1/test-recipients/reset{
"object": "test_recipients_reset",
"cleared": 2,
"emails": ["bounced@test.rootmail.dev", "complained@test.rootmail.dev"]
}