Enviar correo electrónico
POST /v1/email/messages envía un correo electrónico. Proporciona un remitente, destinatarios y contenido en un payload JSON. El API devuelve 202 Accepted con un ID de mensaje y luego entrega el correo de forma asíncrona. Consulta la referencia de API para ver los esquemas completos.
Un envío mínimo
El payload válido más pequeño es un from, al menos un destinatario to, un subject y un cuerpo (html, text o ambos). La dirección from debe estar en un dominio que hayas verificado en este espacio de trabajo, o en el dominio de onboarding.
const msg = await bird.email.send({
from: { email: "onboarding@messagebird.dev", name: "Bird" },
to: ["delivered@messagebird.dev"],
subject: "Hello from Bird",
html: "<p>My first Bird email.</p>",
});
console.log(msg.id, msg.status); // "em_…", "accepted"msg = client.email.send(
from_={"email": "onboarding@messagebird.dev", "name": "Bird"},
to=["delivered@messagebird.dev"],
subject="Hello from Bird",
html="<p>My first Bird email.</p>",
)
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{"delivered@messagebird.dev"},
Subject: "Hello from Bird",
HTML: "<p>My first Bird email.</p>",
})
if err != nil {
log.Fatal(err)
}
fmt.Println(msg.Id, *msg.Status)
}$message = $bird->email->send(
from: 'Bird <onboarding@messagebird.dev>',
to: ['delivered@messagebird.dev'],
subject: 'Hello from Bird',
html: '<p>My first Bird email.</p>',
);
echo $message->getId(), ' ', $message->getStatus();bird email send \
--from hello@yourdomain.com \
--html '<p>It works.</p>' \
--subject 'Hello from Bird' \
--to delivered@messagebird.devcurl -X POST https://us1.platform.bird.com/v1/email/messages \
-H "Authorization: Bearer bk_us1_..." \
-H "Content-Type: application/json" \
-d '{
"from": "hello@yourdomain.com",
"to": ["delivered@messagebird.dev"],
"subject": "Hello from Bird",
"html": "<p>It works.</p>"
}'Usa tu host regional (https://us1.platform.bird.com o https://eu1.platform.bird.com) con una clave bk_{region}_... correspondiente.
El ejemplo de envío usa delivered@messagebird.dev, una dirección de sandbox que siempre acepta correo. El API rechaza dominios de marcador de posición con un 422: example.com, example.net, example.org, example.edu, test.com, y cualquier dominio bajo los TLD reservados .test, .example, .invalid o .localhost. Un envío a estos dominios solo puede rebotar, lo que perjudica tu reputación de remitente.
Enviar antes de verificar un dominio
Durante el onboarding puedes enviar desde nuestro dominio compartido de onboarding, onboarding@messagebird.dev. Esos envíos omiten la verificación de dominio, pero solo llegan a miembros verificados de tu propio espacio de trabajo y a direcciones de sandbox, bajo un límite diario de destinatarios. El quickstart tiene las reglas y límites exactos.
Construir el payload
Destinatarios
to, cc y bcc aceptan hasta 50 direcciones cada uno, y to necesita al menos una. Cada entrada es una cadena de correo electrónico simple, una cadena de buzón RFC 5322 (Jane <jane@acme.com>) o un objeto con un nombre para mostrar opcional.
Los destinatarios en la lista de supresión del espacio de trabajo no hacen fallar la solicitud. Sigue devolviendo un 202, y cada destinatario suprimido aparece en los endpoints de lectura como status: rejected con el motivo recipient_suppressed, incluso cuando todos los destinatarios del envío están suprimidos.
Contenido
subject es obligatorio para envíos inline, hasta 998 caracteres. Proporciona html, text o ambos, cada uno hasta 524.288 caracteres. Envía ambos siempre que puedas: un cliente que no puede renderizar HTML recurre a la parte de texto.
Para personalizar contenido inline, coloca tokens {{ variable }} en el asunto o el cuerpo y pasa sus valores en parameters, hasta 16 KB serializados. Un único conjunto de valores cubre a todos los destinatarios del envío, y un token sin clave coincidente se renderiza vacío. Para contenido que reutilizas, envía una plantilla en su lugar.
Incluye parameters, incluso como objeto vacío ({}), para procesar el asunto y el cuerpo como Liquid. Omítelo para enviar tokens como {{ animal }} tal cual. Cada nombre de parámetro es una sola palabra, como first_name; los nombres con puntos y el nombre reservado bird se rechazan. La sintaxis Liquid inválida y los tags o filtros no soportados devuelven 422.
Los valores insertados en HTML se escapan para que no puedan alterar el marcado circundante. Para un enlace completo o una URL de imagen, usa {{ link }} sin url_encode. Para un valor dentro de una query de URL, codifica ese valor explícitamente, por ejemplo https://example.com/search?q={{ query | url_encode }}.
Reply-to y encabezados personalizados
reply_to acepta de 1 a 25 direcciones, en los mismos formatos que los destinatarios. Todas las respuestas de los destinatarios llegan a todas ellas, así que una o dos es lo habitual.
headers es un objeto de cadena a cadena para tus propios encabezados, por ejemplo {"X-Campaign": "spring-2026"}, con un máximo de 25 encabezados y valores de hasta 998 caracteres. Tres tipos de encabezado se devuelven como un 422:
- Encabezados de direccionamiento y plataforma. Configura el direccionamiento del mensaje a través de los campos dedicados (from, to, cc, bcc, reply_to, subject). Esos nombres, y los encabezados que generamos por ti (Content-Type, Content-Transfer-Encoding, DKIM-Signature, Received, Return-Path), no se pueden establecer aquí.
- List-Unsubscribe y List-Unsubscribe-Post en un envío marketing. Nosotros establecemos un encabezado de cancelación de suscripción con un clic conforme en esos envíos. En un envío transactional dejamos los tuyos exactamente como los configuraste.
- Cualquier valor con un retorno de carro o salto de línea.
Seguimiento
track_opens y track_clicks tienen como valor predeterminado true. Establece cualquiera de los dos en false para omitir la inyección del píxel de apertura o la reescritura de enlaces en este envío. Seguimiento y métricas cubre lo que cada uno cambia en el mensaje.
Categoría y pool de IP
category clasifica el contenido y establece la política de supresión: marketing bloquea la entrega por cualquier motivo de supresión y por cualquier opt-out, y transactional entrega a pesar de una supresión por queja o un opt-out solo de marketing (uno registrado para todos los mensajes también lo bloquea). Su valor predeterminado es la categoría de la plantilla en un envío con plantilla y marketing en caso contrario, así que establece transactional explícitamente para recibos, restablecimientos de contraseña y otro correo operacional. Categorías cubre la elección. El correo enviado por SMTP toma su categoría de la configuración SMTP de la clave.
ip_pool_id selecciona el pool de envío: un ID de pool (ipp_...), o ipp_shared para enrutar por el pool compartido explícitamente. Omítelo para usar el pool predeterminado de tu organización. Un pool desconocido, o uno sin IP dedicadas disponibles para enviar, se rechaza con un 422.
Referencia de campos
| Campo | Tipo | Obligatorio | Límites y notas |
|---|---|---|---|
| from | address | sí | Debe estar en un dominio verificado o en el dominio de onboarding |
| to | address[] | sí | 1 a 50 |
| cc, bcc | address[] | no | Hasta 50 cada uno |
| subject | string | envíos inline | Hasta 998 caracteres; omitir en envíos con plantilla |
| html, text | string | al menos uno | Hasta 524.288 caracteres cada uno; omitir en envíos con plantilla |
| reply_to | address[] | no | 1 a 25; las respuestas llegan a todas las direcciones listadas |
| headers | object (string → string) | no | Hasta 25; nombres reservados rechazados (ver encabezados personalizados) |
| parameters | object | no | Valores para {{ tokens }} en contenido inline; hasta 16 KB serializados; compartidos entre destinatarios |
| tags | {name, value}[] | no | Hasta 20; nombre ≤ 32 chars, valor ≤ 64 chars; solo [A-Za-z0-9_-]; nombres únicos por envío |
| metadata | object | no | JSON arbitrario, hasta 2 KB serializados |
| track_opens | boolean | no | Predeterminado true |
| track_clicks | boolean | no | Predeterminado true |
| category | string | no | marketing o transactional; predeterminado a la de la plantilla en un envío con plantilla, de lo contrario marketing |
| ip_pool_id | string | no | ipp_... o ipp_shared; omitir para el pool predeterminado de tu organización |
| template | object | no | Envía una plantilla publicada por id o slug, con parameters para sus variables y un language opcional |
| attachments | object[] | no | Hasta 20; ver adjuntos |
| scheduled_at | RFC 3339 timestamp | no | Programa contenido en línea o una template; consulta envío programado |
Enviar con una plantilla
En lugar de contenido inline, envía una plantilla publicada: establece template como un objeto que la nombre por id (emt_...) o por slug, exactamente uno de los dos, con los valores de sus variables en template.parameters. Omite subject, html y text, porque la plantilla ya los tiene.
const msg = await bird.email.send({
from: { email: "onboarding@messagebird.dev", name: "Bird" },
to: ["delivered@messagebird.dev"],
category: "transactional",
template: {
slug: "welcome-email",
parameters: { first_name: "Jane" },
},
});
console.log(msg.id, msg.status);msg = client.email.send(
from_={"email": "onboarding@messagebird.dev", "name": "Bird"},
to=["delivered@messagebird.dev"],
category="transactional",
template="welcome-email",
parameters={"first_name": "Jane"},
)
print(msg.id, msg.status)msg, err := client.Email.Send(context.Background(), bird.EmailSendParams{
From: "onboarding@messagebird.dev",
To: []string{"delivered@messagebird.dev"},
Category: "transactional",
Template: "welcome-email",
Parameters: map[string]any{"first_name": "Jane"},
})
if err != nil {
log.Fatal(err)
}
fmt.Println(msg.Id, *msg.Status)$message = $bird->email->send(
from: 'Bird <onboarding@messagebird.dev>',
to: ['delivered@messagebird.dev'],
category: 'transactional',
template: (new EmailMessageSendRequestTemplate())
->setSlug('welcome-email')
->setParameters(['first_name' => 'Jane']),
);
echo $message->getId(), ' ', $message->getStatus();bird email send \
--category transactional \
--from hello@yourdomain.com \
--parameters '{"first_name":"Jane"}' \
--template welcome-email \
--to delivered@messagebird.devcurl -X POST https://us1.platform.bird.com/v1/email/messages \
-H "Authorization: Bearer bk_us1_..." \
-H "Content-Type: application/json" \
-d '{
"from": "hello@yourdomain.com",
"to": ["delivered@messagebird.dev"],
"category": "transactional",
"template": {
"slug": "welcome-email",
"parameters": { "first_name": "Jane" }
}
}'El contenido de una plantilla es Liquid, así que además de la sustitución simple {{ variable }} puede usar filtros, condicionales {% if %} y bucles {% for %}. Personalizar con variables lista los pocos constructos que una publicación rechaza. template.parameters es donde colocas los valores para los parámetros propios de la plantilla, identificados por nombre. Si omites uno, el envío se rechaza con un 422 que lo nombra. Todo lo demás del envío se comporta igual que inline, incluyendo destinatarios, tags, metadata, seguimiento y adjuntos. Lo que es específico de un envío con plantilla:
- Inline o con plantilla, nunca ambos. Enviar template junto con subject, html o text se rechaza con un 422. El API también rechaza valores de variables en el campo de nivel superior parameters; en un envío con plantilla pertenecen a template.parameters.
- bird es el único nombre reservado. Una ruta de marcador de posición que comienza con bird. nombra nuestros propios datos, como el enlace de cancelación de suscripción o el registro de contacto del destinatario, por lo que una clave template.parameters no puede llamarse bird. Todas las demás claves las defines tú, y cada una es una sola palabra: {"order_number": "A-1043"} rellena {{ order_number }}.
- Una plantilla se puede enviar ahora o después. Añade scheduled_at para programar el envío. Fijamos la versión publicada, el idioma seleccionado y los valores de los parámetros en el momento de la aceptación. Si eliminas la plantilla antes de la hora de envío, el mensaje se rechaza con generation_failure.
- Un envío usa la versión publicada de la plantilla. Los borradores nunca se envían. Una plantilla desconocida se rechaza con un 404, y una plantilla sin versión publicada con un 422.
- language selecciona uno de los idiomas de la plantilla. Omítelo para enviar el idioma predeterminado de la plantilla. Si pides uno que la plantilla no tiene, su propia configuración on_missing_language decide si se envía la coincidencia más cercana o se rechaza el envío. Una plantilla que establece language_source_required rechaza un envío que no nombre ningún idioma.
- La categoría de la plantilla es un valor predeterminado, y la tuya lo sobrescribe. Omite category y el envío hereda la de la plantilla, así que una plantilla transaccional no necesita repetirla en cada llamada.
Plantillas de correo electrónico cubre la creación, publicación y los constructos que una plantilla puede contener.
Tags vs metadata
Ambos adjuntan tus propios datos a un envío y difieren en cómo los consultas después:
- tags son pares {name, value} estructurados: hasta 20 por envío, nombre de hasta 32 caracteres, valor de hasta 64, solo letras ASCII, dígitos, guion bajo y guion, y nombres únicos dentro del envío. Los tags son dimensiones de filtro, así que puedes filtrar la lista de mensajes por tag y segmentar analíticas y resúmenes de dashboard por tag. Úsalos para etiquetas de baja cardinalidad como campaign, experiment_variant o source.
- metadata es un objeto JSON arbitrario, de hasta 2 KB serializados. Lo almacenamos, lo devolvemos en lecturas API y lo incluimos en cada evento de webhook, así que es adecuado para contexto que quieres que te devolvamos: IDs internos, claves foráneas, payloads estructurados.
Cada evento de webhook incluye ambos junto con los IDs de correlación (email_id, recipient_id), para que puedas conciliar contra tus propios registros sin una segunda consulta. Los nombres de tag y las claves de metadata de nivel superior que comienzan con __bird se rechazan. No necesitas codificar dispositivo, geografía, proveedor de buzón, tipo de rebote o dominio del destinatario en ninguno de los dos campos, porque ya capturamos cada uno como una dimensión de analíticas.
Ejemplo de código
{
"tags": [{ "name": "campaign", "value": "onboarding" }],
"metadata": { "user_id": "usr_12345", "order_id": "ord_98765" }
}Adjuntos
attachments acepta hasta 20 archivos por mensaje, como bytes codificados en base64 inline. Rechazamos un envío cuyo tamaño estimado de mensaje generado supere 20 MB, medido después de la codificación base64, así que mantén el contenido de adjuntos sin codificar en 15 MB o menos para dejar margen. Adjuntos tiene el contrato de campos, imágenes inline, los tipos de archivo bloqueados y cómo descargar un adjunto.
Qué significa un 202
Un envío exitoso devuelve 202 Accepted con un ID de mensaje con prefijo em_ y status: accepted:
Ejemplo de código
{
"id": "em_01ky7ma8y2es1s2akzk53tmjn0",
"status": "accepted",
"category": "marketing",
"from": { "email": "hello@yourdomain.com" },
"to": [{ "email": "delivered@messagebird.dev" }],
"subject": "Hello from Bird",
"accepted_count": 1,
"processed_count": 0,
"delivered_count": 0,
"deferred_count": 0,
"bounced_count": 0,
"complained_count": 0,
"rejected_count": 0,
"open_count": 0,
"click_count": 0,
"track_opens": true,
"track_clicks": true,
"created_at": "2026-07-23T13:58:20.866Z"
}El 202 significa que hemos aceptado el envío de forma duradera. Los errores que puedes corregir llegan en la propia solicitud como un 422: un dominio de remitente no verificado o un campo que no pasa la validación. Los resultados por destinatario (entregado, rebotado, diferido, reclamado) llegan después a través de webhooks y los endpoints de lectura del mensaje.
De ahí se desprenden dos cosas:
- Las lecturas devuelven el estado sin el cuerpo. GET /v1/email/messages/{message_id} devuelve el estado del mensaje y del destinatario, nunca el cuerpo html o text. Cuando el almacenamiento de contenido está habilitado en el espacio de trabajo, los cuerpos almacenados permanecen disponibles hasta 30 días desde GET /v1/email/messages/{message_id}/content.
- Una lectura puede ir brevemente detrás del envío. Un 404 en los endpoints de lectura justo después de un 202 significa que el mensaje aún no es visible, así que reintenta en un momento.
Reintentar de forma segura
Envía un encabezado Idempotency-Key con un valor único por envío lógico. Si una solicitud tuvo éxito pero nunca viste la respuesta, reprodúcela con la misma clave. El API devuelve el resultado original en lugar de enviar un segundo correo e incluye un encabezado Idempotency-Replay. Idempotencia tiene el formato de clave y la retención.
Envío en lote
Para reducir las solicitudes API, POST /v1/email/batches acepta hasta 100 mensajes independientes y los valida como una sola unidad. También se admite llamar al endpoint de envío individual en un bucle. Cada elemento del lote usa el payload de esta página, incluido scheduled_at, por lo que un lote puede mezclar mensajes inmediatos y programados.
Facturación
Los envíos de correo se miden por destinatario contra la cuota mensual de tu plan, así que un mensaje a tres destinatarios consume tres envíos. Facturación y uso cubre el modelo de medición y la lectura de uso en tiempo real.
Próximos pasos
- Plantillas de correo: crea y publica las plantillas que envías aquí
- Categorías: cómo marketing y transactional cambian el comportamiento de supresión
- Supresiones: a quién no entregamos y por qué
- Envío programado: entrega en un momento futuro con scheduled_at
- Sandbox de pruebas: destinatarios sandbox y envío previo a la verificación
- Referencia de API: los esquemas completos de solicitud y respuesta
Recursos relacionados
Continúa con la documentación, guías y ejemplos sobre este tema. Los recursos están en inglés.
Ver la guíaOrder confirmation emailsExplorar la funcionalidadOrder confirmation emailsSeguir la ruta de aprendizajeBuild your first integration
Prueba el ejercicio y obtén un resumen de implementación