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.

Featured image for article: Hoe je een Google Apps Script bouwt om de contacten van een Sheet in Brevo te importeren

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:

emailfirstNamelastNamecompanycity
[email protected]JaneDoeAcmeBerlin
[email protected]JohnSmithGlobexParis

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

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

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.

  1. Klik op Brevo → Configure API key, plak je xkeysib-...-sleutel, klik op OK.
  2. 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
  3. 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:

LimietWaarde (gratis tier)
Totale runtime per dag90 minuten
Eén uitvoeringstijd6 minuten
UrlFetchApp-aanroepen per dag20.000
UrlFetchApp-payload-grootte50 MB
UrlFetchApp-headergrootte8 KB
Triggers per gebruiker per script20

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

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.

Verder lezen

Frequently Asked Questions

Heb ik een server of infrastructuur nodig om een Google Sheet met Brevo te synchroniseren?
Nee. Google Apps Script draait binnen Google Sheets zelf. Je schrijft een functie, slaat het project op, en Google host en draait het. Het script kan Brevo's API direct aanroepen via UrlFetchApp.
Waar sla ik de Brevo API-sleutel op?
Gebruik PropertiesService.getScriptProperties(). Het is een door Google beheerde key/value-store die scoped is op het Apps Script-project. Hardcode de sleutel niet in de broncode; medewerkers op de Sheet zouden hem kunnen zien.
Hoe draai ik dit elke dag automatisch?
Open Apps Script, ga naar Triggers, Add Trigger. Kies de syncSheetToBrevo-functie, kies 'Time-driven', en stel een dagelijkse/uurlijkse cadans in. Google's quotum is 90 minuten/dag totale Apps Script-uitvoeringstijd op de gratis tier. Ruim voldoende voor een contactsync.
Is er een rijlimiet?
Brevo's import-endpoint accepteert ongeveer 10 MB inline JSON. Dat zijn ruwweg 30.000 tot 50.000 contacten, afhankelijk van hoeveel attributen elk heeft. Apps Script's UrlFetchApp kan een 50 MB-payload verzenden, dus de bottleneck is Brevo, niet Apps Script. Voor grotere jobs batch je de rijen.
Start gratis met Brevo