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.
On this page
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 and your daily sending capacity 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.
Before your account has been reviewed
A new account can send only to team members who have confirmed their email address and to addresses on its own verified domains until a person at SendHeron has approved it, which usually happens within a day. Every other recipient, the simulator addresses included, comes back 201 with a status of suppressed and an errorMessage of SENDER_UNDER_REVIEW. That refusal is about your account rather than the address, and it lifts on approval.
The account owner asks for the review from the getting-started card in the dashboard, with a short note on what you send, to whom and how many, after confirming their address with the button in the welcome email. Until it comes through, start with your own address, where the happy path works as soon as you have confirmed it, and move to the simulator once GET /usage reports plan.sendingReview as APPROVED. A sequence enrollment that reaches a send before then is parked rather than cancelled, and carries on by itself when the account is approved.
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.
- 01 [email protected]Accepted and delivered. The send record comes back with status
sent, aproviderMessageId, and adeliveredAtthat fills in shortly afterwards once the provider event arrives. This is the path your happy-case assertions belong on. - 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 12 stable reasons, and they are the contract; the human-facing wording around them is not.
{
"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.
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
A few 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.
- A send that waits. A
201can also carry a status ofqueued, when the send is waiting for daily sending capacity, new-sender warm-up or the checks on an old imported list, and it goes out by itself on the same record. No simulator address produces it on demand, so cover it with a stubbed response: assert that your code keeps theid, records the send as pending rather than failed or delivered, and does not retry it. - Rate-limit handling. A
429carriesRetry-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, so they count against your monthly pool and your daily sending capacity like any other. Check your position with GET /usage before and after a large test run if either 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 gate the send paths run on the recipient and your organization and tells you per stream whether that gate would let a send through. Sender setup, such as a verified domain, is only checked on a real send.
- 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.