Ett ark med tolv evenemangsanmälningar är lätt att exportera en gång. Vid nästa import finns en adress två gånger och en annan person har avregistrerat sig i Brevo. En upprepningsbar import måste känna igen båda.
Skriptet använder Brevos API för att läsa, skapa och uppdatera kontakter. Det är avsett för små, schemalagda importer, inte massinläsning eller som en inbyggd Google Sheets-anslutning. Det skickar ingen e-post. Du ansvarar för samtyckesbevis och drift.
Förbered arket och listan
Skapa en flik Contacts med exakt dessa rubriker. Använd kryssrutor för consent och sätt TRUE endast när du har tillstånd för den avsedda marknadsföringen.
| firstName | lastName | consent | |
|---|---|---|---|
| [email protected] | Jane | Doe | TRUE |
| [email protected] | Alex | Lee | FALSE |
Skapa eller välj en Brevo-lista och ange dess numeriska ID som LIST_ID i koden. Kontrollera att kontaktattributen FNAME och LNAME finns i Brevo; inga andra fält mappas. Spara underlag för när och hur samtycket gavs om arket inte kan visa det. En TRUE-cell är inget bevis.
Öppna Tillägg → Apps Script → Projektinställningar → Skriptegenskaper och lägg till BREVO_API_KEY. Skriptegenskaper delas av skriptets användare. Begränsa vem som får redigera projektet. Lägg inte nyckeln i en cell eller källkod. Läs Googles åtkomstregler.
Skriptet
Ersätt Code.gs med exemplet och ange LIST_ID före första körningen.
const LIST_ID = 42; // Replace with your existing Brevo list ID.const TAB = 'Contacts';const BASE = 'https://api.brevo.com/v3';
function syncSheetToBrevo() { const key = PropertiesService.getScriptProperties().getProperty('BREVO_API_KEY'); if (!key) throw new Error('Set BREVO_API_KEY in Script Properties first'); const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(TAB); if (!sheet) throw new Error('Contacts tab missing'); const values = sheet.getDataRange().getValues(); const headers = values.shift().map(h => String(h).trim()); const columns = ['email', 'firstName', 'lastName', 'consent'] .map(name => headers.indexOf(name)); if (columns.includes(-1)) throw new Error('Required header missing'); const [emailCol, firstCol, lastCol, consentCol] = columns; const seen = new Set(); const result = { written: 0, skipped: 0 };
for (const row of values) { const email = String(row[emailCol] || '').trim().toLowerCase(); if (row[consentCol] !== true || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || seen.has(email)) { result.skipped++; continue; } seen.add(email); const path = '/contacts/' + encodeURIComponent(email); const current = brevo_(key, path, 'get'); if (current.code !== 200 && current.code !== 404) { throw new Error(`Contact read failed (${current.code}) for row ${result.written + result.skipped + 2}`); } if (current.code === 200) { const contact = JSON.parse(current.text); if (contact.emailBlacklisted || (contact.listUnsubscribed || []).includes(LIST_ID)) { result.skipped++; continue; } } const attributes = {}; if (row[firstCol] !== '') attributes.FNAME = String(row[firstCol]); if (row[lastCol] !== '') attributes.LNAME = String(row[lastCol]); const body = { attributes, listIds: [LIST_ID] }; const saved = current.code === 404 ? brevo_(key, '/contacts', 'post', { email, ...body }) : brevo_(key, path, 'put', body); if (![201, 204].includes(saved.code)) { throw new Error(`Contact write failed (${saved.code}) for ${email}: ${saved.text.slice(0, 200)}`); } result.written++; } Logger.log(JSON.stringify(result)); return result;}
function brevo_(key, path, method, body) { for (let attempt = 0; attempt < 3; attempt++) { const response = UrlFetchApp.fetch(BASE + path, { method, contentType: 'application/json', headers: { 'api-key': key }, payload: body ? JSON.stringify(body) : undefined, muteHttpExceptions: true, }); const code = response.getResponseCode(); if (code !== 429) return { code, text: response.getContentText() }; if (attempt === 2) return { code, text: response.getContentText() }; const reset = Object.entries(response.getHeaders()).find( ([name]) => name.toLowerCase() === 'x-sib-ratelimit-reset' ); const raw = Number(reset && reset[1]); const seconds = Number.isFinite(raw) && raw > 0 ? raw : 2 ** attempt; const waitSeconds = seconds + 2 ** attempt; if (waitSeconds > 60) return { code, text: response.getContentText() }; Utilities.sleep(waitSeconds * 1000); }}Spara, välj syncSheetToBrevo och klicka på Kör. Google begär åtkomst till Sheets och externa anrop. Under Körningar ser du slutloggen {written, skipped}. Ett lässvar 404 leder till att kontakten skapas; 200 till att den uppdateras. Andra läsfel stoppar körningen. En lyckad skrivning bekräftar kontaktanropet, inte att någon kampanj skickats.
Skriptet tar bort dubbla adresser i samma körning och frågar Brevo på nytt vid nästa. Det sätter aldrig emailBlacklisted till false och ändrar inte e-postadressen. Om Brevo visar en allmän spärr eller avregistrering från listan hoppas raden över även med TRUE i arket. Använd inte skriptet för att ångra en avregistrering. Brevo varnar för att en ändrad adress kan häva spärren på en blockerad kontakt.
Kontrollera en körning före schemaläggning
Använd en testlista och kontakter du själv kontrollerar. Kör med en TRUE-rad, en FALSE-rad och en dubbel adress: vänta dig en skrivning och två överhoppade rader. Avregistrera testkontakten i Brevo och kör igen: kontakten ska inte skrivas. Testa ett fel genom att tillfälligt ange ett ogiltigt LIST_ID, kontrollera HTTP-felet under Körningar, återställ ID och kör igen. Använd inte riktiga kunddata. Koden har också testats lokalt med simulerade Brevo-svar, utan live-skrivning.
För schemaläggning väljer du Apps Script → Utlösare → Lägg till utlösare, syncSheetToBrevo och ett tidsintervall. Installerade utlösare körs under skaparens konto. Kontot måste behålla åtkomst till arket. Börja med daglig körning och granska misslyckanden. Google begränsar körtid och URL Fetch-anrop; dela stora källor i begränsade jobb.
Vid 429 läser skriptet x-sib-ratelimit-reset, försöker högst två gånger till och returnerar 429 för en senare körning om väntan behöver bli längre än 60 sekunder. Andra fel stoppar körningen; nästa börjar överst. En kontakt kan avregistrera sig mellan läsning och skrivning. Använd exemplet bara om det korta tidsfönstret är godtagbart. För striktare garantier behövs avstämning via händelser, en auktoritativ samtyckeskälla och en sista kontroll före utskick.
För större ark returnerar Brevos asynkrona import-API ett processId. Det bekräftar att jobbet tagits emot, inte att det är klart. Kontrollera resultatet innan du kallar importen lyckad. Med vägen per kontakt syns felet på den berörda raden.
Vanliga Frågor
Behöver jag en inbyggd Google Sheets-anslutning?
Nej. Exemplet körs i ett Google Apps Script-projekt som är kopplat till arket och anropar Brevos kontakt-API. Du underhåller själv skriptet, API-nyckeln och den schemalagda utlösaren.
Vilka rader skickas till Brevo?
Bara rader med TRUE i consent och en giltig e-postadress. Skriptet tar bort dubbletter vid varje körning och kontrollerar Brevo före skrivning.
Registrerar en ny körning en avregistrerad person igen?
Skriptet hoppar över kontakter som Brevo markerar som emailBlacklisted eller avregistrerade från mållistan. Det skickar aldrig emailBlacklisted: false och ändrar inte e-postadressen. Kontrollera andra samtyckeskällor före produktion.
Hur upprepar jag en misslyckad körning?
Läs felet i Apps Script-körningar, åtgärda orsaken och kör samma funktion igen. Skrivningarna använder samma adress och list-ID, så en tidigare lyckad rad skapar inte en andra kontakt.