In questa pagina
Un foglio con dodici iscrizioni a un evento si esporta facilmente una volta. Alla seconda importazione, però, un indirizzo compare due volte e un’altra persona si è disiscritta in Brevo. Il processo deve riconoscere entrambi i casi.
Lo script usa gli endpoint Brevo per leggere, creare e aggiornare i contatti. Serve per una piccola importazione programmata, non per carichi massivi o come connettore Google Sheets nativo. Non invia email. Le prove del consenso e la gestione dello script restano a tuo carico.
Prepara il foglio e la lista
Crea una scheda Contacts con queste intestazioni esatte. Usa caselle di controllo per consent e metti TRUE solo se hai il permesso per l’uso marketing previsto.
| firstName | lastName | consent | |
|---|---|---|---|
| [email protected] | Jane | Doe | TRUE |
| [email protected] | Alex | Lee | FALSE |
Crea o scegli una lista Brevo e inserisci il suo ID numerico in LIST_ID. Controlla che gli attributi FNAME e LNAME esistano in Brevo: sono gli unici campi associati. Conserva una prova separata di quando e come è stato dato il consenso se il foglio non può dimostrarlo. Una cella TRUE non basta.
In Estensioni → Apps Script → Impostazioni progetto → Proprietà dello script, aggiungi BREVO_API_KEY. Le proprietà sono condivise tra gli utenti dello script: limita chi può modificarlo. Non mettere la chiave in una cella o nel codice. Consulta le regole di accesso di Google.
Lo script
Sostituisci Code.gs con l’esempio e imposta LIST_ID prima dell’avvio.
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); }}Salva, scegli syncSheetToBrevo e premi Esegui. Google chiede accesso a Sheets e alle richieste esterne. In Esecuzioni trovi il riepilogo {written, skipped}. Un 404 nella lettura porta a creare il contatto; un 200 ad aggiornarlo. Altri errori interrompono la procedura. Una scrittura riuscita conferma la richiesta di contatto, non l’invio di una campagna.
Lo script elimina gli indirizzi duplicati nella stessa esecuzione e ricontrolla Brevo alla successiva. Non imposta emailBlacklisted su false e non cambia l’email. Se Brevo segnala un blocco generale o una disiscrizione da questa lista, la riga viene saltata anche con TRUE nel foglio. Non usare questo esempio per annullare una disiscrizione. Brevo avverte che cambiare l’indirizzo di un contatto bloccato può togliere il blocco.
Verifica una prova prima di programmare
Usa una lista di test e contatti che controlli. Avvia una prova con una riga TRUE, una FALSE e un indirizzo duplicato: aspettati una scrittura e due righe saltate. Disiscrivi il contatto di test in Brevo e ripeti: per lui non deve esserci alcuna scrittura. Per verificare un errore, imposta temporaneamente un LIST_ID non valido, controlla il codice HTTP in Esecuzioni, ripristina l’ID e riprova. Non usare dati di clienti reali. Il codice è stato anche provato localmente con risposte Brevo simulate, senza scritture live.
Per pianificare la funzione, apri Apps Script → Trigger → Aggiungi trigger, scegli syncSheetToBrevo e un intervallo temporale. I trigger installabili girano con l’account di chi li ha creati; l’account deve mantenere accesso al foglio. Inizia con una prova giornaliera e controlla gli errori. Google limita durata e chiamate URL Fetch; dividi fonti grandi in lavori limitati.
Per un 429 lo script legge x-sib-ratelimit-reset, riprova al massimo due volte e restituisce 429 per un’esecuzione successiva se l’attesa necessaria supera 60 secondi. Gli altri errori fermano l’esecuzione; la successiva ricomincia dall’inizio. Un contatto può disiscriversi tra lettura e scrittura. Usa questo esempio solo se quel breve intervallo è accettabile. Per garanzie più strette servono riconciliazione per eventi, una fonte autorevole del consenso e un ultimo controllo prima dell’invio.
Per fogli grandi, l’endpoint di importazione asincrona di Brevo restituisce un processId: conferma l’accettazione del lavoro, non il suo completamento. Controlla l’esito prima di dichiarare riuscita l’importazione. Con questa via per singolo contatto, invece, l’errore resta visibile sulla riga coinvolta.
Domande frequenti
Serve un connettore Google Sheets nativo?
No. L’esempio gira in un progetto Google Apps Script collegato al foglio e chiama l’API dei contatti Brevo. Gestisci tu script, chiave API e trigger programmato.
Quali righe vengono inviate a Brevo?
Solo quelle con TRUE nella colonna consent e un indirizzo email valido. Lo script rimuove i duplicati in ogni esecuzione e controlla Brevo prima di scrivere.
Una nuova esecuzione reiscrive chi si è disiscritto?
Lo script salta i contatti che Brevo segnala come emailBlacklisted o disiscritti dalla lista scelta. Non imposta emailBlacklisted su false e non modifica l’indirizzo email. Controlla anche le altre fonti di consenso prima della produzione.
Come riprovo dopo un errore?
Leggi l’errore nelle esecuzioni Apps Script, correggi la causa e avvia di nuovo la stessa funzione. Le scritture usano lo stesso indirizzo e ID lista, quindi una riga già riuscita non crea un secondo contatto.