Como construir um Google Apps Script para importar os contatos de uma planilha para a Brevo

Empurre contatos de uma Planilha Google para a Brevo automaticamente. Um Apps Script completo com armazenamento de API key, execução por edição, triggers de tempo, menu personalizado e tratamento de erros, sem precisar de servidor.

Featured image for article: Como construir um Google Apps Script para importar os contatos de uma planilha para a Brevo

Se seu time já vive numa Planilha Google (leads de vendas, inscrições de eventos, uma lista de contatos de parceiros), levar esses dados para a Brevo não precisa envolver exportar CSVs e re-importar na mão. O Google Apps Script deixa você fiar a Planilha direto na API da Brevo. O script roda dentro da infraestrutura do Google, então não tem nada para hospedar, fazer deploy ou cuidar.

Esta guia passa por um script funcionando: um item de menu personalizado Sync to Brevo na sua Planilha, um trigger automático de hora em hora, armazenamento seguro da API key, manejo de lotes e um pouquinho de log estruturado para você saber o que aconteceu.

O que você precisa

  • Uma Planilha Google com contatos (uma linha por contato, linha de cabeçalho primeiro)
  • Uma conta Brevo e uma API key (Configurações → SMTP & API → API Keys)
  • O ID numérico da lista da Brevo onde você quer que os contatos sejam adicionados

É isso. Sem npm, sem Python, sem servidor.

Layout da planilha

O script desta guia espera uma linha de cabeçalho seguida de um contato por linha. Colunas são mapeadas pelo nome do cabeçalho para atributos da Brevo:

emailfirstNamelastNamecompanycity
[email protected]JaneDoeAcmeBerlin
[email protected]JohnSmithGlobexParis

email é obrigatório e o match é case-insensitive. Todo o resto é mandado para a Brevo como atributo de contato. Atributos personalizados (qualquer coisa além dos padrões como FIRSTNAME, LASTNAME) precisam existir antes na sua conta Brevo, defina eles em Contatos → Configurações → Atributos do contato, ou via API da Brevo.

Abrir o editor do Apps Script

Na sua Planilha: Extensões → Apps Script. Uma nova aba abre com um Code.gs em branco. Substitua o conteúdo pelo script abaixo.

O script completo

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

É isso a coisa toda. Salve (⌘S / Ctrl+S), dê um nome ao projeto tipo “Brevo sync” e volte para sua Planilha.

Primeira execução

Recarregue a Planilha, o novo menu Brevo aparece no topo.

  1. Clique em Brevo → Configure API key, cole sua chave xkeysib-..., clique em OK.
  2. Clique em Brevo → Sync sheet to Brevo. O Google vai pedir permissões na primeira vez:
    • “Ver e gerenciar suas planilhas”, precisa para ler as linhas
    • “Conectar a um serviço externo”, precisa para chamar api.brevo.com
  3. Aprove. O script roda. Um toast verde no canto inferior direito te diz quantos contatos saíram.

Se falhar, clique em Extensões → Apps Script → Visualizar → Logs para ver o código de status por lote da Brevo. A falha mais comum é um 400 por causa de um atributo personalizado faltando, veja a seção de troubleshooting embaixo.

Rode num agendamento

No editor do Apps Script: Triggers (o ícone de relógio na barra lateral esquerda) → Adicionar Trigger.

  • Escolher função: syncSheetToBrevo
  • Fonte do evento: Time-driven
  • Tipo: Hour timer (ou Day timer para uma sincronização uma vez por dia)
  • Intervalo: a cada hora (ou o que encaixar)

Salvar. O Google vai rodar a função nessa cadência para sempre, sem servidor, sem cron, sem manutenção.

Você também pode usar From spreadsheet → On edit se quer que toda mudança de célula dispare uma sincronização. Cuidado com isso, edições até cosméticas vão acionar o trigger, e isso pode bater rápido na cota diária do Apps Script em planilhas movimentadas. O trigger horário de tempo é quase sempre a resposta certa.

Cotas do Apps Script para conhecer

A camada gratuita do Apps Script tem limites que vale respeitar:

LimiteValor (camada gratuita)
Tempo total de execução por dia90 minutos
Tempo de uma execução6 minutos
Chamadas de UrlFetchApp por dia20.000
Tamanho de payload do UrlFetchApp50 MB
Tamanho de headers do UrlFetchApp8 KB
Triggers por usuário por script20

