Импорт контактов из Google Sheets в Brevo через Apps Script

Повторяемый пример Apps Script: проверка согласия, сопоставление полей, сохранение отписок и явные ошибки API.

google apps script brevo

Каков краткий ответ?

Создайте столбцы email, firstName, lastName и consent. Сохраните BREVO_API_KEY в Script Properties, задайте ID списка в коде и запустите syncSheetToBrevo. Скрипт обрабатывает только строки с согласием, проверяет отписки Brevo, удаляет дубли и останавливается при ошибке API. Это управляемый Вами Apps Script, а не встроенный коннектор.

Узнать больше
На этой странице

Один раз выгрузить двенадцать регистраций на мероприятие легко. При втором импорте один адрес может повториться, а другой человек уже отписался в Brevo. Повторяемый импорт обязан учитывать оба случая.

Скрипт использует эндпоинты Brevo для чтения, создания и обновления контактов. Он рассчитан на небольшие запланированные импорты, не является встроенным коннектором Google Sheets или массовым загрузчиком. Письма не отправляются. Доказательства согласия и работу скрипта обеспечиваете Вы.

Подготовьте таблицу и список

Создайте вкладку Contacts строго с этими заголовками. Для consent используйте флажок и ставьте TRUE только при согласии на конкретный маркетинг.

emailfirstNamelastNameconsent
[email protected]JaneDoeTRUE
[email protected]AlexLeeFALSE

Создайте или выберите список Brevo и впишите его числовой ID в LIST_ID. Убедитесь, что в Brevo существуют атрибуты FNAME и LNAME: только они сопоставляются. Если таблица не доказывает, когда и как было дано согласие, храните запись отдельно. Ячейка TRUE сама по себе доказательством не служит.

В Extensions → Apps Script → Project Settings → Script Properties добавьте BREVO_API_KEY. Свойства общие для пользователей скрипта, поэтому ограничьте редактирование. Не помещайте ключ в ячейку или код. См. правила доступа Google.

Скрипт

Замените Code.gs этим примером. До первого запуска задайте 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);
}
}

Сохраните, выберите syncSheetToBrevo и нажмите Run. Google запросит доступ к Sheets и внешним запросам. В Executions найдите итоговую запись {written, skipped}. Ответ 404 при чтении ведёт к созданию контакта, 200 к обновлению. Другие ошибки останавливают запуск. Успешная запись контакта не означает отправку кампании.

Скрипт удаляет повторы адресов внутри одного запуска, а при следующем снова проверяет Brevo. Он не задаёт emailBlacklisted: false и не меняет email. Если Brevo сообщает о блокировке или отписке от целевого списка, строка пропускается даже при TRUE. Не используйте скрипт для отмены отписки. Brevo предупреждает: смена email заблокированного контакта может снять блокировку.

Проверьте один запуск перед расписанием

Используйте тестовый список и свои контакты. Одна строка TRUE, одна FALSE и повтор адреса должны дать одну запись и два пропуска. Отпишите тестовый контакт в Brevo и повторите: для него записи быть не должно. Для проверки ошибки временно задайте неверный LIST_ID, увидьте HTTP-ошибку в Executions, восстановите ID и повторите. Не проверяйте на реальных клиентах. Код локально испытан на имитации ответов Brevo, без живой записи.

Для расписания откройте Apps Script → Triggers → Add Trigger, выберите syncSheetToBrevo и интервал. Триггер запускается от аккаунта создателя, которому нужен постоянный доступ к таблице. Для маленькой таблицы начните с ежедневного запуска и следите за ошибками. Google ограничивает время выполнения и URL Fetch; большие источники разделяйте на ограниченные задания.

После 429 скрипт читает x-sib-ratelimit-reset и повторяет не более двух раз. Если ждать нужно больше 60 секунд, он возвращает 429 для последующего запуска. Другие ошибки также останавливают его; следующий запуск начинается сначала. Контакт может отписаться между чтением и записью. Этот пример подходит только если короткое окно допустимо. Для строгой защиты нужны сверка событий, надёжное хранилище согласий и последняя проверка перед отправкой.

Для крупных таблиц асинхронный импорт Brevo возвращает processId. Он подтверждает принятие задачи, не завершение. Проверяйте результат. Обработка по контактам здесь показывает строку ошибки.

Часто Задаваемые Вопросы

Нужен ли встроенный коннектор Google Sheets?

Нет. Пример работает в привязанном к таблице проекте Apps Script и вызывает API контактов Brevo. Скрипт, ключ и запланированный триггер поддерживаете Вы.

Какие строки передаются в Brevo?

Только строки с consent TRUE и допустимым email. Скрипт удаляет дубли при каждом запуске и перед записью проверяет Brevo.

Повторный запуск подпишет отписавшегося контакта?

Скрипт пропускает контакты, которые Brevo пометил emailBlacklisted или отписал от целевого списка. Он не передаёт emailBlacklisted: false и не меняет адрес. Перед рабочим запуском проверьте и другие источники согласия и блокировок.

Как повторить неудачный запуск?

Прочитайте ошибку в выполнениях Apps Script, устраните причину и запустите ту же функцию. Тот же email и ID списка позволяют обновить ранее успешную строку без создания второго контакта.

Подайте заявку на ранний доступ

Укажите имя и рабочий адрес электронной почты. Мы отправим ссылку для подтверждения запроса на доступ к Tajo.

Получить Brevo