Jak vytvořit Google Apps Script pro import kontaktů z listu do Brevo

Posílejte kontakty z Google Sheet do Brevo automaticky. Kompletní Apps Script s úložištěm API klíče, spuštěním při editaci, časovými triggery, vlastním menu a ošetřením chyb. Bez serveru.

Featured image for article: Jak vytvořit Google Apps Script pro import kontaktů z listu do Brevo

Pokud váš tým již žije v Google Sheet, leady z prodeje, registrace na akce, partnerský kontaktní seznam, dostat ta data do Brevo nemusí znamenat exportování CSV a ruční opětovný import. Google Apps Script vám umožňuje propojit Sheet přímo s API Brevo. Skript běží uvnitř infrastruktury Googlu, takže není co hostovat, deployovat ani opečovávat.

Tato příručka prochází funkčním skriptem: vlastní položka menu Sync to Brevo ve vašem Sheet, automatický hodinový trigger, bezpečné úložiště API klíče, dávkové zpracování a trochu strukturovaného logování, abyste věděli, co se stalo.

Co potřebujete

  • Google Sheet s kontakty (jeden řádek na kontakt, nejprve hlavička)
  • Brevo účet a API klíč (Settings → SMTP & API → API Keys)
  • Číselné ID seznamu Brevo, do kterého chcete kontakty přidat

To je vše. Žádný npm, žádný Python, žádný server.

Rozvržení listu

Skript v této příručce očekává hlavičkový řádek následovaný jedním kontaktem na řádek. Sloupce se mapují podle názvu hlavičky na atributy Brevo:

emailfirstNamelastNamecompanycity
[email protected]JaneDoeAcmeBerlin
[email protected]JohnSmithGlobexParis

email je povinný a párování probíhá nezávisle na velikosti písmen. Vše ostatní jde do Brevo jako atribut kontaktu. Vlastní atributy (cokoli mimo standardní jako FIRSTNAME, LASTNAME) musí ve vašem Brevo účtu nejprve existovat. Definujte je pod Contacts → Settings → Contact attributes nebo přes API Brevo.

Otevřete editor Apps Script

Ve svém Sheet: Extensions → Apps Script. Otevře se nová záložka s prázdným Code.gs. Nahraďte obsah skriptem níže.

Plný skript

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 je celé. Uložte to (⌘S / Ctrl+S), pojmenujte projekt třeba „Brevo sync” a vraťte se zpět do svého Sheet.

První spuštění

Načtěte Sheet znovu, nahoře se objeví nové menu Brevo.

  1. Klikněte Brevo → Configure API key, vložte svůj xkeysib-... klíč, klikněte OK.
  2. Klikněte Brevo → Sync sheet to Brevo. Google si poprvé vyžádá oprávnění:
    • „View and manage your spreadsheets”, potřebné ke čtení řádků
    • „Connect to an external service”, potřebné k volání api.brevo.com
  3. Schvalte. Skript běží. Zelený toast vpravo dole vám řekne, kolik kontaktů odešlo.

Pokud selže, klikněte Extensions → Apps Script → View → Logs pro zobrazení status kódu z Brevo pro každou dávku. Nejčastější selhání je 400 kvůli chybějícímu vlastnímu atributu, viz sekce řešení problémů níže.

Spouštění podle plánu

V editoru Apps Script: Triggers (ikona hodin v levém panelu) → Add Trigger.

  • Choose function: syncSheetToBrevo
  • Event source: Time-driven
  • Type: Hour timer (nebo Day timer pro synchronizaci jednou denně)
  • Interval: každou hodinu (nebo cokoli, co vám sedí)

Uložte. Google bude funkci spouštět v tomto rytmu navždy, žádný server, žádný cron, žádná údržba.

Můžete také použít From spreadsheet → On edit, pokud chcete, aby každá změna buňky spustila synchronizaci. Pozor na to, i kosmetické úpravy spustí trigger, což může na rušných listech rychle vyčerpat denní kvótu Apps Script. Hodinový časový trigger je téměř vždy správnou odpovědí.

Kvóty Apps Script, o kterých byste měli vědět

Bezplatná úroveň Apps Script má limity, které stojí za respektování:

LimitHodnota (bezplatná úroveň)
Celkový čas spuštění za den90 minut
Čas jednoho spuštění6 minut
Volání UrlFetchApp za den20 000
Velikost payloadu UrlFetchApp50 MB
Velikost hlaviček UrlFetchApp8 KB
Triggery na uživatele a skript20

Pro typickou synchronizaci kontaktů (pár tisíc kontaktů, hodinově) jste daleko od kteréhokoli z těchto limitů. Jediný, na který si dát pozor, je 6 minut na jedno spuštění, pokud někdy synchronizujete stovky tisíc kontaktů najednou, dávkujte je do menších kousků (skript výše to již dělá přes BATCH_SIZE).

