# Testing and the sandbox

Test keys, the sandbox inbox and magic .test addresses, for testing sends, bounces and replies without a real customer ever seeing them.

Rookery for developers · Updated October 11, 2026 · https://userookery.com/developers/sandbox/

The sandbox is a separate inbox for building and testing, like test mode in a payments API. Use a **test key** and the API works exactly as it does live, on the sandbox's own conversations: you can send, and see what happens, without anything leaving Rookery or touching your real mail.

## Test keys

Owners and admins make them in **Settings → Integrations → API keys**: under **Works on**, choose **The sandbox**.

- Test keys start with `rk_test_`. Live keys start with `rk_live_`, and the `rk_` keys made before test keys existed are live keys too.
- A test key sees only the sandbox inbox, `sandbox@rookery.test`, and what's in it. Your real inboxes, conversations, documents and tags don't exist for it: asking for one answers `404`.
- Live keys, and people in the app, never see the sandbox.
- The same endpoints and scopes apply. `GET /me` says `"livemode": false` for a test key.
- **Try it** in the [reference](https://userookery.com/developers/reference/) is made for test keys: with one, you can even try sending.

## Start a test: simulate an email

The sandbox starts empty. `POST /v1/sandbox/inbound` makes an email arrive in it, through the real pipeline: it's threaded, checked for scams, stored, and your test webhooks hear about it. It's the only endpoint that's for test keys alone (a live key gets `403`), and it needs the `write` scope. The sandbox takes up to 120 simulated emails a minute, across all your test keys.

```shell
curl -X POST https://app.userookery.com/api/v1/sandbox/inbound \
  -H "Authorization: Bearer $ROOKERY_TEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{"from": "reply+lee@acme.test", "name": "Lee Park", "subject": "Where is my order?", "text": "Hi, order 1182 hasn'\''t arrived."}'
```

The answer has the new `conversationId`. Senders must be on `.test`, so the sandbox never touches a real contact. Pass `conversationId` to make the email a reply on a conversation that's already there.

## Magic addresses

Reply to the conversation, and what happens depends on who it's to. The sandbox reads the address's first part, before any `+`, at any `.test` domain:

| Send to | What happens |
|---|---|
| `delivered@acme.test` | Delivered. Nothing comes back. |
| `bounce@acme.test` | A hard bounce: the email's status is `failed`, and there's no `message.sent` event. |
| `complaint@acme.test` | Delivered, then marked as spam (a `spam-complaint` event on the conversation). Every later email to that address fails, the way a provider holds mail after a complaint. |
| `reply+lee@acme.test` | Delivered, then they write back about 5 seconds later, in the same conversation (`client.replied`). |
| `docs@acme.test` | Delivered, then a reply with a PDF invoice attached arrives about 5 seconds later. It's filed as a document. |
| `ooo@acme.test` | Delivered, then an automatic out-of-office reply arrives. It's a machine's mail, so there's no `client.replied`. |

Anything else at `.test` is delivered, and nothing comes back.

For example: simulate an email from `reply+lee@acme.test`, reply to it with `"send": true`, and Lee writes back about 5 seconds later, in the same conversation, with a `client.replied` event. Read the timeline with `GET /conversations/{id}` to see each step.

`.test` is a domain name that's reserved and can never reach a real inbox. Rookery checks twice: sandbox mail is never handed to the mail service at all, and the mail service itself refuses `.test` addresses.

## How the sandbox differs from live

- **Nothing is sent.** Every email from the sandbox is simulated, whoever it's to.
- **The undo window is 5 seconds**, not 10 minutes, so your tests don't wait.
- **The helpers don't run.** Scout doesn't sort, Quill doesn't draft (even when you assign a conversation to Quill) and Fetch doesn't read documents, so the same calls give the same answers every time and cost nothing. Scam checks still run, and attachments are still filed as documents. Sandbox mail isn't in Ask Rookery's search.
- **Tag names are shared** with your workspace. A tag used only in the sandbox doesn't show in your team's tag list, but a tag name you type in the sandbox is the same tag as a live one with that name.
- **Drafts wait for a yes** as they do live. Nobody approves them in the sandbox yet, so to test a whole send, use a key with `send` and `"send": true`.

## Webhooks in the sandbox

Sandbox events go only to **test endpoints**: in **Settings → Integrations → Webhooks**, under **Which events**, choose **Sandbox**. They're signed the same way as live ones, and every event says where it came from:

```json
{ "id": "evt_…", "type": "client.replied", "livemode": false, "data": { … } }
```

Live events go only to live endpoints, with `"livemode": true`. Check `livemode` anyway: it's the simplest guard against test data in your real systems.

## Coming next

- **The sandbox in the app:** see the sandbox's conversations, approve drafts and open each email as the recipient would see it.
- **Predictable helpers:** Scout, Quill and Fetch giving fixed answers by simple rules, so a test like "an email arrives, the agent drafts, the draft waits for a yes" runs end to end.
- **Raw MIME** for `POST /v1/sandbox/inbound`, and attachments of your own.
- **More magic addresses**, like a delay and a secure-link reply.

Tell us what you'd need at hello@userookery.com.
