Uma planilha com doze inscrições para um evento é fácil de exportar uma vez. Na segunda importação, um endereço aparece duas vezes e outra pessoa já se descadastrou na Brevo. Uma importação repetível precisa detectar ambos os casos.
Este script usa os endpoints da Brevo para ler, criar e atualizar contatos. Ele serve para pequenas importações agendadas, não para cargas em massa nem como conector nativo do Google Sheets. Não envia e-mail. A prova de consentimento e a operação do script são suas responsabilidades.
Prepare a planilha e a lista
Crie uma aba Contacts com estes cabeçalhos exatos. Use caixas de seleção para consent e marque TRUE apenas quando houver permissão para o uso de marketing previsto.
| firstName | lastName | consent | |
|---|---|---|---|
| [email protected] | Jane | Doe | TRUE |
| [email protected] | Alex | Lee | FALSE |
Crie ou escolha uma lista na Brevo e copie o ID numérico para LIST_ID no código. Confirme que os atributos FNAME e LNAME existem na Brevo. São os únicos campos mapeados. Guarde o registro de quando e como o consentimento foi obtido se a planilha não puder comprová-lo. Uma célula TRUE não é prova suficiente.
Em Extensões → Apps Script → Configurações do projeto → Propriedades do script, adicione BREVO_API_KEY. Essas propriedades são compartilhadas entre usuários do script; limite quem pode editá-lo. Não guarde a chave em uma célula nem no código-fonte. Veja as regras de acesso do Google.
O script
Substitua Code.gs por este exemplo e ajuste LIST_ID antes da primeira execução.
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); }}Salve, selecione syncSheetToBrevo e clique em Executar. O Google pedirá acesso ao Sheets e a solicitações externas. Em Execuções, veja o registro final {written, skipped}. Uma leitura 404 leva à criação; uma 200 leva à atualização. Outros erros interrompem a execução. Uma gravação bem-sucedida confirma a requisição de contato, não o envio de campanha.
O script elimina e-mails repetidos na mesma execução e consulta a Brevo novamente na próxima. Nunca define emailBlacklisted como false nem altera o e-mail. Se a Brevo indicar bloqueio global ou descadastro dessa lista, a linha será ignorada mesmo que a planilha mostre TRUE. Não use o script para reverter um descadastro. A Brevo alerta que alterar o endereço de um contato bloqueado pode remover o bloqueio.
Confira uma execução antes de agendar
Use uma lista de teste e contatos que você controla. Rode com uma linha TRUE, uma FALSE e um e-mail duplicado: espere uma gravação e duas linhas ignoradas. Descadastre o contato de teste na Brevo e rode outra vez: ele não deve receber gravação. Para testar uma falha, ajuste LIST_ID temporariamente para um valor inválido, confira o erro HTTP em Execuções, restaure o ID e repita. Não teste com clientes reais. O código também foi validado localmente com respostas simuladas da Brevo, sem gravação ao vivo.
Para agendar, abra Apps Script → Acionadores → Adicionar acionador, escolha syncSheetToBrevo e um intervalo baseado em tempo. Acionadores instaláveis rodam na conta de quem os criou, que precisa manter acesso à planilha. Comece diariamente para uma planilha pequena e confira falhas. O Google limita duração e chamadas URL Fetch; divida fontes grandes em trabalhos limitados.
Ao receber 429, o script lê x-sib-ratelimit-reset, faz no máximo duas novas tentativas e retorna 429 para uma execução posterior se a espera necessária superar 60 segundos. Outros erros param a execução; a próxima rodada começa do início. Um contato pode se descadastrar entre a leitura e a gravação. Use este exemplo somente se essa pequena janela for aceitável. Para garantias mais fortes, combine reconciliação por eventos, uma fonte confiável de consentimento e uma última checagem antes do envio.
Para planilhas grandes, o endpoint de importação assíncrona da Brevo devolve processId. Ele confirma que um trabalho foi aceito, não que terminou. Confira o resultado antes de declarar a importação bem-sucedida. O caminho por contato deste artigo mostra a falha na linha que a causou.
Perguntas frequentes
É necessário um conector nativo do Google Sheets?
Não. O exemplo roda em um projeto Google Apps Script vinculado à planilha e chama a API de contatos da Brevo. Você mantém o script, a chave de API e o acionador agendado.
Quais linhas são enviadas à Brevo?
Somente linhas com TRUE em consent e e-mail válido. O script remove duplicatas em cada execução e consulta a Brevo antes de escrever.
Uma nova execução reinscreve quem se descadastrou?
O script ignora contatos que a Brevo marca como emailBlacklisted ou descadastrados da lista de destino. Ele nunca envia emailBlacklisted: false nem muda o endereço de e-mail. Verifique outras fontes de consentimento antes de usar em produção.
Como repetir uma execução com falha?
Leia o erro nas execuções do Apps Script, corrija a causa e execute a mesma função de novo. As gravações usam o mesmo e-mail e ID de lista; uma linha já concluída não cria um segundo contato.