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 paso a paso
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 que están en la lista de supresión del espacio de trabajo no hacen fallar la solicitud. Esta 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 en los envíos inline, con un máximo de 998 caracteres. Proporciona html, text o ambos, cada uno de hasta 524.288 caracteres. Envía ambos siempre que puedas: un cliente que no puede renderizar HTML muestra 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 sirve para todos los destinatarios del envío, y un token sin clave coincidente se renderiza vacío. Para contenido que reutilizas, envía una plantilla.
Incluye parameters, aunque sea como objeto vacío ({}), para procesar el asunto y el cuerpo como Liquid. Omítelo para enviar tokens como {{ animal }} tal cual están escritos. 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 no válida y los tags o filtros no admitidos devuelven 422.
Los valores insertados en HTML se escapan para que no puedan alterar el marcado que los rodea. Para un enlace completo o una URL de imagen, usa {{ link }} sin url_encode. Para un valor dentro de la query de una 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. Cada respuesta de un destinatario llega a todas ellas, así que lo habitual es usar una o dos.
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. Dos 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í. - Cualquier valor con un retorno de carro o salto de línea.
Los envíos transaccionales aceptan List-Unsubscribe y List-Unsubscribe-Post. Los envíos de marketing usan de forma predeterminada la cancelación de suscripción gestionada por Bird.
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. Estas opciones no modifican los píxeles de seguimiento ni los enlaces que ya contiene el contenido que envías. Seguimiento y métricas explica lo que cada una 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 los demás casos, así que establece transactional explícitamente para recibos, restablecimientos de contraseña y otro correo operativo. Categorías explica cómo elegir. El correo enviado por SMTP toma en cambio 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 explícitamente por el pool compartido. 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; se rechazan los nombres reservados (consulta 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 caracteres, valor ≤ 64 caracteres; solo [A-Za-z0-9_-]; nombres únicos por envío |
metadata | object | no | JSON arbitrario, hasta 2 KB serializados |
reference | string | no | 1–255 caracteres; coincidencia exacta para ?reference= en la lista de mensajes |
track_opens | boolean | no | Predeterminado true |
track_clicks | boolean | no | Predeterminado true |
category | string | no | marketing o transactional; en un envío con plantilla, la de la plantilla por defecto; en otro caso, marketing |
ip_pool_id | string | no | ipp_... o ipp_shared; omítelo para usar 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; consulta adjuntos |
scheduled_at | marca de tiempo RFC 3339 | no | Programa contenido inline o un template; consulta envío programado |
Enviar con una plantilla
Las plantillas pueden usar valores bird.contact.* del contacto que coincide con cada dirección To. Los destinatarios Cc y Bcc reciben una copia del correo personalizado de cada destinatario To. Esto también funciona en el API por lotes y en los envíos programados; consulta personalizar con variables para ver un ejemplo con varios destinatarios.
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. En template.parameters colocas los valores de los parámetros propios de la plantilla, identificados por nombre. Si omites uno, el envío se rechaza con un 422 que lo nombra. El envío admite los mismos tags, metadata, seguimiento y adjuntos que el correo inline. Lo específico de un envío con plantilla:
- Inline o con plantilla, nunca ambos. Enviar
templatejunto consubject,htmlotextse rechaza con un422. El API también rechaza valores de variables en el campo de nivel superiorparameters; en un envío con plantilla pertenecen atemplate.parameters. birdes el único nombre reservado. Una ruta de marcador de posición que comienza conbird.nombra nuestros propios datos, como el enlace de cancelación de suscripción o el registro de contacto del destinatario, por lo que una clavetemplate.parametersno puede llamarsebird. 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_atpara programar el envío. Guardamos la versión publicada, el idioma seleccionado, los parámetros de quien llama y los valores de contacto en el momento de la aceptación, de modo que las ediciones posteriores de contactos o valores de respaldo no cambian los mensajes ya aceptados. Si eliminas la plantilla antes de la hora de envío, el mensaje se rechaza congeneration_failure. - Personaliza con atributos de contacto. Consulta requisitos de contacto y saludos predeterminados.
- La coincidencia de contactos ignora los nombres para mostrar. Las direcciones de correo se convierten a minúsculas, conservando puntos y etiquetas con signo más. Si varios contactos comparten esa dirección, el envío devuelve
EmailTemplateContactAmbiguous; actualiza los registros duplicados antes de reintentar. - El envío usa permisos de correo electrónico. Una clave con
emails:writepuede enviar una plantilla publicada sin permiso de descubrimiento de contactos.emails:readda acceso al contenido no sensible del mensaje resultante. El descubrimiento de contactos y de propiedades mantiene sus permisos actuales. - 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 un422. languageselecciona 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ónon_missing_languagedecide si se envía la coincidencia más cercana o se rechaza el envío. Una plantilla que establecelanguage_source_requiredrechaza 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
categoryy 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.
Leer el contenido de un destinatario
El historial de mensajes con plantilla usa los valores de contacto guardados cuando se aceptó el envío, aunque después edites o elimines el contacto. Cada entrega copiada tiene su propio ID de destinatario.
Lista los destinatarios del mensaje para obtener sus IDs y luego pasa recipient_id a Get stored message content. Un mensaje nuevo con plantilla que tiene instantáneas de varios destinatarios devuelve 422 si falta ese selector. Si hay un solo destinatario, se selecciona automáticamente; un destinatario que pertenece a otro mensaje devuelve 404.
Las respuestas GET y de listado de mensajes usan el asunto aceptado para el primer destinatario To, identificado por subject_recipient_id. El listado de destinatarios proporciona el asunto propio de cada destinatario. Estos asuntos siguen disponibles cuando el almacenamiento del cuerpo está desactivado. Las lecturas de contenido siguen respetando la configuración de almacenamiento y retención del mensaje: 404 si no se almacenó, 425 mientras está pendiente y 410 tras la caducidad.
Si una instantánea de destinatario retenida deja de estar disponible, el estado del mensaje y el asunto representativo siguen siendo legibles, pero se omiten los asuntos por destinatario y el contenido no está disponible. Las lecturas no consultan los valores actuales de los contactos para reconstruir un envío anterior.
Tags vs metadata
Estos campos adjuntan tus propios datos a un envío y difieren en cómo los consultas después:
tagsson 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 comocampaign,experiment_variantosource.metadataes 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.referencees una cadena de 1 a 255 caracteres por la que quieres encontrar el mensaje, como un número de pedido o de ticket.GET /v1/email/messages?reference=la busca con coincidencia exacta, distinguiendo mayúsculas y minúsculas, y varios mensajes pueden compartir el mismo valor. Se devuelve en las lecturas y se incluye en los eventos de webhook del mensaje. Establécelo solo si necesitas buscar por él, y pon cualquier otro dato enmetadata.
Cada evento de webhook incluye los tags y la metadata junto con los IDs de correlación (email_id, recipient_id), para que puedas conciliar con tus propios registros sin una segunda consulta. Se rechazan los nombres de tag y las claves de metadata de nivel superior que comienzan con __bird. No necesitas codificar el dispositivo, la geografía, el proveedor de buzón, el tipo de rebote ni el dominio del destinatario en ninguno de los dos campos, porque ya capturamos cada uno de ellos como una dimensión de analíticas.
{
"tags": [{ "name": "campaign", "value": "onboarding" }],
"metadata": { "user_id": "usr_12345", "order_id": "ord_98765" },
"reference": "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:
{
"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 cuerpohtmlotext. Cuando el almacenamiento de contenido está habilitado en el espacio de trabajo, los cuerpos almacenados permanecen disponibles hasta 30 días desdeGET /v1/email/messages/{message_id}/content. - Una lectura puede ir brevemente detrás del envío. Un
404en los endpoints de lectura justo después de un202significa 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.
Coste y facturación
Los envíos de correo se miden por destinatario, así que un mensaje a tres destinatarios cuenta como tres envíos. En el plan Email Free, los envíos se descuentan de su cuota mensual y diaria. En un plan Email de pago, cada envío se cobra de tu monedero según la tarifa del plan por cada 1.000 correos. Facturación y uso cubre el modelo de medición y la lectura de uso en tiempo real, y la página de precios de correo electrónico indica la tarifa de cada plan.
Próximos pasos
- Plantillas de correo: crea y publica las plantillas que envías aquí
- Categorías: cómo
marketingytransactionalcambian 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 de este tema.