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.
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:
| firstName | lastName | company | city | |
|---|---|---|---|---|
| [email protected] | Jane | Doe | Acme | Berlin |
| [email protected] | John | Smith | Globex | Paris |
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 đủ
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;}Đó 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.
- Click Brevo → Configure API key, dán key
xkeysib-...của bạn, click OK. - 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
- 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ạn | Giá trị (free tier) |
|---|---|
| Tổng runtime mỗi ngày | 90 phút |
| Thời gian thực thi đơn lẻ | 6 phút |
Cuộc gọi UrlFetchApp mỗi ngày | 20.000 |
Kích thước payload UrlFetchApp | 50 MB |
Kích thước header UrlFetchApp | 8 KB |
| Trigger mỗi user mỗi script | 20 |
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.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: 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.