Comment construire un Google Apps Script pour importer les contacts d'une feuille dans Brevo
Poussez automatiquement les contacts d'une Google Sheet vers Brevo. Un Apps Script complet avec stockage de la clé API, exécution on-edit, déclencheurs temporels, menu personnalisé et gestion d'erreurs, sans serveur.
Si votre équipe vit déjà dans une Google Sheet (leads commerciaux, inscriptions à des évènements, une liste de contacts partenaires), faire passer ces données dans Brevo n’a pas besoin de passer par des exports CSV et des réimports manuels. Google Apps Script vous permet de câbler la Sheet directement à l’API de Brevo. Le script tourne dans l’infrastructure de Google, donc il n’y a rien à héberger, déployer ni surveiller.
Ce guide parcourt un script fonctionnel : un menu personnalisé Sync to Brevo dans votre Sheet, un déclencheur horaire automatique, un stockage sûr de la clé API, la gestion des lots et un peu de logging structuré pour que vous sachiez ce qui s’est passé.
Ce qu’il vous faut
- Une Google Sheet avec des contacts (une ligne par contact, ligne d’en-tête en premier)
- Un compte Brevo et une clé API (Paramètres → SMTP & API → API Keys)
- L’identifiant numérique de la liste Brevo dans laquelle ajouter les contacts
C’est tout. Pas de npm, pas de Python, pas de serveur.
Disposition de la feuille
Le script de ce guide attend une ligne d’en-tête suivie d’un contact par ligne. Les colonnes sont mappées par nom d’en-tête vers les attributs Brevo :
| firstName | lastName | company | city | |
|---|---|---|---|---|
| [email protected] | Jane | Doe | Acme | Berlin |
| [email protected] | John | Smith | Globex | Paris |
email est obligatoire et est comparé sans tenir compte de la casse. Tout le reste est envoyé à Brevo en attribut de contact. Les attributs personnalisés (tout ce qui dépasse les standards comme FIRSTNAME, LASTNAME) doivent d’abord exister dans votre compte Brevo, définissez-les sous Contacts → Paramètres → Attributs de contact ou via l’API Brevo.
Ouvrir l’éditeur Apps Script
Dans votre Sheet : Extensions → Apps Script. Un nouvel onglet s’ouvre avec un Code.gs vide. Remplacez le contenu par le script ci-dessous.
Le script complet
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;}C’est tout. Sauvegardez (⌘S / Ctrl+S), nommez le projet quelque chose comme « Brevo sync » et retournez à votre Sheet.
Première exécution
Rechargez la Sheet, le nouveau menu Brevo apparaît en haut.
- Cliquez sur Brevo → Configure API key, collez votre clé
xkeysib-..., cliquez sur OK. - Cliquez sur Brevo → Sync sheet to Brevo. Google demandera des permissions la première fois :
- « Voir et gérer vos feuilles de calcul », nécessaire pour lire les lignes
- « Se connecter à un service externe », nécessaire pour appeler api.brevo.com
- Approuvez. Le script tourne. Un toast vert en bas à droite vous indique combien de contacts sont partis.
En cas d’échec, cliquez sur Extensions → Apps Script → Affichage → Journaux pour voir le code de statut par lot renvoyé par Brevo. L’échec le plus fréquent est un 400 à cause d’un attribut personnalisé manquant, voyez la section dépannage plus bas.
Le faire tourner de façon planifiée
Dans l’éditeur Apps Script : Déclencheurs (l’icône d’horloge dans la barre latérale gauche) → Ajouter un déclencheur.
- Choisir la fonction :
syncSheetToBrevo - Source de l’évènement : Horodaté
- Type : minuteur horaire (ou minuteur quotidien pour une synchronisation une fois par jour)
- Intervalle : toutes les heures (ou ce qui convient)
Sauvegardez. Google exécutera la fonction à cette cadence pour toujours, sans serveur, sans cron, sans maintenance.
Vous pouvez aussi utiliser Depuis la feuille → Lors de la modification si vous voulez que chaque changement de cellule déclenche une synchronisation. Attention avec ça, même les modifications cosmétiques déclenchent le trigger, ce qui peut consommer rapidement le quota quotidien Apps Script sur des feuilles très actives. Le déclencheur horaire est presque toujours la bonne réponse.
Quotas Apps Script à connaître
Le palier gratuit Apps Script a des limites qui méritent qu’on les respecte :
| Limite | Valeur (palier gratuit) |
|---|---|
| Temps total d’exécution par jour | 90 minutes |
| Temps d’une exécution unique | 6 minutes |
Appels UrlFetchApp par jour | 20 000 |
Taille de payload UrlFetchApp | 50 Mo |
Taille des en-têtes UrlFetchApp | 8 Ko |
| Déclencheurs par utilisateur et par script | 20 |
Pour une synchronisation de contacts typique (quelques milliers de contacts, à l’heure), vous êtes très loin de l’un quelconque de ces seuils. Le seul à surveiller, ce sont les 6 minutes par exécution unique, si jamais vous synchronisez des centaines de milliers de contacts d’un coup, mettez-les en plus petits lots (le script ci-dessus le fait déjà via BATCH_SIZE).
Gérer l’import de façon asynchrone
L’endpoint d’import de Brevo est asynchrone : vous récupérez immédiatement un processId, et l’import réel tourne côté serveur. Pour la plupart des synchronisations de feuille, ça suffit, fire-and-forget, Brevo enverra un récapitulatif par e-mail à la fin de chaque lot.
Si vous voulez bloquer jusqu’à ce que l’import soit vraiment terminé, sondez l’endpoint d’état du processus :
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 est l’équivalent Apps Script d’une attente bloquante. Ne dormez pas trop longtemps, vous avez 6 minutes au total par exécution.
Ajouter un webhook de notification
Un motif plus propre que le polling : déployez votre Apps Script en application web et passez son URL en notifyUrl. Brevo y enverra un POST quand l’import sera terminé.
// 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');}Déployer : Déployer → Nouveau déploiement → Application web, mettez « Qui a accès » sur Tout le monde, copiez l’URL obtenue, et passez-la en notifyUrl dans votre payload d’import :
payload.notifyUrl = 'https://script.google.com/macros/s/AKfy.../exec';Maintenant Brevo reposte le résultat vers le script de votre propre Sheet, fermant la boucle sans infrastructure externe.
Dépannage
400 Bad Request avec error: "Attribute X not found". Une colonne de votre Sheet pointe vers un attribut que Brevo ne connaît pas. Soit vous renommez la colonne de la Sheet pour qu’elle corresponde à un attribut existant, soit vous créez l’attribut dans Brevo (Contacts → Paramètres → Attributs de contact).
401 Unauthorized. La clé API est mauvaise ou expirée. Relancez Configure API key, collez une clé fraîche depuis le tableau de bord Brevo.
429 Too Many Requests. Vous tapez la rate limit de Brevo. L’endpoint d’import autorise environ 30 appels par minute. Si vous mettez les lots de façon agressive, ajoutez Utilities.sleep(2000) entre les lots dans la boucle.
Le script ne tourne pas en silence sur sa planification. Vérifiez Déclencheurs dans l’éditeur Apps Script. Si un déclencheur échoue de façon répétée, Google le désactive. Cliquez dans le déclencheur pour voir la raison de l’échec, en général un problème de permissions que vous pouvez réautoriser.
Le menu Brevo n’est pas apparu. onOpen ne s’exécute que quand vous (r)ouvrez la Sheet à neuf. Rechargez l’onglet du navigateur.
Le popup de permissions revient sans cesse. Vous avez probablement modifié les scopes du script (ajouté un nouveau service Google). Apps Script redemande l’autorisation chaque fois que les permissions requises changent. Lancez n’importe quelle fonction une fois depuis l’éditeur pour déclencher le prompt et approuver.
Pourquoi cela bat Zapier et compagnie
Apps Script est gratuit, vit dans l’infra Google et a un accès direct aux données de la Sheet, sans déclenchement par évènement ligne par ligne, sans tarification à la tâche, sans rate limits autres que le quota Google (qui est généreux pour ce type de job). En contrepartie : vous vous engagez à écrire et maintenir un petit morceau de code. Pour une synchronisation de contacts, c’est environ 100 lignes et quasiment zéro maintenance continue.
Couplez ça avec un déclencheur quotidien et une feuille que votre équipe commerciale met déjà à jour, et vous avez une pipeline de contacts vers Brevo avec zéro travail récurrent.