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.

Featured image for article: Come costruire un Google Apps Script per importare i contatti di un foglio in Brevo

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:

emailfirstNamelastNamecompanycity
[email protected]JaneDoeAcmeBerlin
[email protected]JohnSmithGlobexParis

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

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

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.

  1. Clicca Brevo → Configure API key, incolla la tua chiave xkeysib-..., clicca OK.
  2. 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
  3. 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:

LimiteValore (piano gratuito)
Tempo totale di esecuzione al giorno90 minuti
Tempo di una singola esecuzione6 minuti
Chiamate UrlFetchApp al giorno20.000
Dimensione payload UrlFetchApp50 MB
Dimensione header UrlFetchApp8 KB
Trigger per utente per script20

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.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: 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.

Letture aggiuntive

Frequently Asked Questions

Mi serve un server o un'infrastruttura per sincronizzare un Google Sheet con Brevo?
No. Google Apps Script gira dentro Google Sheets stesso. Scrivi una funzione, salvi il progetto, e Google la ospita e la esegue. Lo script può colpire l'API di Brevo direttamente usando UrlFetchApp.
Dove memorizzo la API key di Brevo?
Usa PropertiesService.getScriptProperties(), è uno store key/value gestito da Google e legato al progetto Apps Script. Non scrivere la chiave a mano nel codice sorgente, i collaboratori del Sheet potrebbero vederla.
Come la eseguo automaticamente ogni giorno?
Apri Apps Script → Trigger → Aggiungi trigger. Scegli la funzione syncSheetToBrevo, scegli 'Su evento temporale', e imposta una cadenza giornaliera o oraria. La quota del piano gratuito di Google è di 90 minuti al giorno di tempo totale di esecuzione Apps Script, più che sufficiente per una sincronizzazione di contatti.
C'è un limite di righe?
L'endpoint di importazione di Brevo accetta circa 10 MB di JSON inline. Sono grossomodo 30.000-50.000 contatti a seconda di quanti attributi ha ognuno. UrlFetchApp di Apps Script può inviare un payload di 50 MB, quindi il collo di bottiglia è Brevo, non Apps Script. Per job più grandi, mettere le righe in batch.
Inizia gratis con Brevo