Brevo API:面向开发者的实用指南
面向开发者的 Brevo API 指南:鉴权、基础 URL、联系人、事务性邮件、营销活动、CRM 对象、Webhook、速率限制以及真实环境中的限制。
Brevo 用一套 REST API 同时覆盖事务性消息、营销活动、联系人数据和 CRM 记录。让第一个请求返回 201 大约只要两分钟。但要做出一个不会静默丢数据的生产级集成,时间会长得多,因为最关键的几条约束要么没写进文档,要么与 API 自己报告的情况相互矛盾。
本指南两部分都讲:第一天就需要的端点、SDK 和鉴权,以及上线之前必须提前设计规避的平台限制。
Brevo API 覆盖哪些能力
所有能力都在同一个域名和同一个版本路径下。开发者文档把整个接口面分成四个产品领域:
- 消息:事务性邮件、SMS 和 WhatsApp,包括批量发送、定时发送和消息活动记录。
- 营销平台:联系人、列表、细分和邮件营销活动。
- 电商:商品、订单和客户事件跟踪。
- 对话:聊天挂件以及通过程序管理对话。
这些领域共用一个账号、一个联系人数据库和一个 API 密钥。这既方便,偶尔也危险:一个按预演数据写出来的脚本,操作的是营销活动实际发送的那批联系人。
事务性与营销的区别
这两类的行为差别足够大,把它们弄混是最常见的设计错误。
| 事务性 | 营销 | |
|---|---|---|
| 主要端点 | POST /v3/smtp/email | POST /v3/emailCampaigns |
| 寻址方式 | 请求里显式指定收件人 | listIds 或 segmentIds |
| 触发方式 | 你的应用实时触发 | 定时发送或按需发送 |
| 典型流量形态 | 持续不断,一次一条 | 突发式,一次一大批 |
| 速率限制姿态 | 非常高,标准套餐每秒 1,000 次请求 | 低,营销活动端点归入通用上限 |
如果你还在判断 Brevo 是不是合适的平台,平台概览覆盖了这部分内容。
鉴权与密钥管理
Brevo 使用放在自定义请求头里的纯 API 密钥。请求头名为 api-key,不是 Authorization,也没有 Bearer 前缀。先用过其他消息 API 的人几乎都会在这里栽跟头。
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。
第一次写入比第一次读取更能说明问题,因为它会触及账号里通常配置有误的部分,尤其是已验证发件人:
curl -X POST https://api.brevo.com/v3/smtp/email \ -H "api-key: $BREVO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "sender": { "name": "Ops", "email": "[email protected]" }, "to": [{ "email": "[email protected]", "name": "Dev" }], "subject": "First transactional send", "htmlContent": "<html><body><p>It works.</p></body></html>", "tags": ["smoke-test"] }'发送成功返回 201 和一个 messageId。定时发送返回 202。
你真正会用到的端点
联系人
POST /v3/contacts 创建联系人。请求体接受 email、用于自定义字段的 attributes 映射、listIds、用于你自己外部键的 ext_id,以及实践中最关键的两个开关:updateEnabled,它把这个调用变成 upsert;getId,它让响应返回联系人 id。
读取走 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,这个你应该始终设置。标签会随 Webhook 事件返回,是把送达事件和产生它的代码路径对应起来的唯一廉价手段。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";
const brevo = new BrevoClient({ apiKey: process.env.BREVO_API_KEY });
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 版本变动的影响:
import osimport requests
BASE = "https://api.brevo.com/v3"HEADERS = { "api-key": os.environ["BREVO_API_KEY"], "Content-Type": "application/json",}
def upsert_contact(email, attributes, list_ids): response = requests.post( f"{BASE}/contacts", headers=HEADERS, json={ "email": email, "attributes": attributes, "listIds": list_ids, "updateEnabled": True, }, timeout=30, ) response.raise_for_status() return response面向 AI 助手,还有一个位于 https://mcp.brevo.com/v1/brevo/mcp 的 MCP 服务器,用同一个设置页面生成的 bearer token 鉴权。它适合探索和账号问答,不适合生产数据链路。
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" }'有三件事要做对。第一,一个账号所有类型加起来最多只能有 40 个 Webhook,所以要在处理程序内部按事件分流,而不是给每个事件注册一个端点。第二,预计有量的时候用 batched 开关,因为一个请求携带多个事件,处理成本远低于多个请求。第三,保护接收端:Brevo 公布了自己的发送 IP 段,把你的端点限制到这些 IP 段是官方推荐的做法。再通过 headers 字段加上你自己的共享密钥作为第二层防护。
处理程序必须幂等。把消息 id 加事件类型加时间戳当作去重键。
速率限制与错误处理
Brevo 的速率限制按端点和套餐档位区分,各端点之间的差距极大。
| 端点 | 标准 | 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 | Enterprise 更高 |
GET /v3/smtp/emails | 2 RPS,7,200 RPH | 3 RPS,10,800 RPH |
| 其余所有端点 | 100 RPH | 200 RPH |
最后一行才是真正会咬人的地方。发送几乎不受限,而营销活动管理、CRM 读取和大多数管理类调用在标准套餐上共用每小时 100 次请求的额度。一个在每次写入前都要先读一遍公司记录的天真回填任务,不到两分钟就能耗光一小时的配额。
每个响应都带有 x-sib-ratelimit-limit、x-sib-ratelimit-remaining 和 x-sib-ratelimit-reset。成功时也要读,别只在失败时才看。超出限制会返回 429,正确的应对是等待 reset 请求头给出的时间间隔,然后再做带抖动的指数退避。
async function callBrevo(path, init, attempt = 0) { const response = await fetch(`https://api.brevo.com/v3${path}`, { ...init, headers: { "api-key": process.env.BREVO_API_KEY, "Content-Type": "application/json", ...init.headers }, });
if (response.status === 429 && attempt < 5) { const reset = Number(response.headers.get("x-sib-ratelimit-reset") || 1); const backoff = Math.pow(2, attempt) * 250 + Math.random() * 250; await new Promise((r) => setTimeout(r, reset * 1000 + backoff)); return callBrevo(path, init, attempt + 1); }
return response;}429 和 5xx 要重试。400 或 409 绝不要盲目重试,因为这两者通常意味着请求本身有问题,而不是发得太早;尤其是 409,需要的是换一种处理动作,而不是重复一次。
不发真实邮件的测试方法
在事务性发送请求里加上值为 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 的其余部分。
决定集成设计的那些限制
下面这些约束,只有当集成在真实账号上跑起量之后才会显现。其中好几条与 API 自己的说法相互矛盾。它们都没有商量余地,所以唯一明智的做法是围绕它们来设计。
公司必须有域名,且一个域名只能有一家公司
GET /v3/crm/attributes/companies 把每个属性都报告为非必填,创建公司的参考文档也只把 name 列为必填。实际上,POST /v3/companies 如果没有非空的 domain 属性,会返回 400 并提示缺少强制默认属性。传空字符串和不传的失败方式完全一样。
更麻烦的是,域名唯一性是强制的。在已被占用的域名上创建第二家公司会返回 409。对 B2B 商务来说这是结构性的:共用同一个买家邮箱域名的多家子公司,无法在 Brevo 里各自作为独立公司存在。而且仅仅同步一个联系人,就足以让一家公司凭该联系人的邮箱域名冒出来,于是一次创建可能与谁也没有显式创建过的公司撞车。正确的处理程序应该在收到 409 时接管已存在的公司,而不是失败或重试。
未声明的属性会被静默丢弃
这是平台里最危险的行为,而 Brevo 把它写得清清楚楚:如果某个属性出现在请求里但此前没有在对象架构中定义,那就什么都不会发生。不报错,不创建属性,也不给警告。
因此 2xx 响应并不能证明你的数据落库了。写入前先读取架构,在你自己的客户端里丢弃所有未声明的字段,并且宁可拒绝运行属性不存在的同步任务,也不要写了一个月半截记录之后才有人发现。
属性过滤器会被接受然后忽略
GET /v3/companies?filters[attributes.domain]=... 返回 200 并忽略该过滤条件。两个完全不同的过滤条件返回相同的记录。通过这条路由,没有任何可用的方式按属性查找公司。
再加上大账号上无过滤的列表调用无论页大小多少都会 504 超时,一家已存在的公司通过文档中的路径可能真的找不到。变通办法是扫描 GET /v3/objects/company/records 并带上 sort=desc,它速度快、可分页、会返回属性,把扫描页数限制在一个合理范围内即可。刚刚触发 409 的那家公司几乎总是片刻之前才创建的,所以按最新优先扫描能很快找到它。
每个对象类型一百万条记录,且没有批量删除
当某个对象类型的记录数达到一百万条时,POST /v3/objects/{type}/batch/upsert 会返回 400。它连更新一起挡住:用记录自己的数字 id 定位一条已存在的记录,同样失败。整条对象写入路径一次性关闭。
想退回上限之下很慢,因为 POST /v3/objects/{type}/batch/delete 对 company 这类 Brevo 标准对象类型返回 403。唯一的路子是 DELETE /v3/companies/{id},一次一条,每条约 156 毫秒。用这种方式清理 124,000 条记录,开 20 个并行工作进程也要花上好几个小时。请按计划定期监控记录数,而不是通过一次失败的同步才发现上限,并把高频更新走 PATCH /v3/companies/{id},这条路径没有这类限制。
ext_id 是 Brevo 的 id,不是你的
在对象记录上,identifiers.ext_id 存放的是 Brevo 自己的 CRM 公司 id,一个 Mongo 风格的字符串。它不是可以自由使用的外部键。把 ext_id 设成你自己平台的标识符再做 upsert,结果是产生重复记录而不是匹配上。你的外部 id 应该放在一个自己声明的属性里。
对象 upsert 是异步的,CRM 写入不是
batch/upsert 返回 202 和一个 processId,之后才实际应用。一个不存在的 id 会异步失败,但仍然向调用方返回 202。PATCH /v3/companies/{id} 返回 204,且是同步应用的。如果你的同步任务报告成功,只有同步路径才配得上这个词,其他路径都需要再读一次确认。
一份简短的集成检查清单
- 按服务和环境分别使用独立 API 密钥,人员变动时轮换。
- 所有写入都经由同一个客户端,它读取速率限制请求头并在 429 时退避。
- 启动时校验属性架构,属性缺失就拒绝运行同步。
- 创建公司返回 409 意味着接管,而不是重试。
- 批量路径用对象 API 换吞吐,凡是必须确认结果的走 CRM 路由。
- Webhook 做到幂等、批量、限制 IP,并携带共享密钥请求头。
- 联系人增量同步用
modifiedSince,不要全量遍历。
搭建并维护这一层是实打实的工程工作:架构校验、退避、接管逻辑、对账。Tajo 的存在就是为了把这些吸收掉,让 Shopify 和商务数据与 Brevo 的联系人、公司和事件保持同步,而不用任何人手写重试和去重逻辑。如果你打算自己接线,Brevo 集成指南会带你走一遍写代码之前要做的数据模型选择。
要点回顾
- 这套 API 是位于
https://api.brevo.com/v3/的单一 REST 接口,用api-key请求头鉴权,而不是 bearer token。 - 速率限制极不均衡:发送几乎不受限,而其他大多数端点在标准套餐上共用每小时 100 次请求。
- 官方 SDK 覆盖七种语言,但当你只需要少数几个端点时,HTTP 接口面简单到可以直接调用。
- 沙盒模式只校验请求格式,所以发送之外的任何测试都要另开一个账号。
- 2xx 响应不能证明写入已生效。未声明的属性会被静默丢弃,对象 upsert 是异步的。
- 围绕这些固定限制来设计:一个域名一家公司、每个对象类型一百万条记录、标准对象没有批量删除,以及默默不生效的属性过滤器。