在本页面
创建联系人可能返回 201,但结果仍然错误:来源 ID 丢失、选错列表,或者忽略了此前的退订。这些是数据决策,不只是 HTTP 细节。
本文先展示可运行的联系人写入,再说明小规模、可重复同步所需的检查。
Brevo API 覆盖哪些能力
所有能力都在同一个域名和同一个版本路径下。开发者文档把整个接口面分成四个产品领域:
- 消息:事务性邮件、SMS 和 WhatsApp,包括批量发送、定时发送和消息活动记录。
- 营销平台:联系人、列表、细分和邮件营销活动。
- 电商:商品、订单和客户事件跟踪。
- 对话:聊天挂件以及通过程序管理对话。
这些领域共用一个账号、一个联系人数据库和一个 API 密钥。这既方便,偶尔也危险:一个按预演数据写出来的脚本,操作的是营销活动实际发送的那批联系人。
事务性与营销的区别
这两类的行为差别足够大,把它们弄混是最常见的设计错误。
| 事务性 | 营销 | |
|---|---|---|
| 主要端点 | POST /v3/smtp/email | POST /v3/emailCampaigns |
| 寻址方式 | 请求里显式指定收件人 | listIds 或 segmentIds |
| 触发方式 | 你的应用实时触发 | 定时发送或按需发送 |
| 典型流量形态 | 持续不断,一次一条 | 突发式,一次一大批 |
| 速率限制姿态 | 非常高,general 档位每秒 1,000 次请求 | 低,营销活动端点归入通用上限 |
如果你还在判断 Brevo 是不是合适的平台,平台概览覆盖了这部分内容。
鉴权与密钥管理
Brevo 使用放在自定义请求头里的纯 API 密钥。请求头名为 api-key,不是 Authorization,也没有 Bearer 前缀。先用过其他消息 API 的人几乎都会在这里栽跟头。
支持 OAuth 的操作可以使用 Bearer 令牌;普通 API 密钥仍应放在 api-key 请求头中。
curl https://api.brevo.com/v3/account \ -H "api-key: $BREVO_API_KEY"密钥在 Brevo 应用的账号设置里生成,位置是 SMTP and API 板块下的 API keys 标签页。给每个密钥起一个与使用它的系统对应的描述性名称。密钥值在生成时只显示一次,一旦丢失就只能重新生成,无法找回。
几条实操规则:
- 按部署环境和服务分别签发独立密钥。吊销一个泄露的密钥,不应该同时搞垮三个无关系统。
- 标准 API 密钥的作用范围是整个账号。把任何一个密钥都视为对联系人、发送和 CRM 数据的完全访问权限。
- 对于代表其他 Brevo 账号操作的应用,Brevo 还支持 OAuth 2.0,具体流程与密钥流程一并写在鉴权方案文档中。
- AI 助手使用的 MCP 服务器需要另一个令牌,而且确实使用 bearer 请求头。该令牌在同一个 API keys 页面生成,但与 REST 密钥不能互换。
基础 URL、版本以及第一次写入
基础 URL 是 https://api.brevo.com/v3/。版本写在路径里而不是请求头里,v3 是当前一代。本指南里的每个路径都相对于这个基础 URL。
首次写入请使用自己控制的测试邮箱、有效列表 ID 和稳定的来源 ID。先在 Brevo 创建 FNAME 属性。使用真实密钥时,以下请求会直接写入:
curl -X POST https://api.brevo.com/v3/contacts \ -H "api-key: $BREVO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "email": "[email protected]", "ext_id": "test-source-contact-1", "attributes": { "FNAME": "Dev" }, "listIds": [42], "updateEnabled": true, "getId": true }'将 42 换成测试列表 ID。回读联系人,核对 ID、姓名、列表及屏蔽状态。仅有 2xx 不代表结果符合预期。
你真正会用到的端点
联系人
POST /v3/contacts 创建联系人。请求体接受 email、已定义的 attributes、listIds,以及作为稳定来源键的 ext_id。updateEnabled: true 更新已有联系人;getId: true 请求返回最终 ID。Brevo 文档列出了这些字段。每次运行都使用同一 ext_id,并保存来源 ID 与 Brevo ID 的映射。
写入前确定每个属性由哪个系统管理,以及哪些已同意的联系人应进入哪个列表。明确填写 listIds。常规更新不要发送 emailBlacklisted: false。Brevo 提醒,修改被屏蔽联系人的邮箱可能解除屏蔽,地址变更需要单独处理。加入营销列表前检查屏蔽和退订。读取与写入之间仍存在短暂竞态;严格要求下应在发送前再次核验。
读取走 GET /v3/contacts,用 limit(默认 50,最大 1000)和 offset 分页,并支持 UTC 格式的 modifiedSince 和 createdSince。增量同步应该依赖 modifiedSince,而不是遍历整个列表。注意 filter 参数只支持等值运算符,表达力更强的条件应该放进细分里。
批量导入用 POST /v3/contacts/import,它接受 fileUrl、fileBody 或 jsonBody,指向 listIds,异步执行并返回一个 processId。Brevo 文档写明请求体上限 10 MB,并建议控制在 8 MB 附近,因为解析会让载荷膨胀。记得传 notifyUrl,这样你就能得知结果,而不用轮询。
事务性邮件
POST /v3/smtp/email 是主力端点。除了 sender、to、subject 和 htmlContent,还有几个字段值得了解:
templateId配合params,用 Brevo 模板及其变量替换取代内联内容。单个版本的 params 上限 100 KB,累计上限 1000 KB。messageVersions,一次调用发送个性化变体,每个版本最多 99 个收件人。tags有助于将送达事件关联到触发该事件的应用流程。scheduledAt加上batchId,用于你可能想整批取消的未来发送。headers,采用 Title-Case,用于自定义 SMTP 请求头。
单次请求最多接受 2,000 个收件人。关于这个端点与营销活动发送的区别,事务性邮件指南给出了消息策略层面的视角。
邮件营销活动
POST /v3/emailCampaigns 需要 name 和 sender,外加恰好一个内容来源:htmlContent(至少 10 个字符,小于 1 MB)、htmlUrl 或 templateId。受众放在 recipients 里,以 listIds 或 segmentIds 形式给出,scheduledAt 使用 YYYY-MM-DDTHH:mm:ss.SSSZ 的 UTC 格式。配套路由分别负责立即发送、发送测试、更新状态和拉取活动报告。
公司、交易与对象
Brevo 的 CRM 有两条相互重叠的写入路径,选对很重要。
CRM 路由是 POST /v3/companies、PATCH /v3/companies/{id}、DELETE /v3/companies/{id},以及交易对应的同一组路由。它们是同步的。变更应用后 PATCH 返回 204。
对象 API 是批量路径:POST /v3/objects/{object_type}/batch/upsert 每次请求最多接受 1000 条记录和 1 MB,每条记录最多 500 个属性,每条记录每个对象类型最多 10 条关联记录。它返回 202 和一个 processId,意思是已受理而不是已应用。
curl -X POST https://api.brevo.com/v3/objects/company/batch/upsert \ -H "api-key: $BREVO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "records": [ { "identifiers": { "id": 12345 }, "attributes": { "domain": "acme.example", "industry": "retail" } } ] }'Brevo CRM 指南从运营者视角讲解了这套对象模型。
官方 SDK
Brevo 在 getbrevo GitHub 组织下维护这些客户端:
| 语言 | 代码仓库 |
|---|---|
| Node.js | github.com/getbrevo/brevo-node |
| Python | github.com/getbrevo/brevo-python |
| PHP | github.com/getbrevo/brevo-php |
| Java | github.com/getbrevo/brevo-java |
| C# | github.com/getbrevo/brevo-csharp |
| Go | github.com/getbrevo/brevo-go |
| Ruby | github.com/getbrevo/brevo-ruby |
Node 客户端以 @getbrevo/brevo 安装:
npm install @getbrevo/brevoimport { BrevoClient } from "@getbrevo/brevo";
// Reconcile uncertain send outcomes before retrying.const brevo = new BrevoClient({ apiKey: process.env.BREVO_API_KEY, maxRetries: 0 });
const result = await brevo.transactionalEmails.sendTransacEmail({ subject: "Order confirmed", htmlContent: "<html><body><p>Thanks for your order.</p></body></html>", tags: ["order-confirmation"],});
console.log("Message ID:", result.messageId);Python 客户端用 pip install brevo-python 安装。如果你不想为两个端点背上一个 SDK 依赖,原始 HTTP 接口面小到可以直接调用,这样也能让你免受 SDK 版本变动的影响:
可通过 pip install brevo-python 安装 Python 客户端。只调用少量端点时,也可直接使用 HTTP:
import osimport requests
BASE = "https://api.brevo.com/v3"HEADERS = {"api-key": os.environ["BREVO_API_KEY"]}
def upsert_consented_contact(source_id, email, first_name, list_id, consent): if not consent: raise ValueError("No marketing consent in the source") if not source_id or not isinstance(list_id, int): raise ValueError("Stable source ID and numeric list ID required") # Configure FNAME in Brevo before using this explicit field mapping. response = requests.post( f"{BASE}/contacts", headers=HEADERS, json={ "email": email, "ext_id": source_id, "attributes": {"FNAME": first_name}, "listIds": [list_id], "updateEnabled": True, "getId": True, }, timeout=30, ) response.raise_for_status() return response.json().get("id") if response.content else None成功更新可能返回空的 204 响应。此时 None 表示需要重新读取联系人以获取 ID。
示例假设调用方已核验同意记录及 Brevo 屏蔽状态,不会解除屏蔽。真实密钥会写入账户,请先用模拟响应测试。标识冲突可能返回不同的 4xx;应读取响应并核对 ID,不能总是假定为 409。
Webhook
Webhook 是你得知发送之后发生了什么的途径。POST /v3/webhooks 创建一个,参数有 url、events、type,还可以选填 channel(email 或 sms)、batched、自定义 headers 以及一个 auth 对象。
Webhook 有三种类型,事件词汇各不相同:
- 事务性:
sent、request、delivered、hardBounce、softBounce、blocked、spam、invalid、deferred、click、opened、uniqueOpened、unsubscribed。 - 营销:
spam、opened、click、hardBounce、softBounce、unsubscribed、listAddition、delivered、contactUpdated、contactDeleted。 - 入站:
inboundEmailProcessed和reply,这两者还额外需要一个domain。
curl -X POST https://api.brevo.com/v3/webhooks \ -H "api-key: $BREVO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://yourapp.example/hooks/brevo", "type": "transactional", "events": ["delivered", "hardBounce", "spam", "unsubscribed"], "description": "Deliverability signals" }'联系人变化应注册带有 contactUpdated 的营销 Webhook。Brevo 在事件载荷中使用 contact_updated。按安全文档,通过 auth Bearer 令牌或自定义请求头验证接收请求。处理前校验密钥,不要记录密钥。
以事件类型、联系人 ID 和时间戳组成去重键,并保留整个重投窗口。时间戳本身并不唯一。事件可能重复或乱序到达。收到事件后重新读取当前联系人,与可信来源核对。限定重放窗口,恢复后拒绝过期事件,并定期做增量核对以补上遗漏。Webhook 本身不构成双向同步。参见连接器指南。
速率限制与错误处理
Brevo 的速率限制按端点和套餐档位区分,各端点之间的差距极大。
| 端点 | General(所有套餐) | Advanced(Professional 与 Enterprise) |
|---|---|---|
POST /v3/smtp/email | 1,000 RPS | 2,000 RPS |
POST /v3/transactionalSMS/send | 150 RPS | 200 RPS |
/v3/contacts/... | 10 RPS,36,000 RPH | 20 RPS,72,000 RPH |
POST /v3/events | 10 RPS,36,000 RPH | 20 RPS,72,000 RPH |
GET /v3/smtp/emails | 2 RPS,7,200 RPH | 3 RPS,10,800 RPH |
| 没有单独限制的端点 | 100 RPH | 200 RPH |
Brevo 对受限端点提供 x-sib-ratelimit-limit、x-sib-ratelimit-remaining 和 x-sib-ratelimit-reset 请求头。成功响应也要读取。遇到 429,等待重置,并在固定次数与时间预算内重试。reset 是限流窗口单位表示的时长,通常为秒,不是 Unix 时间戳。
async function callBrevo(path, init) { for (let attempt = 0; attempt < 3; attempt++) { const response = await fetch(`https://api.brevo.com/v3${path}`, { ...init, headers: { "api-key": process.env.BREVO_API_KEY, ...init.headers }, }); if (response.status !== 429 || attempt === 2) return response; const raw = Number(response.headers.get("x-sib-ratelimit-reset")); const seconds = Number.isFinite(raw) && raw > 0 ? raw : 2 ** attempt; const delayMs = (seconds + 2 ** attempt) * 1000; if (delayMs > 60_000) return response; await new Promise(resolve => setTimeout(resolve, delayMs)); }}若所需等待超过 60 秒,函数会返回 429。请稍后安排下一次运行。
辅助函数只适用于已确认可安全重复的请求。发送结果不明时,先用自己的消息 ID 核对,再考虑发送。记录状态和限流头,不记录 API 密钥或联系人数据。
不要盲目重试所有 4xx。联系人创建文档列出 400 和 425;冲突不一定返回 409。写入后的 5xx 要先核实变更是否已经生效。
返回了什么状态?
-
429
按预算等待
遵守重置时间和重试次数上限。
-
写入后 5xx
回读结果
服务器可能已写入。
-
400
修正请求
通常是输入错误。
-
标识冲突
核对 ID
API 不保证固定冲突状态码。
不发真实邮件的测试方法
在事务性发送请求里加上值为 drop 的 X-Sib-Sandbox 请求头。Brevo 会校验请求,返回 201 和一个 messageId,不投递任何东西,也不写入邮件日志。
curl -X POST https://api.brevo.com/v3/smtp/email \ -H "api-key: $BREVO_API_KEY" \ -H "X-Sib-Sandbox: drop" \ -H "Content-Type: application/json" \ -d '{ "sender": { "email": "[email protected]" }, "to": [{ "email": "[email protected]" }], "subject": "Sandbox", "htmlContent": "<p>hi</p>" }'要清楚它能证明什么、不能证明什么。沙盒模式只校验请求格式,对发件人鉴权、模板渲染或送达能力什么都说明不了。凡是涉及联系人或 CRM 数据的集成测试,都要单独准备一个 Brevo 账号,因为沙盒模式只覆盖发送,不覆盖 API 的其余部分。
联系人同步前检查
- 为
ext_id选定稳定来源 ID,并保存 Brevo ID。 - 先定义属性,写入后回读测试联系人。
- 指定列表并保存可靠的同意证据;跳过屏蔽和退订者。
- 分别统计已接受、已应用、跳过和失败的写入。202 起初只表示已接收。
- 验证营销 Webhook、去重,再核对当前状态。
- 监测限流头,达到预算后停止重试。
Tajo 可将商务数据同步到 Brevo。自行构建时,可参考 Brevo 连接器指南。
常见问题
Brevo API 的基础 URL 是什么?
所有 Brevo REST 调用都指向 https://api.brevo.com/v3/。版本号写在路径里,v3 是当前一代,因此像事务性发送这样的端点,完整路径就是 https://api.brevo.com/v3/smtp/email。
如何完成 Brevo API 的鉴权?
把密钥放在名为 api-key 的 HTTP 请求头里发送。标准 API 密钥不使用 Authorization 或 Bearer 请求头。密钥在账号设置的 SMTP and API 里的 API keys 页面生成,值只显示一次。
Brevo API 的速率限制是多少?
限制按端点和速率限制档位区分。在所有套餐都享有的 general 档位上,事务性发送端点允许每秒 1,000 次请求,联系人端点允许每秒 10 次,没有单独限制的端点每小时上限 100 次请求。Professional 和 Enterprise 套餐可以使用 advanced 档位,Enterprise 还可以使用 extended 档位。超出限制会返回 429。
Brevo 有官方 SDK 吗?
有。Brevo 在 getbrevo 这个 GitHub 组织下发布了 Node.js、Python、PHP、Java、C#、Go 和 Ruby 的客户端。Node 包在 npm 上是 @getbrevo/brevo,Python 包在 PyPI 上是 brevo-python。
怎样在不发出真实邮件的情况下测试 Brevo API?
在事务性发送请求里加上值为 drop 的 X-Sib-Sandbox 请求头。Brevo 会校验请求,返回 201 和一个 messageId,但不发送任何邮件,也不写入邮件日志。它只检查请求格式,不检查送达能力。
Brevo 支持哪些 Webhook 事件?
事务性 Webhook 涵盖送达、退信和退订。营销 Webhook 包括 contactUpdated 和 contactDeleted。入站事件的文档列出 inboundEmailProcessed。
联系人更新如何保留已有退订?
使用稳定的来源 ID 和已定义的属性。常规更新中不要设置 emailBlacklisted: false。加入营销列表前,检查联系人状态及同意记录。
Webhook 足以实现双向同步吗?
不足以。验证接收端身份、去除重复投递,并与可信来源核对遗漏或乱序事件。Webhook 只提示发生了变化,不证明两个系统状态一致。