Hoe je een Google Apps Script bouwt om de contacten van een Sheet in Brevo te importeren
Push contacten van een Google Sheet automatisch naar Brevo. Een compleet Apps Script met API-sleutelopslag, run-on-edit, time-based triggers, een aangepast menu en foutafhandeling. Geen server nodig.
Als je team al leeft in een Google Sheet (sales leads, event-aanmeldingen, een partnercontactlijst), hoeft het in Brevo krijgen van die data niet te betekenen dat je CSV’s exporteert en met de hand opnieuw importeert. Google Apps Script laat je de Sheet rechtstreeks aan Brevo’s API koppelen. Het script draait binnen Google’s infrastructuur, dus er is niets te hosten, deployen of babysitten.
Deze gids loopt door een werkend script: een aangepast Sync to Brevo-menu-item in je Sheet, een automatische uurlijkse trigger, veilige API-sleutelopslag, batchverwerking en een klein beetje gestructureerde logging zodat je kunt zien wat er gebeurde.
Wat je nodig hebt
- Een Google Sheet met contacten (één rij per contact, eerst de headerrij)
- Een Brevo-account en een API-sleutel (Settings → SMTP & API → API Keys)
- Het numerieke ID van de Brevo-lijst waar je contacten aan toegevoegd wilt zien
Dat is het. Geen npm, geen Python, geen server.
Bladindeling
Het script in deze gids verwacht een headerrij gevolgd door één contact per rij. Kolommen worden gemapt op basis van headernaam naar Brevo-attributen:
| firstName | lastName | company | city | |
|---|---|---|---|---|
| [email protected] | Jane | Doe | Acme | Berlin |
| [email protected] | John | Smith | Globex | Paris |
email is verplicht en wordt hoofdletter-ongevoelig gematcht. Al het andere wordt naar Brevo gestuurd als een contactattribuut. Aangepaste attributen (alles buiten de standaarden zoals FIRSTNAME, LASTNAME) moeten eerst bestaan in je Brevo-account. Definieer ze onder Contacts → Settings → Contact attributes, of via de Brevo API.
Open de Apps Script-editor
In je Sheet: Extensions → Apps Script. Een nieuw tabblad opent met een lege Code.gs. Vervang de inhoud met het onderstaande script.
Het volledige 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;}Dat is het hele ding. Sla het op (⌘S / Ctrl+S), noem het project iets als “Brevo sync” en ga terug naar je Sheet.
Eerste run
Herlaad de Sheet. Het nieuwe Brevo-menu verschijnt bovenaan.
- Klik op Brevo → Configure API key, plak je
xkeysib-...-sleutel, klik op OK. - Klik op Brevo → Sync sheet to Brevo. Google vraagt de eerste keer om toestemmingen:
- “View and manage your spreadsheets”, nodig om de rijen te lezen
- “Connect to an external service”, nodig om api.brevo.com aan te roepen
- Goedkeuren. Het script draait. Een groene toast rechtsonder vertelt je hoeveel contacten zijn verzonden.
Als het faalt, klik op Extensions → Apps Script → View → Logs om de per-batch statuscode van Brevo te zien. De meest voorkomende fout is een 400 vanwege een ontbrekend aangepast attribuut. Zie de troubleshooting-sectie hieronder.
Draai het op een schema
In de Apps Script-editor: Triggers (het klokicoon in de linker zijbalk) → Add Trigger.
- Choose function:
syncSheetToBrevo - Event source: Time-driven
- Type: Hour timer (of Day timer voor een eenmaal-per-dag-sync)
- Interval: elk uur (of wat past)
Opslaan. Google draait de functie eeuwig op die cadans, zonder server, zonder cron, zonder onderhoud.
Je kunt ook From spreadsheet → On edit gebruiken als je wilt dat elke celwijziging een sync activeert. Wees daar voorzichtig mee. Zelfs cosmetische bewerkingen vuren de trigger af, wat het Apps Script dagelijkse quotum snel kan raken op drukke sheets. De uurlijkse time trigger is bijna altijd het juiste antwoord.
Apps Script-quota om te kennen
De gratis Apps Script-tier heeft limieten die het waard zijn om te respecteren:
| Limiet | Waarde (gratis tier) |
|---|---|
| Totale runtime per dag | 90 minuten |
| Eén uitvoeringstijd | 6 minuten |
UrlFetchApp-aanroepen per dag | 20.000 |
UrlFetchApp-payload-grootte | 50 MB |
UrlFetchApp-headergrootte | 8 KB |
| Triggers per gebruiker per script | 20 |
Voor een typische contactsync (een paar duizend contacten, uurlijks) zit je nergens in de buurt van een van deze. De enige om in de gaten te houden is 6 minuten enkele uitvoering. Als je ooit honderdduizenden contacten in één keer synchroniseert, batch ze in kleinere chunks (het bovenstaande script doet dit al via BATCH_SIZE).
De import asynchroon afhandelen
Brevo’s import-endpoint is asynchroon: je krijgt direct een processId terug en de daadwerkelijke import draait server-side. Voor de meeste sheet-syncs is dit prima: fire-and-forget, Brevo mailt een samenvatting wanneer elke batch klaar is.
Als je wilt blokkeren tot de import echt klaar is, poll dan het process status-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 is het Apps Script-equivalent van een blokkerende wait. Slaap niet te lang. Je hebt totaal 6 minuten per uitvoering.
Een notify-webhook toevoegen
Een schoner patroon dan polling: deploy je Apps Script als een Web App en geef de URL door als notifyUrl. Brevo POST’t naar het wanneer de import is voltooid.
// 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, zet “Who has access” op Anyone, kopieer de resulterende URL en geef hem door als notifyUrl in je import-payload:
payload.notifyUrl = 'https://script.google.com/macros/s/AKfy.../exec';Nu post Brevo het resultaat terug naar het eigen script van je Sheet, en sluit de loop zonder externe infrastructuur.
Troubleshooting
400 Bad Request met error: "Attribute X not found". Een kolom in je Sheet mapt naar een attribuut dat Brevo niet kent. Hernoem de Sheet-kolom om te matchen met een bestaand attribuut, of maak het attribuut aan in Brevo (Contacts → Settings → Contact attributes).
401 Unauthorized. API-sleutel is verkeerd of verlopen. Run Configure API key opnieuw, plak een verse sleutel uit Brevo’s dashboard.
429 Too Many Requests. Je raakt Brevo’s rate limit. Het import-endpoint staat ongeveer 30 aanroepen per minuut toe. Als je agressief batcht, voeg Utilities.sleep(2000) toe tussen batches in de loop.
Script draait stilzwijgend niet op schema. Check Triggers in de Apps Script-editor. Als een trigger herhaaldelijk faalt, schakelt Google hem uit. Klik op de trigger om de faalreden te zien, meestal een toestemmingsprobleem dat je opnieuw kunt autoriseren.
Het Brevo-menu verscheen niet. onOpen draait alleen wanneer je de Sheet opnieuw helemaal opent. Herlaad het browsertabblad.
Toestemmingen-pop-up blijft terugkomen. Je hebt waarschijnlijk de scopes van het script bewerkt (een nieuwe Google-service toegevoegd). Apps Script vraagt opnieuw om autorisatie wanneer de vereiste toestemmingen veranderen. Run één functie vanuit de editor om de prompt te activeren en goed te keuren.
Waarom dit beter is dan Zapier en vrienden
Apps Script is gratis, leeft binnen Google’s infrastructuur en heeft directe toegang tot de Sheet-data. Geen rij-voor-rij event-firing, geen per-task pricing, geen rate limits anders dan Google’s quotum (dat genereus is voor dit soort werk). De keerzijde: je commiteert je aan het schrijven en onderhouden van een klein stukje code. Voor een contactsync is dat ongeveer 100 regels en eigenlijk nul doorlopend onderhoud.
Combineer dit met een dagelijkse trigger en een sheet die je salesteam al bijwerkt, en je hebt een contactpijplijn naar Brevo met nul terugkerend werk.