シートの連絡先をBrevoにインポートするGoogle Apps Scriptを構築する方法

Google SheetからBrevoに自動的に連絡先をプッシュします。APIキーストレージ、run-on-edit、時間ベースのトリガー、カスタムメニュー、エラー処理を備えた完全なApps Script。サーバー不要です。

Featured image for article: シートの連絡先をBrevoにインポートするGoogle 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 属性にマップされます。

emailfirstNamelastNamecompanycity
[email protected]JaneDoeAcmeBerlin
[email protected]JohnSmithGlobexParis

email は必須で、大文字小文字を区別せずにマッチングされます。それ以外のすべては Brevo に連絡先属性として送信されます。カスタム属性(FIRSTNAMELASTNAME のような標準のもの以外)は、まず Brevo アカウントに存在する必要があります。Contacts → Settings → Contact attributes で、または Brevo API 経由で定義してください。

Apps Script エディタを開く

Sheet で Extensions → Apps Script。空の Code.gs を持つ新しいタブが開きます。内容を以下のスクリプトで置き換えます。

完全なスクリプト

Code.gs
const BREVO_API_BASE = 'https://api.brevo.com/v3';
const BREVO_LIST_ID = 42; // <- the Brevo list to import contacts into
const SHEET_NAME = 'Contacts'; // <- name of the sheet tab to read
const 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 メニューが上部に表示されます。

  1. Brevo → Configure API key をクリックし、xkeysib-... キーを貼り付け、OK をクリック。
  2. Brevo → Sync sheet to Brevo をクリック。Google が初回に権限を要求します:
    • “View and manage your spreadsheets”、行を読み取るために必要
    • “Connect to an external service”、api.brevo.com を呼び出すために必要
  3. 承認します。スクリプトが実行されます。右下の緑のトーストが、何件の連絡先が送信されたかを伝えます。

失敗した場合は、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.gs
function 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 Requesterror: "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 への連絡先パイプラインができあがります。

さらに読む

Frequently Asked Questions

Google SheetをBrevoに同期するためにサーバーやインフラが必要ですか?
いいえ。Google Apps ScriptはGoogle Sheets自体の内部で動作します。関数を書き、プロジェクトを保存すれば、Googleがホストして実行します。スクリプトはUrlFetchAppを使ってBrevo APIに直接アクセスできます。
Brevo APIキーはどこに保存しますか?
PropertiesService.getScriptProperties()を使ってください。これはApps ScriptプロジェクトにスコープされたGoogle管理のキー/バリューストアです。ソースコードにキーをハードコードしないでください。シートのコラボレーターが見ることができてしまいます。
毎日自動的に実行するにはどうしますか?
Apps Scriptを開く → Triggers → Add Trigger。syncSheetToBrevo関数を選び、'Time-driven'を選択して、日次/時間ごとのケイデンスを設定します。Googleのクォータは無料ティアで合計のApps Script実行時間が1日90分です。連絡先同期には十分です。
行数の制限はありますか?
BrevoのインポートエンドポイントはインラインJSONを約10MB受け付けます。これは属性数によって約30,000〜50,000の連絡先に相当します。Apps ScriptのUrlFetchAppは50MBのペイロードを送れるため、ボトルネックはApps ScriptではなくBrevoです。より大きなジョブには行をバッチ化してください。
Brevoで無料で始める