Guide

Testing your email integration

There is no sandbox mode, so the honest answer is a set of addresses that exercise the real pipeline. That includes the branch most integrations never test: the one where we refuse to send.

Read this if you are wiring up sends and want to see every outcome before real mail moves. Skip it if you only need the happy path, which is the first address below and nothing else.

There is no sandbox mode, and what to do instead

Some providers give you a fake mode where sends are accepted and quietly discarded. SendHeron does not have one yet. Saying so plainly is more useful than a page that describes the workaround as though it were a feature, because the workaround has a property a sandbox does not: it exercises the real code path, the real compliance gate and the real provider.

What you use instead is the mailbox simulator run by our sending provider. These are real addresses that accept real mail and then behave in a scripted way, so a send to one is a genuine send that produces a genuine outcome. Nothing reaches a person, and no domain reputation is spent on a delivery that was never going to be read.

One consequence to plan for: simulator sends are real sends. They count against your monthly pool exactly as any other send does. That is a feature when you are load-testing, because the number you are testing against is the number you actually have, and it is worth knowing before you loop a thousand of them in a test suite.

The two addresses that matter

Most of the simulator's addresses model provider-side behaviour you cannot do much about. Two of them model the outcomes your code has to branch on, and those are the two worth building a test around.

  1. 01 [email protected]Accepted and delivered. The send record comes back with status sent, a providerMessageId, and a deliveredAt that fills in shortly afterwards once the provider event arrives. This is the path your happy-case assertions belong on.
  2. 02 [email protected]Hard bounces. The first send succeeds and then bounces, and the bounce writes a REAL organization-wide hard suppression, exactly as a real bad address would. That suppression is the point: it is what lets you test the branch below.

Testing the branch where we refuse to send

This is the part almost nobody exercises before production, and it is the part that behaves least like people expect. A refused send is not an error. It is a 201 whose body says we did not send, and code written against the assumption that 201 means delivered will record a receipt that never went out.

The bounce address gives you that state on demand. Send to it once and it succeeds. The bounce lands, the hard suppression is written, and every send to that address from then on comes back 201 with a status of suppressed and an errorMessage of HARD_SUPPRESSED. So the second send is your test case, and it is a faithful one: it is produced by the same gate that will refuse a real bounced address in production.

Assert on the machine-readable value rather than on any text. errorMessage carries one of ten stable reasons, and they are the contract; the human-facing wording around them is not.

Second send to the bounce address, once the suppression exists
{
  "id": "9c1f...",
  "to": "[email protected]",
  "status": "suppressed",
  "errorMessage": "HARD_SUPPRESSED",
  "providerMessageId": null,
  "sentAt": null
}

Cleaning up afterwards

The suppression you just created is real and organization-wide, so leaving it in place will keep refusing sends to that address in every workspace you own. Remove it when the test is done.

Lifting a hard suppression needs an explicit confirmation in the body, because a bulk or accidental version of this call is how somebody resurrects an entire bounced list and burns a sending domain. One address per call, and the flag is required.

DELETE /api/v1/suppressions/{email}
curl -X DELETE \
  'https://api.sendheron.com/api/v1/suppressions/bounce%40simulator.amazonses.com' \
  -H 'Authorization: Bearer <YOUR_API_KEY>' \
  -H 'Content-Type: application/json' \
  -d '{ "confirmHardTier": true }'

What else is worth asserting

Two more checks catch the mistakes that survive a green test suite.

  • Retry safety. Send the same request twice with the same idempotency key and assert you get one send, not two. If you are using the Node SDK this is already true, because it generates and reuses a key for you.
  • The refusal is not an exception. Assert that a suppressed outcome does not throw in your code and does not get retried. A suppressed send retried on a schedule is a job that never drains.
  • Rate-limit handling. A 429 carries Retry-After, and honouring it is the difference between a client that recovers and one that hammers a closed door.

When this does not apply

Do not point a load test at the bounce address. Every send to it that lands before the suppression exists is a real hard bounce recorded against your sending reputation, and a thousand of them is a genuine deliverability problem rather than a test artefact. Send to it once, assert the refusal, clean up.

Questions

Will there be a sandbox mode?
It is wanted and it is not built, so there is no date to give you. The simulator covers the cases a sandbox would, with the advantage that it runs the real pipeline rather than a parallel one that can drift from it.
Do simulator sends count against my monthly pool?
Yes. They are real sends. Check your position with GET /usage before and after a large test run if the pool matters to you.
How do I test a refusal without creating a real suppression?
You cannot, and that is deliberate: the suppression IS the mechanism being tested. What you can do is check the state without sending, by calling GET /suppressions/check, which runs the same gate function the send paths run and tells you per stream whether a send would go out.
What about the complaint and other simulator addresses?
They work and they behave as the provider documents. The complaint address writes a hard suppression exactly as the bounce address does, so for testing your own branching the two are interchangeable and the cleanup is identical.

Read next

Private beta

Guides are free. The API needs a key.

Join the list and your onboarding email carries a scoped key and the base URL, so you can follow this end to end.