Как да създадете Google Apps Script за импорт на контакти от Sheet в Brevo
Изпращайте контакти от Google Sheet в Brevo автоматично. Пълен Apps Script със съхранение на API ключ, изпълнение при редакция, тригери на база време, персонализирано меню и обработка на грешки. Без сървър.
Ако Вашият екип вече живее в Google Sheet, потенциални клиенти от продажбите, регистрации за събития, списък с партньорски контакти, прехвърлянето на тези данни в Brevo не трябва да минава през експортиране на CSV и ръчно повторно импортиране. Google Apps Script Ви позволява да свържете Sheet директно с API-то на Brevo. Скриптът се изпълнява в инфраструктурата на Google, така че няма какво да хоствате, deploy-вате или наглеждате.
Това ръководство преминава през работещ скрипт: персонализиран елемент в менюто Sync to Brevo във Вашия Sheet, автоматичен почасов тригер, безопасно съхранение на API ключ, обработка на групи и малко структурирано логване, за да можете да разберете какво се е случило.
Какво Ви трябва
- Google Sheet с контакти (един ред на контакт, заглавен ред първо)
- Brevo акаунт и API ключ (Settings → SMTP & API → API Keys)
- Числовият ID на списъка на Brevo, в който искате да се добавят контактите
Това е всичко. Без npm, без Python, без сървър.
Оформление на таблицата
Скриптът в това ръководство очаква заглавен ред, последван от един контакт на ред. Колоните се мапват по име на заглавието към атрибутите на Brevo:
| firstName | lastName | company | city | |
|---|---|---|---|---|
| [email protected] | Jane | Doe | Acme | Berlin |
| [email protected] | John | Smith | Globex | Paris |
email е задължителен и съвпадението му не зависи от регистъра. Всичко друго се изпраща в Brevo като атрибут на контакт. Персонализираните атрибути (всичко извън стандартните като FIRSTNAME, LASTNAME) трябва първо да съществуват във Вашия Brevo акаунт. Дефинирайте ги под Contacts → Settings → Contact attributes или чрез API-то на Brevo.
Отворете Apps Script редактора
Във Вашия Sheet: Extensions → Apps Script. Отваря се нов tab с празен Code.gs. Заменете съдържанието със скрипта по-долу.
Пълният скрипт
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;}Това е всичко. Запазете го (⌘S / Ctrl+S), наименувайте проекта например „Brevo sync” и се върнете към Вашия Sheet.
Първо изпълнение
Презаредете Sheet. Новото меню Brevo се появява в горната част.
- Щракнете Brevo → Configure API key, поставете Вашия
xkeysib-...ключ, щракнете OK. - Щракнете Brevo → Sync sheet to Brevo. Google ще поиска разрешения първия път:
- „View and manage your spreadsheets”, нужно за четене на редовете
- „Connect to an external service”, нужно за извикване на api.brevo.com
- Одобрете. Скриптът се изпълнява. Зелено известие в долния десен ъгъл Ви казва колко контакта са изпратени.
Ако се провали, щракнете Extensions → Apps Script → View → Logs, за да видите кода на статуса от Brevo за всяка група. Най-честият отказ е 400 поради липсващ персонализиран атрибут. Вижте секцията за отстраняване на проблеми по-долу.
Изпълнявайте го по график
В Apps Script редактора: Triggers (иконата с часовник в лявата страна) → Add Trigger.
- Choose function:
syncSheetToBrevo - Event source: Time-driven
- Type: Hour timer (или Day timer за синхронизация веднъж дневно)
- Interval: на всеки час (или каквото Ви подхожда)
Запазете. Google ще изпълнява функцията с тази честота завинаги, без сървър, без cron, без поддръжка.
Можете също да използвате From spreadsheet → On edit, ако искате всяка промяна на клетка да задейства синхронизация. Внимавайте с това. Дори козметични редакции ще задействат тригера, което може да изчерпа дневната квота на Apps Script бързо при натоварени таблици. Почасовият тригер на база време почти винаги е правилният отговор.
Квоти на Apps Script, които си струва да знаете
Безплатният план на Apps Script има лимити, които си струва да се спазват:
| Лимит | Стойност (безплатен план) |
|---|---|
| Общо време за изпълнение на ден | 90 минути |
| Време за едно изпълнение | 6 минути |
Извиквания на UrlFetchApp на ден | 20 000 |
Размер на payload за UrlFetchApp | 50 MB |
Размер на headers за UrlFetchApp | 8 KB |
| Тригери на потребител на скрипт | 20 |
За типична синхронизация на контакти (няколко хиляди контакта на час), изобщо не сте близо до тези лимити. Единственият за наблюдение е 6 минути за едно изпълнение. Ако някога синхронизирате стотици хиляди контакти наведнъж, групирайте ги в по-малки части (скриптът по-горе вече прави това чрез BATCH_SIZE).
Обработка на импорта асинхронно
Импорт endpoint-ът на Brevo е асинхронен: получавате processId веднага, а самото импортиране тече от страна на сървъра. За повечето синхронизации на Sheet това е добре, изпратете и забравете, Brevo ще изпрати имейл с обобщение, когато всяка група приключи.
Ако искате да блокирате, докато импортът наистина не приключи, опитвайте endpoint-а за статус на процеса:
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 е еквивалентът на блокиращо чакане в Apps Script. Не спете прекалено дълго, имате общо 6 минути на изпълнение.
Добавяне на notify webhook
По-чист подход от опитването: разгърнете Вашия Apps Script като Web App и подайте URL-а му като notifyUrl. Brevo ще POST-не на него, когато импортът приключи.
// 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');}Разгърнете: Deploy → New deployment → Web app, задайте „Who has access” на Anyone, копирайте получения URL и го подайте като notifyUrl в payload-а на импорта Ви:
payload.notifyUrl = 'https://script.google.com/macros/s/AKfy.../exec';Сега Brevo POST-ва резултата обратно към собствения скрипт на Вашия Sheet, затваряйки веригата без външна инфраструктура.
Отстраняване на проблеми
400 Bad Request с error: "Attribute X not found". Колона във Вашия Sheet се мапва към атрибут, който Brevo не познава. Или преименувайте колоната в Sheet, за да съвпада със съществуващ атрибут, или създайте атрибута в Brevo (Contacts → Settings → Contact attributes).
401 Unauthorized. API ключът е грешен или е изтекъл. Изпълнете отново Configure API key, поставете нов ключ от таблото на Brevo.
429 Too Many Requests. Достигате лимита на скоростта на Brevo. Импорт endpoint-ът позволява около 30 извиквания в минута. Ако групирате агресивно, добавете Utilities.sleep(2000) между групите в цикъла.
Скриптът не се изпълнява тихо по график. Проверете Triggers в Apps Script редактора. Ако тригер се проваля многократно, Google го деактивира. Влезте в тригера, за да видите причината за провала, обикновено е проблем с разрешения, който можете да преоторизирате.
Менюто Brevo не се появи. onOpen се изпълнява само когато (повторно) отворите Sheet от нулата. Презаредете tab-а на браузъра.
Изскачащият прозорец за разрешения продължава да се появява. Вероятно сте редактирали обхватите на скрипта (добавили сте нова услуга на Google). Apps Script преподканва за оторизация всеки път, когато нужните разрешения се променят. Изпълнете която и да е функция веднъж от редактора, за да задействате подканата и да я одобрите.
Защо това е по-добро от Zapier и компания
Apps Script е безплатен, живее в инфраструктурата на Google и има директен достъп до данните на Sheet, без задействане на събития ред по ред, без таксуване на задача, без лимити на скоростта освен квотата на Google (която е щедра за този вид работа). Обратната страна: ангажирате се с писане и поддръжка на малко парче код. За синхронизация на контакти това са около 100 реда и практически нулева поддръжка.
Комбинирайте това с дневен тригер и таблица, която екипът Ви по продажбите вече обновява, и получавате pipeline на контакти към Brevo с нулева повтаряща се работа.