Cum să construiești un Google Apps Script pentru a importa contactele unei foi în Brevo

Trimite automat contacte dintr-un Google Sheet în Brevo. Un Apps Script complet cu stocare a cheii API, rulare la editare, declanșatoare bazate pe timp, meniu personalizat și gestionarea erorilor, fără a fi nevoie de un server.

Featured image for article: Cum să construiești un Google Apps Script pentru a importa contactele unei foi în Brevo

Dacă echipa ta deja trăiește într-un Google Sheet (lead-uri de vânzări, înscrieri la evenimente, o listă de contacte de partener), aducerea acelor date în Brevo nu trebuie să implice exportarea de CSV-uri și re-importarea lor manuală. Google Apps Script îți permite să conectezi Sheet-ul direct la API-ul Brevo. Scriptul rulează în interiorul infrastructurii Google, deci nu există nimic de găzduit, de implementat sau de îngrijit.

Acest ghid trece printr-un script funcțional: un element de meniu personalizat Sync to Brevo în Sheet-ul tău, un declanșator orar automat, stocare sigură a cheii API, gestionare în loturi și un pic de logging structurat astfel încât să poți spune ce s-a întâmplat.

De ce ai nevoie

  • Un Google Sheet cu contacte (un rând pe contact, primul rând antet)
  • Un cont Brevo și o cheie API (Setări → SMTP & API → API Keys)
  • ID-ul numeric al listei Brevo la care vrei să fie adăugate contactele

Asta e tot. Fără npm, fără Python, fără server.

Aspectul Sheet-ului

Scriptul din acest ghid așteaptă un rând antet urmat de un contact pe rând. Coloanele sunt mapate după numele antetului la atributele Brevo:

emailfirstNamelastNamecompanycity
[email protected]JaneDoeAcmeBerlin
[email protected]JohnSmithGlobexParis

email este obligatoriu și este potrivit fără sensibilitate la majuscule. Tot restul este trimis la Brevo ca atribut de contact. Atributele personalizate (orice dincolo de cele standard precum FIRSTNAME, LASTNAME) trebuie să existe mai întâi în contul tău Brevo, definește-le sub Contacte → Setări → Atribute de contact sau prin Brevo API.

Deschide editorul Apps Script

În Sheet-ul tău: Extensions → Apps Script. Se deschide o filă nouă cu un Code.gs gol. Înlocuiește conținutul cu scriptul de mai jos.

Scriptul complet

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

Asta e tot. Salvează-l (⌘S / Ctrl+S), denumește proiectul ceva de genul “Brevo sync” și întoarce-te la Sheet-ul tău.

Prima rulare

Reîncarcă Sheet-ul, noul meniu Brevo apare în partea de sus.

  1. Apasă Brevo → Configure API key, lipește cheia ta xkeysib-..., apasă OK.
  2. Apasă Brevo → Sync sheet to Brevo. Google va cere permisiuni prima dată:
    • “View and manage your spreadsheets”, necesar pentru a citi rândurile
    • “Connect to an external service”, necesar pentru a apela api.brevo.com
  3. Aprobă. Scriptul rulează. Un toast verde în colțul din dreapta jos îți spune câte contacte au fost trimise.

Dacă eșuează, apasă Extensions → Apps Script → View → Logs pentru a vedea codul de stare per-lot de la Brevo. Cel mai comun eșec este un 400 din cauza unui atribut personalizat lipsă, vezi secțiunea de depanare mai jos.

Rulează-l pe un program

În editorul Apps Script: Triggers (pictograma ceasului din bara laterală stângă) → Add Trigger.

  • Choose function: syncSheetToBrevo
  • Event source: Time-driven
  • Type: Hour timer (sau Day timer pentru o sincronizare o dată pe zi)
  • Interval: în fiecare oră (sau orice se potrivește)

Salvează. Google va rula funcția la acea cadență pentru totdeauna, fără server, fără cron, fără întreținere.

Poți folosi și From spreadsheet → On edit dacă vrei ca fiecare modificare a celulei să declanșeze o sincronizare. Fii atent cu asta, chiar și editările cosmetice vor declanșa declanșatorul, ceea ce poate epuiza rapid cota zilnică a Apps Script pe foi aglomerate. Declanșatorul de timp orar este aproape întotdeauna răspunsul corect.

Cote Apps Script de știut

Nivelul gratuit Apps Script are limite care merită respectate:

LimităValoare (nivel gratuit)
Timp total de rulare pe zi90 de minute
Timp de execuție unic6 minute
Apeluri UrlFetchApp pe zi20.000
Dimensiunea încărcăturii UrlFetchApp50 MB
Dimensiunea header-elor UrlFetchApp8 KB
Declanșatoare per utilizator per script20

Pentru o sincronizare tipică de contacte (câteva mii de contacte, orară), nu ești aproape de niciuna dintre acestea. Singurul de urmărit este 6 minute o singură execuție, dacă vreodată sincronizezi sute de mii de contacte într-o singură rundă, grupează-le în bucăți mai mici (scriptul de mai sus o face deja prin BATCH_SIZE).

