Sådan bygger du et Google Apps Script til at importere et arks kontakter ind i Brevo

Skub kontakter fra et Google Sheet ind i Brevo automatisk. Et komplet Apps Script med API-nøgleopbevaring, run-on-edit, time-baserede triggers, en custom-menu og fejlhåndtering. Ingen server.

Featured image for article: Sådan bygger du et Google Apps Script til at importere et arks kontakter ind i Brevo

Hvis dit team allerede lever i et Google Sheet, salgsleads, event-tilmeldinger, en partnerkontaktliste, behøver det at få de data ind i Brevo ikke involvere CSV-eksport og manuel re-import. Google Apps Script lader dig forbinde Sheet’et direkte til Brevos API. Scriptet kører inde i Googles infrastruktur, så der er intet at hoste, deploye eller passe på.

Denne guide går igennem et fungerende script: et custom Sync to Brevo-menupunkt i dit Sheet, en automatisk hourly trigger, sikker API-nøgleopbevaring, batch-håndtering og lidt struktureret logging, så du kan se, hvad der skete.

Hvad du skal bruge

  • Et Google Sheet med kontakter (én række pr. kontakt, header-række først)
  • En Brevo-konto og en API-nøgle (Settings → SMTP & API → API Keys)
  • Det numeriske ID på Brevo-listen, du vil have kontakter tilføjet til

Det er det. Ingen npm, ingen Python, ingen server.

Ark-layout

Scriptet i denne guide forventer en header-række efterfulgt af én kontakt pr. række. Kolonner mappes via header-navn til Brevo-attributter:

emailfirstNamelastNamecompanycity
[email protected]JaneDoeAcmeBerlin
[email protected]JohnSmithGlobexParis

email er obligatorisk og matches case-insensitivt. Alt andet sendes til Brevo som en kontaktattribut. Brugerdefinerede attributter (alt ud over standardsæt som FIRSTNAME, LASTNAME) skal findes i din Brevo-konto først, definér dem under Contacts → Settings → Contact attributes, eller via Brevo-API’et.

Åbn Apps Script-editoren

I dit Sheet: Extensions → Apps Script. En ny fane åbner med en tom Code.gs. Erstat indholdet med scriptet nedenfor.

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

Det er det hele. Gem det (⌘S / Ctrl+S), navngiv projektet noget i stil med “Brevo sync”, og hop tilbage til dit Sheet.

Første kørsel

Genindlæs Sheet’et, den nye Brevo-menu dukker op øverst.

  1. Klik Brevo → Configure API key, indsæt din xkeysib-...-nøgle, klik OK.
  2. Klik Brevo → Sync sheet to Brevo. Google vil bede om tilladelser første gang:
    • “View and manage your spreadsheets”, nødvendigt for at læse rækkerne
    • “Connect to an external service”, nødvendigt for at kalde api.brevo.com
  3. Godkend. Scriptet kører. En grøn toast i nederste højre fortæller dig, hvor mange kontakter der gik ud.

Hvis det fejler, klik Extensions → Apps Script → View → Logs for at se pr-batch-statuskoden fra Brevo. Den hyppigste fejl er en 400 på grund af en manglende brugerdefineret attribut, se troubleshooting-afsnittet nedenfor.

Kør det på en tidsplan

I Apps Script-editoren: Triggers (uret-ikonet i venstre sidebar) → Add Trigger.

  • Choose function: syncSheetToBrevo
  • Event source: Time-driven
  • Type: Hour timer (eller Day timer for en gang-om-dagen-synk)
  • Interval: hver time (eller hvad der passer)

Gem. Google kører funktionen i den kadence for evigt, ingen server, ingen cron, ingen vedligeholdelse.

Du kan også bruge From spreadsheet → On edit, hvis du vil have hver celleændring til at trigge en synk. Vær forsigtig med det, selv kosmetiske ændringer fyrer triggeren, hvilket kan ramme Apps Scripts daglige kvota hurtigt på travle ark. Den hourly time trigger er næsten altid det rigtige svar.

Apps Script-kvotaer du bør kende

Apps Script free tier har grænser, der er værd at respektere:

GrænseVærdi (free tier)
Total køretid pr. dag90 minutter
Enkelt eksekveringstid6 minutter
UrlFetchApp-kald pr. dag20.000
UrlFetchApp payload-størrelse50 MB
UrlFetchApp headers-størrelse8 KB
Triggers pr. bruger pr. script20

Til en typisk kontaktsynk (et par tusind kontakter, hourly) er du intet sted nær nogen af disse. Den eneste at holde øje med er 6-minutters enkelt eksekvering, hvis du nogensinde synker hundredtusinder af kontakter på én gang, batch dem i mindre stykker (scriptet ovenfor gør det allerede via BATCH_SIZE).

