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.

Featured image for article: Kako izgraditi Google Apps Script za uvoz kontakata iz Sheeta u Brevo

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:

emailfirstNamelastNamecompanycity
[email protected]JaneDoeAcmeBerlin
[email protected]JohnSmithGlobexParis

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

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 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.

  1. Kliknite Brevo → Configure API key, zalijepite svoj xkeysib-... ključ, kliknite OK.
  2. 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
  3. 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čenjeVrijednost (besplatna razina)
Ukupno vrijeme izvođenja dnevno90 minuta
Vrijeme jednog izvršavanja6 minuta
UrlFetchApp poziva dnevno20.000
Veličina UrlFetchApp tereta50 MB
Veličina UrlFetchApp zaglavlja8 KB
Okidača po korisniku po skripti20

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

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.

Daljnje čitanje

Frequently Asked Questions

Trebam li poslužitelj ili infrastrukturu za sinkronizaciju Google Sheeta s Brevom?
Ne. Google Apps Script radi unutar samog Google Sheetsa. Napišete funkciju, spremite projekt, a Google ga hosta i pokreće. Skripta može pogoditi Brevo API izravno koristeći UrlFetchApp.
Gdje pohranjujem Brevo API ključ?
Koristite PropertiesService.getScriptProperties(), to je Googleova upravljana pohrana ključ/vrijednost ograničena na Apps Script projekt. Nemojte tvrdo kodirati ključ u izvornom kodu, suradnici na Sheetu mogli bi ga vidjeti.
Kako ovo pokrenuti automatski svaki dan?
Otvorite Apps Script → Triggers → Add Trigger. Odaberite funkciju syncSheetToBrevo, izaberite 'Time-driven' i postavite dnevnu/satnu kadencu. Googleova kvota je 90 minuta dnevno ukupnog vremena izvođenja Apps Scripta na besplatnoj razini, više nego dovoljno za sinkronizaciju kontakata.
Postoji li ograničenje broja redaka?
Brevo endpoint za uvoz prihvaća ~10 MB inline JSON-a. To je otprilike 30.000-50.000 kontakata, ovisno o tome koliko atributa svaki ima. Apps Script UrlFetchApp može poslati 50 MB tereta, pa je usko grlo Brevo, ne Apps Script. Za veće poslove, grupirajte retke.
Započnite besplatno s Brevo