Xuất mười hai lượt đăng ký sự kiện một lần khá dễ. Lần nhập thứ hai có thể có email trùng và một người đã hủy đăng ký trong Brevo. Quy trình chạy lặp lại phải nhận ra cả hai.
Script dùng các endpoint đọc, tạo và cập nhật liên hệ Brevo. Nó dành cho lần nhập nhỏ theo lịch, không phải công cụ nhập hàng loạt hay connector Google Sheets tích hợp. Nó không gửi email. Bạn chịu trách nhiệm về bằng chứng đồng ý và vận hành script.
Chuẩn bị Sheet và danh sách
Tạo tab Contacts với đúng các cột sau. Dùng ô chọn cho consent, chỉ đặt TRUE khi có quyền cho mục đích tiếp thị này.
| firstName | lastName | consent | |
|---|---|---|---|
| [email protected] | Jane | Doe | TRUE |
| [email protected] | Alex | Lee | FALSE |
Tạo hoặc chọn danh sách Brevo rồi nhập ID số vào LIST_ID. Kiểm tra thuộc tính FNAME và LNAME đã tồn tại; script chỉ ánh xạ hai trường này. Nếu Sheet không chứng minh được thời điểm và cách người dùng đồng ý, hãy giữ hồ sơ nguồn bên ngoài. Ô TRUE không tự là bằng chứng.
Trong Extensions → Apps Script → Project Settings → Script Properties, thêm BREVO_API_KEY. Người dùng script cùng thấy thuộc tính này, nên giới hạn quyền sửa. Không để khóa trong ô hay mã nguồn. Xem quy tắc truy cập của Google.
Script
Thay nội dung Code.gs bằng ví dụ sau. Đặt LIST_ID trước khi chạy lần đầu.
const LIST_ID = 42; // Replace with your existing Brevo list ID.const TAB = 'Contacts';const BASE = 'https://api.brevo.com/v3';
function syncSheetToBrevo() { const key = PropertiesService.getScriptProperties().getProperty('BREVO_API_KEY'); if (!key) throw new Error('Set BREVO_API_KEY in Script Properties first'); const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(TAB); if (!sheet) throw new Error('Contacts tab missing'); const values = sheet.getDataRange().getValues(); const headers = values.shift().map(h => String(h).trim()); const columns = ['email', 'firstName', 'lastName', 'consent'] .map(name => headers.indexOf(name)); if (columns.includes(-1)) throw new Error('Required header missing'); const [emailCol, firstCol, lastCol, consentCol] = columns; const seen = new Set(); const result = { written: 0, skipped: 0 };
for (const row of values) { const email = String(row[emailCol] || '').trim().toLowerCase(); if (row[consentCol] !== true || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || seen.has(email)) { result.skipped++; continue; } seen.add(email); const path = '/contacts/' + encodeURIComponent(email); const current = brevo_(key, path, 'get'); if (current.code !== 200 && current.code !== 404) { throw new Error(`Contact read failed (${current.code}) for row ${result.written + result.skipped + 2}`); } if (current.code === 200) { const contact = JSON.parse(current.text); if (contact.emailBlacklisted || (contact.listUnsubscribed || []).includes(LIST_ID)) { result.skipped++; continue; } } const attributes = {}; if (row[firstCol] !== '') attributes.FNAME = String(row[firstCol]); if (row[lastCol] !== '') attributes.LNAME = String(row[lastCol]); const body = { attributes, listIds: [LIST_ID] }; const saved = current.code === 404 ? brevo_(key, '/contacts', 'post', { email, ...body }) : brevo_(key, path, 'put', body); if (![201, 204].includes(saved.code)) { throw new Error(`Contact write failed (${saved.code}) for ${email}: ${saved.text.slice(0, 200)}`); } result.written++; } Logger.log(JSON.stringify(result)); return result;}
function brevo_(key, path, method, body) { for (let attempt = 0; attempt < 3; attempt++) { const response = UrlFetchApp.fetch(BASE + path, { method, contentType: 'application/json', headers: { 'api-key': key }, payload: body ? JSON.stringify(body) : undefined, muteHttpExceptions: true, }); const code = response.getResponseCode(); if (code !== 429) return { code, text: response.getContentText() }; if (attempt === 2) return { code, text: response.getContentText() }; const reset = Object.entries(response.getHeaders()).find( ([name]) => name.toLowerCase() === 'x-sib-ratelimit-reset' ); const raw = Number(reset && reset[1]); const seconds = Number.isFinite(raw) && raw > 0 ? raw : 2 ** attempt; const waitSeconds = seconds + 2 ** attempt; if (waitSeconds > 60) return { code, text: response.getContentText() }; Utilities.sleep(waitSeconds * 1000); }}Lưu, chọn syncSheetToBrevo và nhấn Run. Google yêu cầu quyền dùng Sheets và gọi ra ngoài. Xem log {written, skipped} trong Executions. Đọc trả 404 sẽ tạo, 200 sẽ cập nhật; lỗi khác dừng xử lý. Ghi liên hệ thành công không có nghĩa đã gửi chiến dịch.
Script loại email trùng trong một lần chạy và hỏi Brevo lại ở lần tiếp theo. Nó không đặt emailBlacklisted thành false và không đổi địa chỉ. Nếu Brevo báo chặn toàn cục hoặc hủy đăng ký trên danh sách đích, hàng vẫn bị bỏ qua dù Sheet là TRUE. Đừng dùng script để đảo ngược việc hủy. Brevo cảnh báo rằng đổi email của liên hệ bị chặn có thể gỡ chặn.
Kiểm tra một lần chạy trước khi lên lịch
Dùng danh sách thử và liên hệ của bạn. Chạy với một hàng TRUE, một FALSE và một email trùng: chờ một lần ghi, hai lần bỏ qua. Hủy đăng ký liên hệ thử trong Brevo rồi chạy lại: không được ghi cho người đó. Để thử lỗi, tạm đặt LIST_ID sai, xác nhận lỗi HTTP trong Executions, khôi phục ID rồi chạy lại. Đừng thử với dữ liệu khách hàng thật. Mã đã được thử cục bộ bằng phản hồi Brevo giả lập, không ghi trực tiếp.
Trong Apps Script → Triggers → Add Trigger, chọn syncSheetToBrevo và khoảng thời gian. Trigger chạy bằng tài khoản người tạo, tài khoản đó phải giữ quyền với Sheet. Với Sheet nhỏ, bắt đầu một lần mỗi ngày và theo dõi lỗi. Google giới hạn thời gian chạy và URL Fetch; chia nguồn lớn thành công việc có giới hạn.
Với 429, script đọc x-sib-ratelimit-reset và thử lại tối đa hai lần. Nếu phải đợi hơn 60 giây, nó trả 429 để lần chạy sau xử lý, không thử sớm. Lỗi khác cũng dừng chạy; lần sau bắt đầu từ đầu. Liên hệ có thể hủy đăng ký giữa lúc đọc và ghi. Chỉ dùng ví dụ nếu khoảng thời gian ngắn đó chấp nhận được. Yêu cầu nghiêm ngặt cần đối soát sự kiện, nguồn đồng ý đáng tin và kiểm tra cuối trước khi gửi.
Với Sheet lớn, endpoint nhập bất đồng bộ của Brevo trả processId. Nó chỉ xác nhận công việc được nhận, chưa hoàn tất. Kiểm tra kết quả trước khi báo thành công. Cách ghi từng liên hệ ở đây giúp xác định hàng lỗi.
Câu hỏi Thường gặp
Có cần connector Google Sheets tích hợp không?
Không. Ví dụ chạy trong dự án Apps Script gắn với Sheet và gọi API liên hệ Brevo. Bạn duy trì script, khóa API và trigger định kỳ.
Hàng nào được gửi tới Brevo?
Chỉ hàng có consent TRUE và email hợp lệ. Script loại trùng trong mỗi lần chạy và kiểm tra Brevo trước khi ghi.
Chạy lại có đăng ký lại người đã hủy không?
Script bỏ qua liên hệ Brevo đánh dấu emailBlacklisted hoặc đã hủy trên danh sách đích. Nó không gửi emailBlacklisted: false hay đổi email. Trước khi dùng thật, hãy xem thêm nguồn đồng ý và chặn khác.
Làm sao chạy lại khi thất bại?
Xem lỗi trong Apps Script Executions, sửa nguyên nhân rồi chạy lại cùng hàm. Cùng email và ID danh sách giúp hàng đã thành công được cập nhật, không tạo liên hệ thứ hai.