Hogyan építs Google Apps Scriptet, ami egy tábla kapcsolatait a Brevóba importálja
Tedd át a kapcsolatokat egy Google Sheetből a Brevóba automatikusan. Egy teljes Apps Script API-kulcs tárolással, szerkesztésre futtatással, idő-alapú triggerekkel, egyéni menüvel és hibakezeléssel, szerver nélkül.
Ha a csapatod már egy Google Sheetben él, értékesítési leadek, eseményregisztrációk, partner-kapcsolatlista, akkor az adat Brevóba juttatása nem kell, hogy kézi CSV exportokról és újraimportokról szóljon. A Google Apps Script lehetővé teszi, hogy közvetlenül a táblát kösd a Brevo API-jához. A script a Google infrastruktúráján belül fut, így nincs mit hosztolni, deployolni vagy babázni.
Ez az útmutató végigmegy egy működő scripten: egy egyéni Sync to Brevo menüpont a tábládban, automatikus óránkénti trigger, biztonságos API kulcs tárolás, csomagolt feldolgozás és egy kis strukturált naplózás, hogy lásd, mi történt.
Mire van szükséged
- Egy Google Sheet kapcsolatokkal (egy sor egy kapcsolat, fejlécsor először)
- Egy Brevo fiók és egy API kulcs (Settings -> SMTP & API -> API Keys)
- Annak a Brevo listának a numerikus azonosítója, amibe a kapcsolatokat hozzá akarod adni
Ennyi. Nincs npm, nincs Python, nincs szerver.
Tábla-elrendezés
Az ebben az útmutatóban szereplő script egy fejlécsort vár, utána egy kapcsolatot soronként. Az oszlopok fejlécnév alapján vannak hozzárendelve a Brevo attribútumokhoz:
| firstName | lastName | company | city | |
|---|---|---|---|---|
| [email protected] | Jane | Doe | Acme | Berlin |
| [email protected] | John | Smith | Globex | Paris |
Az email kötelező és kis-nagybetű érzéketlenül van egyeztetve. Minden más kapcsolat-attribútumként megy a Brevónak. Az egyéni attribútumoknak (a standardokon, mint FIRSTNAME, LASTNAME túl bárminek) először létezniük kell a Brevo fiókodban, definiáld őket a Contacts -> Settings -> Contact attributes alatt, vagy a Brevo API-n keresztül.
Nyisd meg az Apps Script szerkesztőt
A tábládban: Extensions -> Apps Script. Egy új fül nyílik üres Code.gs fájllal. Cseréld le a tartalmat az alábbi scriptre.
A teljes script
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;}Ennyi az egész. Mentsd el (⌘S / Ctrl+S), nevezd el a projektet valami olyannak, mint “Brevo sync”, és menj vissza a tábládba.
Első futtatás
Töltsd újra a táblát, az új Brevo menü megjelenik a tetején.
- Kattints a Brevo -> Configure API key menüpontra, illeszd be az
xkeysib-...kulcsodat, kattints az OK gombra. - Kattints a Brevo -> Sync sheet to Brevo menüpontra. A Google először engedélyt kér:
- “View and manage your spreadsheets”, a sorok olvasásához kell
- “Connect to an external service”, az api.brevo.com hívásához kell
- Hagyd jóvá. A script lefut. Egy zöld toast a jobb alsó sarokban megmondja, hány kapcsolat ment ki.
Ha hibát ad, kattints az Extensions -> Apps Script -> View -> Logs menüpontra, hogy lásd a Brevótól érkező csomagonkénti státuszkódot. A leggyakoribb hiba egy 400, ami egy hiányzó egyéni attribútum miatt jön, lásd a hibaelhárítási szakaszt alább.
Futtasd ütemezve
Az Apps Script szerkesztőben: Triggers (az óra ikon a bal oldalsávon) -> Add Trigger.
- Choose function:
syncSheetToBrevo - Event source: Time-driven
- Type: Hour timer (vagy Day timer napi egyszeri szinkronra)
- Interval: óránként (vagy bármi, ami illik)
Mentsd el. A Google örökre futtatja a függvényt azzal az ütemezéssel, szerver nélkül, cron nélkül, karbantartás nélkül.
A From spreadsheet -> On edit triggert is használhatod, ha minden cellaváltozás indítson szinkronizálást. Ezzel óvatosan, még a kozmetikai szerkesztések is elsütik a triggert, ami forgalmas táblákon gyorsan lemeríti az Apps Script napi kvótát. Az óránkénti idő-trigger szinte mindig a megfelelő válasz.
Apps Script kvóták, amiket érdemes ismerni
Az ingyenes Apps Script szintnek vannak korlátai, amiket érdemes tisztelni:
| Korlát | Érték (ingyenes szint) |
|---|---|
| Teljes futási idő naponta | 90 perc |
| Egyetlen futás ideje | 6 perc |
UrlFetchApp hívások naponta | 20 000 |
UrlFetchApp payload mérete | 50 MB |
UrlFetchApp fejlécek mérete | 8 KB |
| Triggerek scriptenként és felhasználónként | 20 |
Egy tipikus kapcsolatszinkron (néhány ezer kapcsolat, óránként) egyik közelébe sem kerül. Az egyetlen, amit figyelni kell, a 6 perces egyetlen futás, ha valaha százezreket szinkronizálsz egyszerre, csomagold kisebb darabokra (a fenti script ezt már megteszi a BATCH_SIZE-on keresztül).
Az import aszinkron kezelése
A Brevo import végpontja aszinkron: azonnal visszakapsz egy processId-t, és a tényleges import a szerver oldalon fut. A legtöbb tábla-szinkronra ez rendben van, kilőni és elfelejteni, a Brevo e-mailt küld összegzéssel, amikor minden csomag befejeződik.
Ha blokkolni szeretnél, amíg az import tényleg elkészül, kérdezd le a folyamat-állapot végpontot:
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';}Az Utilities.sleep az Apps Script megfelelője a blokkoló várakozásnak. Ne aludj túl sokat, összesen 6 perced van futásonként.
Notify webhook hozzáadása
Tisztább minta, mint a lekérdezés: deployold az Apps Scriptet Web App-ként, és add át az URL-jét notifyUrl-ként. A Brevo POST-ol rá, amikor az import befejeződik.
// 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');}Deploy: Deploy -> New deployment -> Web app, állítsd a “Who has access” beállítást Anyone-ra, másold ki a kapott URL-t, és add át notifyUrl-ként az import payloadban:
payload.notifyUrl = 'https://script.google.com/macros/s/AKfy.../exec';A Brevo most visszaposztolja az eredményt a tábla saját scriptjére, lezárva a kört külső infrastruktúra nélkül.
Hibaelhárítás
400 Bad Request error: "Attribute X not found" üzenettel. Egy oszlop a tábládban olyan attribútumra mutat, amit a Brevo nem ismer. Vagy nevezd át a tábla oszlopát egy létező attribútum nevére, vagy hozd létre az attribútumot a Brevóban (Contacts -> Settings -> Contact attributes).
401 Unauthorized. Az API kulcs rossz vagy lejárt. Futtasd újra a Configure API key menüpontot, illeszd be a friss kulcsot a Brevo dashboardjáról.
429 Too Many Requests. A Brevo rate limitjébe ütközöl. Az import végpont nagyjából percenként 30 hívást enged. Ha agresszíven csomagolsz, adj Utilities.sleep(2000) hívást a csomagok közé a ciklusban.
A script csendben nem fut ütemezve. Ellenőrizd a Triggers részt az Apps Script szerkesztőben. Ha egy trigger ismételten meghibásodik, a Google letiltja. Kattints bele a triggerbe, hogy lásd a hiba okát, általában egy engedélyezési probléma, amit újra kell hagynod.
A Brevo menü nem jelent meg. Az onOpen csak akkor fut, amikor (újra)megnyitod a táblát nulláról. Töltsd újra a böngészőfület.
Az engedélykérő ablak újra és újra felugrik. Valószínűleg módosítottad a script scope-jait (új Google szolgáltatást adtál hozzá). Az Apps Script újra kéri az engedélyt, valahányszor a szükséges jogosultságok változnak. Futtass bármilyen függvényt egyszer a szerkesztőből, hogy elindítsd a kérést, és hagyd jóvá.
Miért jobb ez, mint a Zapier és társai
Az Apps Script ingyenes, a Google infrájában él, és közvetlenül hozzáfér a tábla adataihoz, nincs soronkénti esemény, nincs feladatonkénti árazás, nincs más rate limit, mint a Google kvótája (ami nagyvonalú erre a fajta munkára). A másik oldal: elkötelezed magad egy kis kód megírása és karbantartása mellett. Egy kapcsolatszinkronra ez nagyjából 100 sor és gyakorlatilag nulla folyamatos karbantartás.
Párosítsd ezt egy napi triggerrel és egy táblával, amit az értékesítési csapat úgyis frissít, és máris kész egy kapcsolat-pipeline-od a Brevóba, nulla ismétlődő munkával.