Cách xây dựng Google Apps Script để import danh bạ của một Sheet vào Brevo

Đẩy danh bạ từ một Google Sheet vào Brevo tự động. Một Apps Script hoàn chỉnh với việc lưu API key, chạy khi chỉnh sửa, trigger theo thời gian, menu tùy chỉnh và xử lý lỗi. Không cần server.

Featured image for article: Cách xây dựng Google Apps Script để import danh bạ của một Sheet vào Brevo

Nếu nhóm bạn đã sống trong một Google Sheet (lead bán hàng, đăng ký sự kiện, danh sách danh bạ đối tác), việc đưa dữ liệu đó vào Brevo không cần phải xuất CSV và import lại bằng tay. Google Apps Script cho phép bạn nối Sheet trực tiếp với API của Brevo. Script chạy bên trong hạ tầng của Google, nên không có gì để host, deploy hay trông nom.

Hướng dẫn này đi qua một script hoạt động được: một mục menu Sync to Brevo tùy chỉnh trong Sheet của bạn, một trigger hàng giờ tự động, lưu API key an toàn, xử lý batch và một chút logging có cấu trúc để bạn biết được điều gì đã xảy ra.

Bạn cần gì

  • Một Google Sheet có danh bạ (mỗi danh bạ một hàng, hàng header trước)
  • Một tài khoản Brevo và API key (Settings → SMTP & API → API Keys)
  • ID số của list Brevo mà bạn muốn thêm danh bạ vào

Vậy thôi. Không npm, không Python, không server.

Bố cục Sheet

Script trong hướng dẫn này mong đợi một hàng header theo sau là một danh bạ mỗi hàng. Cột được ánh xạ theo tên header tới các attribute của Brevo:

emailfirstNamelastNamecompanycity
[email protected]JaneDoeAcmeBerlin
[email protected]JohnSmithGlobexParis

email là bắt buộc và được khớp không phân biệt chữ hoa thường. Mọi thứ khác được gửi đến Brevo dưới dạng contact attribute. Custom attributes (bất cứ thứ gì ngoài các attribute chuẩn như FIRSTNAME, LASTNAME) cần tồn tại trong tài khoản Brevo của bạn trước. Hãy định nghĩa chúng trong Contacts → Settings → Contact attributes, hoặc qua API của Brevo.

Mở trình soạn thảo Apps Script

Trong Sheet của bạn: Extensions → Apps Script. Một tab mới mở với một Code.gs trống. Thay nội dung bằng script bên dưới.

Script đầy đủ

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;
}

Đó là toàn bộ. Lưu nó (⌘S / Ctrl+S), đặt tên project là gì đó như “Brevo sync”, và quay lại Sheet của bạn.

Lần chạy đầu tiên

Tải lại Sheet, menu Brevo mới xuất hiện ở trên cùng.

  1. Click Brevo → Configure API key, dán key xkeysib-... của bạn, click OK.
  2. Click Brevo → Sync sheet to Brevo. Google sẽ yêu cầu quyền lần đầu:
    • “View and manage your spreadsheets”: cần để đọc các hàng
    • “Connect to an external service”: cần để gọi api.brevo.com
  3. Phê duyệt. Script chạy. Một toast màu xanh ở góc dưới bên phải cho bạn biết bao nhiêu danh bạ đã được gửi đi.

Nếu thất bại, click Extensions → Apps Script → View → Logs để xem mã trạng thái mỗi batch từ Brevo. Lỗi phổ biến nhất là 400 do thiếu custom attribute, xem mục khắc phục sự cố bên dưới.

Chạy nó theo lịch

Trong trình soạn thảo Apps Script: Triggers (biểu tượng đồng hồ ở thanh bên trái) → Add Trigger.

  • Choose function: syncSheetToBrevo
  • Event source: Time-driven
  • Type: Hour timer (hoặc Day timer cho đồng bộ một lần một ngày)
  • Interval: mỗi giờ (hoặc theo nhu cầu của bạn)

Lưu. Google sẽ chạy function với cadence đó mãi mãi, không server, không cron, không bảo trì.

Bạn cũng có thể dùng From spreadsheet → On edit nếu bạn muốn mỗi thay đổi ô kích hoạt một đồng bộ. Hãy cẩn thận, ngay cả các chỉnh sửa thẩm mỹ cũng sẽ kích hoạt trigger, có thể đụng quota hàng ngày của Apps Script nhanh trên các sheet bận. Trigger thời gian hàng giờ hầu như luôn là câu trả lời đúng.

Các quota Apps Script cần biết

Tier free Apps Script có những giới hạn cần tôn trọng:

Giới hạnGiá trị (free tier)
Tổng runtime mỗi ngày90 phút
Thời gian thực thi đơn lẻ6 phút
Cuộc gọi UrlFetchApp mỗi ngày20.000
Kích thước payload UrlFetchApp50 MB
Kích thước header UrlFetchApp8 KB
Trigger mỗi user mỗi script20

Cho đồng bộ danh bạ điển hình (vài nghìn danh bạ, hàng giờ), bạn không gần với bất kỳ giới hạn nào. Cái duy nhất cần để ý là 6 phút thực thi đơn lẻ: nếu bạn từng đồng bộ hàng trăm nghìn danh bạ trong một lần, hãy batch chúng thành các phần nhỏ hơn (script trên đã làm điều này qua BATCH_SIZE).

