Kako napraviti Google Apps Script za uvoz kontakata iz lista u Brevo

Šaljite kontakte iz Google Sheet u Brevo automatski. Kompletan Apps Script sa skladištenjem API ključa, izvršavanjem pri izmeni, vremenskim okidačima, prilagođenim menijem i obradom grešaka. Bez servera.

Featured image for article: Kako napraviti Google Apps Script za uvoz kontakata iz lista u Brevo

Ako vaš tim već živi u Google Sheet (prodajni leadovi, prijave za događaje, partnerska lista kontakata), dobijanje tih podataka u Brevo ne mora da uključuje izvoženje CSV-ova i ručno ponovno uvoženje. Google Apps Script vam omogućava da povežete list direktno na Brevo API. Skripta se izvršava unutar Google infrastrukture, pa nema šta da hostujete, deploj-ujete ili da se brinete oko toga.

Ovaj vodič prolazi kroz funkcionalnu skriptu: prilagođenu stavku menija Sync to Brevo u vašem listu, automatski satni okidač, sigurno skladištenje API ključa, obradu paketa i mali deo strukturisanog beleženja da možete reći šta se desilo.

Šta vam treba

  • Google Sheet sa kontaktima (jedan red po kontaktu, prvo red zaglavlja)
  • Brevo nalog i API ključ (Settings → SMTP & API → API Keys)
  • Numerički ID Brevo liste na koju želite da dodate kontakte

To je sve. Bez npm, bez Pythona, bez servera.

Raspored lista

Skripta u ovom vodiču očekuje red zaglavlja praćen jednim kontaktom po redu. Kolone se mapiraju preko imena zaglavlja na Brevo atribute:

emailfirstNamelastNamecompanycity
[email protected]JaneDoeAcmeBerlin
[email protected]JohnSmithGlobexParis

email je obavezan i poklapa se nezavisno od veličine slova. Sve ostalo se šalje u Brevo kao atribut kontakta. Prilagođeni atributi (sve van standardnih kao što su FIRSTNAME, LASTNAME) moraju prvo postojati u vašem Brevo nalogu. Definišite ih pod Contacts → Settings → Contact attributes, ili preko Brevo API.

Otvorite Apps Script editor

U vašem listu: Extensions → Apps Script. Otvara se nov tab sa praznim Code.gs. Zamenite sadržaj donjom skriptom.

Kompletna skripta

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

To je cela stvar. Sačuvajte je (⌘S / Ctrl+S), nazovite projekat nešto kao „Brevo sync” i vratite se u svoj list.

Prvo izvršavanje

Ponovo učitajte list. Na vrhu se pojavljuje novi meni Brevo.

  1. Kliknite na Brevo → Configure API key, nalepite svoj xkeysib-... ključ, kliknite OK.
  2. Kliknite na Brevo → Sync sheet to Brevo. Google će vas prvi put pitati za dozvole:
    • „View and manage your spreadsheets”, potrebno za čitanje redova
    • „Connect to an external service”, potrebno za poziv api.brevo.com
  3. Odobrite. Skripta se izvršava. Zeleno obaveštenje u donjem desnom uglu vam kaže koliko je kontakata otišlo.

Ako ne uspe, kliknite Extensions → Apps Script → View → Logs da vidite status kod po paketu od Brevo. Najčešći neuspeh je 400 zbog nedostajućeg prilagođenog atributa, pogledajte odeljak za rešavanje problema ispod.

Pokrenite po rasporedu

U Apps Script editoru: Triggers (ikona sata u levoj bočnoj traci) → Add Trigger.

  • Choose function: syncSheetToBrevo
  • Event source: Time-driven
  • Type: Hour timer (ili Day timer za sinhronizaciju jednom dnevno)
  • Interval: svakog sata (ili šta god vam odgovara)

Sačuvajte. Google će izvršavati funkciju u toj kadenci zauvek, bez servera, bez crona, bez održavanja.

Možete koristiti i From spreadsheet → On edit ako želite da svaka promena ćelije pokrene sinhronizaciju. Budite oprezni s tim. Čak i kozmetičke izmene će aktivirati okidač, što može brzo dostići dnevnu kvotu Apps Script na zauzetim listovima. Satni vremenski okidač je skoro uvek pravi odgovor.

Apps Script kvote koje treba znati

Besplatni nivo Apps Script ima ograničenja vredna poštovanja:

OgraničenjeVrednost (besplatni nivo)
Ukupno vreme izvršavanja dnevno90 minuta
Vreme jednog izvršavanja6 minuta
Pozivi UrlFetchApp dnevno20.000
Veličina tovara UrlFetchApp50 MB
Veličina zaglavlja UrlFetchApp8 KB
Okidači po korisniku po skripti20

