Sign inGet Started

Scheduled sending

Set scheduled_at to hold a message until a specific time. When that time arrives, the message enters the normal delivery lifecycle and produces the same events as an immediate send. Your application does not need to run its own scheduler.

Scheduling a send

Add a scheduled_at timestamp to a normal POST /v1/email/messages send. Nothing else about the payload changes.

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"

The call returns 202 Accepted with the em_-prefixed message ID and status: accepted straight away. It is the same message object an immediate send returns, plus scheduled_at echoed back in UTC, so you can confirm the send time without a follow-up read. Acceptance is synchronous; delivery is deferred. Omit scheduled_at from the request and the message goes out right away, and its response carries no scheduled_at key.

Reads update asynchronously. A message you just scheduled can initially return 404 from Get a message and be absent from the message list or dashboard. Large content or attachments can extend this wait while we store them. Keep the ID and scheduled_at from the 202 response, and retry reads with backoff using that ID. You can also cancel with this ID before the message appears.

When a message appears while still waiting to send, reads show status: scheduled and its scheduled_at:

Code example
{
  "id": "em_01ky7q24hafjgvzfg02v3m177p",
  "status": "scheduled",
  "scheduled_at": "2027-01-15T09:00:00Z",
  "category": "marketing"
}

When the time comes, we release the message and its status advances through the usual states (accepted, then processed, then delivered, and so on). scheduled_at stays set afterwards, so you can always see what a message was scheduled for.

A message can start sending before reads catch up, so you may first see a later status. There is no fixed delay after which a read is guaranteed to show the message.

Scheduling consumes one unit of your organization's scheduled-email allowance for the billing period. Exceeding that allowance is rejected with a 422 (E10003).

Schedule inline content or a template

Use scheduled_at with inline content or a stored template, built as for an immediate template send. We pin the published version, selected language and parameter values when we accept the request, and send that version at the scheduled time. Publishing a newer version does not change the selection. Deleting the template before the scheduled time rejects the message without sending it.

A marketing message gets an unsubscribe link as a small footer at the end of its body. To place the link yourself, put {{ bird.unsubscribe_url }} in each body you supply, or in the template's bodies for a template send.

A batch item takes scheduled_at on the same terms, so one batch can mix scheduled and immediate messages. Each scheduled item draws its own unit of the allowance, and the whole batch is rejected if any item's time is out of range. Each scheduled item carries its own scheduled_at back in the batch response, and an item that sends immediately has no scheduled_at key; the batch reference shows both in one response.

An immediate-send payload can still be too large to schedule. If its body, recipient list, or metadata exceed the scheduling limit, the API returns 422. Reduce those fields or send the message immediately.

Choosing the send time

scheduled_at is an absolute RFC 3339 timestamp. Two rules govern it:

  • It has to be between 30 seconds and 30 days in the future. Nearer than 30 seconds, or further out than 30 days, is rejected with a 422. The floor keeps a schedule from racing an immediate send. Thirty days is the furthest horizon we hold a message for.
  • Provide an exact instant. Include a UTC Z (2027-01-15T09:00:00Z) or an explicit offset (2026-07-30T09:00:00-04:00, the same instant as 13:00:00Z). We compare the instant against the current time and never interpret a bare local time or apply a recipient's timezone. To send at 9am in each recipient's local time, work out those instants yourself and schedule one send per timezone.

Relative expressions like "in 2 hours" are not accepted. Send a resolved timestamp.

Listing scheduled messages

Filter the message list by status to see messages that have appeared and are still waiting to send. A newly accepted schedule can be absent while its content is uploading or reads are catching up:

for await (const message of bird.email.list({ status: "scheduled" })) {
  console.log(message.id, message.scheduled_at);
}

status=canceled lists the ones you canceled before they sent. Once a scheduled message fires it moves into the pipeline and appears with the delivery statuses, the same as any other send. The dashboard's email log offers the same Scheduled and Canceled filters.

Canceling a scheduled send

Cancel a message any time before it starts sending with POST /v1/email/messages/{message_id}/cancel:

await bird.email.cancel("em_abc123");

A successful cancel returns 204 No Content. The message's status becomes canceled, it never sends, and an email.canceled webhook fires. Four things to know:

  • Only a still-scheduled message can be canceled. A message that has already started sending, already sent, or was already canceled comes back 409:

    Code example
    {
      "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."
      }
    }

    As the send time arrives, a cancel can also lose the race to the send itself and come back 409 for the same reason.

  • You can cancel while content is still uploading. A scheduled send with attachments or a large body may still be storing content after the 202. A successful cancel keeps it canceled even if that upload finishes later.

  • Canceling does not give the scheduled-email allowance back. The unit you consumed at schedule time stays consumed, which is what stops a schedule-then-cancel loop from working around the allowance. Your regular send allowance is untouched, because that is only charged when a message actually sends.

  • Cancel is safe to retry with an Idempotency-Key, like any other write.

To move a scheduled send to a different time, cancel it and submit a new send with the new scheduled_at. You get a fresh em_ ID.

What happens at send time

Scheduling changes only when a message is released. Its construction and governance stay the same. Attachments, category, tags, and metadata all behave exactly as on an immediate send, and are echoed on the webhook events the same way. Five checks split across the two moments:

  • Payload and domain validation run up front. A malformed scheduled send fails on the API call with a 422, so you find out now rather than at 9am.
  • The sender domain is re-checked at send time. If your from domain is no longer verified when the scheduled time arrives, the message is not sent. Its recipients return as rejected with a reason instead of receiving mail from an unverified domain. Keep the domain verified for the whole window.
  • Your send allowance is charged at send time. The regular send quota is consumed when the message fires. Scheduling leaves the quota unchanged. If the allowance is exhausted at send time, the recipients are rejected.
  • Suppression is evaluated at send time, against your suppression list as it stands then, so someone who unsubscribes between scheduling and sending is still honored.
  • A stored template must still exist at send time. If you delete the template after scheduling, the message is not sent. Its recipients return as rejected with generation_failure.

Errors

StatusCodeWhen
422E10003Your organization's scheduled-email allowance for the billing period is used up
422scheduled_at is under 30 seconds or over 30 days away
422The payload is too large to park; reduce the body, recipients, or metadata, or send now
409E10005The message can no longer be canceled: it already started sending, sent, or was canceled
404A message read has not caught up with acceptance, or no message with that ID exists in this workspace

Webhooks

Two events are specific to scheduling, on top of the usual delivery events:

  • email.scheduled reports a message waiting for its future scheduled_at. For template sends, the selected version is loaded again and its content is prepared at send time. This event can arrive before that work completes.
  • email.canceled fires when a scheduled message is canceled before it sends.

When the message fires, the normal email.accepted chain follows unchanged.

Scheduling events are published asynchronously. A message can leave the scheduled state before email.scheduled is published. Receiving a webhook does not mean read endpoints already show that state.

Next steps

Continue with the documentation, guides and examples for this topic.