发送邮件
POST /v1/email/messages 发送一封邮件。在 JSON 载荷中提供发件人、收件人和内容。API 返回 202 Accepted 及一个消息 ID,然后异步投递邮件。完整的 schema 请参阅 API 参考文档。
最简发送
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>"
}'使用你的区域主机(https://us1.platform.bird.com 或 https://eu1.platform.bird.com)并搭配对应的 bk_{region}_... 密钥。
发送示例使用 delivered@messagebird.dev,这是一个始终接收邮件的沙盒地址。API 会以 422 拒绝占位域名:example.com、example.net、example.org、example.edu、test.com,以及保留顶级域 .test、.example、.invalid 或 .localhost 下的任何域名。发送到这些域名只会产生退信,损害你的发件人信誉。
在验证域名之前发送
在引导期间,你可以使用我们共享的引导域名 onboarding@messagebird.dev 发送邮件。这些发送跳过域名检查,但只能送达你工作区中已验证的成员和沙盒地址,且受每日收件人上限限制。快速入门中有具体规则和限制。
构建载荷
收件人
to、cc 和 bcc 各接受最多 50 个地址,且 to 至少需要一个。每个条目可以是纯邮件地址字符串、RFC 5322 邮箱字符串(Jane <jane@acme.com>),或带可选显示名称的对象。
位于工作区屏蔽列表中的收件人不会导致请求失败。请求仍然返回 202,每个被屏蔽的收件人在读取端点上显示为 status: rejected,原因为 recipient_suppressed,即使所有收件人都被屏蔽也是如此。
内容
subject 为内联发送的必填项,最多 998 个字符。提供 html、text 或两者,每项最多 524,288 个字符。建议同时提供两者:无法渲染 HTML 的客户端会回退到纯文本部分。
要个性化内联内容,在主题或正文中放入 {{ variable }} 占位符,并在 parameters 中传递对应的值,序列化后最大 16 KB。一组值覆盖该次发送的所有收件人,没有匹配键的占位符渲染为空。对于重复使用的内容,请改用模板发送。
包含 parameters(即使是空对象 {})即可将主题和正文作为 Liquid 处理。省略它则 {{ animal }} 等占位符原样发送。每个参数名是单个单词,例如 first_name;带点号的名称和保留名称 bird 会被拒绝。无效的 Liquid 语法和不支持的标签或过滤器会返回 422。
插入 HTML 中的值会被转义,因此不会改变周围的标记。对于完整的链接或图片 URL,使用 {{ link }} 而不加 url_encode。对于 URL 查询参数中的值,需要显式编码,例如 https://example.com/search?q={{ query | url_encode }}。
回复地址和自定义请求头
reply_to 接受 1 到 25 个地址,格式与收件人相同。收件人的每次回复都会发送到 all 地址,因此通常设置一到两个。
headers 是一个字符串到字符串的对象,用于你自己的请求头,例如 {"X-Campaign": "spring-2026"},最多 25 个,值最长 998 个字符。以下三类请求头会以 422 返回:
- 地址和平台请求头。 通过专用字段(from、to、cc、bcc、reply_to、subject)设置消息的寻址信息。这些名称以及我们为你生成的请求头(Content-Type、Content-Transfer-Encoding、DKIM-Signature、Received、Return-Path)不能在此设置。
- List-Unsubscribe 和 List-Unsubscribe-Post(marketing 发送时)。 我们会为这类发送自动设置符合规范的一键退订请求头。在 transactional 发送中,你设置的值将原样保留。
- 包含回车符或换行符的任何值。
跟踪
类别和 IP 池
category 对内容进行分类并设置屏蔽策略:marketing 在所有屏蔽原因和任何退订情况下阻止投递,transactional 在投诉屏蔽或仅营销类退订时仍会投递(针对所有消息的退订记录同样会阻止投递)。在模板发送中默认使用模板的类别,其他情况默认为 marketing,因此对于收据、密码重置和其他事务性邮件,请显式设置 transactional。类别详述了如何选择。通过 SMTP 提交的邮件则从密钥的 SMTP 配置中获取类别。
ip_pool_id 选择发送池:一个池 ID(ipp_...),或 ipp_shared 以显式使用共享池。省略此项则使用组织的默认池。未知的池或没有可用专用 IP 的池会以 422 被拒绝。
字段参考
| 字段 | 类型 | 必填 | 限制与说明 |
|---|---|---|---|
| from | address | 是 | 必须位于已验证的域名或引导域名上 |
| to | address[] | 是 | 1 到 50 |
| cc、bcc | address[] | 否 | 各最多 50 个 |
| subject | string | 内联发送时 | 最多 998 个字符;模板发送时省略 |
| html、text | string | 至少一项 | 每项最多 524,288 个字符;模板发送时省略 |
| reply_to | address[] | 否 | 1 到 25;回复发送至所有列出的地址 |
| headers | object (string → string) | 否 | 最多 25 个;保留名称会被拒绝(见自定义请求头) |
| parameters | object | 否 | 内联内容中 {{ tokens }} 的值;序列化后最大 16 KB;所有收件人共享 |
| tags | {name, value}[] | 否 | 最多 20 个;name ≤ 32 字符,value ≤ 64 字符;仅限 [A-Za-z0-9_-];每次发送名称唯一 |
| metadata | object | 否 | 任意 JSON,序列化后最大 2 KB |
| track_opens | boolean | 否 | 默认 true |
| track_clicks | boolean | 否 | 默认 true |
| category | string | 否 | marketing 或 transactional;模板发送时默认使用模板的类别,否则为 marketing |
| ip_pool_id | string | 否 | ipp_... 或 ipp_shared;省略则使用组织的默认池 |
| template | object | 否 | 通过 id 或 slug 发送已发布的模板,用 parameters 传递变量值,可选 language |
| attachments | object[] | 否 | 最多 20 个;见附件 |
| scheduled_at | RFC 3339 时间戳 | 否 | 调度内联内容或 template;参见定时发送 |
使用模板发送
除了内联内容外,你也可以发送已发布的模板:将 template 设为一个对象,通过 id(emt_...)或 slug 指定模板(二者只选其一),并在 template.parameters 中传入变量值。省略 subject、html 和 text,因为模板中已包含这些内容。
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" }
}
}'模板的内容是 Liquid,因此除了简单的 {{ variable }} 替换外,还可以使用过滤器、{% if %} 条件和 {% for %} 循环。使用变量个性化列出了发布时会拒绝的少数语法结构。template.parameters 用于传入模板自身参数的值,以名称为键。缺少某个值时,发送会以 422 被拒绝并指出参数名。发送的其他方面与内联发送行为一致,包括收件人、tags、metadata、跟踪和附件。模板发送的特殊之处:
- 内联或模板,不可同时使用。 将 template 与 subject、html 或 text 一起发送会被以 422 拒绝。API 同样会拒绝在顶层 parameters 字段中传入变量值;模板发送时它们应放在 template.parameters 中。
- bird 是唯一的保留名称。 以 bird. 开头的占位路径引用的是我们自己的数据,例如退订链接或收件人的联系人记录,因此 template.parameters 键不能命名为 bird。其他键均可自定义,且每个键是单个扁平单词:{"order_number": "A-1043"} 填充 {{ order_number }}。
- 模板可以立即发送,也可以稍后发送。 添加 scheduled_at 以定时发送。我们在接受时锁定已发布的版本、所选语言和参数值。如果您在发送时间之前删除了模板,消息会被拒绝并返回 generation_failure。
- 发送使用模板的已发布版本。 草稿不会被发送。未知模板会被以 404 拒绝,没有已发布版本的模板则被以 422 拒绝。
- language 选择模板的其中一种语言。 省略此项则发送模板的默认语言。请求一种模板不支持的语言时,模板自身的 on_missing_language 设置决定是发送最接近的匹配还是拒绝发送。设置了 language_source_required 的模板会拒绝未指定语言的发送。
- 模板的类别是默认值,你的设置会覆盖它。 省略 category,发送会继承模板的类别,因此事务性模板无需在每次调用时重复指定。
邮件模板介绍了模板的编写、发布以及模板可包含的语法结构。
标签与元数据
两者都可以将你的自定义数据附加到发送中,区别在于后续的查询方式:
- tags 是结构化的 {name, value} 键值对:每次发送最多 20 个,名称最长 32 个字符,值最长 64 个字符,仅限 ASCII 字母、数字、下划线和连字符,且名称在发送内唯一。标签是筛选维度,你可以按标签筛选消息列表,也可以在分析和仪表板汇总中按标签切分数据。适用于低基数标签,例如 campaign、experiment_variant 或 source。
- metadata 是任意 JSON 对象,序列化后最大 2 KB。我们会存储它,在 API 读取时返回,并在每个 webhook 事件中回传,因此适合你希望回传给自己的上下文信息:内部 ID、外键、结构化载荷。
每个 webhook 事件同时包含两者以及关联 ID(email_id、recipient_id),因此你无需二次查询即可与自己的记录进行对账。以 __bird 开头的标签名称和顶层元数据键会被拒绝。你不需要将设备、地理位置、邮箱服务商、退信类型或收件人域名编码到任何一个字段中,因为我们已经将它们作为分析维度进行了捕获。
代码示例
{
"tags": [{ "name": "campaign", "value": "onboarding" }],
"metadata": { "user_id": "usr_12345", "order_id": "ord_98765" }
}附件
attachments 每条消息最多接受 20 个文件,以 base64 编码的字节内联传入。我们会拒绝 base64 编码后生成消息估计大小超过 20 MB 的发送,因此原始附件内容应控制在 15 MB 以内以留出余量。附件包含字段约定、内联图片、被阻止的文件类型以及如何下载附件。
202 的含义
成功的发送返回 202 Accepted,其中包含一个以 em_ 为前缀的消息 ID 和 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"
}202 表示我们已持久化接受了该发送。你可以修复的失败会在请求本身以 422 返回:例如未验证的发件人域名或未通过校验的字段。每个收件人的投递结果(已送达、已退信、已延迟、已投诉)会在之后通过 webhook 和消息读取端点送达。
由此产生两个结论:
- 读取返回状态,不含正文。GET /v1/email/messages/{message_id} 返回消息和收件人状态,不返回 html 或 text 正文。当工作区启用了内容存储时,存储的正文可在 GET /v1/email/messages/{message_id}/content 中保留最多 30 天。
- 读取可能短暂滞后于发送。 在 202 之后立即在读取端点收到 404,表示消息尚未可见,请稍后重试。
安全重试
发送时附带 Idempotency-Key 请求头,每次逻辑发送使用唯一值。如果请求已成功但你未收到响应,使用相同的键重放请求。API 会返回原始结果而非再发一封邮件,并附带 Idempotency-Replay 请求头。幂等性包含密钥格式和保留策略。
批量发送
为减少 API 请求,POST /v1/email/batches 接受最多 100 条独立消息并作为一个整体进行校验。也支持在循环中调用单发端点。批量中的每个项使用本页的载荷格式(含 scheduled_at),因此一个批量可以混合即时和定时消息。
计费
邮件发送按收件人计费,从你的计划月度配额中扣除,因此发送给三个收件人的一封邮件消耗三次发送额度。计费与用量介绍了计量模型和实时用量查询。
后续步骤
- 邮件模板:编写和发布你在此处发送的模板
- 类别:marketing 和 transactional 如何改变屏蔽行为
- 屏蔽:我们不投递给谁,以及原因
- 定时发送:使用 scheduled_at 在未来时间投递
- 测试沙盒:沙盒收件人和验证前发送
- API 参考文档:完整的请求和响应 schema
相关资源
继续查阅此主题的文档、指南和示例。资源为英文。