Kako izgraditi Google Apps Script za uvoz kontakata iz Sheeta u Brevo
Automatski šaljite kontakte iz Google Sheeta u Brevo. Kompletan Apps Script s pohranom API ključa, pokretanjem na uređivanje, vremenskim okidačima, prilagođenim izbornikom i obradom pogrešaka, bez potrebe za poslužiteljem.
Ako vaš tim već živi u Google Sheetu (prodajni leadovi, prijave na događaje, popis partnerskih kontakata), prebacivanje tih podataka u Brevo ne mora uključivati ručni izvoz CSV-ova i ponovni uvoz. Google Apps Script omogućuje vam povezivanje Sheeta izravno s Brevo API-jem. Skripta radi unutar Googleove infrastrukture, pa nema ničega za hostanje, postavljanje ili dadiljanje.
Ovaj vodič vodi vas kroz radnu skriptu: prilagođenu stavku izbornika Sync to Brevo u vašem Sheetu, automatski satni okidač, sigurnu pohranu API ključa, obradu grupa i malo strukturiranog zapisivanja kako biste mogli reći što se dogodilo.
Što vam treba
- Google Sheet s kontaktima (jedan red po kontaktu, prvo redak zaglavlja)
- Brevo račun i API ključ (Postavke → SMTP & API → API Keys)
- Numerički ID Brevo popisa kojem se kontakti dodaju
To je to. Bez npm-a, bez Pythona, bez poslužitelja.
Raspored Sheeta
Skripta u ovom vodiču očekuje redak zaglavlja praćen jednim kontaktom po retku. Stupci se mapiraju po imenu zaglavlja na Brevo atribute:
| firstName | lastName | company | city | |
|---|---|---|---|---|
| [email protected] | Jane | Doe | Acme | Berlin |
| [email protected] | John | Smith | Globex | Paris |
email je obavezan i podudara se neovisno o velikim i malim slovima. Sve ostalo se šalje u Brevo kao atribut kontakta. Prilagođeni atributi (sve izvan standardnih poput FIRSTNAME, LASTNAME) moraju prvo postojati u vašem Brevo računu, definirajte ih pod Kontakti → Postavke → Atributi kontakta ili putem Brevo API-ja.
Otvorite Apps Script editor
U svom Sheetu: Extensions → Apps Script. Otvara se nova kartica s praznim Code.gs. Zamijenite sadržaj donjom skriptom.
Cijela skripta
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;}To je to. Spremite je (⌘S / Ctrl+S), nazovite projekt nešto poput “Brevo sync” i vratite se na svoj Sheet.
Prvo pokretanje
Ponovno učitajte Sheet, novi izbornik Brevo pojavljuje se na vrhu.
- Kliknite Brevo → Configure API key, zalijepite svoj
xkeysib-...ključ, kliknite OK. - Kliknite Brevo → Sync sheet to Brevo. Google će prvi put zatražiti dozvole:
- “View and manage your spreadsheets”, potrebno za čitanje redaka
- “Connect to an external service”, potrebno za poziv api.brevo.com
- Odobrite. Skripta se izvršava. Zelena obavijest dolje desno govori vam koliko je kontakata otišlo.
Ako ne uspije, kliknite Extensions → Apps Script → View → Logs kako biste vidjeli statusni kod po grupi od Brevoa. Najčešći neuspjeh je 400 zbog nedostajućeg prilagođenog atributa, vidi odjeljak za rješavanje problema u nastavku.
Pokrenite po rasporedu
U Apps Script editoru: Triggers (ikona sata u lijevoj bočnoj traci) → Add Trigger.
- Choose function:
syncSheetToBrevo - Event source: Time-driven
- Type: Hour timer (ili Day timer za jednokratnu dnevnu sinkronizaciju)
- Interval: svaki sat (ili što vam odgovara)
Spremite. Google će izvršavati funkciju u toj kadenci zauvijek, bez poslužitelja, bez crona, bez održavanja.
Možete također koristiti From spreadsheet → On edit ako želite da svaka promjena ćelije pokrene sinkronizaciju. Budite oprezni s tim, čak i kozmetičke promjene aktivirat će okidač, što može brzo iscrpiti dnevnu kvotu Apps Scripta na prometnim listovima. Satni vremenski okidač je gotovo uvijek pravi odgovor.
Apps Script kvote koje treba znati
Besplatna razina Apps Scripta ima ograničenja koja vrijedi poštovati:
| Ograničenje | Vrijednost (besplatna razina) |
|---|---|
| Ukupno vrijeme izvođenja dnevno | 90 minuta |
| Vrijeme jednog izvršavanja | 6 minuta |
UrlFetchApp poziva dnevno | 20.000 |
Veličina UrlFetchApp tereta | 50 MB |
Veličina UrlFetchApp zaglavlja | 8 KB |
| Okidača po korisniku po skripti | 20 |
Za tipičnu sinkronizaciju kontakata (nekoliko tisuća kontakata, na sat), niste blizu nijednog od njih. Jedini koji treba pratiti je 6 minuta jednog izvršavanja, ako ikad sinkronizirate stotine tisuća kontakata odjednom, grupirajte ih u manje dijelove (gornja skripta to već radi preko BATCH_SIZE).
Asinkrona obrada uvoza
Brevo endpoint za uvoz je asinkron: odmah dobivate processId, a stvarni uvoz radi se na strani poslužitelja. Za većinu sinkronizacija lista to je u redu, “ispali i zaboravi”, Brevo će poslati e-mail sažetak kad svaka grupa završi.
Ako želite blokirati dok uvoz stvarno ne završi, ispitujte status procesa endpoint:
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 je Apps Script ekvivalent blokirajućeg čekanja. Nemojte spavati predugo, imate 6 minuta ukupno po izvršavanju.
Dodavanje notify webhooka
Čistiji obrazac od ispitivanja: postavite svoj Apps Script kao Web App i proslijedite njegov URL kao notifyUrl. Brevo će poslati POST na njega kad uvoz završi.
// 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');}Postavite: Deploy → New deployment → Web app, postavite “Who has access” na Anyone, kopirajte rezultirajući URL i proslijedite ga kao notifyUrl u svom uvoznom teretu:
payload.notifyUrl = 'https://script.google.com/macros/s/AKfy.../exec';Sada Brevo šalje rezultat natrag u skriptu vašeg Sheeta, zatvarajući petlju bez vanjske infrastrukture.
Rješavanje problema
400 Bad Request s error: "Attribute X not found", stupac u vašem Sheetu mapira se na atribut koji Brevo ne poznaje. Ili preimenujte stupac Sheeta da odgovara postojećem atributu, ili stvorite atribut u Brevu (Kontakti → Postavke → Atributi kontakta).
401 Unauthorized, API ključ je pogrešan ili istekao. Ponovno pokrenite Configure API key, zalijepite svjež ključ s Brevo nadzorne ploče.
429 Too Many Requests, pogađate Brevo ograničenje stope. Endpoint za uvoz dopušta oko 30 poziva u minuti. Ako agresivno grupirate, dodajte Utilities.sleep(2000) između grupa u petlji.
Skripta se tiho ne pokreće po rasporedu, provjerite Triggers u Apps Script editoru. Ako okidač ponavljano ne uspijeva, Google ga onemogućuje. Kliknite na okidač da vidite razlog neuspjeha, obično problem s dozvolama koji možete ponovno autorizirati.
Izbornik Brevo se nije pojavio, onOpen se izvršava samo kada (ponovno) otvorite Sheet ispočetka. Ponovno učitajte karticu preglednika.
Skočni prozor za dozvole stalno se vraća, vjerojatno ste uredili opsege skripte (dodali novu Google uslugu). Apps Script ponovno traži autorizaciju svaki put kad se potrebne dozvole promijene. Pokrenite bilo koju funkciju jednom iz editora kako biste pokrenuli prozor i odobrili.
Zašto je ovo bolje od Zapiera i sličnih
Apps Script je besplatan, živi unutar Googleove infrastrukture i ima izravan pristup podacima Sheeta, bez događaja po retku, bez cijene po zadatku, bez ograničenja stope osim Googleove kvote (koja je velikodušna za ovu vrstu posla). Druga strana medalje: obvezujete se na pisanje i održavanje malog komada koda. Za sinkronizaciju kontakata to je oko 100 linija i u biti nula tekućeg održavanja.
Uparite ovo s dnevnim okidačem i listom koju vaš prodajni tim već ažurira, i imate cjevovod kontakata u Brevo s nula ponavljajućeg posla.