Test mode
Send with an mf_test_ key to exercise the real send path without delivering mail, force per-recipient bounces and complaints, and read back which recipient was affected.
An mf_test_ key follows the same send path as a live key. The request is validated and accepted, and real delivery events come back as webhooks and timeline events. Every recipient is rewritten to a mailbox-simulator address, so no mail reaches a real inbox. Test sends go out from a Mailfully test address with your display name, so your from domain doesn't have to be verified yet. The API and webhooks still show your from.
curl -X POST https://api.mailfully.com/v1/emails \
-H "Authorization: Bearer mf_test_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"from": "orders@mail.acme.com",
"to": "ada@example.com",
"subject": "Your order shipped",
"text": "Order 4212 shipped."
}'Test sends skip the verified-domain check and are never metered against your monthly quota, so you can build an integration before DNS is in place. Test sends count toward a separate test-mode limit of 200 recipients a day; see Rate limits. Test messages are only visible to mf_test_ keys. See Authentication for how the two environments separate your data.
Force an outcome
The local part of each recipient decides the simulated outcome:
| Send to | Simulated outcome |
|---|---|
bounce@… | Permanent hard bounce |
complaint@… | Spam complaint |
suppressionlist@… | Permanent bounce, as if the address were on a suppression list |
ooto@… | Delivered, followed by an out-of-office auto-reply |
success@…, or anything else | Delivered |
Only the local part selects the outcome, so keep whatever fixture domain you already use: bounce@example.com, bounce@acme.test, and bounce@yourcompany.com all bounce. Matching is case-insensitive.
The whole local part has to be the keyword. bounce@example.com bounces, but bounce-1@example.com and bounce+retry@example.com are ordinary addresses and deliver.
How recipients are rewritten
Each recipient becomes a labeled simulator address, <outcome>+<label>@simulator.amazonses.com, where the label identifies the address you asked for:
ada@example.com → success+ada=example.com@simulator.amazonses.com
bounce@example.com → bounce+bounce=example.com@simulator.amazonses.com
Ada <ada@example.com> → success+ada=example.com@simulator.amazonses.com
ada+news@example.com → success+ada_news=example.com@simulator.amazonses.com
The label is the lowercased address with @ written as = and every character outside [a-z0-9._=-] written as _. Long addresses are shortened with a hash. Events always report the address you sent to.
Read back the affected recipient
Bounce and complaint events carry a recipients array, and the labels are resolved back to the addresses you originally requested. A message sent to ada@example.com and bounce@example.com reports the bounce like this:
curl https://api.mailfully.com/v1/emails/01J1X6P9T3KQ7ZW2V5R8YBAGMD/events \
-H "Authorization: Bearer mf_test_xxxxxxxxxxxx"
{
"data": [
{ "type": "send", "event_at": "2026-07-08T14:02:12.000Z", "detail": null },
{
"type": "bounce",
"event_at": "2026-07-08T14:02:15.000Z",
"detail": {
"bounce_type": "Permanent",
"bounce_subtype": "General",
"diagnostic_code": "smtp; 550 5.1.1 user unknown",
"recipients": ["bounce@example.com"]
}
}
]
}
The email.bounced webhook resolves the same way, in data.bounce.recipients, so your handler needs no test-only branch.
What test mode leaves alone
- The message keeps your sender and recipients:
GET /v1/emails/:idreturns thefrom,to,cc, andbccyou sent, and list filters match those addresses. SeeGET /v1/emails/{id}. - Templates, domains, webhooks, and suppressions are shared with live mode. One webhook endpoint receives events from both environments; tag your test sends if your handler needs to tell them apart.
- Idempotency, scheduling, cancellation, suppression filtering, and template rendering behave as they do in live mode.
- Rate limits apply too, counted separately from live at the same rate, so test traffic doesn't use your live allowance.
- Bounce and complaint notices by email go to Mailfully, not to you, because the sender is the test address. The events themselves still reach you through webhooks and the API.
Suppressions from test bounces
A test bounce or complaint suppresses the simulator address, for example bounce+ada=example.com@simulator.amazonses.com, and never ada@example.com. A bounce drill can't block a live send to that person.
Those entries show up in your suppression list, one per distinct test recipient.
Suppression entries are org-global, so simulator addresses appear when you list suppressions with a live key too. The list has no domain filter; skip anything ending in @simulator.amazonses.com in an operational view, or delete the entries on the Blocklist page in the dashboard once a drill is done.
