Probar la entrega de correo (sandbox de correo)
El sandbox de correo prueba los manejadores de webhooks, la lógica de supresión y los resultados de entrega sin enviar a una bandeja de entrada real. Envía a través del API normal a una dirección en messagebird.dev. La parte local determina el resultado: bounce@messagebird.dev rebota y delivered@messagebird.dev entrega.
Un envío con sandbox usa las rutas normales de aceptación, eventos y webhooks. Devuelve la misma respuesta 202 y produce las mismas estructuras de payload de evento por destinatario que un envío en producción. El payload no tiene ninguna marca de prueba. El mensaje no llega a la infraestructura de entrega externa ni a una bandeja de entrada real. Los rebotes y quejas simulados no afectan la reputación de envío ni escriben en la lista de supresión, así que puedes reutilizar las direcciones.
El sandbox no necesita configuración: sin interruptor, sin modo de prueba, sin clave API especial. Se activa exclusivamente por la dirección del destinatario, en los endpoints normales de envío (POST /v1/email/messages y POST /v1/email/batches) y en un broadcast: un contacto en la audiencia cuya dirección es una dirección de sandbox se simula en lugar de enviarse, que es como ensayas una campaña sin enviar correo a nadie. Un destinatario simulado cuenta contra tu cuota de envío, así que el ensayo ejercita la misma cuota que usaría el envío real.
Direcciones mágicas
Todas las direcciones están en @messagebird.dev. La parte local selecciona el resultado:
| Dirección | Resultado simulado | Secuencia de webhooks | Notas |
|---|---|---|---|
| delivered@ | El servidor de correo receptor acepta el mensaje | email.accepted → email.processed → email.delivered | El camino exitoso |
| bounce@ / hardbounce@ | Rebote duro: SMTP 550, 5.1.1 Unknown User, bounce class 10 | email.accepted → email.processed → email.bounced con bounce_type: "hard" | Sin escritura en la lista de supresión, así que la dirección sigue siendo reutilizable |
| softbounce@ | Rebote suave: SMTP 451, 4.3.0 Temporary failure, please retry, class 20 | email.accepted → email.processed → email.bounced con bounce_type: "soft" | Los rebotes suaves nunca suprimen, reales o simulados |
| deferred@ / delay@ | Aplazamiento: SMTP 451, 4.2.1 Mailbox temporarily unavailable, will retry, class 21 | email.accepted → email.processed → email.deferred | El aplazamiento simulado es terminal: no hay reintento posterior, así que el destinatario queda como deferred |
| complaint@ / spam@ | El destinatario reporta el mensaje como spam (feedback_type: "abuse") | email.accepted → email.processed → email.complained | Sin escritura de supresión; reutilizable |
| suppressed@ | El destinatario se trata como si ya estuviera en tu lista de supresión | email.accepted → email.rejected con rejection_reason: "recipient_suppressed" | Se cortocircuita durante el procesamiento exactamente como un destinatario suprimido real: sin email.processed, sin eventos de entrega |
| reject@ | El mensaje se rechaza antes de cualquier intento de entrega | email.accepted → email.rejected con rejection_reason: "transmission_failed" | No siguen email.processed ni eventos de entrega |
Las secuencias anteriores son los webhooks que produce un envío individual. En un broadcast, omite el email.accepted inicial: un destinatario de broadcast se acepta por derecho propio, pero registramos ese evento sin enviar un webhook para él, así que cada secuencia empieza en lo que sigue, email.processed en las rutas de entrega y email.rejected para suppressed@ y reject@. Todo lo demás es idéntico, y la referencia de eventos cubre la regla en detalle.
Un envío puede mezclar destinatarios de sandbox y reales. Cada destinatario tiene su propio ciclo de vida: los destinatarios reales se entregan normalmente, los de sandbox se simulan.
Los mismos eventos también aparecen en la línea de tiempo del mensaje en el registro de correo y en el API de eventos, así que puedes usar el sandbox sin un endpoint de webhooks y leer los resultados directamente.
Reglas de direccionamiento
- La detección es solo por la parte local, y solo en el dominio messagebird.dev. bounce@yourdomain.com es una dirección normal.
- Solo las partes locales en la tabla de direcciones mágicas son mágicas. Cualquier otra dirección en messagebird.dev es un destinatario normal. Cuando envías desde el dominio compartido de incorporación, esa dirección debe pertenecer a un miembro verificado del espacio de trabajo.
- La coincidencia no distingue mayúsculas de minúsculas: Bounce@messagebird.dev y bounce@messagebird.dev se comportan de forma idéntica.
- El subdireccionamiento +label se elimina antes de la coincidencia: bounce+signup-flow@messagebird.dev sigue rebotando. Usa etiquetas para correlacionar casos de prueba; la dirección completa, etiqueta incluida, aparece en tus eventos y webhooks, así que cada ejecución de prueba puede etiquetar a sus propios destinatarios.
Paso a paso: simular un rebote de principio a fin
No necesitas un dominio de envío verificado. Envía desde onboarding@messagebird.dev, como se describe en Enviar tu primer correo. Las direcciones de sandbox reconocidas están exentas de la restricción de miembro verificado del dominio de incorporación, pero cuentan para su cuota diaria.
Asegúrate de tener un endpoint de webhooks suscrito a eventos de correo (consulta Webhooks) y luego envía:
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" }
}'Si tu clave empieza con bk_eu1_, llama a https://eu1.platform.bird.com en su lugar.
El API responde 202 Accepted con un ID de mensaje em_*, indistinguible de un envío en producción. Ese es el punto: la ruta de código que estás probando es la real. Tu endpoint de webhooks recibe entonces email.accepted, email.processed y finalmente email.bounced. Cada evento incluye el tags y el metadata del envío (null cuando el envío no tenía ninguno), y el payload de email.bounced tiene la clasificación completa del rebote:
Ejemplo de código
{
"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
}
}El significado de los campos está en la referencia de eventos. No aparece ninguna marca de simulador en ningún lugar del payload: el tipo y la estructura del evento son exactamente los que produce un rebote duro real. Lo único que lo identifica como simulado es la dirección del destinatario, así que si tu manejador necesita tratar de forma especial el tráfico de prueba, usa como clave el dominio de destinatario messagebird.dev.
Para verificar que un destinatario ya suprimido nunca recibe el envío, repite el envío con suppressed@messagebird.dev. Comprueba que recibes email.accepted, luego email.rejected con rejection_reason: "recipient_suppressed". No deberías recibir email.processed ni eventos de entrega. Esto refleja exactamente un destinatario suprimido real: el mensaje se acepta y luego se cortocircuita durante el procesamiento antes de cualquier envío.
Qué hace y qué no hace el sandbox
- Sin escrituras en la lista de supresión. Los rebotes duros y quejas simulados no añaden al destinatario a tu lista de supresión; eso es lo que mantiene las direcciones reutilizables. Tu webhook sigue disparándose (email.bounced, email.complained), así que tu propia lógica de supresión se ejercita completamente. Para probar la ruta de rechazo por supresión previa, usa la dirección dedicada suppressed@.
- Sin entrega real, nunca. Los destinatarios de sandbox se interceptan antes de que el mensaje llegue a la infraestructura de entrega. No se transmite nada, no interviene ninguna bandeja de entrada y tu reputación de envío queda intacta.
- La validación de la solicitud sigue aplicándose. Un envío con sandbox usa los endpoints normales, así que las comprobaciones de esquema, los límites de tamaño y las reglas de encabezados rechazan una solicitud inválida como de costumbre. Lo que el sandbox omite es todo lo posterior a la transferencia: el renderizado y el comportamiento de entrega a partir de ese punto no se ejercitan.
- Las aperturas y los clics no se simulan. Una dirección mágica simula el resultado de la transmisión, y nadie abre el mensaje, así que email.opened y email.clicked solo provienen de correo real.
- Las estadísticas incluyen tráfico de sandbox. Los envíos de sandbox cuentan para las estadísticas agregadas de tu espacio de trabajo y para las tasas de rebote y quejas. Un alto volumen de rebotes de sandbox distorsiona tus paneles sin afectar tu reputación.
Próximos pasos
- Eventos: el vocabulario completo de eventos y el ciclo de vida por destinatario
- Webhooks: suscripción, verificación de firma y reintentos
- Enviar tu primer correo: la guía de inicio rápido con el dominio de incorporación sobre la que se basa este paso a paso
- Supresiones: cómo funciona la lista de supresión real
- Prueba emails sin enviar spam a nadie: un video que recorre las direcciones de sandbox y los eventos que produce cada una
Recursos relacionados
Continúa con la documentación, guías y ejemplos sobre este tema. Los recursos están en inglés.