Xử lý import bất đồng bộ

Endpoint import của Brevo bất đồng bộ: bạn nhận được processId ngay lập tức và import thực tế chạy ở phía server. Cho hầu hết các đồng bộ sheet, điều này ổn (fire and forget, Brevo sẽ email một tóm tắt khi mỗi batch hoàn thành).

Nếu bạn muốn block cho đến khi import thực sự xong, hãy poll endpoint trạng thái process:

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 là phiên bản tương đương của Apps Script cho việc chờ block. Đừng ngủ quá lâu, bạn có 6 phút tổng cộng cho mỗi lần thực thi.

Thêm webhook thông báo

Một mẫu sạch hơn polling: deploy Apps Script của bạn dưới dạng Web App và truyền URL của nó là notifyUrl. Brevo sẽ POST đến đó khi import hoàn thành.

// 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: Deploy → New deployment → Web app, đặt “Who has access” thành Anyone, copy URL kết quả, và truyền nó là notifyUrl trong payload import của bạn:

payload.notifyUrl = 'https://script.google.com/macros/s/AKfy.../exec';

Giờ Brevo post kết quả lại cho chính script của Sheet bạn, đóng vòng lặp mà không cần hạ tầng bên ngoài.

Khắc phục sự cố

400 Bad Request với error: "Attribute X not found": Một cột trong Sheet của bạn ánh xạ tới một attribute mà Brevo không biết. Hoặc đổi tên cột Sheet để khớp với một attribute hiện có, hoặc tạo attribute trong Brevo (Contacts → Settings → Contact attributes).

401 Unauthorized: API key sai hoặc đã hết hạn. Chạy lại Configure API key, dán một key mới từ dashboard của Brevo.

429 Too Many Requests: Bạn đang đụng giới hạn tốc độ của Brevo. Endpoint import cho phép khoảng 30 cuộc gọi mỗi phút. Nếu bạn batch quá đà, hãy thêm Utilities.sleep(2000) giữa các batch trong vòng lặp.

Script âm thầm không chạy theo lịch: Kiểm tra Triggers trong trình soạn thảo Apps Script. Nếu một trigger thất bại lặp đi lặp lại, Google sẽ tắt nó. Click vào trigger để xem lý do thất bại, thường là vấn đề quyền mà bạn có thể cấp lại.

Menu Brevo không xuất hiện: onOpen chỉ chạy khi bạn (mở lại) Sheet từ đầu. Tải lại tab trình duyệt.

Popup quyền tiếp tục quay lại: Có thể bạn đã chỉnh sửa scope của script (thêm một dịch vụ Google mới). Apps Script yêu cầu cấp quyền lại bất cứ khi nào quyền cần thiết thay đổi. Chạy bất kỳ function nào một lần từ trình soạn thảo để kích hoạt prompt và phê duyệt.

Tại sao điều này thắng Zapier và bạn bè

Apps Script miễn phí, sống trong hạ tầng của Google và có truy cập trực tiếp vào dữ liệu Sheet, không có sự kiện kích hoạt từng hàng, không có giá theo task, không có giới hạn tốc độ ngoài quota của Google (rộng rãi cho loại công việc này). Mặt trái: bạn cam kết viết và bảo trì một đoạn code nhỏ. Cho việc đồng bộ danh bạ, đó là khoảng 100 dòng và về cơ bản không có bảo trì định kỳ.

Ghép cặp điều này với một trigger hàng ngày và một sheet mà nhóm bán hàng của bạn đã cập nhật, và bạn có một pipeline danh bạ vào Brevo với công việc lặp lại bằng 0.

Đọc thêm

Frequently Asked Questions

Tôi có cần server hay hạ tầng nào để đồng bộ Google Sheet với Brevo không?
Không. Google Apps Script chạy bên trong chính Google Sheets. Bạn viết một function, lưu project và Google host và chạy nó. Script có thể gọi trực tiếp đến API của Brevo bằng UrlFetchApp.
Tôi nên lưu Brevo API key ở đâu?
Dùng PropertiesService.getScriptProperties(), một kho key/value do Google quản lý, có phạm vi cho project Apps Script. Đừng hardcode key trong source; những người cộng tác trên Sheet có thể nhìn thấy nó.
Làm thế nào để chạy điều này tự động hàng ngày?
Mở Apps Script → Triggers → Add Trigger. Chọn function syncSheetToBrevo, chọn 'Time-driven' và đặt cadence hàng ngày/hàng giờ. Quota của Google là 90 phút/ngày tổng thời gian thực thi Apps Script trên free tier, dư dả cho việc đồng bộ danh bạ.
Có giới hạn số hàng không?
Endpoint import của Brevo chấp nhận khoảng 10 MB JSON inline. Đó tương đương khoảng 30.000-50.000 danh bạ tùy thuộc vào số attribute mỗi cái có. UrlFetchApp của Apps Script có thể gửi payload 50 MB, nên nút thắt cổ chai là Brevo, không phải Apps Script. Cho các tác vụ lớn hơn, hãy batch các hàng.
Bắt đầu miễn phí với Brevo