E-mailbezorging testen (mail-sandbox)
De mail-sandbox test webhookhandlers, suppressielogica en bezorguitkomsten zonder naar een echte inbox te sturen. Verstuur via de normale API naar een adres op messagebird.dev. Het lokale deel bepaalt de uitkomst: bounce@messagebird.dev bounced en delivered@messagebird.dev wordt bezorgd.
Een sandbox-verzending gebruikt de normale acceptatie-, event- en webhookpaden. Het retourneert hetzelfde 202-antwoord en produceert dezelfde event-payloads per ontvanger als een productieverzending. De payload bevat geen testvlag. Het bericht bereikt geen externe bezorginfrastructuur of echte inbox. Gesimuleerde bounces en klachten hebben geen invloed op je verzendreputatie en schrijven niet naar de suppressielijst, zodat je de adressen kunt hergebruiken.
De sandbox vereist geen configuratie: geen schakelaar, geen testmodus, geen speciale API-sleutel. De activering werkt puur op het ontvangersadres, op de normale verzendendpoints (POST /v1/email/messages en POST /v1/email/batches) en op een broadcast: een contact in de doelgroep met een sandbox-adres wordt gesimuleerd in plaats van daadwerkelijk verzonden, en zo repeteer je een campagne zonder iemand te mailen. Een gesimuleerde ontvanger telt wel mee voor je verzendlimiet, zodat de repetitie hetzelfde quotum gebruikt als de echte verzending.
Magische adressen
Alle adressen staan op @messagebird.dev. Het lokale deel bepaalt de uitkomst:
| Adres | Gesimuleerde uitkomst | Webhooksequentie | Opmerkingen |
|---|---|---|---|
| delivered@ | De ontvangende mailserver accepteert het bericht | email.accepted → email.processed → email.delivered | Het succespad |
| bounce@ / hardbounce@ | Hard bounce: SMTP 550, 5.1.1 Unknown User, bounce class 10 | email.accepted → email.processed → email.bounced met bounce_type: "hard" | Geen schrijfactie naar de suppressielijst, dus het adres blijft herbruikbaar |
| softbounce@ | Soft bounce: SMTP 451, 4.3.0 Temporary failure, please retry, class 20 | email.accepted → email.processed → email.bounced met bounce_type: "soft" | Soft bounces leiden nooit tot suppressie, echt of gesimuleerd |
| deferred@ / delay@ | Deferral: SMTP 451, 4.2.1 Mailbox temporarily unavailable, will retry, class 21 | email.accepted → email.processed → email.deferred | De gesimuleerde deferral is definitief: er volgt geen nieuwe poging, dus de ontvanger blijft deferred |
| complaint@ / spam@ | De ontvanger meldt het bericht als spam (feedback_type: "abuse") | email.accepted → email.processed → email.complained | Geen schrijfactie naar de suppressielijst; herbruikbaar |
| suppressed@ | De ontvanger wordt behandeld alsof deze al op je suppressielijst staat | email.accepted → email.rejected met rejection_reason: "recipient_suppressed" | Wordt tijdens verwerking kortgesloten, precies zoals een echte onderdrukte ontvanger: geen email.processed, geen bezorgevents |
| reject@ | Het bericht wordt geweigerd vóór enige bezorgpoging | email.accepted → email.rejected met rejection_reason: "transmission_failed" | Geen email.processed of bezorgevents volgen |
De sequenties hierboven zijn de webhooks die één enkele verzending produceert. Bij een broadcast laat je de eerste email.accepted weg: een broadcastontvanger wordt op zichzelf geaccepteerd, maar dat event wordt vastgelegd zonder er een webhook voor te sturen, dus elke sequentie begint bij wat erna komt: email.processed op de bezorgpaden en email.rejected voor suppressed@ en reject@. Alles daarna is identiek, en de eventreferentie beschrijft de regel volledig.
Een verzending kan sandbox- en echte ontvangers combineren. Elke ontvanger doorloopt zijn eigen levenscyclus: echte ontvangers worden normaal bezorgd, sandbox-ontvangers worden gesimuleerd.
Dezelfde events verschijnen ook op de tijdlijn van het bericht in het e-maillogboek en in de events-API, zodat je de sandbox ook zonder webhook-endpoint kunt gebruiken en de uitkomsten kunt teruglezen.
Adresregels
- Detectie werkt alleen op het lokale deel, en alleen op het messagebird.dev-domein. bounce@yourdomain.com is een normaal adres.
- Alleen de lokale delen in de tabel met magische adressen zijn magisch. Elk ander adres op messagebird.dev is een normale ontvanger. Als je verstuurt vanaf het gedeelde onboardingdomein, moet dat adres toebehoren aan een geverifieerd lid van de werkruimte.
- Matching is hoofdletterongevoelig: Bounce@messagebird.dev en bounce@messagebird.dev gedragen zich identiek.
- +label-subadressering wordt gestript vóór matching: bounce+signup-flow@messagebird.dev bounced nog steeds. Gebruik labels om testgevallen te correleren; het volledige adres, inclusief label, verschijnt in je events en webhooks, zodat elke testrun zijn eigen ontvangers kan taggen.
Walkthrough: een bounce simuleren, van begin tot eind
Je hebt geen geverifieerd verzenddomein nodig. Verstuur vanaf onboarding@messagebird.dev, zoals beschreven in Je eerste e-mail versturen. Herkende sandbox-adressen zijn vrijgesteld van de geverifieerd-lidbeperking van het onboardingdomein, maar tellen wel mee voor het dagelijkse quotum.
Zorg dat je een webhook-endpoint hebt dat is geabonneerd op e-mailevents (zie Webhooks) en verstuur dan:
const msg = await bird.email.send({
from: { email: "onboarding@messagebird.dev", name: "Bird" },
to: ["bounce+signup-flow@messagebird.dev"],
subject: "Sandbox bounce test",
html: "<p>This message will hard-bounce.</p>",
tags: [{ name: "flow", value: "signup" }],
metadata: { test_run: "docs-capture-1" },
});
console.log(msg.id, msg.status); // "em_…", "accepted"msg = client.email.send(
from_={"email": "onboarding@messagebird.dev", "name": "Bird"},
to=["bounce+signup-flow@messagebird.dev"],
subject="Sandbox bounce test",
html="<p>This message will hard-bounce.</p>",
tags=[{"name": "flow", "value": "signup"}],
metadata={"test_run": "docs-capture-1"},
)
print(msg.id, msg.status)package main
import (
"context"
"fmt"
"log"
"os"
bird "github.com/messagebird/bird-sdk-go"
"github.com/messagebird/bird-sdk-go/option"
)
func main() {
client, err := bird.NewClient(option.WithAPIKey(os.Getenv("BIRD_API_KEY")))
if err != nil {
log.Fatal(err)
}
msg, err := client.Email.Send(context.Background(), bird.EmailSendParams{
From: "onboarding@messagebird.dev",
To: []string{"bounce+signup-flow@messagebird.dev"},
Subject: "Sandbox bounce test",
HTML: "<p>This message will hard-bounce.</p>",
Tags: []bird.Tag{{Name: "flow", Value: "signup"}},
Metadata: map[string]any{"test_run": "docs-capture-1"},
})
if err != nil {
log.Fatal(err)
}
fmt.Println(msg.Id, *msg.Status)
}$message = $bird->email->send(
from: 'Bird <onboarding@messagebird.dev>',
to: ['bounce+signup-flow@messagebird.dev'],
subject: 'Sandbox bounce test',
html: '<p>This message will hard-bounce.</p>',
);
echo $message->getId(), ' ', $message->getStatus();bird email send \
--from onboarding@messagebird.dev \
--html '<p>This message will hard-bounce.</p>' \
--metadata '{"test_run":"docs-capture-1"}' \
--subject 'Sandbox bounce test' \
--tag flow=signup \
--to bounce+signup-flow@messagebird.dev{
"name": "email_send",
"arguments": {
"from": {
"email": "onboarding@messagebird.dev"
},
"html": "<p>This message will hard-bounce.</p>",
"metadata": {
"test_run": "docs-capture-1"
},
"subject": "Sandbox bounce test",
"tags": [
{
"name": "flow",
"value": "signup"
}
],
"to": [
{
"email": "bounce+signup-flow@messagebird.dev"
}
]
}
}curl -X POST "https://us1.platform.bird.com/v1/email/messages" \
-H "Authorization: Bearer $BIRD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "onboarding@messagebird.dev",
"to": ["bounce+signup-flow@messagebird.dev"],
"subject": "Sandbox bounce test",
"html": "<p>This message will hard-bounce.</p>",
"tags": [{ "name": "flow", "value": "signup" }],
"metadata": { "test_run": "docs-capture-1" }
}'Als je sleutel begint met bk_eu1_, gebruik dan https://eu1.platform.bird.com.
De API antwoordt met 202 Accepted en een em_* bericht-ID, niet te onderscheiden van een productieverzending. Dat is precies het punt: het codepad dat je test is je echte pad. Je webhook-endpoint ontvangt vervolgens email.accepted, email.processed en ten slotte email.bounced. Elk event bevat de tags en metadata van de verzending (null als de verzending er geen had), en de email.bounced-payload bevat de volledige bounceclassificatie:
Codevoorbeeld
{
"type": "email.bounced",
"timestamp": "2026-07-23T14:51:00.362Z",
"data": {
"email_id": "em_01ky7qanhrejer0bn34v38hrxh",
"recipient_id": "er_01ky7qanhrejds4decpk83q5qq",
"workspace_id": "ws_01ky7m21hjffh9s8kq76gyb1xf",
"recipient": "bounce+signup-flow@messagebird.dev",
"recipient_role": "to",
"bounce_type": "hard",
"bounce_class": 10,
"bounce_code": "550",
"bounce_description": "5.1.1 Unknown User",
"sending_ip": null,
"tags": [{ "name": "flow", "value": "signup" }],
"metadata": { "test_run": "docs-capture-1" },
"broadcast_id": null
}
}Veldbetekenissen staan in de eventreferentie. Er verschijnt nergens een simulatorvlag in de payload: het eventtype en de structuur zijn precies wat een echte hard bounce produceert. Het enige dat het als gesimuleerd identificeert, is het ontvangersadres zelf. Als je handler testverkeer apart moet behandelen, gebruik dan het messagebird.dev-ontvangersdomein als sleutel.
Om te verifiëren dat een al onderdrukte ontvanger nooit wordt verzonden, herhaal je de verzending met suppressed@messagebird.dev. Controleer dat je email.accepted ontvangt, dan email.rejected met rejection_reason: "recipient_suppressed". Je hoort geen email.processed of bezorgevents te ontvangen. Dit spiegelt exact een echte onderdrukte ontvanger: het bericht wordt geaccepteerd en vervolgens kortgesloten tijdens verwerking vóór enige verzending.
Wat de sandbox wel en niet doet
- Geen schrijfacties naar de suppressielijst. Gesimuleerde hard bounces en klachten voegen de ontvanger niet toe aan je suppressielijst; dat is waardoor de adressen herbruikbaar blijven. Je webhook wordt wel geactiveerd (email.bounced, email.complained), dus je eigen suppressielogica wordt volledig doorlopen. Om het weigeringspad voor een al onderdrukte ontvanger te testen, gebruik je het speciale suppressed@-adres.
- Geen echte bezorging, ooit. Sandbox-ontvangers worden onderschept voordat het bericht de bezorginfrastructuur bereikt. Er wordt niets verzonden, er is geen inbox betrokken en je verzendreputatie blijft onaangetast.
- Requestvalidatie blijft van toepassing. Een sandbox-verzending gebruikt de normale endpoints, dus schemacontroles, groottelimieten en headerregels weigeren een ongeldig verzoek zoals gebruikelijk. Wat de sandbox overslaat is alles na de overdracht: rendering en bezorggedrag voorbij dat punt worden niet doorlopen.
- Opens en kliks worden niet gesimuleerd. Een magisch adres simuleert het verzendresultaat, en niemand opent het bericht, dus email.opened en email.clicked komen alleen van echte e-mail.
- Statistieken bevatten sandbox-verkeer. Sandbox-verzendingen tellen mee voor de totale statistieken, bounce- en klachtpercentages van je werkruimte. Veel sandbox-bounces vertekenen je dashboards, maar laten je reputatie ongemoeid.
Volgende stappen
- Events: het volledige eventvocabulaire en de levenscyclus per ontvanger
- Webhooks: abonneren, handtekeningverificatie en opnieuw proberen
- Je eerste e-mail versturen: de quickstart met het onboardingdomein waar deze walkthrough op voortbouwt
- Suppressies: hoe de echte suppressielijst werkt
- E-mail testen zonder iemand te spammen: een video die de sandbox-adressen en de events die elk adres produceert, doorloopt
Gerelateerde bronnen
Ga verder met de documentatie, gidsen en voorbeelden voor dit onderwerp. De bronnen zijn in het Engels.