Za tipičnu sinhronizaciju kontakata (par hiljada kontakata, satno) niste blizu nijednom od ovih. Jedino što treba pratiti je 6-minutno jedno izvršavanje. Ako ikada sinhronizujete stotine hiljada kontakata u jednom mahu, podelite ih na manje delove (gornja skripta to već radi preko BATCH_SIZE).

Asinhrono rukovanje uvozom

Brevo endpoint za uvoz je asinhron: odmah dobijate processId, a sam uvoz se izvršava na strani servera. Za većinu sinhronizacija lista to je u redu, opali i zaboravi, Brevo će poslati rezime imejlom kada svaki paket završi.

Ako želite da blokirate dok uvoz stvarno ne završi, proveravajte endpoint statusa procesa:

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. Ne spavajte predugo. Imate ukupno 6 minuta po izvršavanju.

Dodavanje notify webhooka

Čistiji obrazac od proveravanja: deplojujte vašu Apps Script kao Web App i prosledite njen URL kao notifyUrl. Brevo će poslati POST na njega kada se uvoz završi.

// 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');
}

Deploj-ujte: Deploy → New deployment → Web app, postavite „Who has access” na Anyone, kopirajte rezultatski URL i prosledite ga kao notifyUrl u vašem tovaru za uvoz:

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

Sada Brevo šalje rezultat nazad na sopstvenu skriptu vašeg lista, zatvarajući petlju bez spoljne infrastrukture.

Rešavanje problema

400 Bad Request sa error: "Attribute X not found". Kolona u vašem listu se mapira na atribut koji Brevo ne poznaje. Bilo preimenujte kolonu lista da odgovara postojećem atributu, bilo kreirajte atribut u Brevo (Contacts → Settings → Contact attributes).

401 Unauthorized. API ključ je pogrešan ili istekao. Ponovo pokrenite Configure API key, nalepite svež ključ sa Brevo dashboarda.

429 Too Many Requests. Pogađate Brevo rate limit. Endpoint za uvoz dozvoljava oko 30 poziva u minuti. Ako agresivno pakirate, dodajte Utilities.sleep(2000) između paketa u petlji.

Skripta se tiho ne izvršava po rasporedu. Proverite Triggers u Apps Script editoru. Ako okidač ponavljano ne uspeva, Google ga onemogućava. Kliknite na okidač da vidite razlog neuspeha, obično je to problem sa dozvolama koji možete ponovo autorizovati.

Meni Brevo se nije pojavio. onOpen se pokreće samo kada (ponovo) otvorite list iz početka. Ponovo učitajte tab pretraživača.

Pop-up za dozvole se vraća. Verovatno ste izmenili scope-ove skripte (dodali novu Google uslugu). Apps Script ponovo traži autorizaciju svaki put kada se potrebne dozvole promene. Pokrenite bilo koju funkciju jednom iz editora da pokrenete prompt i odobrite.

Zašto ovo pobeđuje Zapier i prijatelje

Apps Script je besplatan, živi unutar Google infrastrukture i ima direktan pristup podacima lista. Bez okidanja događaja red po red, bez cena po zadatku, bez rate limita osim Google kvote (koja je darežljiva za ovu vrstu posla). Druga strana medalje: obavezujete se da napišete i održavate mali deo koda. Za sinhronizaciju kontakata, to je oko 100 linija i u suštini nula tekućeg održavanja.

Uparite ovo sa dnevnim okidačem i listom koji vaš prodajni tim već ažurira, i imate cevovod kontakata u Brevo sa nula ponavljajućeg posla.

Dalje čitanje

Frequently Asked Questions

Da li mi treba server ili bilo kakva infrastruktura za sinhronizaciju Google Sheet sa Brevo?
Ne. Google Apps Script se izvršava unutar samog Google Sheets. Napišete funkciju, sačuvate projekat i Google ga hostuje i izvršava. Skripta može direktno da pogodi Brevo API koristeći UrlFetchApp.
Gde da sačuvam Brevo API ključ?
Koristite PropertiesService.getScriptProperties(). To je skladište ključ/vrednost koje upravlja Google, ograničeno na projekat Apps Script. Nemojte hardkodovati ključ u izvor. Saradnici na listu bi mogli da ga vide.
Kako ovo da pokrećem automatski svaki dan?
Otvorite Apps Script → Triggers → Add Trigger. Izaberite funkciju syncSheetToBrevo, izaberite 'Time-driven' i postavite dnevnu ili satnu kadencu. Google kvota je 90 minuta dnevno ukupnog vremena izvršavanja Apps Script na besplatnom nivou, više nego dovoljno za sinhronizaciju kontakata.
Postoji li ograničenje broja redova?
Brevo endpoint za uvoz prihvata oko 10 MB inline JSON. To je grubo 30.000 do 50.000 kontakata u zavisnosti od broja atributa svakog. UrlFetchApp u Apps Script može poslati 50 MB tovar, pa je usko grlo Brevo, ne Apps Script. Za veće poslove, podelite redove na pakete.
Započnite besplatno sa Brevo