Så bygger du ett Google Apps Script som importerar ett kalkylblads kontakter till Brevo
Skicka kontakter från ett Google Sheet till Brevo automatiskt. Ett komplett Apps Script med lagring av API-nyckel, körning vid redigering, tidsbaserade triggers, en anpassad meny och felhantering. Ingen server behövs.
Om ditt team redan bor i ett Google Sheet, säljleads, eventanmälningar, en partnerkontaktlista, behöver du inte exportera CSV:er och importera om dem för hand för att få datan in i Brevo. Google Apps Script låter dig koppla arket direkt till Brevos API. Skriptet körs inuti Googles infrastruktur, så det finns inget att hosta, deploya eller passa.
Den här guiden går igenom ett fungerande skript: en anpassad menypost Sync to Brevo i ditt ark, en automatisk timtrigger, säker lagring av API-nyckeln, batchhantering och en liten bit strukturerad loggning så att du kan se vad som hände.
Vad du behöver
- Ett Google Sheet med kontakter (en rad per kontakt, rubrikrad först)
- Ett Brevo-konto och en API-nyckel (Settings → SMTP & API → API Keys)
- Det numeriska ID:t för den Brevo-lista du vill att kontakterna ska läggas till i
Det är allt. Inget npm, ingen Python, ingen server.
Arklayout
Skriptet i den här guiden förväntar sig en rubrikrad följt av en kontakt per rad. Kolumner mappas via rubriknamn till Brevo-attribut:
| firstName | lastName | company | city | |
|---|---|---|---|---|
| [email protected] | Jane | Doe | Acme | Berlin |
| [email protected] | John | Smith | Globex | Paris |
email är obligatorisk och matchas skiftlägesokänsligt. Allt annat skickas till Brevo som ett kontaktattribut. Anpassade attribut (allt utöver standardvarianterna som FIRSTNAME, LASTNAME) måste finnas i ditt Brevo-konto först. Definiera dem under Contacts → Settings → Contact attributes, eller via Brevos API.
Öppna Apps Script-editorn
I ditt ark: Extensions → Apps Script. En ny flik öppnas med ett tomt Code.gs. Ersätt innehållet med skriptet nedan.
Hela skriptet
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;}Det är hela grejen. Spara det (⌘S / Ctrl+S), namnge projektet något i stil med “Brevo sync” och gå tillbaka till ditt ark.
Första körningen
Ladda om arket. Den nya menyn Brevo dyker upp i toppen.
- Klicka på Brevo → Configure API key, klistra in din
xkeysib-...-nyckel, klicka OK. - Klicka på Brevo → Sync sheet to Brevo. Google ber om behörigheter första gången:
- “View and manage your spreadsheets”, behövs för att läsa raderna
- “Connect to an external service”, behövs för att anropa api.brevo.com
- Godkänn. Skriptet körs. En grön toast i nedre högra hörnet talar om hur många kontakter som gick ut.
Om det misslyckas, klicka på Extensions → Apps Script → View → Logs för att se statuskoden per batch från Brevo. Vanligaste felet är ett 400 på grund av ett saknat anpassat attribut. Se felsökningsavsnittet nedan.
Kör det enligt schema
I Apps Script-editorn: Triggers (klockikonen i sidopanelen till vänster) → Add Trigger.
- Choose function:
syncSheetToBrevo - Event source: Time-driven
- Type: Hour timer (eller Day timer för en synk en gång om dagen)
- Interval: varje timme (eller vad som passar)
Spara. Google kör funktionen i den kadensen för alltid, utan server, utan cron, utan underhåll.
Du kan också använda From spreadsheet → On edit om du vill att varje cellförändring ska trigga en synk. Var försiktig med det. Även kosmetiska redigeringar fyrar triggern, vilket kan slå i Apps Scripts dygnskvot snabbt på upptagna ark. Tim-triggern är nästan alltid rätt svar.
Apps Script-kvoter att känna till
Den fria Apps Script-nivån har gränser värda att respektera:
| Gräns | Värde (gratisnivå) |
|---|---|
| Total körtid per dag | 90 minuter |
| Enskild körningstid | 6 minuter |
UrlFetchApp-anrop per dag | 20 000 |
UrlFetchApp-payloadstorlek | 50 MB |
UrlFetchApp-headerstorlek | 8 KB |
| Triggers per användare per skript | 20 |
För en typisk kontaktsynk (några tusen kontakter, varje timme) är du långt ifrån någon av dessa. Den enda att hålla ögonen på är 6-minuters enskild körning. Om du någonsin synkar hundratusentals kontakter i ett svep, batcha dem i mindre bitar (skriptet ovan gör redan det via BATCH_SIZE).
Hantera importen asynkront
Brevos import-endpoint är asynkron: du får ett processId direkt, och själva importen körs på serversidan. För de flesta arksynker går det bra: skicka och glöm, Brevo mejlar en sammanfattning när varje batch är klar.
Om du vill blockera tills importen verkligen är klar, polla process-statusendpointen:
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 är Apps Scripts motsvarighet till en blockerande väntan. Sov inte för länge. Du har 6 minuter totalt per körning.
Lägg till en notify-webhook
Ett renare mönster än pollning: deploya ditt Apps Script som en Web App och skicka dess URL som notifyUrl. Brevo postar till den när importen är klar.
// 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');}Deploya: Deploy → New deployment → Web app, sätt “Who has access” till Anyone, kopiera URL:en du får och skicka den som notifyUrl i din import-payload:
payload.notifyUrl = 'https://script.google.com/macros/s/AKfy.../exec';Nu postar Brevo resultatet tillbaka till ditt eget skript på arket, och stänger loopen utan extern infrastruktur.
Felsökning
400 Bad Request med error: "Attribute X not found". En kolumn i ditt ark mappar mot ett attribut Brevo inte känner till. Antingen byt namn på arkkolumnen så den matchar ett befintligt attribut, eller skapa attributet i Brevo (Contacts → Settings → Contact attributes).
401 Unauthorized. API-nyckeln är fel eller utgången. Kör om Configure API key, klistra in en ny nyckel från Brevos dashboard.
429 Too Many Requests. Du slår i Brevos rate limit. Import-endpointen tillåter runt 30 anrop per minut. Om du batchar aggressivt, lägg till Utilities.sleep(2000) mellan batcherna i loopen.
Skriptet körs tyst inte enligt schema. Kolla Triggers i Apps Script-editorn. Om en trigger misslyckas upprepade gånger inaktiverar Google den. Klicka in i triggern för att se orsaken till felet, oftast en behörighetsfråga du kan godkänna om.
Brevo-menyn dök inte upp. onOpen körs bara när du (åter)öppnar arket från grunden. Ladda om webbläsarfliken.
Behörighetspopupen kommer tillbaka. Du har förmodligen redigerat skriptets scopes (lagt till en ny Google-tjänst). Apps Script ber om godkännande på nytt varje gång de nödvändiga behörigheterna ändras. Kör vilken funktion som helst en gång från editorn för att trigga prompten och godkänn.
Varför det här slår Zapier och vänner
Apps Script är gratis, bor inuti Googles infrastruktur och har direkt åtkomst till arkets data. Ingen radvis händelseaktivering, ingen pris-per-task, inga rate limits utöver Googles kvot (som är generös för den här typen av jobb). Baksidan: du förbinder dig att skriva och underhålla en liten bit kod. För en kontaktsynk är det ungefär 100 rader och i princip noll löpande underhåll.
Para ihop det här med en daglig trigger och ett ark ditt säljteam redan uppdaterar, så har du en kontaktpipeline in i Brevo med noll återkommande arbete.