Cara Membangun Google Apps Script untuk Mengimpor Kontak Sheet ke Brevo

Dorong kontak dari Google Sheet ke Brevo secara otomatis. Apps Script lengkap dengan penyimpanan API key, jalankan saat edit, time-based trigger, menu kustom, dan penanganan error, tanpa server.

Featured image for article: Cara Membangun Google Apps Script untuk Mengimpor Kontak Sheet ke Brevo

Jika tim Anda sudah hidup di Google Sheet, lead penjualan, pendaftaran event, daftar kontak mitra, memasukkan data tersebut ke Brevo tidak perlu melibatkan ekspor CSV dan mengimpor ulang secara manual. Google Apps Script memungkinkan Anda menghubungkan Sheet langsung ke API Brevo. Script berjalan di dalam infrastruktur Google, jadi tidak ada yang perlu di-host, di-deploy, atau diawasi.

Panduan ini membahas script yang berfungsi: item menu Sync to Brevo kustom di Sheet Anda, trigger per jam otomatis, penyimpanan API key yang aman, penanganan batch, dan sedikit logging terstruktur sehingga Anda dapat mengetahui apa yang terjadi.

Yang Anda butuhkan

  • Google Sheet dengan kontak (satu baris per kontak, baris header pertama)
  • Akun Brevo dan API key (Settings -> SMTP & API -> API Keys)
  • ID numerik dari daftar Brevo tempat Anda ingin kontak ditambahkan

Itu saja. Tidak ada npm, tidak ada Python, tidak ada server.

Tata letak Sheet

Script di panduan ini mengharapkan baris header diikuti satu kontak per baris. Kolom dipetakan oleh nama header ke atribut Brevo:

emailfirstNamelastNamecompanycity
[email protected]JaneDoeAcmeBerlin
[email protected]JohnSmithGlobexParis

email wajib dan dicocokkan tidak peka huruf besar/kecil. Semua yang lain dikirim ke Brevo sebagai atribut kontak. Atribut kustom (apa pun di luar yang standar seperti FIRSTNAME, LASTNAME) harus ada di akun Brevo Anda terlebih dahulu, definisikan di bawah Contacts -> Settings -> Contact attributes, atau melalui Brevo API.

Buka editor Apps Script

Di Sheet Anda: Extensions -> Apps Script. Tab baru terbuka dengan Code.gs kosong. Ganti konten dengan script di bawah.

Script lengkap

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

Itulah seluruhnya. Simpan (⌘S / Ctrl+S), beri nama proyek seperti “Brevo sync”, dan kembali ke Sheet Anda.

Eksekusi pertama

Reload Sheet, menu Brevo baru muncul di bagian atas.

  1. Klik Brevo -> Configure API key, tempel key xkeysib-... Anda, klik OK.
  2. Klik Brevo -> Sync sheet to Brevo. Google akan meminta izin pertama kali:
    • “View and manage your spreadsheets”, diperlukan untuk membaca baris
    • “Connect to an external service”, diperlukan untuk memanggil api.brevo.com
  3. Setujui. Script berjalan. Toast hijau di kanan bawah memberi tahu Anda berapa banyak kontak yang keluar.

Jika gagal, klik Extensions -> Apps Script -> View -> Logs untuk melihat status code per batch dari Brevo. Kegagalan paling umum adalah 400 karena atribut kustom yang hilang, lihat bagian troubleshooting di bawah.

Jalankan sesuai jadwal

Di editor Apps Script: Triggers (ikon jam di sidebar kiri) -> Add Trigger.

  • Choose function: syncSheetToBrevo
  • Event source: Time-driven
  • Type: Hour timer (atau Day timer untuk sinkronisasi sekali sehari)
  • Interval: setiap jam (atau apa pun yang cocok)

Simpan. Google akan menjalankan fungsi pada cadence tersebut selamanya, tanpa server, tanpa cron, tanpa pemeliharaan.

Anda juga dapat menggunakan From spreadsheet -> On edit jika Anda ingin setiap perubahan sel memicu sinkronisasi. Berhati-hatilah dengan itu, bahkan edit kosmetik akan memicu trigger, yang dapat menyentuh quota harian Apps Script dengan cepat di sheet yang sibuk. Trigger waktu per jam hampir selalu jawaban yang tepat.

Quota Apps Script yang perlu diketahui

Tier Apps Script gratis memiliki batasan yang patut dihormati:

BatasNilai (tier gratis)
Total runtime per hari90 menit
Waktu eksekusi tunggal6 menit
Panggilan UrlFetchApp per hari20.000
Ukuran payload UrlFetchApp50 MB
Ukuran header UrlFetchApp8 KB
Trigger per pengguna per script20

Untuk sinkronisasi kontak yang khas (beberapa ribu kontak, per jam), Anda jauh dari salah satu dari ini. Satu-satunya yang harus diperhatikan adalah eksekusi tunggal 6 menit, jika Anda pernah menyinkron ratusan ribu kontak sekaligus, batch ke chunk yang lebih kecil (script di atas sudah melakukan ini melalui BATCH_SIZE).

