Sign inGet Started

发送邮件

POST /v1/email/messages 发送一封邮件。在 JSON 载荷中提供发件人、收件人和内容。API 返回 202 Accepted 及一个消息 ID,然后异步投递邮件。完整的 schema 请参阅 API 参考文档。

最简发送

最小有效载荷包括一个 from、至少一个 to 收件人、一个 subject 以及正文(html、text 或两者)。from 地址必须位于你已在此工作区中验证的域名上,或位于引导域名上。

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"

使用你的区域主机(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 个地址,格式与收件人相同。收件人的每次回复都会发送到 所有 这些地址,因此通常设置一到两个。

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。营销类发送默认使用由 Bird 管理的退订。

跟踪

track_opens 和 track_clicks 默认均为 true。将其中任一项设为 false 可跳过此次发送的打开像素注入或链接改写。这些标志不会改动你提交的内容中已有的跟踪像素和链接。跟踪与指标介绍了每项设置对消息的具体影响。

类别和 IP 池

category 对内容进行分类并设置屏蔽策略:marketing 在所有屏蔽原因和任何退订情况下阻止投递,transactional 在投诉屏蔽或仅营销类退订时仍会投递(针对所有消息记录的退订同样会阻止投递)。在模板发送中默认使用模板的类别,其他情况默认为 marketing,因此对于收据、密码重置和其他事务性邮件,请显式设置 transactional。类别详述了如何选择。通过 SMTP 提交的邮件则从密钥的 SMTP 配置中获取类别。

ip_pool_id 选择发送池:一个池 ID(ipp_...),或 ipp_shared 以显式使用共享池。省略此项则使用组织的默认池。未知的池或没有可用专用 IP 的池会以 422 被拒绝。

字段参考

字段类型必填限制与说明
fromaddress是必须位于已验证的域名或引导域名上
toaddress[]是1 到 50
cc、bccaddress[]否各最多 50 个
subjectstring内联发送时最多 998 个字符;模板发送时省略
html、textstring至少一项每项最多 524,288 个字符;模板发送时省略
reply_toaddress[]否1 到 25;回复会发送至所有列出的地址
headersobject (string → string)否最多 25 个;保留名称会被拒绝(见自定义请求头)
parametersobject否内联内容中 {{ tokens }} 的值;序列化后最大 16 KB;所有收件人共享
tags{name, value}[]否最多 20 个;名称不超过 32 个字符,值不超过 64 个字符;仅限 [A-Za-z0-9_-];名称在每次发送中唯一
metadataobject否任意 JSON,序列化后最大 2 KB
referencestring否1 到 255 个字符;在消息列表中按 ?reference= 精确匹配
track_opensboolean否默认 true
track_clicksboolean否默认 true
categorystring否marketing 或 transactional;模板发送时默认使用模板的类别,否则为 marketing
ip_pool_idstring否ipp_... 或 ipp_shared;省略则使用组织的默认池
templateobject否通过 id 或 slug 发送已发布的模板,用 parameters 传递变量值,可选 language
attachmentsobject[]否最多 20 个;见附件
scheduled_atRFC 3339 时间戳否定时发送内联内容或 template;参见定时发送

使用模板发送

模板可以使用与每个 To 地址匹配的联系人的 bird.contact.* 值。Cc 和 Bcc 收件人会收到每位 To 收件人个性化邮件的副本。这同样适用于批量 API 和定时发送;包含多个收件人的示例请参阅使用变量个性化。

除了内联内容外,你也可以发送已发布的模板:将 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);

