Reference
Testing Without Sending
Exercise your webhook handlers, your retry logic and your bounce and complaint paths against real SendFleet events, without emailing a real person and without moving a single one of your numbers.
The test addresses
Use them anywhere you would use a real recipient address. The request is accepted, the log row is written, and the matching webhook fires, with the same payload shape as a genuine delivery event, so your handler cannot tell the difference by its keys.
| Address | You get | Webhook |
|---|---|---|
delivered@sendfleet.net | Log status delivered, with a delivered_at timestamp | email.delivered |
bounce@sendfleet.net | Log status bounced, bounce type Permanent / subtype General | email.bounced |
complaint@sendfleet.net | Log status complained, with a complained_at timestamp | email.complained |
success@sendfleet.net and sent@sendfleet.net are accepted aliases for delivered@, and bounced@ / complained@ for the other two, in case the spelling in your fixtures already reads that way. Matching is case-insensitive, and a display name (QA <bounce@sendfleet.net>) is fine.
Making a test send
There is nothing to configure. It is the same endpoint, with a test address in to:
curl -X POST https://sendfleet.net/api/send/ \
-H "X-API-Key: $SENDFLEET_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "you@yourdomain.com",
"to": "bounce@sendfleet.net",
"subject": "Webhook smoke test",
"html": "<p>If you can read this in your handler, testing works.</p>"
}'The response is an ordinary 200 with an extra entry in warnings:
{
"success": true,
"message": "Email queued for delivery.",
"message_id": "…",
"mode": "shared",
"warnings": [
"Test send: this counts towards your monthly quota like any other send, but is not counted towards your bounce or complaint rates."
]
}What the webhook payload looks like
Identical in shape to a genuine event, so the same verification and parsing code handles both. Two values necessarily differ: the message ID is a synthetic sendfleet-test-… string rather than an AWS one, and SMTP-level detail is a fixed placeholder (smtp_response on a delivery, for instance) because no SMTP conversation happened.
{
"idempotency_key": "8f14e45fceea167a5a36dedd4bea2543",
"ses_message_id": "sendfleet-test-3f9c…",
"recipient": "bounce@sendfleet.net",
"status": "bounced",
"bounce_type": "Permanent",
"bounce_subtype": "General",
"bounced_recipients": ["bounce@sendfleet.net"],
"timestamp": "2026-09-26T14:03:11.482913+00:00"
}| Field | Behaviour |
|---|---|
to | Must be one of the addresses above. Any other address is real traffic and is treated as such. |
from | Must still be a verified sender domain in your account. The normal sending gates run first, so a test send cannot be used to bypass domain verification. |
attachments | Accepted and staged as normal, then discarded with the rest of the test send. Nothing is transmitted. |
idempotency_key | Behaves exactly as on a real send. A retried test send with the same key is deduplicated the same way. |
What counts and what does not
| Counter | Effect of a test send |
|---|---|
| Monthly send allowance | Counts, like any other send. At your limit a test send gets the same 429 usage_limit_exceeded. |
| Bounce rate | None. This is the reason the addresses exist: pointing a staging deploy at bounce@sendfleet.net cannot push an account over a limit. |
| Complaint rate | None, for the same reason. |
| Email log | A row is written, exactly as for real traffic, and is purged on the normal 14-day schedule. |
| Customer webhooks | Fire normally. That is the point. |
The AWS SES simulator
Anything at simulator.amazonses.com is treated the same way for your numbers (it counts against your monthly allowance and never against your rates), with one important difference: it is handed to AWS. The SES simulator accepts and swallows simulator addresses, which is what makes it useful when you are testing your SES-level configuration.
curl -X POST https://sendfleet.net/api/send/ \
-H "X-API-Key: $SENDFLEET_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "you@yourdomain.com",
"to": "bounce@simulator.amazonses.com",
"subject": "SES-level smoke test",
"html": "<p>Handed to AWS and swallowed there.</p>"
}'| Item | Test address | SES simulator |
|---|---|---|
| Handed to AWS | No | Yes, and swallowed there |
| Appears in the email log | Yes | Yes |
| Fires your webhook | Yes | Yes, if AWS reports the outcome |
| Counts against your monthly allowance | Yes | Yes |
| Counts toward bounce / complaint rates | No | No |
| Use it to test | Your SendFleet integration: handlers, retries, error paths | Your AWS SES configuration, from the SendFleet API |
BYOC
This is not an oversight and it is worth understanding why. The test addresses work by SendFleet fabricating the delivery event itself: that is what gives you a webhook without a message leaving our infrastructure. On BYOC your own AWS account performs the send, and that account has no event destination wired to SendFleet, so there is nothing for us to fabricate. A BYOC send to bounce@sendfleet.net would put a real message into your SES, addressed to a mailbox that does not exist: your account would look the address up, get a real non-delivery report, and count it against the sender reputation you own, and you would get no webhook, because BYOC delivers no delivery events at all.
| Recipient | Result |
|---|---|
bounce@ / complaint@ / delivered@sendfleet.net | Refused with 403 unauthorized. Nothing queued, nothing billed, no message in your account. |
*@simulator.amazonses.com | Sent. SES accepts and swallows simulator addresses in any account, so on BYOC this does what you asked without touching your reputation. On Free BYOC it counts against the monthly allowance like any other send. |
A real address | Ordinary BYOC send. Nothing changes. |
To exercise a BYOC integration, send to a real address you control. A plus-addressed alias on a domain you own is enough to verify SPF, DKIM, DMARC and your own suppression handling end to end, and it costs you one message against your own quota rather than a permanent NDR in your account.
Testing a production-shaped flow
A pattern that keeps a permanent staging configuration from ever touching a real address list: keep your sandbox credentials pointed at the test addresses, and let promotion to production be the only thing that swaps them.
const TEST_ADDRESSES = new Set([
"delivered@sendfleet.net",
"bounce@sendfleet.net",
"complaint@sendfleet.net",
])
// `config` stands for however your app already reads its environment.
async function send({ to, ...mail }) {
// Outside production, refuse to mail a real person. Inside production,
// refuse to pretend: a test address there is almost always a mistake.
const isTest = TEST_ADDRESSES.has(to) || to.endsWith("@simulator.amazonses.com")
if (!config.isProduction && !isTest) {
throw new Error(`refusing to send to ${to} outside production`)
}
if (config.isProduction && isTest) {
throw new Error(`refusing to send to test address ${to} in production`)
}
return post("/api/send/", {
...mail,
to,
apiKey: config.sendfleetApiKey,
})
}Frequently asked questions
Test addresses, what they cost you, and where they work.