Brevo API:実務者のための開発ガイド
開発者向けの Brevo API ガイド。認証、ベース URL、コンタクト、トランザクションメール、キャンペーン、CRM オブジェクト、Webhook、レート制限、そして実運用で効いてくる制約まで解説します。
Brevo は、トランザクションメッセージ、マーケティングキャンペーン、コンタクトデータ、CRM レコードにまたがる 1 つの REST API を公開しています。最初のリクエストで 201 を返すところまでは 2 分ほどです。しかし、データを黙って失わない本番品質の連携を組み上げるには、はるかに長い時間がかかります。最も重要な制約のいくつかが、文書化されていないか、API 自身の申告と矛盾しているからです。
このガイドでは両方の側面を扱います。初日に必要となるエンドポイント、SDK、認証と、リリース前に設計へ織り込んでおくべきプラットフォームの制限です。
Brevo API がカバーする範囲
すべてが単一のホストと単一のバージョンパスの下にあります。開発者向けドキュメントは、この API を 4 つのプロダクト領域に分類しています。
- メッセージング: トランザクションメール、SMS、WhatsApp。バッチ送信、スケジュール送信、メッセージのアクティビティを含みます。
- マーケティングプラットフォーム: コンタクト、リスト、セグメント、メールキャンペーン。
- eコマース: 商品、注文、顧客イベントのトラッキング。
- カンバセーション: チャットウィジェットと、プログラムからの会話管理。
これらの領域は 1 つのアカウント、1 つのコンタクトデータベース、1 つの API キーを共有します。便利である一方で、ときに危険でもあります。検証環境のつもりで書いたスクリプトが、キャンペーンの送信先と同じコンタクトを相手にしているからです。
トランザクションとマーケティングの違い
この 2 つの系統は挙動が大きく異なり、混同することが最も多い設計ミスにつながります。
| トランザクション | マーケティング | |
|---|---|---|
| 主なエンドポイント | POST /v3/smtp/email | POST /v3/emailCampaigns |
| 宛先の指定 | リクエスト内で受信者を明示 | listIds または segmentIds |
| きっかけ | アプリケーションからリアルタイムで | スケジュール、または任意のタイミングで送信 |
| 典型的な流量の形 | 連続的、1 通ずつ | 突発的、1 回の大量送信 |
| レート制限の余裕 | 非常に大きい。標準プランで毎秒 1,000 リクエスト | 小さい。キャンペーン系は一般上限の対象 |
そもそも Brevo が適したプラットフォームかどうかを検討中であれば、プラットフォームの概要がその範囲を扱っています。
認証とキー管理
Brevo はカスタムヘッダーに素の API キーを入れる方式です。ヘッダー名は Authorization ではなく api-key で、Bearer の接頭辞もありません。他のメッセージング API を先に触った人はほぼ全員がここでつまずきます。
curl https://api.brevo.com/v3/account \ -H "api-key: $BREVO_API_KEY"キーは Brevo アプリのアカウント設定内、SMTP and API セクションの API keys タブで生成します。キーごとに、それを使うシステムがわかる名前を付けてください。キーの値は生成時に一度だけ表示されるため、紛失した場合は復旧ではなく新規発行になります。
実務上のルールをいくつか挙げます。
- デプロイ先ごと、サービスごとに別のキーを発行しましょう。漏えいしたキーの失効が、無関係な 3 つのシステムを止めることがあってはいけません。
- 標準の API キーはアカウント全体に及びます。どのキーも、コンタクト、送信、CRM データへのフルアクセスだと考えてください。
- Brevo は、他の Brevo アカウントを代理して動作するアプリケーション向けに OAuth 2.0 にも対応しています。詳細はキーのフローと並べて認証方式に記載されています。
- AI アシスタントが使う MCP サーバーは別のトークンを使い、こちらは bearer ヘッダーを利用します。そのトークンは同じ API keys 画面で生成しますが、REST 用のキーとは互換性がありません。
ベース URL、バージョン、最初の書き込み
ベース URL は https://api.brevo.com/v3/ です。バージョンはヘッダーではなくパスに含まれ、現行世代は v3 です。このガイドのパスはすべてこのベースからの相対です。
最初に試すなら、読み取りより書き込みのほうが得るものが多くなります。アカウントの中でも設定漏れが起きやすい部分、とりわけ認証済み送信者を実際に通るからです。
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"] }'送信が成功すると messageId とともに 201 が返ります。スケジュール送信では 202 が返ります。
実際に使うことになるエンドポイント
コンタクト
POST /v3/contacts でコンタクトを作成します。ボディには email、カスタムフィールド用の attributes マップ、listIds、独自の外部キーとしての ext_id、そして実務で最も重要な 2 つのフラグを指定します。呼び出しを upsert に変える updateEnabled と、レスポンスにコンタクト ID を返させる getId です。
読み取りは 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。1 回の呼び出しでパーソナライズした複数のバリエーションを送れます。1 バージョンあたり最大 99 名です。tags。これは必ず設定してください。タグは Webhook イベントに返ってくるため、配信イベントとそれを生んだコードパスを安価に突き合わせられる唯一の手段です。scheduledAtとbatchId。グループ単位でキャンセルする可能性がある将来の送信に使います。headers。カスタム SMTP ヘッダーを Title-Case で指定します。
1 リクエストあたりの受信者は最大 2,000 名です。このエンドポイントとキャンペーン送信の違いについては、トランザクションメールのガイドがメッセージング戦略の観点から解説しています。
メールキャンペーン
POST /v3/emailCampaigns には name と sender、そしてコンテンツの供給元をちょうど 1 つ指定します。htmlContent(最小 10 文字、1 MB 未満)、htmlUrl、templateId のいずれかです。配信対象は recipients に listIds または segmentIds として渡し、scheduledAt は YYYY-MM-DDTHH:mm:ss.SSSZ の UTC 形式を使います。付随するルートとして、即時送信、テスト送信、ステータス更新、キャンペーンレポートの取得があります。
会社、取引、オブジェクト
Brevo の CRM には書き込み経路が 2 つあり、部分的に重なっています。どちらを選ぶかが重要です。
CRM のルートは POST /v3/companies、PATCH /v3/companies/{id}、DELETE /v3/companies/{id} と、取引向けの同等の一式です。これらは同期的です。PATCH は変更が適用された時点で 204 を返します。
オブジェクト API は一括処理の経路です。POST /v3/objects/{object_type}/batch/upsert は 1 リクエストあたり最大 1000 レコード、1 MB、1 レコードあたり最大 500 属性、オブジェクト型ごとに最大 10 件の関連レコードを受け取ります。返るのは processId 付きの 202 で、適用済みではなく受理済みという意味です。
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 で導入します。エンドポイント 2 つのために 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 responseAI アシスタント向けには、https://mcp.brevo.com/v1/brevo/mcp に MCP サーバーもあります。認証には同じ設定画面で生成する bearer トークンを使います。調査やアカウントに関する問い合わせには便利ですが、本番のデータ経路向けではありません。
Webhook
Webhook は、送信後に何が起きたかを知るための仕組みです。POST /v3/webhooks で作成し、url、events、type と、任意で channel(email または sms)、batched、カスタムの headers、auth オブジェクトを指定します。
Webhook には 3 つの種類があり、それぞれイベントの語彙が異なります。
- トランザクション:
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" }'押さえるべき点が 3 つあります。第一に、1 アカウントが保持できる Webhook は全種類あわせて最大 40 件です。イベントごとにエンドポイントを登録するのではなく、ハンドラー内部でイベントごとに振り分けましょう。第二に、量が見込まれる場合は batched フラグを使ってください。多数のイベントを 1 リクエストで運ぶほうが、リクエストを多数さばくよりはるかに安く済みます。第三に、受信側を保護してください。Brevo は送信元 IP レンジを公開しており、エンドポイントをそのレンジに限定するのが文書化された方法です。headers フィールドから独自の共有シークレットを追加し、二重の防御にしましょう。
ハンドラーは冪等でなければなりません。メッセージ ID とイベント種別とタイムスタンプの組を重複排除キーとして扱ってください。
レート制限とエラー処理
Brevo のレート制限はエンドポイントごと、プラン階層ごとに定められており、エンドポイント間の開きは非常に大きくなっています。
| エンドポイント | Standard | 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 の読み取り、大半の管理系呼び出しは、標準プランでは 1 時間あたり 100 リクエストという予算を分け合います。書き込みのたびに会社レコードを読むような素朴なバックフィルは、2 分足らずで 1 時間分の枠を使い切ります。
すべてのレスポンスには x-sib-ratelimit-limit、x-sib-ratelimit-remaining、x-sib-ratelimit-reset が含まれます。失敗時だけでなく成功時にも読んでください。上限を超えると 429 が返るので、正しい対応はリセットヘッダーの示す時間だけ待ち、そのうえでジッター付きの指数バックオフを適用することです。
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 は繰り返しではなく別の対応を必要とします。
メールを送らずにテストする
トランザクション送信に X-Sib-Sandbox ヘッダーを値 drop で追加します。Brevo はリクエストを検証して messageId 付きの 201 を返しますが、何も配信せず、メールログにも記録しません。
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 自身の申告と食い違います。どれも交渉の余地はないので、唯一まともな対応は、それらを前提に設計することです。
会社にはドメインが必須で、1 ドメインにつき 1 社まで
GET /v3/crm/attributes/companies はすべての属性を必須ではないと報告し、会社作成のリファレンスも必須は name だけだと記載しています。しかし実際には、空でない domain 属性を欠いた POST /v3/companies は、必須の既定属性が足りないというメッセージとともに 400 を返します。空文字列も、省略した場合とまったく同じように失敗します。
さらに厄介なことに、ドメインの一意性が強制されています。すでに使われているドメインで 2 社目を作ろうとすると 409 が返ります。B2B コマースではこれは構造的な問題です。買い手のメールドメインを共有する子会社群を、Brevo 上で別々の会社として並存させることはできません。しかもコンタクトを同期するだけで、そのコンタクトのメールドメインに対応する会社が現れることがあるため、誰も明示的に作っていない会社と作成が衝突しえます。正しいハンドラーは、409 を受けたら失敗や再試行ではなく、既存の会社を採用します。
未定義の属性は黙って破棄される
これはこのプラットフォームで最も危険な挙動であり、Brevo 自身がはっきり文書化しています。リクエストに含まれる属性がオブジェクトスキーマにあらかじめ定義されていない場合、何も起きません。エラーも、属性の作成も、警告もありません。
したがって 2xx のレスポンスは、データが着地した証拠にはなりません。書き込む前にスキーマを読み、未定義のものは自分のクライアント側で落とし、属性が存在しない同期は実行を拒否してください。半分だけ書けたレコードが、誰も気づかないまま 1 か月放置される事態を避けられます。
属性フィルターは受け付けられ、そして無視される
GET /v3/companies?filters[attributes.domain]=... は 200 を返し、フィルターを無視します。まったく異なる 2 つのフィルターが同じレコードを返します。このルートを通じて属性で会社を引く手段は、事実上存在しません。
さらに、フィルターなしの一覧はページサイズを問わず大規模アカウントで 504 のタイムアウトになるため、既存の会社が文書化された経路では本当に見つけられなくなります。回避策は GET /v3/objects/company/records を sort=desc で走査することです。この経路は高速で、ページングでき、属性も返します。ページ数は妥当な範囲に区切ってください。409 を引き起こしたばかりの会社は、ほぼ必ず直前に作成されているので、新しい順の走査ならすぐに見つかります。
オブジェクト型あたり 100 万レコード、そして一括削除なし
POST /v3/objects/{type}/batch/upsert は、オブジェクト型が 100 万レコードに達すると 400 を返します。作成だけでなく更新も止まります。既存レコードを自身の数値 ID で指定しても同じように失敗します。オブジェクトへの書き込み経路がまとめて閉じるのです。
上限より下に戻すのは時間がかかります。POST /v3/objects/{type}/batch/delete は company のような Brevo 標準オブジェクト型に対して 403 を返すからです。残された経路は DELETE /v3/companies/{id} のみで、1 呼び出しあたり 1 レコード、およそ 156 ms です。この方法で 124,000 レコードを消したときは、20 並列でも数時間かかりました。同期の失敗で上限に気づくのではなく、レコード数を定期的に監視しましょう。そして大量の更新は、この制限のない PATCH /v3/companies/{id} に流してください。
ext_id は Brevo の ID であって、あなたの 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 でバックオフする 1 つのクライアントに通す。
- 起動時に属性スキーマを検証し、属性が欠けていれば同期の実行を拒否する。
- 会社作成での 409 は、再試行ではなく採用を意味する。
- 一括処理はスループット重視でオブジェクト API、確認が必須のものは CRM ルートを使う。
- Webhook は冪等にし、バッチ化し、IP を制限し、共有シークレットのヘッダーを付ける。
- コンタクトの差分同期は全件走査ではなく
modifiedSinceを使う。
この層を作って維持することは、まぎれもないエンジニアリング作業です。スキーマ検証、バックオフ、採用ロジック、突合。Tajo はそれを引き受けるために存在し、Shopify とコマースのデータを Brevo のコンタクト、会社、イベントと同期させます。再試行や重複排除のロジックを誰かが手書きする必要はありません。自分で配線するのであれば、コードの前に決めるべきデータモデルの選択を Brevo 連携ガイドがひととおり解説しています。
要点
- この API は
https://api.brevo.com/v3/にある 1 つの REST インターフェースで、bearer トークンではなくapi-keyヘッダーで認証します。 - レート制限は極端に不均一です。送信は事実上無制限に近く、他の大半のエンドポイントは標準プランで 1 時間あたり 100 リクエストを分け合います。
- 公式 SDK は 7 言語ぶんありますが、必要なエンドポイントが少数なら HTTP を直接呼ぶだけで十分なほど単純です。
- サンドボックスモードが検証するのはリクエストの形式だけなので、送信以外をテストするなら別アカウントを用意してください。
- 2xx のレスポンスは書き込みが適用された証拠になりません。未定義の属性は黙って落ち、オブジェクトの upsert は非同期です。
- 固定された制限を前提に設計しましょう。1 ドメインにつき 1 社、オブジェクト型あたり 100 万レコード、標準オブジェクトには一括削除なし、そして静かに何もしない属性フィルターです。