如何构建 Google Apps Script,把 Sheet 中的联系人导入 Brevo

自动把 Google Sheet 中的联系人推送到 Brevo。一个完整的 Apps Script,包含 API 密钥存储、按编辑触发、基于时间的触发器、自定义菜单和错误处理。无需服务器。

google apps script brevo
如何构建 Google Apps Script,把 Sheet 中的联系人导入 Brevo?

如果你的团队已经生活在 Google Sheet 里(销售线索、活动报名、合作伙伴联系人列表),把这些数据导入 Brevo 不必涉及导出 CSV 然后手动重新导入。Google Apps Script 让你把 Sheet 直接接到 Brevo 的 API。脚本运行在 Google 的基础设施内,所以没有任何东西需要托管、部署或照看。

本指南走过一个可工作的脚本:你 Sheet 中自定义的 Sync to Brevo 菜单项、自动每小时触发器、安全的 API 密钥存储、批处理,以及一点结构化日志,让你能知道发生了什么。

你需要什么

  • 一个有联系人的 Google Sheet(每联系人一行,先是表头行)
  • 一个 Brevo 账户和一个 API 密钥(Settings → SMTP & API → API Keys)
  • 你想把联系人添加到的 Brevo 列表的数字 ID

就这些。无 npm,无 Python,无服务器。

Sheet 布局

本指南中的脚本期望一个表头行,后面跟每行一个联系人。列按表头名映射到 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. 批准。脚本运行。右下角的绿色 toast 告诉你有多少个联系人发送了出去。

如果失败,点击 Extensions → Apps Script → View → Logs 查看 Brevo 返回的每批次状态码。最常见的失败是因缺少自定义属性而出现的 400,请见下面的故障排除部分。

按计划运行

在 Apps Script 编辑器中:Triggers(左侧栏的时钟图标)→ Add Trigger

  • Choose functionsyncSheetToBrevo
  • Event source:Time-driven
  • Type:Hour timer(或 Day timer 用于每天一次的同步)
  • Interval:每小时(或任何适合的)

保存。Google 会按这个频率永远运行该函数,无服务器、无 cron、无维护。

如果你希望每次单元格变更都触发同步,也可以使用 From spreadsheet → On edit。要小心,即便是装饰性编辑也会触发触发器,这在繁忙的 Sheet 上会很快耗尽 Apps Script 的每日配额。每小时时间触发器几乎总是正确答案。

需要了解的 Apps Script 配额

免费 Apps Script 层级有需要尊重的限制:

限制值(免费层)
每天总运行时间90 分钟
单次执行时间6 分钟
每天 UrlFetchApp 调用20,000
UrlFetchApp 负载大小50 MB
UrlFetchApp 头大小8 KB
每用户每脚本触发器数20

对于典型的联系人同步(几千个联系人,每小时一次),你远远没有接近任何限制。唯一需要注意的是 6 分钟单次执行:如果你曾经一次同步几十万个联系人,请把它们分成更小的块(上面的脚本已经通过 BATCH_SIZE 做到了这一点)。

异步处理导入

Brevo 的导入端点是异步的:你立即拿到 processId,实际导入在服务器端运行。对大多数 Sheet 同步来说,这没问题(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 自己的脚本,闭合循环而无需外部基础设施。

故障排除

error: "Attribute X not found"400 Bad Request:你 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 在所需权限发生变化时会再次提示授权。从编辑器运行任何函数一次以触发提示并批准。

这为什么胜过 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(),这是一个由 Google 管理、作用域为 Apps Script 项目的键值存储。不要在源代码中硬编码密钥;Sheet 的协作者会看到它。
如何让它每天自动运行?
打开 Apps Script → Triggers → Add Trigger。选择 syncSheetToBrevo 函数,选择 'Time-driven',并设置每天/每小时的频率。Google 在免费层的配额是每天 90 分钟的总 Apps Script 执行时间,对联系人同步来说绰绰有余。
有行数限制吗?
Brevo 的导入端点接受约 10 MB 的内联 JSON。这大约相当于 30,000 到 50,000 个联系人,取决于每个联系人有多少属性。Apps Script 的 UrlFetchApp 可以发送 50 MB 的负载,所以瓶颈是 Brevo,而不是 Apps Script。对于更大的任务,请把行分批。

Subscribe to updates

blog-updates

Drop your email or phone number — we'll send you what matters next.

auto-detect
获取Brevo