定时发送
设置 scheduled_at 将消息保留到指定时间。到达该时间后,消息进入正常的投递生命周期,产生与立即发送相同的事件。你的应用无需运行自己的调度器。
安排发送
在常规的 POST /v1/email/messages 发送中添加一个 scheduled_at 时间戳。载荷的其他部分无需更改。
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"
}'调用立即返回 202 Accepted,包含 em_ 前缀的消息 ID 和 status: accepted。它与立即发送返回的消息对象相同,另外还包含以 UTC 回显的 scheduled_at,你无需再次读取即可确认发送时间。接受是同步的,投递是延迟的。请求中省略 scheduled_at,消息会立即发出,响应中不会包含 scheduled_at 键。
代码示例
{
"id": "em_01ky7q24hafjgvzfg02v3m177p",
"status": "scheduled",
"scheduled_at": "2027-01-15T09:00:00Z",
"category": "marketing"
}时间到达后,我们释放消息,其状态按常规顺序推进(accepted,然后 processed,然后 delivered,依此类推)。scheduled_at 在之后仍然保留,因此你始终可以查看消息被安排在什么时间发送。
每次定时发送会消耗你所在组织当前计费周期内定时邮件配额的一个单位。超出配额的请求将被拒绝,返回 422(E10003)。
定时发送使用内联内容
scheduled_at 和 template 互斥,同时设置两者的发送请求将被拒绝,返回 422。这是约定:定时发送使用自己的 subject 和正文,而模板发送则立即发出。如需定时发送模板内容,请先渲染其主题和正文。控制面板和 bird CLI 可预览模板发送将投递的确切主题、HTML 和纯文本。将这些渲染后的值作为内联内容进行定时发送。
批量发送中的条目同样接受 scheduled_at,因此一个批次可以混合定时和即时消息。每个定时条目各自消耗一个配额单位,如果任何条目的时间超出范围,整个批次将被拒绝。每个定时条目在批量响应中携带自己的 scheduled_at,立即发送的条目没有 scheduled_at 键;批量参考文档在一个响应中展示了两种情况。
即时发送的载荷仍可能过大而无法定时发送。如果正文、收件人列表或元数据超出定时发送限制,API 返回 422。请缩减这些字段或立即发送消息。
选择发送时间
scheduled_at 是一个绝对的 RFC 3339 时间戳。它受两条规则约束:
- 必须在未来 30 秒到 30 天之间。 不足 30 秒或超过 30 天的将被拒绝,返回 422。下限防止定时发送与即时发送竞争。30 天是我们保留消息的最远期限。
- 提供一个精确时刻。 包含 UTC Z(2027-01-15T09:00:00Z)或显式偏移量(2026-07-30T09:00:00-04:00,与 13:00:00Z 是同一时刻)。我们将该时刻与当前时间进行比较,不会解析裸本地时间或套用收件人的时区。如需在每位收件人的本地时间上午 9 点发送,请自行计算这些时刻,并按时区分别安排发送。
不接受 "in 2 hours" 等相对表达式。请发送解析后的时间戳。
列出定时消息
按状态过滤消息列表,查看尚未触发的消息:
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 列出你在发送前取消的消息。定时消息触发后进入投递管道,以投递状态显示,与其他发送无异。控制面板的邮件日志提供相同的 Scheduled 和 Canceled 过滤器。
取消定时发送
在消息开始发送前,随时使用 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"成功取消后返回 204 No Content。消息状态变为 canceled,不会发送,并触发 email.canceled webhook。需要注意四点:
-
只能取消仍处于定时状态的消息。 已开始发送、已发送或已取消的消息将返回 409:代码示例
{ "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." } }在发送时间到达时,取消请求也可能在与发送本身的竞争中失败,因同样的原因返回 409。 -
大型发送可能需要几秒钟才能变为可取消状态。 带附件或正文较大的定时发送在 202 之后仍在存储内容,因此:
- 在此窗口内取消会返回 409,消息仍保持定时状态。
- 重新读取消息。
- 如果仍显示 status: scheduled,重试取消。
-
取消不会退还定时邮件配额。 安排时消耗的单位仍然保持消耗状态,这就是为什么先安排再取消的循环无法绕过配额限制。你的常规发送配额不受影响,因为它仅在消息实际发送时才扣减。
-
取消可安全重试,与其他写操作一样,使用 Idempotency-Key。
要将定时发送改到其他时间,请取消它并使用新的 scheduled_at 提交新发送。你会获得一个新的 em_ ID。
发送时会发生什么
- 载荷和域名验证在提交时执行。 格式错误的定时发送在 API 调用时即以 422 失败,你可以立即发现问题,而非等到上午 9 点。
- 发送时会重新检查发件人域名。 如果你的 from 域名在定时发送时间到达时不再处于已验证状态,消息将不会发送。其收件人将以 rejected 返回并附带原因,而不是从未验证域名接收邮件。请在整个等待窗口内保持域名已验证。
- 发送配额在发送时扣减。 常规发送配额在消息触发时消耗。安排定时发送不会影响配额。如果发送时配额已耗尽,收件人将被拒绝。
- 退订名单在发送时评估,依据的是你的退订名单在那一刻的状态,因此在安排和发送之间退订的人仍会被尊重。
错误
| 状态 | 代码 | 触发条件 |
|---|---|---|
| 422 | E10003 | 你所在组织当前计费周期的定时邮件配额已用完 |
| 422 | scheduled_at 不足 30 秒或超过 30 天 | |
| 422 | scheduled_at 与 template 同时使用 | |
| 422 | 载荷过大,无法暂存;请缩减正文、收件人或元数据,或立即发送 | |
| 409 | E10005 | 消息已无法取消:已开始发送、已发送或已被取消 |
| 404 | 此工作区中没有该 ID 对应的消息 |
Webhooks
在常规投递事件之外,有两个事件专属于定时发送:
- email.scheduled 在消息以未来的 scheduled_at 被接受时触发,并报告该时间。
- email.canceled 在定时消息于发送前被取消时触发。
消息触发后,常规的 email.accepted 事件链照常执行。
后续步骤
- 发送邮件:完整的发送载荷和异步 202 模型
- 退订管理:我们不投递给谁、原因,以及在发送时评估
- 事件和 webhooks:定时消息触发后产生的事件
- 幂等性:安排和取消调用的安全重试
- API 参考文档:取消端点的完整约定
- 定时发送邮件:演示定时发送及如何取消的视频