Slik bygger du et Google Apps Script for å importere et arks kontakter til Brevo
Send kontakter fra et Google Sheet inn i Brevo automatisk. Et komplett Apps Script med API-nøkkellagring, kjør-på-redigering, tidsbaserte triggere, en egendefinert meny, og feilhåndtering, ingen server nødvendig.
Hvis teamet ditt allerede lever i et Google Sheet, salgsleads, eventpåmeldinger, en partnerkontaktliste, trenger ikke å få den dataen inn i Brevo å involvere eksport av CSV-er og reimport for hånd. Google Apps Script lar deg koble arket direkte til Brevos API. Skriptet kjører inne i Googles infrastruktur, så det er ingenting å hoste, deploye eller passe på.
Denne guiden går gjennom et fungerende skript: et egendefinert Sync to Brevo-menyelement i arket ditt, en automatisk timetrigger, sikker API-nøkkellagring, batch-håndtering, og litt strukturert logging så du kan fortelle hva som skjedde.
Hva du trenger
- Et Google Sheet med kontakter (én rad per kontakt, header-rad først)
- En Brevo-konto og en API-nøkkel (Settings -> SMTP & API -> API Keys)
- Den numeriske ID-en til Brevo-listen du vil legge kontakter til
Det er det. Ingen npm, ingen Python, ingen server.
Ark-layout
Skriptet i denne guiden forventer en header-rad fulgt av én kontakt per rad. Kolonner mappes etter header-navn til Brevo-attributter:
| firstName | lastName | company | city | |
|---|---|---|---|---|
| [email protected] | Jane | Doe | Acme | Berlin |
| [email protected] | John | Smith | Globex | Paris |
email er obligatorisk og matches case-insensitivt. Alt annet sendes til Brevo som et kontaktattributt. Egendefinerte attributter (alt utover standardene som FIRSTNAME, LASTNAME) må finnes i Brevo-kontoen din først, definer dem under Contacts -> Settings -> Contact attributes, eller via Brevo API.
Åpne Apps Script-editoren
I arket ditt: Extensions -> Apps Script. En ny fane åpnes med en tom Code.gs. Erstatt innholdet med skriptet under.
Det fullstendige 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 er hele greia. Lagre det (⌘S / Ctrl+S), gi prosjektet et navn som “Brevo sync”, og gå tilbake til arket ditt.
Første kjøring
Last inn arket på nytt, den nye Brevo-menyen dukker opp øverst.
- Klikk Brevo -> Configure API key, lim inn
xkeysib-...-nøkkelen din, klikk OK. - Klikk Brevo -> Sync sheet to Brevo. Google vil be om tillatelser første gang:
- “View and manage your spreadsheets”, trengs for å lese radene
- “Connect to an external service”, trengs for å kalle api.brevo.com
- Godkjenn. Skriptet kjører. En grønn toast nede til høyre forteller deg hvor mange kontakter som gikk ut.
Hvis det feiler, klikk Extensions -> Apps Script -> View -> Logs for å se per-batch statuskoden fra Brevo. Den vanligste feilen er en 400 på grunn av et manglende egendefinert attributt, se feilsøkingsseksjonen under.
Kjør det etter en tidsplan
I Apps Script-editoren: Triggers (klokkeikonet i venstre sidefelt) -> Add Trigger.
- Choose function:
syncSheetToBrevo - Event source: Time-driven
- Type: Hour timer (eller Day timer for daglig synkronisering)
- Interval: hver time (eller hva som passer)
Lagre. Google vil kjøre funksjonen på den kadensen for alltid, ingen server, ingen cron, ingen vedlikehold.
Du kan også bruke From spreadsheet -> On edit hvis du vil at hver celleendring skal trigge en synkronisering. Vær forsiktig med det, selv kosmetiske redigeringer vil fyre triggeren, som kan treffe Apps Scripts daglige kvote raskt på travle ark. Den timebaserte tidstriggeren er nesten alltid det riktige svaret.
Apps Script-kvoter å vite om
Det gratis Apps Script-nivået har grenser verdt å respektere:
| Grense | Verdi (gratisnivå) |
|---|---|
| Total runtime per dag | 90 minutter |
| Enkelt eksekveringstid | 6 minutter |
UrlFetchApp-kall per dag | 20.000 |
UrlFetchApp-payload-størrelse | 50 MB |
UrlFetchApp-headers-størrelse | 8 KB |
| Triggere per bruker per skript | 20 |
For en typisk kontaktsynkronisering (noen tusen kontakter, hver time) er du ikke i nærheten av noen av disse. Den eneste å se opp for er 6-minutters enkelt eksekvering, hvis du noen gang synkroniserer hundretusener av kontakter på en gang, batch dem inn i mindre biter (skriptet over gjør allerede dette via BATCH_SIZE).
Håndtere importen asynkront
Brevos import-endepunkt er asynkront: du får en processId tilbake umiddelbart, og selve importen kjører serverside. For de fleste arksynkroniseringer er dette greit, fyr og glem, Brevo vil sende e-post med et sammendrag når hver batch er ferdig.
Hvis du vil blokkere til importen virkelig er ferdig, poll prosess-status-endepunktet:
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 er Apps Script-ekvivalenten til en blokkerende vent. Ikke sov for lenge, du har 6 minutter totalt per eksekvering.
Legge til en notify-webhook
Et renere mønster enn polling: deploy Apps Script-en din som en Web App og send URL-en som notifyUrl. Brevo vil POSTe til den når importen er ferdig.
// 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, sett “Who has access” til Anyone, kopier den resulterende URL-en, og send den som notifyUrl i import-payloaden din:
payload.notifyUrl = 'https://script.google.com/macros/s/AKfy.../exec';Nå poster Brevo resultatet tilbake til arkets eget skript, lukker loopen uten ekstern infrastruktur.
Feilsøking
400 Bad Request med error: "Attribute X not found". En kolonne i arket ditt mappes til et attributt Brevo ikke kjenner til. Enten gi nytt navn til arkkolonnen for å matche et eksisterende attributt, eller opprett attributtet i Brevo (Contacts -> Settings -> Contact attributes).
401 Unauthorized. API-nøkkelen er feil eller utløpt. Kjør Configure API key på nytt, lim inn en fersk nøkkel fra Brevos dashbord.
429 Too Many Requests. Du treffer Brevos rate-grense. Import-endepunktet tillater rundt 30 kall per minutt. Hvis du batcher aggressivt, legg til Utilities.sleep(2000) mellom batchene i loopen.
Skriptet kjører stille ikke etter tidsplan. Sjekk Triggers i Apps Script-editoren. Hvis en trigger feiler gjentatte ganger, deaktiverer Google den. Klikk inn i triggeren for å se feilårsaken, vanligvis en tillatelsesproblem du kan reautorisere.
Brevo-menyen dukket ikke opp. onOpen kjører bare når du (gjen)åpner arket fra bunnen. Last inn nettleserfanen på nytt.
Tillatelses-popup-en kommer stadig tilbake. Du har sannsynligvis redigert skriptets scopes (lagt til en ny Google-tjeneste). Apps Script ber om autorisasjon på nytt hver gang de nødvendige tillatelsene endres. Kjør hvilken som helst funksjon én gang fra editoren for å trigge spørsmålet og godkjenne det.
Hvorfor dette slår Zapier og venner
Apps Script er gratis, lever inne i Googles infra, og har direkte tilgang til arkets data, ingen rad-for-rad eventfyring, ingen per-task-prising, ingen rate-grense annet enn Googles kvote (som er sjenerøs for denne typen jobb). Baksiden: du forplikter deg til å skrive og vedlikeholde et lite stykke kode. For en kontaktsynkronisering er det omtrent 100 linjer og praktisk talt null løpende vedlikehold.
Par dette med en daglig trigger og et ark salgsteamet ditt allerede oppdaterer, og du har en kontakt-pipeline inn i Brevo med null tilbakevendende arbeid.