Gestionarea importului asincron

Endpoint-ul de import al Brevo este asincron: primești un processId imediat înapoi, iar importul propriu-zis rulează pe partea de server. Pentru majoritatea sincronizărilor de foi acest lucru este în regulă, trage și uită, Brevo va trimite un rezumat prin e-mail când fiecare lot se termină.

Dacă vrei să blochezi până când importul este cu adevărat terminat, verifică endpoint-ul de stare a procesului:

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 este echivalentul Apps Script al unei așteptări blocante. Nu dormi prea mult, ai 6 minute total pe execuție.

Adăugarea unui webhook de notificare

Un model mai curat decât verificarea: implementează Apps Script-ul tău ca Web App și trimite URL-ul său ca notifyUrl. Brevo va trimite POST la el când importul se termină.

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

Implementează: Deploy → New deployment → Web app, setează “Who has access” pe Anyone, copiază URL-ul rezultat și trimite-l ca notifyUrl în încărcătura ta de import:

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

Acum Brevo postează rezultatul înapoi în scriptul Sheet-ului tău, închizând bucla fără infrastructură externă.

Depanare

400 Bad Request cu error: "Attribute X not found", o coloană din Sheet-ul tău se mapează la un atribut pe care Brevo nu îl cunoaște. Fie redenumește coloana Sheet pentru a se potrivi cu un atribut existent, fie creează atributul în Brevo (Contacte → Setări → Atribute de contact).

401 Unauthorized, cheia API este greșită sau expirată. Rulează din nou Configure API key, lipește o cheie proaspătă din panoul Brevo.

429 Too Many Requests, lovești limita de rată a Brevo. Endpoint-ul de import permite aproximativ 30 de apeluri pe minut. Dacă grupezi agresiv, adaugă Utilities.sleep(2000) între loturi în buclă.

Scriptul nu rulează în tăcere pe program, verifică Triggers în editorul Apps Script. Dacă un declanșator eșuează în mod repetat, Google îl dezactivează. Apasă pe declanșator pentru a vedea motivul eșecului, de obicei o problemă de permisiuni pe care o poți reautoriza.

Meniul Brevo nu a apărut, onOpen rulează doar când (re)deschizi Sheet-ul de la zero. Reîncarcă fila browser-ului.

Pop-up-ul de permisiuni continuă să revină, probabil ai editat scope-urile scriptului (ai adăugat un nou serviciu Google). Apps Script reîntreabă pentru autorizare ori de câte ori permisiunile necesare se schimbă. Rulează orice funcție o dată din editor pentru a declanșa solicitarea și a aproba.

De ce asta bate Zapier și prietenii

Apps Script este gratuit, trăiește în interiorul infrastructurii Google și are acces direct la datele Sheet-ului, fără declanșare de evenimente rând cu rând, fără preț pe-task, fără limite de rată altele decât cota Google (care este generoasă pentru acest tip de job). Reversul medaliei: te angajezi să scrii și să întreții o mică bucată de cod. Pentru o sincronizare de contacte, asta înseamnă aproximativ 100 de linii și practic zero întreținere continuă.

Combină asta cu un declanșator zilnic și o foaie pe care echipa ta de vânzări o actualizează deja, și ai o conductă de contacte în Brevo cu zero muncă recurentă.

Lectură suplimentară

Frequently Asked Questions

Am nevoie de un server sau de orice infrastructură pentru a sincroniza un Google Sheet cu Brevo?
Nu. Google Apps Script rulează în interiorul Google Sheets în sine. Scrii o funcție, salvezi proiectul, iar Google îl găzduiește și îl rulează. Scriptul poate lovi direct API-ul Brevo folosind UrlFetchApp.
Unde stochez cheia API Brevo?
Folosește PropertiesService.getScriptProperties(), este un magazin cheie/valoare gestionat de Google, limitat la proiectul Apps Script. Nu codifica cheia în sursă, colaboratorii la Sheet ar putea-o vedea.
Cum rulez asta automat în fiecare zi?
Deschide Apps Script → Triggers → Add Trigger. Alege funcția syncSheetToBrevo, alege 'Time-driven' și setează o cadență zilnică/orară. Cota Google este de 90 de minute pe zi de timp total de execuție Apps Script pe nivelul gratuit, suficient pentru o sincronizare de contacte.
Există o limită de rânduri?
Endpoint-ul de import al Brevo acceptă ~10 MB de JSON inline. Asta înseamnă aproximativ 30.000-50.000 de contacte, în funcție de câte atribute are fiecare. UrlFetchApp din Apps Script poate trimite o încărcătură de 50 MB, deci blocajul este Brevo, nu Apps Script. Pentru joburi mai mari, grupează rândurile.
Începe gratuit cu Brevo