Para uma sincronização típica de contatos (uns poucos milhares de contatos, de hora em hora), você está longe de qualquer um desses. O único para ficar de olho é os 6 minutos de execução única, se você um dia sincronizar centenas de milhares de contatos de uma vez, divida em pedaços menores (o script acima já faz isso via BATCH_SIZE).

Lidando com a importação de forma assíncrona

O endpoint de importação da Brevo é assíncrono: você recebe um processId na hora, e a importação real roda no servidor. Para a maioria das sincronizações de planilha isso está bom, atira e esquece, a Brevo manda email de resumo quando cada lote termina.

Se você quer bloquear até a importação realmente terminar, faça polling no endpoint de status do processo:

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 é o equivalente de uma espera bloqueante no Apps Script. Não durma demais, você tem 6 minutos no total por execução.

Adicionar um webhook de notificação

Um padrão mais limpo que polling: deploy do seu Apps Script como um Web App e passe a URL como notifyUrl. A Brevo vai fazer POST nele quando a importação terminar.

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

Deploy: Deploy → Novo deploy → Web app, defina “Quem tem acesso” para Qualquer pessoa, copie a URL resultante e passe como notifyUrl no payload da importação:

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

Agora a Brevo posta o resultado de volta para o próprio script da Planilha, fechando o loop sem infraestrutura externa.

Troubleshooting

400 Bad Request com error: "Attribute X not found": uma coluna na sua Planilha mapeia para um atributo que a Brevo não conhece. Ou renomeie a coluna da Planilha para bater com um atributo existente, ou crie o atributo na Brevo (Contatos → Configurações → Atributos do contato).

401 Unauthorized: a API key está errada ou expirou. Rode Configure API key de novo, cole uma chave fresca do dashboard da Brevo.

429 Too Many Requests: você está batendo no rate limit da Brevo. O endpoint de importação permite uns 30 calls por minuto. Se você está fazendo lotes muito agressivos, adicione Utilities.sleep(2000) entre os lotes no loop.

O script silenciosamente não roda no agendamento: confira Triggers no editor do Apps Script. Se um trigger está falhando repetidamente, o Google desabilita. Clique no trigger para ver o motivo da falha, geralmente é um problema de permissões que dá para reautorizar.

O menu Brevo não apareceu: o onOpen só roda quando você (re)abre a Planilha do zero. Recarregue a aba do navegador.

Popup de permissões fica voltando: você provavelmente editou os escopos do script (adicionou um novo serviço Google). O Apps Script pede autorização de novo toda vez que as permissões necessárias mudam. Rode qualquer função uma vez do editor para disparar o prompt e aprovar.

Por que isto bate Zapier e amigos

O Apps Script é grátis, vive dentro da infra do Google, e tem acesso direto aos dados da Planilha, sem disparo de evento linha por linha, sem cobrança por tarefa, sem rate limit além da cota do Google (que é generosa para esse tipo de trabalho). O contraponto: você está se comprometendo com escrever e manter um pedaço pequeno de código. Para uma sincronização de contatos, isso é umas 100 linhas e basicamente zero manutenção contínua.

Combine isso com um trigger diário e uma planilha que seu time de vendas já está atualizando, e você tem um pipeline de contatos para a Brevo com zero trabalho recorrente.

Leitura adicional

Frequently Asked Questions

Preciso de um servidor ou alguma infraestrutura para sincronizar uma Planilha Google com a Brevo?
Não. O Google Apps Script roda dentro do próprio Google Sheets. Você escreve uma função, salva o projeto, e o Google hospeda e roda. O script consegue bater na API da Brevo direto usando UrlFetchApp.
Onde guardo a API key da Brevo?
Use PropertiesService.getScriptProperties(), é uma loja chave-valor gerenciada pelo Google e com escopo no projeto Apps Script. Não cravar a chave no código fonte, colaboradores na Planilha conseguiriam ver.
Como rodo isto automaticamente todo dia?
Abra Apps Script → Triggers → Adicionar Trigger. Escolha a função syncSheetToBrevo, selecione 'Time-driven' e defina uma cadência diária/horária. A cota do Google na camada gratuita é de 90 minutos/dia de tempo total de execução do Apps Script, mais que suficiente para uma sincronização de contatos.
Tem limite de linhas?
O endpoint de importação da Brevo aceita uns 10 MB de JSON inline. Isso dá em torno de 30.000 a 50.000 contatos dependendo de quantos atributos cada um tem. O UrlFetchApp do Apps Script consegue mandar payload de 50 MB, então o gargalo é a Brevo, não o Apps Script. Para trabalhos maiores, divida as linhas em lotes.
Comece grátis com Brevo