Come costruire un Google Apps Script per importare i contatti di un foglio in Brevo
Spingi automaticamente i contatti da un Google Sheet a Brevo. Un Apps Script completo con storage della API key, esecuzione on-edit, trigger temporali, un menu personalizzato e gestione errori, senza server.
Se il tuo team vive già in un Google Sheet (lead commerciali, iscrizioni a eventi, una lista contatti partner), portare quei dati in Brevo non deve passare per esportare CSV e reimportarli a mano. Google Apps Script ti permette di cablare il Sheet direttamente all’API di Brevo. Lo script gira dentro l’infrastruttura di Google, quindi non c’è niente da ospitare, deployare o babysittare.
Questa guida cammina attraverso uno script funzionante: una voce di menu personalizzata Sync to Brevo nel tuo Sheet, un trigger orario automatico, storage sicuro della API key, gestione dei batch e un po’ di logging strutturato così sai cos’è successo.
Cosa ti serve
- Un Google Sheet con i contatti (una riga per contatto, riga di header per prima)
- Un account Brevo e una API key (Impostazioni → SMTP & API → API Keys)
- L’ID numerico della lista Brevo a cui vuoi aggiungere i contatti
Tutto qui. Niente npm, niente Python, niente server.
Layout del foglio
Lo script di questa guida si aspetta una riga di header seguita da un contatto per riga. Le colonne vengono mappate per nome di header agli attributi Brevo:
| firstName | lastName | company | city | |
|---|---|---|---|---|
| [email protected] | Jane | Doe | Acme | Berlin |
| [email protected] | John | Smith | Globex | Paris |
email è obbligatoria e viene confrontata senza distinzione di maiuscole/minuscole. Tutto il resto viene mandato a Brevo come attributo del contatto. Gli attributi personalizzati (qualsiasi cosa oltre quelli standard come FIRSTNAME, LASTNAME) devono prima esistere nel tuo account Brevo, definiscili in Contatti → Impostazioni → Attributi del contatto, o tramite l’API di Brevo.
Aprire l’editor di Apps Script
Nel tuo Sheet: Estensioni → Apps Script. Si apre una nuova scheda con un Code.gs vuoto. Sostituisci il contenuto con lo script qui sotto.
Lo script completo
const BREVO_API_BASE = 'https://api.brevo.com/v3';const BREVO_LIST_ID = 42; // <- the Brevo list to import contacts intoconst SHEET_NAME = 'Contacts'; // <- name of the sheet tab to readconst 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;}Tutto qui. Salvalo (⌘S / Ctrl+S), chiama il progetto qualcosa come “Brevo sync” e torna al tuo Sheet.
Prima esecuzione
Ricarica il Sheet, il nuovo menu Brevo compare in alto.
- Clicca Brevo → Configure API key, incolla la tua chiave
xkeysib-..., clicca OK. - Clicca Brevo → Sync sheet to Brevo. Google chiederà i permessi la prima volta:
- “Visualizzare e gestire i tuoi fogli di calcolo”, serve a leggere le righe
- “Connetterti a un servizio esterno”, serve a chiamare api.brevo.com
- Approva. Lo script gira. Un toast verde in basso a destra ti dice quanti contatti sono usciti.
Se fallisce, clicca Estensioni → Apps Script → Visualizza → Log per vedere il codice di stato per batch da Brevo. Il fallimento più comune è un 400 per un attributo personalizzato mancante, vedi la sezione di troubleshooting più sotto.
Eseguirla in modo programmato
Nell’editor Apps Script: Trigger (l’icona dell’orologio nella sidebar a sinistra) → Aggiungi trigger.
- Scegli funzione:
syncSheetToBrevo - Sorgente evento: Su evento temporale
- Tipo: timer orario (o timer giornaliero per una sincronizzazione una volta al giorno)
- Intervallo: ogni ora (o quello che serve)
Salva. Google eseguirà la funzione con quella cadenza per sempre, senza server, senza cron, senza manutenzione.
Puoi anche usare Dal foglio di lavoro → Alla modifica se vuoi che ogni cambio di cella attivi una sincronizzazione. Attenzione con questa, anche le modifiche estetiche scattano il trigger, il che può consumare in fretta la quota giornaliera di Apps Script su fogli molto attivi. Il trigger orario è quasi sempre la risposta giusta.
Quote di Apps Script da conoscere
Il piano gratuito di Apps Script ha limiti che vale la pena rispettare:
| Limite | Valore (piano gratuito) |
|---|---|
| Tempo totale di esecuzione al giorno | 90 minuti |
| Tempo di una singola esecuzione | 6 minuti |
Chiamate UrlFetchApp al giorno | 20.000 |
Dimensione payload UrlFetchApp | 50 MB |
Dimensione header UrlFetchApp | 8 KB |
| Trigger per utente per script | 20 |
Per una sincronizzazione tipica di contatti (qualche migliaio di contatti, oraria) sei lontanissimo da uno qualsiasi di questi. L’unico da osservare è la singola esecuzione di 6 minuti, se mai sincronizzi centinaia di migliaia di contatti in un colpo solo, mettile in batch più piccoli (lo script qui sopra lo fa già tramite BATCH_SIZE).
Gestire l’importazione in modo asincrono
L’endpoint di importazione di Brevo è asincrono: ricevi subito un processId, e l’importazione vera gira lato server. Per la maggior parte delle sincronizzazioni di Sheet questo va bene, fire-and-forget, Brevo manderà un riassunto via email quando ogni batch finisce.
Se vuoi bloccare finché l’importazione non è davvero finita, fai polling sull’endpoint dello stato del 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 è l’equivalente Apps Script di un’attesa bloccante. Non dormire troppo, hai 6 minuti totali per esecuzione.
Aggiungere un webhook di notifica
Un pattern più pulito del polling: deploya il tuo Apps Script come App web e passa il suo URL come notifyUrl. Brevo gli farà POST quando l’importazione finisce.
// Add to Code.gsfunction 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: Distribuisci → Nuova distribuzione → App web, imposta “Chi ha accesso” su Chiunque, copia l’URL risultante e passalo come notifyUrl nel tuo payload di importazione:
payload.notifyUrl = 'https://script.google.com/macros/s/AKfy.../exec';Ora Brevo posta il risultato indietro allo script del tuo Sheet, chiudendo il cerchio senza infrastruttura esterna.
Troubleshooting
400 Bad Request con error: "Attribute X not found". Una colonna nel tuo Sheet punta a un attributo che Brevo non conosce. O rinomini la colonna del Sheet per matchare un attributo esistente, o crei l’attributo in Brevo (Contatti → Impostazioni → Attributi del contatto).
401 Unauthorized. API key sbagliata o scaduta. Rilancia Configure API key, incolla una chiave fresca dalla dashboard di Brevo.
429 Too Many Requests. Stai colpendo il rate limit di Brevo. L’endpoint di importazione consente circa 30 chiamate al minuto. Se stai facendo batch in modo aggressivo, aggiungi Utilities.sleep(2000) tra i batch nel loop.
Lo script non parte in silenzio sul programma. Controlla Trigger nell’editor Apps Script. Se un trigger fallisce ripetutamente, Google lo disabilita. Clicca dentro al trigger per vedere il motivo del fallimento, di solito un problema di permessi che puoi riautorizzare.
Il menu Brevo non è apparso. onOpen parte solo quando (ri)apri il Sheet da zero. Ricarica la scheda del browser.
Il popup dei permessi continua a tornare. Probabilmente hai modificato gli scope dello script (aggiunto un nuovo servizio Google). Apps Script richiede di nuovo l’autorizzazione ogni volta che cambiano i permessi necessari. Lancia una qualsiasi funzione una volta dall’editor per attivare il prompt e approvare.
Perché questo batte Zapier e affini
Apps Script è gratis, vive dentro l’infra di Google e ha accesso diretto ai dati del Sheet, niente eventi che si attivano riga per riga, niente prezzi a task, niente rate limit oltre alla quota di Google (che è generosa per questo tipo di job). L’altro lato della medaglia: ti impegni a scrivere e mantenere un piccolo pezzo di codice. Per una sincronizzazione di contatti, sono circa 100 righe e praticamente zero manutenzione continua.
Combina questo con un trigger giornaliero e un foglio che il tuo team commerciale sta già aggiornando, e hai una pipeline di contatti verso Brevo con zero lavoro ricorrente.