API di Brevo: guida pratica per sviluppatori
Guida all’API di Brevo per sviluppatori: autenticazione, base URL, contatti, email transazionali, campagne, oggetti CRM, webhook, limiti di frequenza e vincoli reali.
Brevo espone una sola API REST che attraversa messaggistica transazionale, campagne marketing, dati dei contatti e record CRM. Far restituire 201 alla prima richiesta richiede circa due minuti. Costruire un’integrazione di produzione che non perda dati in silenzio richiede molto di più, perché diversi dei vincoli che contano davvero sono non documentati oppure contraddicono ciò che l’API dichiara di sé.
Questa guida copre entrambe le metà: gli endpoint, gli SDK e l’autenticazione che ti servono il primo giorno, e i limiti di piattaforma attorno ai quali devi progettare prima di andare in produzione.
Cosa copre l’API di Brevo
Tutto vive sotto un unico host e un unico percorso di versione. La documentazione per sviluppatori raggruppa la superficie in quattro aree di prodotto:
- Messaggistica: email transazionali, SMS e WhatsApp, inclusi invii batch, programmazione e attività dei messaggi.
- Piattaforma di marketing: contatti, liste, segmenti e campagne email.
- eCommerce: prodotti, ordini e tracciamento degli eventi cliente.
- Conversations: il widget di chat e la gestione programmatica delle conversazioni.
Queste aree condividono un solo account, un solo database di contatti e una sola chiave API. È comodo e ogni tanto pericoloso: uno script scritto pensando a un ambiente di staging sta parlando con gli stessi contatti a cui inviano le tue campagne.
Transazionale contro marketing
Le due famiglie si comportano in modo abbastanza diverso da rendere la confusione tra loro l’errore di progettazione più comune.
| Transazionale | Marketing | |
|---|---|---|
| Endpoint principale | POST /v3/smtp/email | POST /v3/emailCampaigns |
| Destinatari | Espliciti nella richiesta | listIds o segmentIds |
| Attivazione | La tua applicazione, in tempo reale | Programmata o inviata su richiesta |
| Forma tipica del volume | Continua, un messaggio alla volta | A picchi, un unico invio grande |
| Posizione sui limiti di frequenza | Molto alta, 1.000 richieste al secondo sui piani standard | Bassa, gli endpoint campagna ricadono nel limite generale |
Se stai ancora decidendo se Brevo sia la piattaforma giusta, la panoramica della piattaforma copre quel terreno.
Autenticazione e gestione delle chiavi
Brevo usa una semplice chiave API in un header personalizzato. L’header si chiama api-key, non Authorization, e non ha prefisso Bearer. Questo confonde quasi tutti quelli che hanno usato prima un’altra API di messaggistica.
curl https://api.brevo.com/v3/account \ -H "api-key: $BREVO_API_KEY"Le chiavi si generano nell’app di Brevo, nelle impostazioni dell’account, nella sezione SMTP e API, alla scheda delle chiavi API. Dai a ogni chiave un nome descrittivo legato al sistema che la usa. Il valore della chiave viene mostrato esattamente una volta al momento della generazione, quindi se lo perdi ne generi una nuova invece di recuperare la vecchia.
Qualche regola pratica:
- Emetti una chiave separata per ogni ambiente di deploy e per ogni servizio. Revocare una chiave compromessa non dovrebbe mai mettere fuori uso tre sistemi scollegati.
- Le chiavi API standard valgono per l’intero account. Considera qualsiasi chiave come accesso completo a contatti, invii e dati CRM.
- Brevo supporta anche OAuth 2.0 per le applicazioni che agiscono per conto di altri account Brevo, descritto insieme al flusso a chiave negli schemi di autenticazione.
- Il server MCP usato dagli assistenti AI richiede un token separato e usa davvero un header bearer. Quel token si genera nella stessa schermata delle chiavi API, ma non è intercambiabile con una chiave REST.
Base URL, versionamento e la tua prima scrittura
Il base URL è https://api.brevo.com/v3/. La versione sta nel percorso invece che in un header, e v3 è la generazione attuale. Ogni percorso in questa guida è relativo a quella base.
Una prima scrittura è più istruttiva di una prima lettura, perché mette alla prova le parti dell’account che di solito sono configurate male, i mittenti verificati in particolare:
curl -X POST https://api.brevo.com/v3/smtp/email \ -H "api-key: $BREVO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "sender": { "name": "Ops", "email": "[email protected]" }, "to": [{ "email": "[email protected]", "name": "Dev" }], "subject": "Primo invio transazionale", "htmlContent": "<html><body><p>Funziona.</p></body></html>", "tags": ["smoke-test"] }'Un invio riuscito restituisce 201 con un messageId. Un invio programmato restituisce 202.
Gli endpoint che userai davvero
Contatti
POST /v3/contacts crea un contatto. Il corpo accetta email, una mappa attributes per i campi personalizzati, listIds, ext_id per la tua chiave esterna e i due flag che contano di più nella pratica: updateEnabled, che trasforma la chiamata in un upsert, e getId, che fa restituire alla risposta l’id del contatto.
Le letture passano da GET /v3/contacts, che pagina con limit (predefinito 50, massimo 1000) e offset, e supporta modifiedSince e createdSince in UTC. Le sincronizzazioni incrementali dovrebbero appoggiarsi a modifiedSince invece di percorrere l’intera lista. Nota che il parametro filter supporta solo un operatore di uguaglianza, quindi qualsiasi cosa più espressiva va in un segmento.
Per il caricamento massivo, POST /v3/contacts/import accetta fileUrl, fileBody o jsonBody, punta a listIds e gira in modo asincrono, restituendo un processId. Brevo documenta un corpo massimo di 10 MB e consiglia di restare vicino agli 8 MB, perché il parsing gonfia il payload. Passa notifyUrl per conoscere l’esito invece di fare polling.
Email transazionali
POST /v3/smtp/email è il cavallo da tiro. Oltre a sender, to, subject e htmlContent, i campi che vale la pena conoscere sono:
templateIdconparams, che sostituisce il contenuto inline con un template di Brevo e le sue variabili. I parametri della singola versione arrivano al massimo a 100 KB, quelli cumulativi a 1000 KB.messageVersions, che invia varianti personalizzate in una sola chiamata, con un massimo di 99 destinatari per versione.tags, che dovresti impostare sempre. I tag tornano indietro negli eventi webhook e sono l’unico modo economico per correlare un evento di consegna al ramo di codice che lo ha prodotto.scheduledAtinsieme abatchId, per invii futuri che potresti voler annullare in blocco.headers, in Title-Case, per header SMTP personalizzati.
Una singola richiesta accetta al massimo 2.000 destinatari. Per la differenza tra questo endpoint e l’invio di campagne, la guida alle email transazionali offre la prospettiva di strategia dei messaggi.
Campagne email
POST /v3/emailCampaigns richiede name e sender, più esattamente una sorgente di contenuto: htmlContent (minimo 10 caratteri, sotto 1 MB), htmlUrl oppure templateId. Il pubblico va in recipients come listIds o segmentIds, e scheduledAt usa il formato UTC YYYY-MM-DDTHH:mm:ss.SSSZ. Le rotte complementari coprono l’invio immediato, l’invio di un test, l’aggiornamento dello stato e l’estrazione del report della campagna.
Aziende, trattative e oggetti
Il CRM di Brevo ha due percorsi di scrittura sovrapposti, e scegliere quello giusto conta.
Le rotte CRM sono POST /v3/companies, PATCH /v3/companies/{id}, DELETE /v3/companies/{id} e l’insieme equivalente per le trattative. Sono sincrone. Una PATCH restituisce 204 una volta applicata la modifica.
L’API objects è il percorso massivo: POST /v3/objects/{object_type}/batch/upsert accetta fino a 1000 record e 1 MB per richiesta, fino a 500 attributi per record e fino a 10 record di associazione per tipo di oggetto per record. Restituisce 202 con un processId, cioè accettato e non applicato.
curl -X POST https://api.brevo.com/v3/objects/company/batch/upsert \ -H "api-key: $BREVO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "records": [ { "identifiers": { "id": 12345 }, "attributes": { "domain": "acme.example", "industry": "retail" } } ] }'La guida al CRM di Brevo copre il modello a oggetti dal punto di vista di chi lo usa ogni giorno.
SDK ufficiali
Brevo mantiene i client nell’organizzazione GitHub getbrevo:
| Linguaggio | Repository |
|---|---|
| Node.js | github.com/getbrevo/brevo-node |
| Python | github.com/getbrevo/brevo-python |
| PHP | github.com/getbrevo/brevo-php |
| Java | github.com/getbrevo/brevo-java |
| C# | github.com/getbrevo/brevo-csharp |
| Go | github.com/getbrevo/brevo-go |
| Ruby | github.com/getbrevo/brevo-ruby |
Il client Node si installa come @getbrevo/brevo:
npm install @getbrevo/brevoimport { BrevoClient } from "@getbrevo/brevo";
const brevo = new BrevoClient({ apiKey: process.env.BREVO_API_KEY });
const result = await brevo.transactionalEmails.sendTransacEmail({ subject: "Ordine confermato", htmlContent: "<html><body><p>Grazie per il tuo ordine.</p></body></html>", tags: ["order-confirmation"],});
console.log("Message ID:", result.messageId);Il client Python si installa con pip install brevo-python. Se preferisci non portarti dietro una dipendenza SDK per due endpoint, la superficie HTTP grezza è abbastanza piccola da chiamare direttamente, il che ti tiene anche al riparo dal ricambio di versioni dell’SDK:
import osimport requests
BASE = "https://api.brevo.com/v3"HEADERS = { "api-key": os.environ["BREVO_API_KEY"], "Content-Type": "application/json",}
def upsert_contact(email, attributes, list_ids): response = requests.post( f"{BASE}/contacts", headers=HEADERS, json={ "email": email, "attributes": attributes, "listIds": list_ids, "updateEnabled": True, }, timeout=30, ) response.raise_for_status() return responseEsiste anche un server MCP su https://mcp.brevo.com/v1/brevo/mcp per gli assistenti AI, autenticato con un token bearer generato nella stessa schermata delle impostazioni. È utile per l’esplorazione e le domande sull’account, non per i percorsi dati di produzione.
Webhook
I webhook sono il modo in cui scopri cosa è successo dopo un invio. POST /v3/webhooks ne crea uno, con url, events, type e facoltativamente channel (email o sms), batched, headers personalizzati e un oggetto auth.
Esistono tre tipi di webhook con vocabolari di eventi distinti:
- Transazionali:
sent,request,delivered,hardBounce,softBounce,blocked,spam,invalid,deferred,click,opened,uniqueOpened,unsubscribed. - Marketing:
spam,opened,click,hardBounce,softBounce,unsubscribed,listAddition,delivered,contactUpdated,contactDeleted. - Inbound:
inboundEmailProcessedereply, che richiedono in più undomain.
curl -X POST https://api.brevo.com/v3/webhooks \ -H "api-key: $BREVO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://tuaapp.example/hooks/brevo", "type": "transactional", "events": ["delivered", "hardBounce", "spam", "unsubscribed"], "description": "Segnali di recapitabilità" }'Tre cose da fare bene. Primo, un account può contenere al massimo 40 webhook tra tutti i tipi, quindi smista per evento dentro il tuo handler invece di registrare un endpoint per evento. Secondo, usa il flag batched quando ti aspetti volume, perché una richiesta che porta molti eventi è molto più economica da elaborare di tante richieste. Terzo, proteggi il ricevitore: Brevo pubblica i suoi intervalli di IP di invio, e limitare il tuo endpoint a quegli intervalli è l’approccio documentato. Aggiungi un tuo segreto condiviso tramite il campo headers come secondo livello.
Gli handler devono essere idempotenti. Usa l’id del messaggio più il tipo di evento più il timestamp come chiave di deduplicazione.
Limiti di frequenza e gestione degli errori
I limiti di frequenza di Brevo sono per endpoint e per livello di piano, e la distanza tra un endpoint e l’altro è enorme.
| Endpoint | Standard | Professional ed Enterprise |
|---|---|---|
POST /v3/smtp/email | 1.000 RPS | 2.000 RPS |
POST /v3/transactionalSMS/send | 150 RPS | 200 RPS |
/v3/contacts/... | 10 RPS, 36.000 RPH | 20 RPS, 72.000 RPH |
POST /v3/events | 10 RPS, 36.000 RPH | più alto su Enterprise |
GET /v3/smtp/emails | 2 RPS, 7.200 RPH | 3 RPS, 10.800 RPH |
| Tutto il resto | 100 RPH | 200 RPH |
È l’ultima riga quella che fa male. L’invio è di fatto senza contatore, mentre la gestione delle campagne, le letture CRM e la maggior parte delle chiamate amministrative condividono un budget di 100 richieste all’ora sui piani standard. Un backfill ingenuo che legge un record azienda prima di ogni scrittura esaurisce un’ora di quota in meno di due minuti.
Ogni risposta porta x-sib-ratelimit-limit, x-sib-ratelimit-remaining e x-sib-ratelimit-reset. Leggili sui successi, non solo sui fallimenti. Superare un limite restituisce 429, e la risposta corretta è aspettare l’intervallo indicato nell’header di reset e poi applicare un backoff esponenziale con jitter.
async function callBrevo(path, init, attempt = 0) { const response = await fetch(`https://api.brevo.com/v3${path}`, { ...init, headers: { "api-key": process.env.BREVO_API_KEY, "Content-Type": "application/json", ...init.headers }, });
if (response.status === 429 && attempt < 5) { const reset = Number(response.headers.get("x-sib-ratelimit-reset") || 1); const backoff = Math.pow(2, attempt) * 250 + Math.random() * 250; await new Promise((r) => setTimeout(r, reset * 1000 + backoff)); return callBrevo(path, init, attempt + 1); }
return response;}Riprova su 429 e 5xx. Non riprovare mai alla cieca su 400 o 409, perché entrambi di solito significano che la richiesta è sbagliata e non prematura, e un 409 in particolare richiede un’azione diversa invece di una ripetizione.
Testare senza inviare email
Aggiungi l’header X-Sib-Sandbox con valore drop a un invio transazionale. Brevo convalida la richiesta, restituisce 201 con un messageId, non consegna nulla e non scrive alcun log email.
curl -X POST https://api.brevo.com/v3/smtp/email \ -H "api-key: $BREVO_API_KEY" \ -H "X-Sib-Sandbox: drop" \ -H "Content-Type: application/json" \ -d '{ "sender": { "email": "[email protected]" }, "to": [{ "email": "[email protected]" }], "subject": "Sandbox", "htmlContent": "<p>ciao</p>" }'Capisci bene cosa dimostra e cosa non dimostra. La modalità sandbox convalida solo il formato della richiesta. Non dice nulla sull’autenticazione del mittente, sul rendering dei template o sulla recapitabilità. Tieni un account Brevo separato per i test di integrazione di tutto ciò che tocca contatti o dati CRM, perché la modalità sandbox copre l’invio e non il resto dell’API.
Limiti che condizionano il design della tua integrazione
Questi sono i vincoli che emergono solo quando un’integrazione gira contro un account reale e a volume. Diversi contraddicono quello che l’API dichiara di sé. Nessuno è negoziabile, quindi l’unica risposta sensata è progettare tenendoli in conto.
Le aziende richiedono un dominio, e un solo record per dominio
GET /v3/crm/attributes/companies riporta ogni attributo come non obbligatorio, e il riferimento sulla creazione di un’azienda elenca solo name come necessario. In pratica, POST /v3/companies senza un attributo domain non vuoto restituisce 400 con un messaggio sugli attributi predefiniti obbligatori mancanti. Una stringa vuota fallisce esattamente come l’omissione.
Peggio ancora, l’unicità del dominio viene imposta. Una seconda azienda su un dominio già in uso restituisce 409. Per il commercio B2B questo è strutturale: le controllate che condividono un unico dominio email dell’acquirente non possono esistere tutte come aziende separate in Brevo. Anche la sola sincronizzazione di un contatto basta a far comparire un’azienda sul dominio email di quel contatto, quindi una creazione può scontrarsi con un’azienda che nessuno ha creato esplicitamente. L’handler giusto adotta l’azienda esistente sul 409 invece di fallire o riprovare.
Gli attributi non dichiarati vengono scartati in silenzio
Questo è il comportamento più pericoloso della piattaforma, e Brevo lo documenta senza giri di parole: se un attributo compare in una richiesta ma non era già definito nello schema dell’oggetto, non succede niente. Nessun errore, nessuna creazione dell’attributo, nessun avviso.
Una risposta 2xx non è quindi la prova che i tuoi dati siano arrivati. Leggi lo schema prima di scrivere, scarta nel tuo client tutto ciò che non è dichiarato e rifiutati di eseguire una sincronizzazione i cui attributi non esistono, invece di scrivere mezzo record per un mese prima che qualcuno se ne accorga.
I filtri sugli attributi vengono accettati e ignorati
GET /v3/companies?filters[attributes.domain]=... restituisce 200 e ignora il filtro. Due filtri completamente diversi restituiscono gli stessi record. Non esiste un modo funzionante di cercare un’azienda per attributo attraverso quella rotta.
Unito al fatto che l’elenco non filtrato va in timeout con 504 sugli account grandi a qualsiasi dimensione di pagina, un’azienda esistente può risultare davvero introvabile lungo il percorso documentato. La soluzione è scandire GET /v3/objects/company/records con sort=desc, che è veloce, paginato e restituisce gli attributi, limitandosi a un numero ragionevole di pagine. Un’azienda che ha appena provocato un 409 è quasi sempre stata creata pochi istanti prima, quindi la scansione dal più recente la trova in fretta.
Un milione di record per tipo di oggetto, e nessuna cancellazione in blocco
POST /v3/objects/{type}/batch/upsert restituisce 400 non appena un tipo di oggetto contiene un milione di record. Blocca gli aggiornamenti oltre alle creazioni: indirizzare un record esistente tramite il suo id numerico fallisce allo stesso modo. L’intero percorso di scrittura sugli oggetti si chiude in un colpo solo.
Tornare sotto il tetto è lento, perché POST /v3/objects/{type}/batch/delete restituisce 403 per i tipi di oggetto standard di Brevo come company. L’unica rotta è DELETE /v3/companies/{id}, un record per chiamata a circa 156 ms. Svuotare 124.000 record in quel modo ha richiesto ore con 20 worker in parallelo. Monitora il conteggio dei record con una schedulazione invece di scoprire il tetto attraverso una sincronizzazione fallita, e instrada gli aggiornamenti ad alto volume su PATCH /v3/companies/{id}, che non ha questo limite.
ext_id è l’id di Brevo, non il tuo
Sui record oggetto, identifiers.ext_id contiene l’id azienda CRM di Brevo stesso, una stringa in stile Mongo. Non è una chiave esterna libera. Impostare un upsert su ext_id con l’identificatore della tua piattaforma crea duplicati invece di trovare corrispondenze. Il tuo id esterno va in un attributo dichiarato tutto suo.
Gli upsert sugli oggetti sono asincroni, le scritture CRM no
batch/upsert restituisce 202 e un processId, poi applica più tardi. Un id inesistente fallisce in modo asincrono e restituisce comunque 202 a chi ha chiamato. PATCH /v3/companies/{id} restituisce 204 ed è applicata in modo sincrono. Se la tua sincronizzazione riporta un successo, solo il percorso sincrono si merita quella parola senza una lettura di conferma.
Una breve checklist di integrazione
- Chiavi API separate per servizio e per ambiente, ruotate ai cambi di personale.
- Tutte le scritture passano da un solo client che legge gli header dei limiti di frequenza e rallenta sul 429.
- Lo schema degli attributi viene verificato all’avvio, e la sincronizzazione si rifiuta di partire se i suoi attributi mancano.
- Un 409 sulla creazione di un’azienda significa adottare, non riprovare.
- I percorsi massivi usano l’API objects per il throughput e le rotte CRM per tutto ciò che deve essere confermato.
- I webhook sono idempotenti, batchati, limitati per IP e portano un header con segreto condiviso.
- Le sincronizzazioni incrementali dei contatti usano
modifiedSince, non la scansione dell’intera lista.
Costruire e mantenere questo livello è vero lavoro di ingegneria: verifica degli schemi, backoff, logica di adozione, riconciliazione. Tajo esiste per assorbirlo, tenendo i dati di Shopify e del commercio sincronizzati con contatti, aziende ed eventi di Brevo senza che nessuno debba scrivere a mano la logica di retry e deduplicazione. Se invece lo stai cablando da solo, la guida all’integrazione di Brevo accompagna le scelte sul modello dati che vengono prima del codice.
Punti chiave
- L’API è un’unica superficie REST su
https://api.brevo.com/v3/, autenticata con un headerapi-keyinvece che con un token bearer. - I limiti di frequenza sono estremamente irregolari: l’invio è di fatto senza contatore, mentre la maggior parte degli altri endpoint condivide 100 richieste all’ora sui piani standard.
- Esistono SDK ufficiali per sette linguaggi, ma la superficie HTTP è abbastanza semplice da chiamare direttamente quando ti servono solo pochi endpoint.
- La modalità sandbox convalida solo il formato della richiesta, quindi tieni un account separato per testare qualsiasi cosa vada oltre l’invio.
- Una risposta 2xx non dimostra che una scrittura sia stata applicata. Gli attributi non dichiarati vengono scartati in silenzio e gli upsert sugli oggetti sono asincroni.
- Progetta tenendo conto dei limiti fissi: un’azienda per dominio, un milione di record per tipo di oggetto, nessuna cancellazione in blocco per gli oggetti standard e filtri sugli attributi che semplicemente non fanno nulla.