シートの連絡先をBrevoにインポートするGoogle Apps Scriptを構築する方法
Google SheetからBrevoに自動的に連絡先をプッシュします。APIキーストレージ、run-on-edit、時間ベースのトリガー、カスタムメニュー、エラー処理を備えた完全なApps Script。サーバー不要です。
チームがすでに Google Sheet で生活しているなら(セールスリード、イベント登録、パートナー連絡先リスト)、そのデータを Brevo に取り込むのに CSV をエクスポートして手動で再インポートする必要はありません。Google Apps Script を使えば、Sheet を Brevo の API に直接配線できます。スクリプトは Google のインフラ内で動作するので、ホスト、デプロイ、面倒を見るものは何もありません。
このガイドは動作するスクリプトを通じて進めます。Sheet 内のカスタム Sync to Brevo メニュー項目、自動の毎時トリガー、安全な API キー保存、バッチ処理、そして何が起きたかを確認できる少しの構造化ロギングです。
必要なもの
- 連絡先のある Google Sheet(連絡先 1 件につき 1 行、最初にヘッダー行)
- Brevo アカウントと API キー(Settings → SMTP & API → API Keys)
- 連絡先を追加したい Brevo リストの数値 ID
それだけです。npm も Python もサーバーもありません。
シートレイアウト
このガイドのスクリプトは、ヘッダー行の後に 1 行に 1 件の連絡先が続くものを期待します。列はヘッダー名で Brevo 属性にマップされます。
| firstName | lastName | company | city | |
|---|---|---|---|---|
| [email protected] | Jane | Doe | Acme | Berlin |
| [email protected] | John | Smith | Globex | Paris |
email は必須で、大文字小文字を区別せずにマッチングされます。それ以外のすべては Brevo に連絡先属性として送信されます。カスタム属性(FIRSTNAME、LASTNAME のような標準のもの以外)は、まず Brevo アカウントに存在する必要があります。Contacts → Settings → Contact attributes で、または Brevo API 経由で定義してください。
Apps Script エディタを開く
Sheet で Extensions → Apps Script。空の Code.gs を持つ新しいタブが開きます。内容を以下のスクリプトで置き換えます。
完全なスクリプト
const BREVO_API_BASE = 'https://api.brevo.com/v3';const BREVO_LIST_ID = 42; // <- the Brevo list to import contacts intoconst SHEET_NAME = 'Contacts'; // <- name of the sheet tab to readconst BATCH_SIZE = 1000; // contacts per import call
/** * Adds a "Brevo" menu to the Sheet so users can run the sync from the UI. * Triggered automatically when the Sheet opens. */function onOpen() { SpreadsheetApp.getUi() .createMenu('Brevo') .addItem('Sync sheet to Brevo', 'syncSheetToBrevo') .addItem('Configure API key', 'configureApiKey') .addToUi();}
/** * Reads every contact row from the sheet, batches them, and sends each batch * to Brevo's import endpoint. Returns a summary string for logging. */function syncSheetToBrevo() { const apiKey = getApiKey_(); if (!apiKey) { SpreadsheetApp.getUi().alert( 'No Brevo API key configured. Run "Configure API key" first.' ); return; }
const contacts = readContactsFromSheet_(); if (contacts.length === 0) { SpreadsheetApp.getUi().alert('No contacts found in the sheet.'); return; }
const batches = chunk_(contacts, BATCH_SIZE); const results = [];
for (let i = 0; i < batches.length; i++) { const result = importBatchToBrevo_(apiKey, batches[i]); results.push(result); Logger.log( `Batch ${i + 1}/${batches.length}: ${result.ok ? 'ok' : 'FAILED'} ` + `(processId=${result.processId || '-'}, status=${result.status})` ); }
const summary = `Sent ${contacts.length} contacts in ${batches.length} batch(es). ` + `Successful: ${results.filter(r => r.ok).length}/${results.length}.`; Logger.log(summary); SpreadsheetApp.getActiveSpreadsheet().toast(summary, 'Brevo sync', 5); return summary;}
/** * Reads the active spreadsheet's "Contacts" tab into an array of * { email, attributes } objects shaped for Brevo's jsonBody. */function readContactsFromSheet_() { const sheet = SpreadsheetApp .getActiveSpreadsheet() .getSheetByName(SHEET_NAME); if (!sheet) { throw new Error(`Sheet tab "${SHEET_NAME}" not found`); }
const range = sheet.getDataRange().getValues(); if (range.length < 2) return [];
const headers = range[0].map(String); const emailColumn = headers.findIndex(h => h.toLowerCase() === 'email'); if (emailColumn === -1) { throw new Error('Sheet must have an "email" column'); }
const contacts = []; for (let i = 1; i < range.length; i++) { const row = range[i]; const email = String(row[emailColumn] || '').trim().toLowerCase(); if (!email || !email.includes('@')) continue; // skip invalid
const attributes = {}; for (let c = 0; c < headers.length; c++) { if (c === emailColumn) continue; const value = row[c]; if (value === '' || value === null) continue; // Brevo convention: ATTRIBUTES ARE UPPERCASE attributes[headers[c].toUpperCase()] = value; } contacts.push({ email, attributes }); } return contacts;}
/** * POSTs a batch of contacts to Brevo's import endpoint. * Returns { ok, status, processId, error }. */function importBatchToBrevo_(apiKey, contacts) { const payload = { jsonBody: contacts, listIds: [BREVO_LIST_ID], updateExistingContacts: true, emptyContactsAttributes: false, };
const response = UrlFetchApp.fetch(`${BREVO_API_BASE}/contacts/import`, { method: 'post', contentType: 'application/json', headers: { 'api-key': apiKey, 'accept': 'application/json', }, payload: JSON.stringify(payload), muteHttpExceptions: true, // we'll inspect status ourselves });
const status = response.getResponseCode(); const body = response.getContentText();
if (status === 202) { const json = JSON.parse(body); return { ok: true, status, processId: json.processId }; } return { ok: false, status, error: body };}
/** * Stores the Brevo API key in script properties — encrypted at rest by Google * and not visible in the source code or to viewers of the sheet. */function configureApiKey() { const ui = SpreadsheetApp.getUi(); const response = ui.prompt( 'Brevo API key', 'Paste your Brevo API key (xkeysib-...). It will be stored in Script Properties.', ui.ButtonSet.OK_CANCEL ); if (response.getSelectedButton() !== ui.Button.OK) return; const key = response.getResponseText().trim(); if (!key.startsWith('xkeysib-')) { ui.alert('That doesn\'t look like a Brevo API key (should start with xkeysib-).'); return; } PropertiesService.getScriptProperties().setProperty('BREVO_API_KEY', key); ui.alert('API key saved.');}
function getApiKey_() { return PropertiesService.getScriptProperties().getProperty('BREVO_API_KEY');}
function chunk_(arr, size) { const out = []; for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size)); return out;}これですべてです。保存(⌘S / Ctrl+S)し、プロジェクトを「Brevo sync」のように名付け、Sheet に戻ります。
初回実行
Sheet を再読み込みします。新しい Brevo メニューが上部に表示されます。
- Brevo → Configure API key をクリックし、
xkeysib-...キーを貼り付け、OK をクリック。 - Brevo → Sync sheet to Brevo をクリック。Google が初回に権限を要求します:
- “View and manage your spreadsheets”、行を読み取るために必要
- “Connect to an external service”、api.brevo.com を呼び出すために必要
- 承認します。スクリプトが実行されます。右下の緑のトーストが、何件の連絡先が送信されたかを伝えます。
失敗した場合は、Extensions → Apps Script → View → Logs をクリックして、Brevo からのバッチごとのステータスコードを確認してください。最も一般的な失敗は、カスタム属性の欠落による 400 です。以下のトラブルシューティングセクションを参照してください。
スケジュール実行する
Apps Script エディタで: Triggers(左サイドバーの時計アイコン)→ Add Trigger。
- Choose function:
syncSheetToBrevo - Event source: Time-driven
- Type: Hour timer(または 1 日 1 回の同期なら Day timer)
- Interval: 1 時間ごと(または合うもの)
保存。Google はサーバーも cron もメンテナンスもなしに、そのケイデンスで関数を永遠に実行します。
すべてのセル変更で同期をトリガーしたい場合は、From spreadsheet → On edit も使えます。それには注意してください。装飾的な編集でもトリガーが発火し、忙しいシートでは Apps Script の日次クォータに素早く達する可能性があります。毎時の時間トリガーがほぼ常に正解です。
知っておくべき Apps Script クォータ
無料の Apps Script ティアには、尊重する価値のある制限があります。
| 制限 | 値(無料ティア) |
|---|---|
| 1 日の合計ランタイム | 90 分 |
| 単一実行時間 | 6 分 |
1 日あたりの UrlFetchApp 呼び出し | 20,000 |
UrlFetchApp ペイロードサイズ | 50MB |
UrlFetchApp ヘッダーサイズ | 8KB |
| ユーザーごとスクリプトごとのトリガー | 20 |
典型的な連絡先同期(数千の連絡先、毎時)では、これらのいずれにも全く近くありません。注意すべき唯一は 6 分の単一実行 です。何十万もの連絡先を一度に同期する場合は、より小さなチャンクにバッチ化してください(上記のスクリプトは BATCH_SIZE を介してすでにこれを行っています)。
インポートを非同期に処理する
Brevo のインポートエンドポイントは非同期です: processId がすぐに返り、実際のインポートはサーバーサイドで実行されます。ほとんどのシート同期ではこれで構いません、fire-and-forget、Brevo は各バッチが完了するとサマリーをメールします。
インポートが本当に完了するまでブロックしたい 場合は、プロセスステータスエンドポイントをポーリングします。
function waitForImport_(apiKey, processId, timeoutMs = 5 * 60 * 1000) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { const resp = UrlFetchApp.fetch(`${BREVO_API_BASE}/processes/${processId}`, { headers: { 'api-key': apiKey }, muteHttpExceptions: true, }); if (resp.getResponseCode() === 200) { const status = JSON.parse(resp.getContentText()).status; if (status === 'completed' || status === 'failed') return status; } Utilities.sleep(5000); // 5s between checks } return 'timeout';}Utilities.sleep は Apps Script のブロッキング待機相当です。長くスリープしないでください。実行ごとに合計 6 分しかありません。
通知 webhook を追加する
ポーリングよりきれいなパターン: Apps Script を Web App としてデプロイし、その URL を notifyUrl として渡します。Brevo はインポート完了時にそこへ POST します。
// Add to Code.gsfunction doPost(e) { const payload = JSON.parse(e.postData.contents); Logger.log(`Brevo import ${payload.processId} finished: ${payload.status}`); // optionally: write the result back to a "Sync log" tab in the sheet return ContentService.createTextOutput('ok');}デプロイ: Deploy → New deployment → Web app、“Who has access” を Anyone に設定し、結果の URL をコピーして、インポートペイロードで notifyUrl として渡します。
payload.notifyUrl = 'https://script.google.com/macros/s/AKfy.../exec';これで Brevo は結果を Sheet 自身のスクリプトに POST し返し、外部インフラなしでループを閉じます。
トラブルシューティング
400 Bad Request で error: "Attribute X not found"。 Sheet 内の列が、Brevo が知らない属性にマップされています。Sheet 列の名前を既存の属性に合わせて変更するか、Brevo(Contacts → Settings → Contact attributes)で属性を作成してください。
401 Unauthorized。 API キーが間違っているか期限切れです。Configure API key を再実行し、Brevo ダッシュボードから新しいキーを貼り付けてください。
429 Too Many Requests。 Brevo のレートリミットに達しています。インポートエンドポイントは毎分約 30 回の呼び出しを許可します。攻撃的にバッチ化しているなら、ループ内のバッチ間に Utilities.sleep(2000) を追加してください。
スクリプトが静かにスケジュール実行されない。 Apps Script エディタの Triggers を確認してください。トリガーが繰り返し失敗すると、Google はそれを無効化します。トリガーをクリックして失敗理由を確認してください。通常は再認可できる権限の問題です。
Brevo メニューが表示されない。 onOpen は Sheet を最初から(再)開いたときにのみ実行されます。ブラウザタブを再読み込みしてください。
権限ポップアップが何度も戻ってくる。 おそらくスクリプトのスコープを編集しました(新しい Google サービスを追加した)。Apps Script は必要な権限が変わるたびに再認可を求めます。エディタから任意の関数を 1 回実行して、プロンプトをトリガーして承認してください。
なぜこれが Zapier や友人たちに勝るのか
Apps Script は無料で、Google のインフラ内に住み、Sheet のデータに直接アクセスできます。行ごとのイベント発火なし、タスクごとの料金設定なし、Google のクォータ以外のレートリミットなし(このタイプの仕事には十分すぎるくらい寛大)。裏面: 小さなコードを書いて維持することにコミットしています。連絡先同期では、それは約 100 行で、本質的に継続的な維持はゼロです。
これを毎日のトリガーと、セールスチームがすでに更新しているシートと組み合わせれば、繰り返しの作業ゼロで Brevo への連絡先パイプラインができあがります。