Asynchronní zpracování importu

Import endpoint Brevo je asynchronní: dostanete processId okamžitě a samotný import běží na straně serveru. Pro většinu synchronizací listu je to v pohodě, pošli a zapomeň, Brevo pošle souhrn e-mailem, až každá dávka skončí.

Pokud chcete blokovat, dokud není import opravdu hotový, dotazujte se na endpoint stavu 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 je ekvivalent blokujícího čekání v Apps Script. Nespěte příliš dlouho, máte celkem 6 minut na spuštění.

Přidání notify webhooku

Čistší vzor než dotazování: nasaďte svůj Apps Script jako Web App a předejte jeho URL jako notifyUrl. Brevo na ni POSTne, až import skončí.

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

Nasaďte: Deploy → New deployment → Web app, nastavte „Who has access” na Anyone, zkopírujte výslednou URL a předejte ji jako notifyUrl v payloadu importu:

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

Teď Brevo posílá výsledek zpět do skriptu vašeho Sheet, uzavírá smyčku bez externí infrastruktury.

Řešení problémů

400 Bad Request s error: "Attribute X not found". Sloupec ve vašem Sheet se mapuje na atribut, který Brevo nezná. Buď přejmenujte sloupec v Sheet, aby odpovídal existujícímu atributu, nebo atribut vytvořte v Brevo (Contacts → Settings → Contact attributes).

401 Unauthorized. API klíč je špatný nebo expirovaný. Spusťte znovu Configure API key, vložte čerstvý klíč z dashboardu Brevo.

429 Too Many Requests. Naráží to na rate limit Brevo. Import endpoint umožňuje asi 30 volání za minutu. Pokud agresivně dávkujete, přidejte Utilities.sleep(2000) mezi dávky ve smyčce.

Skript se tiše nespouští podle plánu. Zkontrolujte Triggers v editoru Apps Script. Pokud trigger opakovaně selhává, Google ho zakáže. Klikněte do triggeru, abyste viděli důvod selhání, obvykle je to problém s oprávněním, který můžete znovu autorizovat.

Menu Brevo se neobjevilo. onOpen se spouští pouze, když Sheet (znovu)otevřete od nuly. Načtěte záložku prohlížeče znovu.

Vyskakovací okno s oprávněními se stále vrací. Pravděpodobně jste editovali scopes skriptu (přidali jste novou Google službu). Apps Script si znovu žádá autorizaci pokaždé, když se požadovaná oprávnění změní. Spusťte jakoukoli funkci jednou z editoru, abyste vyvolali výzvu a schválili ji.

Proč to vyhrává nad Zapierem a spol.

Apps Script je zdarma, žije uvnitř infrastruktury Googlu a má přímý přístup k datům Sheet, žádné spouštění událostí řádek po řádku, žádné ceny za úkol, žádné rate limity kromě kvóty Googlu (která je pro tento druh práce štědrá). Druhá strana mince: zavazujete se psát a udržovat malý kus kódu. Pro synchronizaci kontaktů je to asi 100 řádků a v podstatě nulová průběžná údržba.

Spárujte to s denním triggerem a listem, který váš obchodní tým již aktualizuje, a máte pipeline kontaktů do Brevo s nulovou opakující se prací.

Další čtení

Frequently Asked Questions

Potřebuji server nebo nějakou infrastrukturu pro synchronizaci Google Sheet s Brevo?
Ne. Google Apps Script běží přímo v Google Sheets. Napíšete funkci, projekt uložíte a Google ho hostuje a spouští. Skript může zasáhnout API Brevo přímo pomocí UrlFetchApp.
Kam uložit API klíč Brevo?
Použijte PropertiesService.getScriptProperties(). Je to klíč/hodnota úložiště spravované Googlem a vázané na projekt Apps Script. Nezapisujte klíč napevno do zdroje, spolupracovníci na listu by ho viděli.
Jak to spustit automaticky každý den?
Otevřete Apps Script → Triggers → Add Trigger. Vyberte funkci syncSheetToBrevo, zvolte „Time-driven” a nastavte denní/hodinovou kadenci. Kvóta Googlu je 90 minut/den celkového času spuštění Apps Script na bezplatné úrovni, na synchronizaci kontaktů víc než dost.
Existuje limit počtu řádků?
Import endpoint Brevo přijímá ~10 MB inline JSON. To je zhruba 30 000 až 50 000 kontaktů podle počtu atributů na každém. UrlFetchApp Apps Script umí poslat 50 MB payload, takže úzkým hrdlem je Brevo, ne Apps Script. Pro větší úlohy řádky dávkujte.
Začněte zdarma s Brevo