Envío programado
Establece scheduled_at para retener un mensaje hasta un momento específico. Cuando ese momento llega, el mensaje entra en el ciclo de entrega normal y produce los mismos eventos que un envío inmediato. Tu aplicación no necesita ejecutar su propio programador.
Programar un envío
Añade una marca de tiempo scheduled_at a un envío normal de POST /v1/email/messages. Nada más del payload cambia.
const msg = await bird.email.send({
from: "news@yourdomain.com",
to: ["delivered@messagebird.dev"],
subject: "Your weekly digest",
html: "<p>Here is what happened this week...</p>",
category: "marketing",
scheduled_at: "2027-01-15T09:00:00Z",
});
console.log(msg.id, msg.status); // "em_…", "accepted"msg = client.email.send(
from_="news@yourdomain.com",
to=["delivered@messagebird.dev"],
subject="Your weekly digest",
html="<p>Here is what happened this week...</p>",
category="marketing",
scheduled_at="2027-01-15T09:00:00Z",
)
print(msg.id, msg.status)package main
import (
"context"
"fmt"
"log"
"os"
"time"
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: "news@yourdomain.com",
To: []string{"delivered@messagebird.dev"},
Subject: "Your weekly digest",
HTML: "<p>Here is what happened this week...</p>",
Category: bird.CategoryMarketing,
ScheduledAt: time.Date(2026, 7, 30, 9, 0, 0, 0, time.UTC),
})
if err != nil {
log.Fatal(err)
}
fmt.Println(msg.Id, *msg.Status)
}$message = $bird->email->send(
from: 'news@yourdomain.com',
to: ['delivered@messagebird.dev'],
subject: 'Your weekly digest',
html: '<p>Here is what happened this week...</p>',
category: 'marketing',
scheduledAt: new \DateTimeImmutable('2027-01-15T09:00:00Z'),
);
echo $message->getId(), ' ', $message->getStatus();bird email send \
--from news@yourdomain.com \
--to delivered@messagebird.dev \
--subject 'Your weekly digest' \
--html '<p>Here is what happened this week...</p>' \
--category marketing \
--scheduled-at 2027-01-15T09:00:00Zcurl -X POST https://us1.platform.bird.com/v1/email/messages \
-H "Authorization: Bearer bk_us1_..." \
-H "Content-Type: application/json" \
-d '{
"from": "news@yourdomain.com",
"to": ["delivered@messagebird.dev"],
"subject": "Your weekly digest",
"html": "<p>Here is what happened this week...</p>",
"category": "marketing",
"scheduled_at": "2027-01-15T09:00:00Z"
}'La llamada devuelve 202 Accepted con el ID de mensaje prefijado con em_ y status: accepted de inmediato. Es el mismo objeto de mensaje que devuelve un envío inmediato, más scheduled_at reflejado en UTC, para que puedas confirmar la hora de envío sin una lectura adicional. La aceptación es síncrona; la entrega es diferida. Si omites scheduled_at de la solicitud, el mensaje se envía de inmediato y su respuesta no incluye la clave scheduled_at.
En los endpoints de lectura el mensaje muestra status: scheduled con su scheduled_at hasta que llega la hora de envío:
Ejemplo de código
{
"id": "em_01ky7q24hafjgvzfg02v3m177p",
"status": "scheduled",
"scheduled_at": "2027-01-15T09:00:00Z",
"category": "marketing"
}Cuando llega el momento, liberamos el mensaje y su estado avanza por los estados habituales (accepted, luego processed, luego delivered, y así sucesivamente). scheduled_at permanece establecido después, para que siempre puedas ver para cuándo se programó un mensaje.
Programar un envío consume una unidad de la cuota de correos programados de tu organización para el período de facturación. Si se supera esa cuota, la solicitud se rechaza con un 422 (E10003).
Un envío programado usa contenido en línea
scheduled_at y template son mutuamente excluyentes, y un envío que establece ambos se rechaza con un 422. Ese es el contrato: un envío programado tiene su propio subject y cuerpo, y un envío con plantilla se envía de inmediato. Para programar contenido de plantilla, renderiza primero su asunto y cuerpo. El dashboard y bird CLI previsualizan el asunto, HTML y texto exactos que entregaría un envío con plantilla. Programa esos valores renderizados como contenido en línea.
Un elemento de lote acepta scheduled_at en las mismas condiciones, así que un solo lote puede mezclar mensajes programados e inmediatos. Cada elemento programado consume su propia unidad de la cuota, y el lote completo se rechaza si la hora de algún elemento está fuera de rango. Cada elemento programado incluye su propio scheduled_at en la respuesta del lote, y un elemento que se envía de inmediato no tiene clave scheduled_at; la referencia de lotes muestra ambos en una sola respuesta.
Un payload de envío inmediato aún puede ser demasiado grande para programarse. Si su cuerpo, lista de destinatarios o metadatos exceden el límite de programación, API devuelve 422. Reduce esos campos o envía el mensaje de inmediato.
Elegir la hora de envío
scheduled_at es una marca de tiempo absoluta RFC 3339. Dos reglas la gobiernan:
- Debe estar entre 30 segundos y 30 días en el futuro. Menos de 30 segundos o más de 30 días se rechaza con un 422. El mínimo evita que una programación compita con un envío inmediato. Treinta días es el horizonte más lejano durante el cual retenemos un mensaje.
- Proporciona un instante exacto. Incluye un sufijo UTC Z (2027-01-15T09:00:00Z) o un desplazamiento explícito (2026-07-30T09:00:00-04:00, el mismo instante que 13:00:00Z). Comparamos el instante con la hora actual y nunca interpretamos una hora local sin zona ni aplicamos la zona horaria del destinatario. Para enviar a las 9 de la mañana en la hora local de cada destinatario, calcula esos instantes tú mismo y programa un envío por zona horaria.
Las expresiones relativas como "in 2 hours" no se aceptan. Envía una marca de tiempo resuelta.
Listar mensajes programados
Filtra la lista de mensajes por estado para ver los mensajes que aún no se han enviado:
for await (const message of bird.email.list({ status: "scheduled" })) {
console.log(message.id, message.scheduled_at);
}for message in client.email.list(status="scheduled"):
print(message.id, message.scheduled_at)for msg, err := range client.Email.List(context.Background(), bird.EmailListParams{Status: bird.EmailStatusScheduled}) {
if err != nil {
log.Fatal(err)
}
fmt.Println(msg.Id)
}foreach ($bird->email->list(['status' => 'scheduled']) as $message) {
echo $message->getId(), "\n";
}bird email list --status scheduledcurl "https://us1.platform.bird.com/v1/email/messages?status=scheduled" \
-H "Authorization: Bearer bk_us1_..."status=canceled lista los que cancelaste antes de que se enviaran. Una vez que un mensaje programado se dispara, pasa al pipeline y aparece con los estados de entrega, igual que cualquier otro envío. El registro de correos del dashboard ofrece los mismos filtros Scheduled y Canceled.
Cancelar un envío programado
Cancela un mensaje en cualquier momento antes de que comience a enviarse con POST /v1/email/messages/{message_id}/cancel:
await bird.email.cancel("em_abc123");client.email.cancel("em_abc123")if err := client.Email.Cancel(context.Background(), "em_abc123"); err != nil {
log.Fatal(err)
}$bird->email->cancel('em_01krdgeqcxet5s7t44vh8rt9mg');bird email cancel <message-id> --yes{
"name": "email_cancel",
"arguments": {
"message_id": "<message-id>"
}
}curl -X POST "https://{region}.platform.bird.com/v1/email/messages/{message_id}/cancel" \
-H "Authorization: Bearer $TOKEN"Una cancelación exitosa devuelve 204 No Content. El estado del mensaje pasa a canceled, nunca se envía y se dispara un webhook email.canceled. Cuatro cosas que debes saber:
-
Solo se puede cancelar un mensaje que aún esté programado. Un mensaje que ya comenzó a enviarse, ya se envió o ya fue cancelado responde 409:Ejemplo de código
{ "error": { "type": "conflict_error", "code": "E10005", "name": "EmailNotCancelable", "message": "This message cannot be canceled.", "remediation": "Only a scheduled message that has not started sending can be canceled. Once sending begins there is no way to pull it back." } }Cuando llega la hora de envío, una cancelación también puede perder la carrera contra el propio envío y responder 409 por la misma razón. -
Un envío grande puede tardar unos segundos en ser cancelable. Un envío programado con adjuntos o un cuerpo grande aún está almacenando contenido después del 202, así que:
- Una cancelación en esa ventana devuelve 409 y el mensaje sigue programado.
- Vuelve a leer el mensaje.
- Si aún muestra status: scheduled, reintenta la cancelación.
-
Cancelar no devuelve la cuota de correos programados. La unidad que consumiste al programar sigue consumida, y eso es lo que impide que un ciclo de programar-y-cancelar eluda la cuota. Tu cuota de envío regular no se ve afectada, porque solo se cobra cuando un mensaje realmente se envía.
-
La cancelación se puede reintentar de forma segura con un Idempotency-Key, como cualquier otra escritura.
Para mover un envío programado a otra hora, cancélalo y envía uno nuevo con el nuevo scheduled_at. Obtienes un ID em_ nuevo.
Qué sucede en el momento del envío
La programación solo cambia cuándo se libera un mensaje. Su construcción y gobernanza permanecen iguales. Los adjuntos, la categoría, las etiquetas y los metadatos se comportan exactamente igual que en un envío inmediato y se reflejan en los eventos de webhook de la misma forma. Cuatro comprobaciones se dividen entre los dos momentos:
- El payload y la validación de dominio se ejecutan de entrada. Un envío programado con formato incorrecto falla en la llamada API con un 422, así que te enteras ahora y no a las 9 de la mañana.
- El dominio del remitente se vuelve a comprobar en el momento del envío. Si tu dominio from ya no está verificado cuando llega la hora programada, el mensaje no se envía. Sus destinatarios se devuelven como rejected con un motivo, en lugar de recibir correo de un dominio no verificado. Mantén el dominio verificado durante toda la ventana.
- Tu cuota de envío se cobra en el momento del envío. La cuota de envío regular se consume cuando el mensaje se dispara. La programación no altera la cuota. Si la cuota está agotada en el momento del envío, los destinatarios se rechazan.
- La supresión se evalúa en el momento del envío, contra tu lista de supresión tal como esté en ese instante, de modo que alguien que se dé de baja entre la programación y el envío se respeta igualmente.
Errores
| Estado | Código | Cuándo |
|---|---|---|
| 422 | E10003 | La cuota de correos programados de tu organización para el período de facturación se agotó |
| 422 | scheduled_at está a menos de 30 segundos o a más de 30 días | |
| 422 | scheduled_at se combinó con template | |
| 422 | El payload es demasiado grande para almacenar; reduce el cuerpo, los destinatarios o los metadatos, o envía ahora | |
| 409 | E10005 | El mensaje ya no se puede cancelar: ya comenzó a enviarse, se envió o fue cancelado |
| 404 | No hay ningún mensaje con ese ID en este espacio de trabajo |
Webhooks
Dos eventos son específicos de la programación, además de los eventos de entrega habituales:
- email.scheduled se dispara cuando un mensaje se acepta con un scheduled_at futuro y reporta esa hora.
- email.canceled se dispara cuando un mensaje programado se cancela antes de enviarse.
Cuando el mensaje se dispara, la cadena normal de email.accepted continúa sin cambios.
Próximos pasos
- Enviar correo: el payload completo de envío y el modelo asíncrono 202
- Supresiones: a quién no entregamos y por qué, evaluado en el momento del envío
- Eventos y webhooks: los eventos que produce un mensaje programado una vez que se dispara
- Idempotencia: reintentos seguros para las llamadas de programación y cancelación
- Referencia de API: el contrato completo del endpoint de cancelación
- Programa un email para enviarlo más tarde: un video que muestra un envío programado y cómo cancelarlo
Recursos relacionados
Continúa con la documentación, guías y ejemplos sobre este tema. Los recursos están en inglés.