Jak zbudować Google Apps Script, aby zaimportować kontakty z arkusza do Brevo
Wysyłaj kontakty z Google Sheet do Brevo automatycznie. Kompletny Apps Script z przechowywaniem klucza API, run-on-edit, wyzwalaczami czasowymi, niestandardowym menu i obsługą błędów. Bez serwera.
Jeśli Twój zespół już żyje w Google Sheet (sales leads, zapisy na wydarzenia, lista kontaktów partnerów), dostarczanie tych danych do Brevo nie musi obejmować eksportowania CSV i ręcznego ich ponownego importowania. Google Apps Script pozwala podłączyć Sheet bezpośrednio do API Brevo. Skrypt działa wewnątrz infrastruktury Google, więc nie ma nic do hostowania, deployowania ani pilnowania.
Ten przewodnik omawia działający skrypt: niestandardowy element menu Sync to Brevo w Twoim Sheet, automatyczny godzinny wyzwalacz, bezpieczne przechowywanie klucza API, obsługę batchy i odrobinę uporządkowanego logowania, abyś mógł zobaczyć, co się stało.
Czego potrzebujesz
- Google Sheet z kontaktami (jeden wiersz na kontakt, najpierw wiersz nagłówka)
- Konto Brevo i klucz API (Settings → SMTP & API → API Keys)
- Numeryczny ID listy Brevo, do której chcesz dodać kontakty
To wszystko. Bez npm, bez Pythona, bez serwera.
Układ arkusza
Skrypt w tym przewodniku oczekuje wiersza nagłówka, po którym następuje jeden kontakt na wiersz. Kolumny są mapowane przez nazwę nagłówka do atrybutów Brevo:
| firstName | lastName | company | city | |
|---|---|---|---|---|
| [email protected] | Jane | Doe | Acme | Berlin |
| [email protected] | John | Smith | Globex | Paris |
email jest obowiązkowy i jest dopasowywany niezależnie od wielkości liter. Wszystko inne jest wysyłane do Brevo jako atrybut kontaktu. Niestandardowe atrybuty (wszystko poza standardowymi jak FIRSTNAME, LASTNAME) muszą najpierw istnieć w Twoim koncie Brevo. Zdefiniuj je w Contacts → Settings → Contact attributes, lub przez API Brevo.
Otwórz edytor Apps Script
W swoim Sheet: Extensions → Apps Script. Otwiera się nowa karta z pustym Code.gs. Zastąp zawartość poniższym skryptem.
Pełny skrypt
const BREVO_API_BASE = 'https://api.brevo.com/v3';const BREVO_LIST_ID = 42; // <- the Brevo list to import contacts intoconst SHEET_NAME = 'Contacts'; // <- name of the sheet tab to readconst 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;}To cała rzecz. Zapisz to (⌘S / Ctrl+S), nazwij projekt jakoś jak “Brevo sync” i wróć do swojego Sheet.
Pierwsze uruchomienie
Przeładuj Sheet. Nowe menu Brevo pojawia się u góry.
- Kliknij Brevo → Configure API key, wklej swój klucz
xkeysib-..., kliknij OK. - Kliknij Brevo → Sync sheet to Brevo. Google poprosi o uprawnienia za pierwszym razem:
- “View and manage your spreadsheets”, potrzebne do odczytu wierszy
- “Connect to an external service”, potrzebne do wywołania api.brevo.com
- Zatwierdź. Skrypt działa. Zielony toast w prawym dolnym rogu mówi Ci, ile kontaktów zostało wysłanych.
Jeśli się nie powiedzie, kliknij Extensions → Apps Script → View → Logs, aby zobaczyć kod statusu per batch z Brevo. Najczęstszą porażką jest 400 z powodu brakującego niestandardowego atrybutu, zobacz sekcję rozwiązywania problemów poniżej.
Uruchom to według harmonogramu
W edytorze Apps Script: Triggers (ikona zegara w lewym pasku bocznym) → Add Trigger.
- Choose function:
syncSheetToBrevo - Event source: Time-driven
- Type: Hour timer (lub Day timer dla synchronizacji raz dziennie)
- Interval: co godzinę (lub cokolwiek pasuje)
Zapisz. Google uruchomi funkcję na tym cadansie na zawsze, bez serwera, bez crona, bez konserwacji.
Możesz również użyć From spreadsheet → On edit, jeśli chcesz, aby każda zmiana komórki wyzwalała synchronizację. Bądź z tym ostrożny. Nawet kosmetyczne edycje uruchomią wyzwalacz, co może szybko trafić w dzienne kwoty Apps Script na zajętych arkuszach. Godzinny wyzwalacz czasowy jest prawie zawsze właściwą odpowiedzią.
Kwoty Apps Script, które warto znać
Darmowa warstwa Apps Script ma limity warte respektowania:
| Limit | Wartość (darmowa warstwa) |
|---|---|
| Całkowity czas działania na dzień | 90 minut |
| Czas pojedynczego wykonania | 6 minut |
Wywołania UrlFetchApp na dzień | 20 000 |
Rozmiar payload UrlFetchApp | 50 MB |
Rozmiar nagłówków UrlFetchApp | 8 KB |
| Wyzwalacze na użytkownika na skrypt | 20 |
Dla typowej synchronizacji kontaktów (kilka tysięcy kontaktów, godzinnie) jesteś nigdzie blisko żadnego z tych. Jedynym, na który warto uważać, jest 6-minutowe pojedyncze wykonanie. Jeśli kiedykolwiek synchronizujesz setki tysięcy kontaktów za jednym razem, pakuj je w mniejsze chunki (powyższy skrypt już to robi przez BATCH_SIZE).
Asynchroniczna obsługa importu
Endpoint importu Brevo jest asynchroniczny: dostajesz processId natychmiast, a faktyczny import działa po stronie serwera. Dla większości synchronizacji arkuszy to jest w porządku, fire-and-forget, Brevo wyśle e-mail z podsumowaniem, gdy każdy batch się zakończy.
Jeśli chcesz zablokować się aż do faktycznego zakończenia importu, pollujesz endpoint statusu procesu:
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 to odpowiednik Apps Script blokującego oczekiwania. Nie śpij za długo. Masz 6 minut całkowicie na wykonanie.
Dodawanie webhooka powiadomień
Czystszy wzorzec niż polling: wdroż swój Apps Script jako Web App i przekaż jego URL jako notifyUrl. Brevo wyśle do niego POST, gdy import się zakończy.
// Add to Code.gsfunction 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');}Wdrożenie: Deploy → New deployment → Web app, ustaw “Who has access” na Anyone, skopiuj wynikowy URL i przekaż go jako notifyUrl w payload importu:
payload.notifyUrl = 'https://script.google.com/macros/s/AKfy.../exec';Teraz Brevo postuje wynik z powrotem do własnego skryptu Twojego Sheet, zamykając pętlę bez zewnętrznej infrastruktury.
Rozwiązywanie problemów
400 Bad Request z error: "Attribute X not found". Kolumna w Twoim Sheet mapuje się do atrybutu, którego Brevo nie zna. Albo zmień nazwę kolumny Sheet, aby pasowała do istniejącego atrybutu, albo utwórz atrybut w Brevo (Contacts → Settings → Contact attributes).
401 Unauthorized. Klucz API jest zły lub wygasły. Uruchom ponownie Configure API key, wklej świeży klucz z dashboardu Brevo.
429 Too Many Requests. Trafiasz w rate limit Brevo. Endpoint importu pozwala na około 30 wywołań na minutę. Jeśli batchujesz agresywnie, dodaj Utilities.sleep(2000) między batchami w pętli.
Skrypt po cichu nie uruchamia się według harmonogramu. Sprawdź Triggers w edytorze Apps Script. Jeśli wyzwalacz wielokrotnie zawodzi, Google go wyłącza. Kliknij wyzwalacz, aby zobaczyć powód niepowodzenia, zwykle problem z uprawnieniami, który możesz ponownie autoryzować.
Menu Brevo nie pojawiło się. onOpen uruchamia się tylko wtedy, gdy (ponownie) otworzysz Sheet od podstaw. Przeładuj kartę przeglądarki.
Wyskakujące okienko z uprawnieniami ciągle wraca. Prawdopodobnie edytowałeś zakresy skryptu (dodałeś nową usługę Google). Apps Script ponownie pyta o autoryzację za każdym razem, gdy wymagane uprawnienia się zmieniają. Uruchom dowolną funkcję raz z edytora, aby wyzwolić prompt i zatwierdzić.
Dlaczego to bije Zapier i przyjaciół
Apps Script jest darmowy, żyje wewnątrz infrastruktury Google i ma bezpośredni dostęp do danych Sheet. Bez wyzwalania zdarzeń wiersz po wierszu, bez pricing per task, bez rate limits poza kwotą Google (która jest hojna dla tego rodzaju pracy). Druga strona: zobowiązujesz się do napisania i utrzymania małego kawałka kodu. Dla synchronizacji kontaktów to około 100 linii i zasadniczo zerowa bieżąca konserwacja.
Połącz to z dziennym wyzwalaczem i arkuszem, który Twój zespół sprzedaży już aktualizuje, i masz potok kontaktów do Brevo z zerową powtarzalną pracą.