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.

Featured image for article: Slik bygger du et Google Apps Script for å importere et arks kontakter til Brevo

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:

emailfirstNamelastNamecompanycity
[email protected]JaneDoeAcmeBerlin
[email protected]JohnSmithGlobexParis

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

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

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.

  1. Klikk Brevo -> Configure API key, lim inn xkeysib-...-nøkkelen din, klikk OK.
  2. 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
  3. 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:

GrenseVerdi (gratisnivå)
Total runtime per dag90 minutter
Enkelt eksekveringstid6 minutter
UrlFetchApp-kall per dag20.000
UrlFetchApp-payload-størrelse50 MB
UrlFetchApp-headers-størrelse8 KB
Triggere per bruker per skript20

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.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, 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.

Videre lesning

Frequently Asked Questions

Trenger jeg en server eller noen infrastruktur for å synkronisere et Google Sheet til Brevo?
Nei. Google Apps Script kjører inne i Google Sheets selv. Du skriver en funksjon, lagrer prosjektet, og Google hoster og kjører det. Skriptet kan treffe Brevos API direkte med UrlFetchApp.
Hvor lagrer jeg Brevo API-nøkkelen?
Bruk PropertiesService.getScriptProperties(), det er et Google-administrert nøkkel/verdi-lager scoped til Apps Script-prosjektet. Ikke hardkode nøkkelen i kilden, samarbeidspartnere på arket vil kunne se den.
Hvordan kjører jeg dette automatisk hver dag?
Åpne Apps Script -> Triggers -> Add Trigger. Velg syncSheetToBrevo-funksjonen, velg 'Time-driven', og sett en daglig/timebasert kadens. Googles kvote er 90 minutter/dag total Apps Script-eksekveringstid på gratisnivået, mer enn nok for en kontaktsynkronisering.
Er det en radgrense?
Brevos import-endepunkt aksepterer omtrent 10 MB inline JSON. Det er omtrent 30.000-50.000 kontakter avhengig av hvor mange attributter hver enkelt har. Apps Scripts UrlFetchApp kan sende en 50 MB payload, så flaskehalsen er Brevo, ikke Apps Script. For større jobber, batch radene.
Start gratis med Brevo