Una hoja con doce inscripciones a un evento es fácil de exportar una vez. En la segunda importación aparece una dirección duplicada y otra persona se ha dado de baja en Brevo. Una importación repetible tiene que comprobar ambas cosas.
Este script usa los endpoints de Brevo para leer, crear y actualizar contactos. Sirve para una importación pequeña y programada, no para cargas masivas ni como conector nativo de Google Sheets. No envía correos. Tú respondes por la prueba de consentimiento y por el funcionamiento del script.
Prepara la hoja y la lista
Crea una pestaña Contacts con estas cabeceras exactas. Usa casillas para consent y marca TRUE solo si tienes permiso para el uso de marketing previsto.
| firstName | lastName | consent | |
|---|---|---|---|
| [email protected] | Jane | Doe | TRUE |
| [email protected] | Alex | Lee | FALSE |
Crea o elige una lista de Brevo y copia su ID numérico a LIST_ID en el código. Comprueba que existen los atributos de contacto FNAME y LNAME en Brevo. Son los únicos campos asignados. Guarda aparte el registro de cuándo y cómo se obtuvo el consentimiento si la hoja no puede demostrarlo. Una celda TRUE no es una prueba.
En Extensiones → Apps Script → Configuración del proyecto → Propiedades del script, añade BREVO_API_KEY. Las propiedades del script se comparten entre sus usuarios; limita quién puede editar el proyecto. No pongas la clave en una celda ni en el código fuente. Consulta las reglas de acceso de Google.
El script
Sustituye Code.gs por este ejemplo. Configura LIST_ID antes de la primera ejecución.
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); }}Guarda el proyecto, selecciona syncSheetToBrevo y pulsa Ejecutar. Google pedirá permisos para Sheets y solicitudes externas. En Ejecuciones, consulta el registro final {written, skipped}. Una lectura 404 lleva a crear el contacto; una 200 lleva a actualizarlo. Otros errores de lectura detienen la ejecución. Una escritura correcta confirma la solicitud de contacto, no el envío de una campaña.
El script elimina correos duplicados en una ejecución y vuelve a consultar Brevo en la siguiente. Nunca establece emailBlacklisted en false ni cambia el email. Si Brevo indica un bloqueo global o una baja de esta lista, omite la fila aunque la hoja siga mostrando TRUE. No lo uses para deshacer una baja. Brevo advierte que cambiar el correo de un contacto bloqueado puede quitar ese bloqueo.
Comprueba una ejecución antes de programarla
Usa una lista de prueba y contactos de prueba bajo tu control. Ejecuta el script con una fila TRUE, una FALSE y un correo duplicado: espera una escritura y dos filas omitidas. Da de baja el contacto de prueba en Brevo y vuelve a ejecutar: ya no debe escribirse. Para probar un fallo, introduce de forma temporal un LIST_ID inválido, comprueba el error HTTP en Ejecuciones, restaura el ID y repite. No hagas la prueba con datos reales de clientes. El código de este artículo también se ejecutó localmente con respuestas de Brevo simuladas, sin escrituras reales.
Para programarlo, abre Apps Script → Activadores → Añadir activador, elige syncSheetToBrevo y una frecuencia basada en tiempo. Los activadores instalables se ejecutan con la cuenta que los creó, que debe mantener acceso a la hoja. Empieza con una ejecución diaria para una hoja pequeña y revisa los fallos. Google limita el tiempo de ejecución y las llamadas a URL Fetch. Divide las fuentes más grandes en tareas acotadas.
Ante un 429, el script lee x-sib-ratelimit-reset, hace como máximo dos reintentos y devuelve 429 para una ejecución posterior si la espera requerida supera 60 segundos. Otros errores detienen la ejecución. La próxima ejecución manual o programada vuelve al principio. Un contacto puede darse de baja entre la lectura y la escritura; usa este ejemplo solo si ese pequeño intervalo es aceptable. Si necesitas una garantía más estricta, combina la reconciliación por eventos con una fuente de consentimiento autorizada y una última comprobación antes del envío.
Para hojas más grandes, el endpoint de importación asíncrona de Brevo devuelve un processId. Confirma que se aceptó un trabajo, no que terminó. Comprueba el resultado del proceso antes de dar por correcta una carga masiva. La ruta por contacto de este artículo muestra el fallo en la fila que lo causó.
Preguntas frecuentes
¿Hace falta un conector nativo de Google Sheets?
No. Este ejemplo se ejecuta en un proyecto Google Apps Script vinculado a la hoja y llama a la API de contactos de Brevo. Tú mantienes el script, su clave de API y el activador programado.
¿Qué filas se envían a Brevo?
Solo las filas con TRUE en consent y un email válido. El script elimina direcciones duplicadas en cada ejecución y consulta Brevo antes de escribir.
¿Una nueva ejecución vuelve a suscribir a alguien que se dio de baja?
El script omite los contactos que Brevo marca como emailBlacklisted o dados de baja de la lista de destino. Nunca envía emailBlacklisted: false ni cambia su dirección de correo. Revisa otras fuentes de consentimiento antes de usarlo en producción.
¿Cómo repito una ejecución fallida?
Consulta el error en las ejecuciones de Apps Script, corrige la causa y ejecuta de nuevo la misma función. Las escrituras usan el mismo email y el mismo ID de lista, así que una fila ya procesada no crea otro contacto.