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.

Featured image for article: Jak zbudować Google Apps Script, aby zaimportować kontakty z arkusza do Brevo

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:

emailfirstNamelastNamecompanycity
[email protected]JaneDoeAcmeBerlin
[email protected]JohnSmithGlobexParis

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

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

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.

  1. Kliknij Brevo → Configure API key, wklej swój klucz xkeysib-..., kliknij OK.
  2. 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
  3. 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:

LimitWartość (darmowa warstwa)
Całkowity czas działania na dzień90 minut
Czas pojedynczego wykonania6 minut
Wywołania UrlFetchApp na dzień20 000
Rozmiar payload UrlFetchApp50 MB
Rozmiar nagłówków UrlFetchApp8 KB
Wyzwalacze na użytkownika na skrypt20

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

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

Dalsza lektura

Frequently Asked Questions

Czy potrzebuję serwera lub jakiejkolwiek infrastruktury, aby zsynchronizować Google Sheet z Brevo?
Nie. Google Apps Script działa wewnątrz samego Google Sheets. Piszesz funkcję, zapisujesz projekt, a Google hostuje go i uruchamia. Skrypt może bezpośrednio trafić w API Brevo używając UrlFetchApp.
Gdzie przechowuję klucz API Brevo?
Użyj PropertiesService.getScriptProperties(). To zarządzana przez Google magazyn klucz/wartość ograniczony do projektu Apps Script. Nie hardcoduj klucza w kodzie źródłowym; współpracownicy na arkuszu mogliby go zobaczyć.
Jak uruchomić to automatycznie codziennie?
Otwórz Apps Script → Triggers → Add Trigger. Wybierz funkcję syncSheetToBrevo, wybierz 'Time-driven' i ustaw cadans dzienny/godzinny. Kwota Google to 90 minut/dzień całkowitego czasu wykonywania Apps Script na darmowej warstwie. Mnóstwo dla synchronizacji kontaktów.
Czy istnieje limit wierszy?
Endpoint importu Brevo akceptuje około 10 MB inline JSON. To około 30 000 do 50 000 kontaktów, w zależności od tego, ile atrybutów ma każdy. UrlFetchApp Apps Script może wysłać payload 50 MB, więc wąskim gardłem jest Brevo, a nie Apps Script. Dla większych zadań pakuj wiersze.
Zacznij za darmo z Brevo