Na tej stronie
Arkusz z dwunastoma zapisami na wydarzenie łatwo wyeksportować raz. Przy drugim imporcie jeden adres występuje dwukrotnie, a inna osoba wypisała się już w Brevo. Powtarzalny import musi uwzględnić oba przypadki.
Skrypt korzysta z endpointów Brevo do odczytu, tworzenia i aktualizacji kontaktów. Służy do małego, planowanego importu, nie do masowego ładowania ani jako natywny konektor Google Sheets. Nie wysyła wiadomości. Za dowód zgody i utrzymanie skryptu odpowiadasz Ty.
Przygotuj arkusz i listę
Utwórz kartę Contacts z dokładnie takimi nagłówkami. Użyj pól wyboru w kolumnie consent; ustaw TRUE tylko przy zgodzie na zamierzone działania marketingowe.
| firstName | lastName | consent | |
|---|---|---|---|
| [email protected] | Jane | Doe | TRUE |
| [email protected] | Alex | Lee | FALSE |
Utwórz lub wybierz listę Brevo i wpisz jej numeryczne ID jako LIST_ID. Sprawdź, czy atrybuty FNAME i LNAME istnieją w Brevo; tylko te pola są mapowane. Zachowaj poza arkuszem informację, kiedy i jak uzyskano zgodę, jeśli arkusz tego nie dowodzi. Sama komórka TRUE nie jest dowodem.
W Rozszerzenia → Apps Script → Ustawienia projektu → Właściwości skryptu dodaj BREVO_API_KEY. Właściwości są wspólne dla użytkowników skryptu, więc ogranicz prawo edycji. Nie umieszczaj klucza w komórce ani w kodzie. Przeczytaj zasady dostępu Google.
Skrypt
Zastąp Code.gs tym przykładem. Przed uruchomieniem ustaw LIST_ID.
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); }}Zapisz projekt, wybierz syncSheetToBrevo i kliknij Uruchom. Google poprosi o dostęp do Sheets i żądań zewnętrznych. W Wykonaniach zobaczysz końcowy zapis {written, skipped}. Odczyt 404 prowadzi do utworzenia kontaktu, a 200 do aktualizacji. Inne błędy odczytu zatrzymują proces. Udany zapis potwierdza żądanie kontaktowe, nie wysłanie kampanii.
Skrypt usuwa powtórzone adresy w obrębie jednego przebiegu i przy następnym ponownie odpytuje Brevo. Nigdy nie ustawia emailBlacklisted na false ani nie zmienia adresu. Jeśli Brevo zgłasza blokadę lub wypisanie z tej listy, wiersz jest pomijany nawet przy TRUE w arkuszu. Nie używaj tego kodu do cofania wypisania. Brevo ostrzega, że zmiana adresu zablokowanego kontaktu może usunąć blokadę.
Sprawdź przebieg przed zaplanowaniem
Użyj listy testowej i kontaktów pod Twoją kontrolą. Uruchom skrypt z wierszem TRUE, wierszem FALSE i powtórzonym adresem: oczekuj jednego zapisu i dwóch pominięć. Wypisz kontakt testowy w Brevo i uruchom ponownie: nie powinien być zapisany. Na próbę błędu ustaw chwilowo błędny LIST_ID, odczytaj błąd HTTP w Wykonaniach, przywróć ID i powtórz. Nie testuj na prawdziwych danych klientów. Kod sprawdzono także lokalnie na symulowanych odpowiedziach Brevo, bez zapisu live.
Aby zaplanować przebiegi, wybierz Apps Script → Wyzwalacze → Dodaj wyzwalacz, funkcję syncSheetToBrevo i interwał czasowy. Wyzwalacze działają z konta osoby, która je utworzyła. Konto musi zachować dostęp do arkusza. Zacznij od uruchomienia raz dziennie i przeglądaj błędy. Google ogranicza czas wykonania i wywołania URL Fetch; duże źródła dziel na mniejsze zadania.
Przy 429 skrypt czyta x-sib-ratelimit-reset, ponawia najwyżej dwa razy i zwraca 429 do późniejszego przebiegu, jeśli wymagane oczekiwanie przekracza 60 sekund. Inne błędy kończą przebieg; następny zaczyna od początku. Kontakt może wypisać się między odczytem a zapisem. Ten przykład nadaje się tylko wtedy, gdy akceptujesz to krótkie okno. Gdy wymagasz mocniejszej gwarancji, potrzebujesz uzgadniania zdarzeń, źródła prawdy o zgodzie i końcowej kontroli przed wysyłką.
Przy większych arkuszach asynchroniczny endpoint importu Brevo zwraca processId: oznacza przyjęcie zadania, nie jego zakończenie. Sprawdź wynik procesu przed ogłoszeniem sukcesu. W pokazanej ścieżce błędy są widoczne przy konkretnym kontakcie.
Najczęściej Zadawane Pytania
Czy potrzebujesz natywnego konektora Google Sheets?
Nie. Przykład działa w projekcie Google Apps Script powiązanym z arkuszem i wywołuje API kontaktów Brevo. Samodzielnie utrzymujesz skrypt, klucz API i zaplanowany wyzwalacz.
Które wiersze trafiają do Brevo?
Tylko te z wartością TRUE w kolumnie consent i poprawnym adresem e-mail. Skrypt usuwa duplikaty w każdym przebiegu i sprawdza Brevo przed zapisem.
Czy ponowny przebieg zapisze wypisaną osobę?
Skrypt pomija kontakty oznaczone w Brevo jako emailBlacklisted lub wypisane z listy docelowej. Nigdy nie wysyła emailBlacklisted: false ani nie zmienia adresu e-mail. Przed produkcją sprawdź także inne źródła zgody.
Jak wznowić nieudany przebieg?
Sprawdź błąd w wykonaniach Apps Script, usuń przyczynę i ponownie uruchom tę samą funkcję. Zapisy używają tego samego adresu i ID listy, więc wcześniej obsłużony wiersz nie tworzy drugiego kontaktu.