模板的内容是 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。
  • 使用联系人属性进行个性化。 请参阅联系人要求和默认问候语。
  • 联系人匹配忽略显示名称。 邮箱地址会转换为小写,并保留点号和加号标签。如果多个联系人共用该地址,发送会返回 EmailTemplateContactAmbiguous;请先更新重复记录再重试。
  • 发送使用邮件权限。 拥有 emails:write 的密钥无需联系人发现权限即可发送已发布的模板。emails:read 授予对所生成消息中非敏感内容的访问权限。联系人和属性发现仍沿用现有权限。
  • 发送使用模板的已发布版本。 草稿不会被发送。未知模板会被以 404 拒绝,没有已发布版本的模板则被以 422 拒绝。
  • language 选择模板的其中一种语言。 省略此项则发送模板的默认语言。请求一种模板不支持的语言时,模板自身的 on_missing_language 设置决定是发送最接近的匹配还是拒绝发送。设置了 language_source_required 的模板会拒绝未指定语言的发送。
  • 模板的类别是默认值,你的设置会覆盖它。 省略 category,发送会继承模板的类别,因此事务性模板无需在每次调用时重复指定。

邮件模板介绍了模板的编写、发布以及模板可包含的语法结构。

读取某个收件人的内容

模板消息历史使用发送被接受时保存的联系人值,即使你之后编辑或删除了该联系人也是如此。每份副本投递都有自己的收件人 ID。

先列出消息的收件人以获取其 ID,然后将 recipient_id 传给获取已存储的消息内容。对于包含多个收件人快照的新模板消息,如果不提供该选择参数,会返回 422。只有一个收件人时会自动选中;属于其他消息的收件人会返回 404。

消息的 GET 和列表响应使用第一个 To 收件人已接受的主题,该收件人由 subject_recipient_id 标识。收件人列表会提供每个收件人各自的主题。即使禁用了正文存储,这些主题仍然可用。内容读取仍遵循消息的存储和保留设置:未存储时返回 404,处理中返回 425,过期后返回 410。

如果保留的收件人快照不可用,消息状态和代表性主题仍可读取,但会省略收件人主题,内容也不可用。读取不会查询当前的联系人值来重建之前的发送。

标签与元数据

以下字段都可以将你的自定义数据附加到发送中,区别在于后续的查询方式:

  • tags 是结构化的 {name, value} 键值对:每次发送最多 20 个,名称最长 32 个字符,值最长 64 个字符,仅限 ASCII 字母、数字、下划线和连字符,且名称在发送内唯一。标签是筛选维度,你可以按标签筛选消息列表,也可以在分析和仪表板汇总中按标签切分数据。适用于低基数标签,例如 campaign、experiment_variant 或 source。
  • metadata 是任意 JSON 对象,序列化后最大 2 KB。我们会存储它,在 API 读取时返回,并在每个 webhook 事件中回传,因此适合你希望回传给自己的上下文信息:内部 ID、外键、结构化载荷。
  • reference 是一个 1 到 255 个字符的字符串,用于查找消息,例如订单号或工单号。GET /v1/email/messages?reference= 对其进行区分大小写的精确匹配,多条消息可以共用同一个值。它会在读取时返回,并在该消息的 webhook 事件中回传。仅在需要按它搜索时设置,其他数据请放在 metadata 中。

每个 webhook 事件都会同时包含标签、元数据以及关联 ID(email_id、recipient_id),因此你无需二次查询即可与自己的记录进行对账。以 __bird 开头的标签名称和顶层元数据键会被拒绝。你不需要将设备、地理位置、邮箱服务商、退信类型或收件人域名编码到任何一个字段中,因为我们已经将它们分别作为分析维度进行了捕获。

代码示例
{
  "tags": [{ "name": "campaign", "value": "onboarding" }],
  "metadata": { "user_id": "usr_12345", "order_id": "ord_98765" },
  "reference": "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),因此一个批量可以混合即时和定时消息。

费用和计费

邮件发送按收件人计量,因此发送给三个收件人的一封邮件计为三次发送。在 Email Free 计划中,发送次数从该计划的每月和每日配额中扣除。在付费 Email 计划中,每次发送按计划的每 1,000 封邮件费率从你的钱包中扣费。计费与用量介绍了计量模型和实时用量查询,邮件定价页面列出了各计划的费率。

后续步骤