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><p><a href="{{ bird.unsubscribe_url }}">Unsubscribe</a></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><p><a href=\"{{ bird.unsubscribe_url }}\">Unsubscribe</a></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><p><a href=\"{{ bird.unsubscribe_url }}\">Unsubscribe</a></p>",
Category: bird.CategoryMarketing,
ScheduledAt: time.Date(2027, 1, 15, 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><p><a href="{{ bird.unsubscribe_url }}">Unsubscribe</a></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><p><a href="{{ bird.unsubscribe_url }}">Unsubscribe</a></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><p><a href=\"{{ bird.unsubscribe_url }}\">Unsubscribe</a></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.
Las lecturas se actualizan de forma asíncrona. Un mensaje que acabas de programar puede devolver inicialmente 404 en Obtener un mensaje y no aparecer en la lista de mensajes ni en el panel. El contenido de gran tamaño o los archivos adjuntos pueden alargar esta espera mientras los almacenamos. Guarda el ID y scheduled_at de la respuesta 202 y reintenta las lecturas con ese ID, con pausas entre intentos. También puedes cancelar con ese ID antes de que aparezca el mensaje.
Cuando un mensaje aparece mientras aún espera su envío, las lecturas muestran status: scheduled y su scheduled_at:
{
"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.
Un mensaje puede empezar a enviarse antes de que las lecturas se actualicen, por lo que quizá veas primero un estado posterior. No hay un plazo fijo tras el cual esté garantizado que una lectura muestre el 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).
Programa contenido incluido en la solicitud o una plantilla
Usa scheduled_at con contenido incluido en la solicitud o una plantilla guardada, construida igual que un envío con plantilla inmediato. Fijamos la versión publicada, el idioma seleccionado y los valores de los parámetros al aceptar la solicitud y enviamos esa versión a la hora programada. Publicar una versión más reciente no cambia la selección. Si se elimina la plantilla antes de la hora programada, el mensaje se rechaza sin enviarse.
Un mensaje de la categoría marketing recibe un enlace para darse de baja como un pequeño pie de página al final de su cuerpo. Para colocar el enlace tú mismo, pon {{ bird.unsubscribe_url }} en cada cuerpo que proporciones o, en un envío con plantilla, en los cuerpos de la plantilla.
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 que13: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 ya aparecen y aún esperan su envío. Un envío programado recién aceptado puede no aparecer mientras se carga su contenido o se actualizan las lecturas:
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> --yescurl -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
409por la misma razón. -
Puedes cancelar mientras el contenido sigue subiendo. Un envío programado con archivos adjuntos o un cuerpo grande puede seguir almacenando contenido después de la respuesta
202. Si lo cancelas correctamente, seguirá cancelado aunque la subida termine más tarde. -
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. Cinco 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
fromya no está verificado cuando llega la hora programada, el mensaje no se envía. Sus destinatarios se devuelven comorejectedcon 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.
- Una plantilla guardada debe seguir existiendo en el momento del envío. Si eliminas la plantilla después de programar, el mensaje no se envía. Sus destinatarios se devuelven como
rejectedcongeneration_failure.
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 | 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 | La lectura del mensaje aún no refleja su aceptación, o no existe 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.scheduledinforma de un mensaje que espera la hora futura indicada enscheduled_at. En los envíos con plantilla, la versión seleccionada se carga de nuevo y su contenido se prepara en el momento del envío. Este evento puede llegar antes de que termine esa preparación.email.canceledse 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.
Los eventos de programación se publican de forma asíncrona. Un mensaje puede dejar el estado programado antes de que se publique email.scheduled. Recibir un webhook no significa que los endpoints de lectura ya muestren ese estado.
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 de este tema.