E-Mail-Zustellung testen (Mail-Sandbox)
Die Mail-Sandbox testet Webhook-Handler, Suppressionslogik und Zustellergebnisse, ohne an ein echtes Postfach zu senden. Senden Sie über die normale API an eine Adresse bei messagebird.dev. Der Local-Part bestimmt das Ergebnis: bounce@messagebird.dev bounct und delivered@messagebird.dev stellt zu.
Ein Sandbox-Versand nutzt die normalen Annahme-, Event- und Webhook-Pfade. Er gibt dieselbe 202-Antwort zurück und erzeugt dieselben Event-Payload-Strukturen pro Empfänger wie ein Produktionsversand. Das Payload enthält kein Test-Flag. Die Nachricht erreicht weder die externe Zustellinfrastruktur noch ein echtes Postfach. Simulierte Bounces und Beschwerden beeinflussen weder die Sendereputation noch schreiben sie in die Suppressionsliste, sodass Sie die Adressen wiederverwenden können.
Die Sandbox erfordert kein Setup: keinen Schalter, keinen Testmodus, keinen speziellen API-Schlüssel. Sie löst ausschließlich anhand der Empfängeradresse aus, auf den normalen Sende-Endpunkten (POST /v1/email/messages und POST /v1/email/batches) und bei einem Broadcast: Ein Kontakt in der Zielgruppe, dessen Adresse eine Sandbox-Adresse ist, wird simuliert statt tatsächlich angeschrieben – so proben Sie eine Kampagne, ohne jemanden anzumailen. Ein simulierter Empfänger zählt trotzdem gegen Ihr Sendekontingent, sodass die Probe dasselbe Kontingent beansprucht wie der echte Versand.
Magic-Adressen
Alle Adressen liegen auf @messagebird.dev. Der Local-Part wählt das Ergebnis:
| Adresse | Simuliertes Ergebnis | Webhook-Sequenz | Hinweise |
|---|---|---|---|
| delivered@ | Der empfangende Mailserver nimmt die Nachricht an | email.accepted → email.processed → email.delivered | Der Erfolgsfall |
| bounce@ / hardbounce@ | Hard Bounce: SMTP 550, 5.1.1 Unknown User, Bounce-Klasse 10 | email.accepted → email.processed → email.bounced mit bounce_type: "hard" | Kein Eintrag in die Suppressionsliste, die Adresse bleibt wiederverwendbar |
| softbounce@ | Soft Bounce: SMTP 451, 4.3.0 Temporary failure, please retry, Klasse 20 | email.accepted → email.processed → email.bounced mit bounce_type: "soft" | Soft Bounces unterdrücken nie, weder echt noch simuliert |
| deferred@ / delay@ | Deferral: SMTP 451, 4.2.1 Mailbox temporarily unavailable, will retry, Klasse 21 | email.accepted → email.processed → email.deferred | Das simulierte Deferral ist endgültig: kein erneuter Versuch folgt, der Empfänger bleibt deferred |
| complaint@ / spam@ | Der Empfänger meldet die Nachricht als Spam (feedback_type: "abuse") | email.accepted → email.processed → email.complained | Kein Suppressionseintrag; wiederverwendbar |
| suppressed@ | Der Empfänger wird behandelt, als stünde er bereits auf Ihrer Suppressionsliste | email.accepted → email.rejected mit rejection_reason: "recipient_suppressed" | Bricht die Verarbeitung genauso ab wie bei einem echten unterdrückten Empfänger: kein email.processed, keine Zustell-Events |
| reject@ | Die Nachricht wird vor jedem Zustellversuch abgelehnt | email.accepted → email.rejected mit rejection_reason: "transmission_failed" | Kein email.processed und keine Zustell-Events folgen |
Die obigen Sequenzen sind die Webhooks, die ein einzelner Versand erzeugt. Bei einem Broadcast entfällt das führende email.accepted: Ein Broadcast-Empfänger wird eigenständig angenommen, aber dieses Event wird aufgezeichnet, ohne einen Webhook dafür zu senden. Jede Sequenz beginnt also beim jeweils nächsten Schritt, email.processed auf den Zustellpfaden und email.rejected für suppressed@ und reject@. Alles Weitere ist identisch, und die Event-Referenz beschreibt die Regel vollständig.
Ein Versand kann Sandbox- und echte Empfänger mischen. Jeder Empfänger durchläuft seinen eigenen Lebenszyklus: Echte Empfänger werden normal zugestellt, Sandbox-Empfänger werden simuliert.
Dieselben Events erscheinen auch in der Timeline der Nachricht im E-Mail-Log und in der Events-API. So können Sie die Sandbox auch ohne Webhook-Endpunkt nutzen und die Ergebnisse dort ablesen.
Adressregeln
- Die Erkennung erfolgt ausschließlich anhand des Local-Parts und nur auf der messagebird.dev-Domain. bounce@yourdomain.com ist eine normale Adresse.
- Nur die Local-Parts in der Magic-Adressen-Tabelle sind magic. Jede andere Adresse bei messagebird.dev ist ein normaler Empfänger. Wenn Sie über die gemeinsame Onboarding-Domain senden, muss die Adresse einem verifizierten Workspace-Mitglied gehören.
- Der Abgleich ist nicht case-sensitiv: Bounce@messagebird.dev und bounce@messagebird.dev verhalten sich identisch.
- +label-Subadressierung wird vor dem Abgleich entfernt: bounce+signup-flow@messagebird.dev bounct trotzdem. Verwenden Sie Labels, um Testfälle zuzuordnen; die vollständige Adresse einschließlich Label erscheint in Ihren Events und Webhooks, sodass jeder Testlauf seine eigenen Empfänger taggen kann.
Walkthrough: einen Bounce simulieren, von Anfang bis Ende
Sie benötigen keine verifizierte Sendedomain. Senden Sie von onboarding@messagebird.dev, wie in Erste E-Mail senden beschrieben. Erkannte Sandbox-Adressen sind von der Einschränkung auf verifizierte Mitglieder der Onboarding-Domain ausgenommen, zählen aber dennoch gegen deren Tageskontingent.
Stellen Sie sicher, dass Sie einen Webhook-Endpunkt haben, der E-Mail-Events abonniert hat (siehe Webhooks), und senden Sie dann:
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" }
}'Wenn Ihr Schlüssel mit bk_eu1_ beginnt, rufen Sie stattdessen https://eu1.platform.bird.com auf.
Die API antwortet mit 202 Accepted und einer em_*-Nachrichten-ID, nicht von einem Produktionsversand zu unterscheiden. Genau das ist der Sinn: Der Codepfad, den Sie testen, ist Ihr echter. Ihr Webhook-Endpunkt empfängt dann email.accepted, email.processed und schließlich email.bounced. Jedes Event enthält die tags und metadata des Versands (null, wenn der Versand keine hatte), und das email.bounced-Payload enthält die vollständige Bounce-Klassifikation:
Codebeispiel
{
"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
}
}Feldbedeutungen finden Sie in der Events-Referenz. Im Payload taucht kein Simulator-Flag auf: Event-Typ und -Struktur entsprechen exakt dem, was ein echter Hard Bounce erzeugt. Das Einzige, was den Versand als simuliert kennzeichnet, ist die Empfängeradresse selbst. Wenn Ihr Handler Testverkehr gesondert behandeln muss, orientieren Sie sich an der messagebird.dev-Empfängerdomain.
Um zu überprüfen, dass einem bereits unterdrückten Empfänger nie zugestellt wird, wiederholen Sie den Versand mit suppressed@messagebird.dev. Stellen Sie sicher, dass Sie email.accepted empfangen, dann email.rejected mit rejection_reason: "recipient_suppressed". Sie sollten kein email.processed und keine Zustell-Events erhalten. Das spiegelt einen echten unterdrückten Empfänger exakt wider: Die Nachricht wird angenommen und dann während der Verarbeitung vor jedem Versand abgebrochen.
Was die Sandbox kann und was nicht
- Keine Einträge in die Suppressionsliste. Simulierte Hard Bounces und Beschwerden fügen den Empfänger nicht zu Ihrer Suppressionsliste hinzu – deshalb bleiben die Adressen wiederverwendbar. Ihr Webhook feuert trotzdem (email.bounced, email.complained), sodass Ihre eigene Suppressionslogik vollständig durchlaufen wird. Um den Ablehnungspfad für bereits unterdrückte Empfänger zu testen, verwenden Sie die dedizierte suppressed@-Adresse.
- Keine echte Zustellung, niemals. Sandbox-Empfänger werden abgefangen, bevor die Nachricht die Zustellinfrastruktur erreicht. Es wird nichts übertragen, kein Postfach ist beteiligt, und Ihre Sendereputation bleibt unberührt.
- Request-Validierung greift weiterhin. Ein Sandbox-Versand nutzt die normalen Endpunkte, daher lehnen Schema-Prüfungen, Größenbeschränkungen und Header-Regeln einen ungültigen Request wie gewohnt ab. Was die Sandbox überspringt, ist alles nach der Übergabe: Rendering und Zustellverhalten jenseits dieses Punkts werden nicht durchlaufen.
- Opens und Klicks werden nicht simuliert. Eine Magic-Adresse simuliert das Übertragungsergebnis, und niemand öffnet die Nachricht, daher stammen email.opened und email.clicked ausschließlich aus echten E-Mails.
- Statistiken enthalten Sandbox-Traffic. Sandbox-Versendungen zählen zu den aggregierten Statistiken sowie den Bounce- und Beschwerderaten Ihres Workspace. Umfangreiches Sandbox-Bouncing verzerrt Ihre Dashboards, lässt aber Ihre Reputation unverändert.
Nächste Schritte
- Events: das vollständige Event-Vokabular und der Lebenszyklus pro Empfänger
- Webhooks: Abonnieren, Signaturverifizierung und Wiederholungen
- Erste E-Mail senden: der Onboarding-Domain-Schnellstart, auf dem dieser Walkthrough aufbaut
- Suppressions: wie die echte Suppressionsliste funktioniert
- E-Mails testen, ohne jemanden zuzuspammen: ein Video, das die Sandbox-Adressen und die jeweils erzeugten Events durchgeht
Verwandte Ressourcen
Weiter mit der Dokumentation, Anleitungen und Beispielen zu diesem Thema. Die Ressourcen sind auf Englisch.