Håndtering af importen asynkront

Brevos import-endpoint er asynkront: du får et processId tilbage med det samme, og selve importen kører server-side. For de fleste sheet-synker er det fint, fyr og glem, Brevo sender en summary-e-mail, når hver batch er færdig.

Hvis du vil blokere, indtil importen virkelig er færdig, poll process status-endpointet:

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 Scripts ækvivalent til en blokerende vent. Sov ikke for længe, du har 6 minutter total pr. eksekvering.

Tilføj en notify-webhook

Et renere mønster end polling: deploy dit Apps Script som en Web App og send dets URL som notifyUrl. Brevo POSTer til den, når importen er færdig.

// 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, sæt “Who has access” til Anyone, kopiér den resulterende URL og send den som notifyUrl i din import-payload:

payload.notifyUrl = 'https://script.google.com/macros/s/AKfy.../exec';

Nu poster Brevo resultatet tilbage til dit Sheets eget script, lukker loopet uden ekstern infrastruktur.

Troubleshooting

400 Bad Request med error: "Attribute X not found". En kolonne i dit Sheet mappes til en attribut, Brevo ikke kender. Enten omdøb Sheet-kolonnen til at matche en eksisterende attribut, eller opret attributten i Brevo (Contacts → Settings → Contact attributes).

401 Unauthorized. API-nøglen er forkert eller udløbet. Kør Configure API key igen, indsæt en frisk nøgle fra Brevos dashboard.

429 Too Many Requests. Du rammer Brevos rate limit. Import-endpointet tillader omkring 30 kald pr. minut. Hvis du batcher aggressivt, tilføj Utilities.sleep(2000) mellem batches i loopet.

Scriptet kører stille ikke på tidsplanen. Tjek Triggers i Apps Script-editoren. Hvis en trigger fejler gentagne gange, deaktiverer Google den. Klik ind i triggeren for at se fejlårsagen, normalt et permissions-problem, du kan re-autorisere.

Brevo-menuen dukkede ikke op. onOpen kører kun, når du (gen)åbner Sheet’et fra bunden. Genindlæs browser-fanen.

Permissions-pop-up bliver ved med at komme tilbage. Du har sandsynligvis redigeret scriptets scopes (tilføjet en ny Google-tjeneste). Apps Script genprompter for autorisation, hver gang de krævede tilladelser ændrer sig. Kør hvilken som helst funktion én gang fra editoren for at trigge prompten og godkende.

Hvorfor det slår Zapier og venner

Apps Script er gratis, lever inde i Googles infrastruktur og har direkte adgang til Sheets data, ingen række-for-række event-firing, ingen pr-task-pricing, ingen rate limits ud over Googles kvota (som er generøs for denne slags job). Bagsiden: du forpligter dig til at skrive og vedligeholde et lille stykke kode. For en kontaktsynk er det ca. 100 linjer og dybest set nul løbende vedligeholdelse.

Par det med en daglig trigger og et ark, dit salgsteam allerede opdaterer, og du har en kontakt-pipeline til Brevo med nul tilbagevendende arbejde.

Yderligere læsning

Frequently Asked Questions

Skal jeg bruge en server eller infrastruktur for at synke et Google Sheet til Brevo?
Nej. Google Apps Script kører inde i Google Sheets selv. Du skriver en funktion, gemmer projektet, og Google hoster og kører det. Scriptet kan ramme Brevos API direkte via UrlFetchApp.
Hvor opbevarer jeg Brevo API-nøglen?
Brug PropertiesService.getScriptProperties(). Det er en Google-administreret nøgle/værdi-store knyttet til Apps Script-projektet. Skriv ikke nøglen direkte i kildekoden, samarbejdspartnere på arket vil kunne se den.
Hvordan kører jeg dette automatisk hver dag?
Åbn Apps Script → Triggers → Add Trigger. Vælg syncSheetToBrevo-funktionen, vælg „Time-driven”, og sæt en daglig/timebaseret kadence. Googles kvota er 90 minutter/dag samlet Apps Script-køretid på free tier, rigeligt til en kontaktsynk.
Er der en rækkegrænse?
Brevos import-endpoint accepterer ~10 MB inline JSON. Det er ca. 30.000 til 50.000 kontakter afhængigt af, hvor mange attributter hver har. Apps Scripts UrlFetchApp kan sende en 50 MB payload, så flaskehalsen er Brevo, ikke Apps Script. Til større jobs batch rækkerne.
Start gratis med Brevo