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.

Featured image for article: Så bygger du ett Google Apps Script som importerar ett kalkylblads kontakter till Brevo

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:

emailfirstNamelastNamecompanycity
[email protected]JaneDoeAcmeBerlin
[email protected]JohnSmithGlobexParis

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

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

  1. Klicka på Brevo → Configure API key, klistra in din xkeysib-...-nyckel, klicka OK.
  2. 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
  3. 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änsVärde (gratisnivå)
Total körtid per dag90 minuter
Enskild körningstid6 minuter
UrlFetchApp-anrop per dag20 000
UrlFetchApp-payloadstorlek50 MB
UrlFetchApp-headerstorlek8 KB
Triggers per användare per skript20

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

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.

Vidare läsning

Frequently Asked Questions

Behöver jag en server eller någon infrastruktur för att synka ett Google Sheet till Brevo?
Nej. Google Apps Script körs inuti Google Sheets självt. Du skriver en funktion, sparar projektet, och Google hostar och kör det. Skriptet kan träffa Brevos API direkt med UrlFetchApp.
Var lagrar jag Brevo-API-nyckeln?
Använd PropertiesService.getScriptProperties(). Det är en Google-hanterad nyckel/värde-lagring scopad till Apps Script-projektet. Hårdkoda inte nyckeln i källan. Personer som samarbetar på arket skulle kunna se den.
Hur kör jag det här automatiskt varje dag?
Öppna Apps Script → Triggers → Add Trigger. Välj funktionen syncSheetToBrevo, välj 'Time-driven' och sätt en daglig eller timvis kadens. Googles kvot är 90 minuter per dag total Apps Script-körtid på gratisnivån, gott och väl för en kontaktsynk.
Finns det någon radgräns?
Brevos import-endpoint accepterar runt 10 MB inline JSON. Det är ungefär 30 000 till 50 000 kontakter beroende på hur många attribut var och en har. Apps Scripts UrlFetchApp kan skicka en payload på 50 MB, så flaskhalsen är Brevo, inte Apps Script. För större jobb, batcha raderna.
Börja gratis med Brevo