Menangani impor secara asynchronous

Endpoint impor Brevo bersifat asynchronous: Anda mendapatkan processId kembali segera, dan impor sebenarnya berjalan di sisi server. Untuk sebagian besar sinkronisasi sheet ini baik-baik saja, tembak dan lupakan, Brevo akan mengirim email ringkasan ketika setiap batch selesai.

Jika Anda ingin memblokir hingga impor benar-benar selesai, polling endpoint status proses:

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 adalah ekuivalen Apps Script dari blocking wait. Jangan tidur terlalu lama, Anda punya total 6 menit per eksekusi.

Menambahkan webhook notifikasi

Pola yang lebih bersih daripada polling: deploy Apps Script Anda sebagai Web App dan kirim URL-nya sebagai notifyUrl. Brevo akan mem-POST ke sana ketika impor selesai.

// 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, setel “Who has access” ke Anyone, salin URL yang dihasilkan, dan kirim sebagai notifyUrl di payload impor Anda:

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

Sekarang Brevo memposting hasilnya kembali ke script Sheet Anda sendiri, menutup loop tanpa infrastruktur eksternal.

Troubleshooting

400 Bad Request dengan error: "Attribute X not found". Sebuah kolom di Sheet Anda dipetakan ke atribut yang tidak diketahui Brevo. Entah ganti nama kolom Sheet untuk mencocokkan atribut yang ada, atau buat atribut di Brevo (Contacts -> Settings -> Contact attributes).

401 Unauthorized. API key salah atau kedaluwarsa. Jalankan ulang Configure API key, tempel key baru dari dashboard Brevo.

429 Too Many Requests. Anda mencapai rate limit Brevo. Endpoint impor mengizinkan sekitar 30 panggilan per menit. Jika Anda mem-batch secara agresif, tambahkan Utilities.sleep(2000) antara batch dalam loop.

Script diam-diam tidak berjalan sesuai jadwal. Periksa Triggers di editor Apps Script. Jika trigger gagal berulang kali, Google menonaktifkannya. Klik ke trigger untuk melihat alasan kegagalan, biasanya masalah izin yang dapat Anda otorisasi ulang.

Menu Brevo tidak muncul. onOpen hanya berjalan ketika Anda (membuka kembali) Sheet dari nol. Reload tab browser.

Popup izin terus muncul. Anda mungkin mengedit scope script (menambah layanan Google baru). Apps Script akan meminta otorisasi ulang setiap kali izin yang diperlukan berubah. Jalankan fungsi apa pun sekali dari editor untuk memicu prompt dan menyetujuinya.

Mengapa ini lebih baik dari Zapier dan teman-temannya

Apps Script gratis, hidup di dalam infra Google, dan memiliki akses langsung ke data Sheet, tidak ada penembakan event per baris, tidak ada harga per task, tidak ada rate limit selain quota Google (yang dermawan untuk pekerjaan semacam ini). Sisi sebaliknya: Anda berkomitmen untuk menulis dan memelihara sedikit kode. Untuk sinkronisasi kontak, itu sekitar 100 baris dan pada dasarnya nol pemeliharaan berkelanjutan.

Pasangkan ini dengan trigger harian dan sheet yang sudah diperbarui tim penjualan Anda, dan Anda mendapatkan pipeline kontak ke Brevo dengan nol pekerjaan berulang.

Bacaan lanjutan

Frequently Asked Questions

Apakah saya memerlukan server atau infrastruktur untuk menyinkron Google Sheet ke Brevo?
Tidak. Google Apps Script berjalan di dalam Google Sheets sendiri. Anda menulis fungsi, menyimpan proyek, dan Google meng-host serta menjalankannya. Script dapat memanggil API Brevo secara langsung menggunakan UrlFetchApp.
Di mana saya menyimpan API key Brevo?
Gunakan PropertiesService.getScriptProperties(), itu adalah penyimpanan key/value yang dikelola Google yang dilingkupi ke proyek Apps Script. Jangan hardcode key di source, kolaborator di Sheet akan dapat melihatnya.
Bagaimana cara menjalankan ini secara otomatis setiap hari?
Buka Apps Script -> Triggers -> Add Trigger. Pilih fungsi syncSheetToBrevo, pilih 'Time-driven', dan setel cadence harian/per jam. Quota Google adalah 90 menit/hari total waktu eksekusi Apps Script di tier gratis, lebih dari cukup untuk sinkronisasi kontak.
Apakah ada batas baris?
Endpoint impor Brevo menerima sekitar 10 MB JSON inline. Itu sekitar 30.000-50.000 kontak tergantung berapa banyak atribut yang dimiliki masing-masing. UrlFetchApp Apps Script dapat mengirim payload 50 MB, jadi bottleneck-nya adalah Brevo, bukan Apps Script. Untuk pekerjaan yang lebih besar, batch baris-barisnya.
Brevo के साथ मुफ्त में शुरू करें