L’API Brevo : guide pratique pour les développeurs
Guide de l’API Brevo pour les développeurs : authentification, URL de base, contacts, e-mail transactionnel, campagnes, objets CRM, webhooks, limites de débit et contraintes réelles.
Brevo expose une seule API REST qui couvre la messagerie transactionnelle, les campagnes marketing, les données de contact et les fiches CRM. Obtenir un premier code 201 prend environ deux minutes. Obtenir une intégration de production qui ne perd pas silencieusement de données prend nettement plus de temps, parce que plusieurs des contraintes les plus importantes sont soit non documentées, soit en contradiction avec ce que l’API dit d’elle-même.
Ce guide couvre les deux moitiés : les endpoints, les SDK et l’authentification dont vous avez besoin dès le premier jour, et les limites de la plateforme autour desquelles vous devez concevoir votre intégration avant la mise en production.
Ce que couvre l’API Brevo
Tout se trouve sous un seul hôte et un seul chemin de version. La documentation développeur regroupe la surface en quatre domaines produit :
- Messagerie : e-mail transactionnel, SMS et WhatsApp, y compris les envois par lot, la programmation et l’activité des messages.
- Plateforme marketing : contacts, listes, segments et campagnes e-mail.
- E-commerce : produits, commandes et suivi des événements clients.
- Conversations : le widget de chat et la gestion programmatique des conversations.
Ces domaines partagent un même compte, une même base de contacts et une même clé API. C’est pratique, et parfois dangereux : un script écrit avec une idée d’environnement de test en tête s’adresse aux contacts auxquels vos campagnes envoient réellement.
Transactionnel contre marketing
Les deux familles se comportent de façon suffisamment différente pour que les confondre soit l’erreur de conception la plus fréquente.
| Transactionnel | Marketing | |
|---|---|---|
| Endpoint principal | POST /v3/smtp/email | POST /v3/emailCampaigns |
| Adressage | Destinataires explicites dans la requête | listIds ou segmentIds |
| Déclencheur | Votre application, en temps réel | Programmé ou envoyé à la demande |
| Profil de volume typique | Continu, un message à la fois | En rafale, un gros envoi |
| Posture de limite de débit | Très élevée, 1 000 requêtes par seconde sur les forfaits standard | Faible, les endpoints de campagne tombent sous le plafond général |
Si vous hésitez encore à savoir si Brevo est la bonne plateforme, la présentation de la plateforme traite ce sujet.
Authentification et gestion des clés
Brevo utilise une simple clé API dans un en-tête personnalisé. Cet en-tête s’appelle api-key, pas Authorization, et il n’y a aucun préfixe Bearer. C’est ce qui fait trébucher presque toutes les personnes venant d’une autre API de messagerie.
curl https://api.brevo.com/v3/account \ -H "api-key: $BREVO_API_KEY"Les clés se génèrent dans l’application Brevo, dans les paramètres du compte, section SMTP et API, onglet clés API. Donnez à chaque clé un nom descriptif lié au système qui l’utilise. La valeur de la clé s’affiche exactement une fois au moment de sa génération : si vous la perdez, vous en générez une nouvelle plutôt que de récupérer l’ancienne.
Quelques règles pratiques :
- Émettez une clé distincte par environnement de déploiement et par service. Révoquer une clé compromise ne doit jamais mettre à l’arrêt trois systèmes sans rapport entre eux.
- Les clés API standard couvrent tout le compte. Considérez toute clé comme un accès complet aux contacts, à l’envoi et aux données CRM.
- Brevo prend aussi en charge OAuth 2.0 pour les applications qui agissent au nom d’autres comptes Brevo, décrit à côté du flux par clé dans les schémas d’authentification.
- Le serveur MCP utilisé par les assistants IA prend un jeton distinct et utilise, lui, un en-tête bearer. Ce jeton se génère dans le même écran de clés API mais n’est pas interchangeable avec une clé REST.
URL de base, versionnement et première écriture
L’URL de base est https://api.brevo.com/v3/. La version se trouve dans le chemin plutôt que dans un en-tête, et v3 est la génération actuelle. Tous les chemins de ce guide sont relatifs à cette base.
Une première écriture est plus instructive qu’une première lecture, parce qu’elle sollicite les parties du compte qui sont habituellement mal configurées, en particulier les expéditeurs vérifiés :
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": "Premier envoi transactionnel", "htmlContent": "<html><body><p>Ça fonctionne.</p></body></html>", "tags": ["smoke-test"] }'Un envoi réussi renvoie un code 201 avec un messageId. Un envoi programmé renvoie un code 202.
Les endpoints que vous utiliserez vraiment
Contacts
POST /v3/contacts crée un contact. Le corps accepte email, une carte attributes pour les champs personnalisés, listIds, ext_id pour votre propre clé externe, et les deux indicateurs qui comptent le plus en pratique : updateEnabled, qui transforme l’appel en upsert, et getId, qui fait renvoyer l’identifiant du contact dans la réponse.
Les lectures passent par GET /v3/contacts, qui pagine avec limit (50 par défaut, 1000 au maximum) et offset, et prend en charge modifiedSince et createdSince en UTC. Les synchronisations incrémentales doivent s’appuyer sur modifiedSince plutôt que de parcourir la liste entière. Notez que le paramètre filter ne prend en charge qu’un opérateur d’égalité : tout ce qui demande plus d’expressivité appartient à un segment.
Pour le chargement en masse, POST /v3/contacts/import accepte fileUrl, fileBody ou jsonBody, cible des listIds et s’exécute de façon asynchrone en renvoyant un processId. Brevo documente un corps maximal de 10 Mo et recommande de rester autour de 8 Mo, car l’analyse gonfle la charge utile. Fournissez notifyUrl pour connaître le résultat au lieu d’interroger l’API en boucle.
E-mail transactionnel
POST /v3/smtp/email est le cheval de trait. Au-delà de sender, to, subject et htmlContent, les champs à connaître sont :
templateIdavecparams, qui remplace le contenu en ligne par un modèle Brevo et ses substitutions de variables. Les paramètres d’une version individuelle sont plafonnés à 100 Ko, les paramètres cumulés à 1000 Ko.messageVersions, qui envoie des variantes personnalisées en un seul appel, avec jusqu’à 99 destinataires par version.tags, que vous devriez toujours renseigner. Les tags reviennent dans les événements de webhook, et c’est le seul moyen peu coûteux de corréler un événement de livraison avec le chemin de code qui l’a produit.scheduledAtaccompagné debatchId, pour les envois futurs que vous voudrez peut-être annuler en groupe.headers, en Title-Case, pour les en-têtes SMTP personnalisés.
Une seule requête accepte au maximum 2 000 destinataires. Pour la différence entre cet endpoint et l’envoi de campagnes, le guide de l’e-mail transactionnel propose le point de vue stratégique.
Campagnes e-mail
POST /v3/emailCampaigns exige name et sender, plus exactement une source de contenu : htmlContent (10 caractères minimum, moins de 1 Mo), htmlUrl ou templateId. L’audience se met dans recipients sous forme de listIds ou de segmentIds, et scheduledAt utilise le format UTC YYYY-MM-DDTHH:mm:ss.SSSZ. Des routes complémentaires couvrent l’envoi immédiat, l’envoi d’un test, la mise à jour du statut et la récupération du rapport de campagne.
Entreprises, transactions et objets
Le CRM de Brevo comporte deux chemins d’écriture qui se recouvrent, et bien choisir a son importance.
Les routes CRM sont POST /v3/companies, PATCH /v3/companies/{id}, DELETE /v3/companies/{id}, et l’ensemble équivalent pour les transactions. Elles sont synchrones. Un PATCH renvoie un code 204 une fois la modification appliquée.
L’API objets est le chemin de masse : POST /v3/objects/{object_type}/batch/upsert accepte jusqu’à 1000 enregistrements et 1 Mo par requête, jusqu’à 500 attributs par enregistrement et jusqu’à 10 enregistrements d’association par type d’objet et par enregistrement. Elle renvoie un code 202 avec un processId, ce qui signifie accepté et non appliqué.
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" } } ] }'Le guide du CRM Brevo traite le modèle d’objets du point de vue opérationnel.
SDK officiels
Brevo maintient des clients sous l’organisation GitHub getbrevo :
| Langage | Dépôt |
|---|---|
| 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 |
Le client Node s’installe sous le nom @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: "Commande confirmée", htmlContent: "<html><body><p>Merci pour votre commande.</p></body></html>", tags: ["order-confirmation"],});
console.log("Message ID:", result.messageId);Le client Python s’installe avec pip install brevo-python. Si vous préférez ne pas embarquer une dépendance SDK pour deux endpoints, la surface HTTP brute est assez réduite pour être appelée directement, ce qui vous protège en prime des changements de version du 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 responseIl existe également un serveur MCP à l’adresse https://mcp.brevo.com/v1/brevo/mcp pour les assistants IA, authentifié par un jeton bearer généré dans le même écran de paramètres. Il est utile pour l’exploration et les questions sur le compte, pas pour les chemins de données en production.
Webhooks
Les webhooks vous permettent de savoir ce qui s’est passé après un envoi. POST /v3/webhooks en crée un, avec url, events, type, et éventuellement channel (email ou sms), batched, des headers personnalisés et un objet auth.
Il existe trois types de webhooks, avec des vocabulaires d’événements distincts :
- Transactionnel :
sent,request,delivered,hardBounce,softBounce,blocked,spam,invalid,deferred,click,opened,uniqueOpened,unsubscribed. - Marketing :
spam,opened,click,hardBounce,softBounce,unsubscribed,listAddition,delivered,contactUpdated,contactDeleted. - Entrant :
inboundEmailProcessedetreply, qui exigent en plus undomain.
curl -X POST https://api.brevo.com/v3/webhooks \ -H "api-key: $BREVO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://yourapp.example/hooks/brevo", "type": "transactional", "events": ["delivered", "hardBounce", "spam", "unsubscribed"], "description": "Signaux de délivrabilité" }'Trois points à ne pas rater. D’abord, un compte peut contenir au maximum 40 webhooks tous types confondus : routez donc par événement à l’intérieur de votre gestionnaire plutôt que d’enregistrer un endpoint par événement. Ensuite, utilisez l’indicateur batched quand vous attendez du volume, puisqu’une requête portant plusieurs événements coûte bien moins cher à traiter que de nombreuses requêtes. Enfin, protégez le récepteur : Brevo publie ses plages d’adresses IP d’envoi, et restreindre votre endpoint à ces plages est l’approche documentée. Ajoutez votre propre secret partagé via le champ headers comme seconde couche.
Les gestionnaires doivent être idempotents. Utilisez l’identifiant de message, le type d’événement et l’horodatage comme clé de déduplication.
Limites de débit et gestion des erreurs
Les limites de débit de Brevo s’appliquent par endpoint et par niveau de forfait, et l’écart entre endpoints est énorme.
| Endpoint | Standard | Professional et 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 | plus élevé sur Enterprise |
GET /v3/smtp/emails | 2 RPS, 7 200 RPH | 3 RPS, 10 800 RPH |
| Tout le reste | 100 RPH | 200 RPH |
C’est cette dernière ligne qui fait mal. L’envoi est pratiquement illimité, tandis que la gestion des campagnes, les lectures CRM et la plupart des appels administratifs se partagent un budget de 100 requêtes par heure sur les forfaits standard. Un rattrapage naïf qui lit une fiche entreprise avant chaque écriture épuisera une heure de quota en moins de deux minutes.
Chaque réponse porte les en-têtes x-sib-ratelimit-limit, x-sib-ratelimit-remaining et x-sib-ratelimit-reset. Lisez-les en cas de succès, pas seulement en cas d’échec. Un dépassement renvoie un code 429, et la bonne réaction consiste à attendre l’intervalle indiqué dans l’en-tête de réinitialisation puis à appliquer un backoff exponentiel avec gigue.
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;}Réessayez sur 429 et 5xx. Ne réessayez jamais aveuglément sur 400 ou 409, car les deux signifient en général que la requête est incorrecte plutôt que prématurée, et un 409 en particulier appelle une action différente plutôt qu’une répétition.
Tester sans envoyer de courrier
Ajoutez l’en-tête X-Sib-Sandbox avec la valeur drop à un envoi transactionnel. Brevo valide la requête, renvoie un code 201 avec un messageId, ne livre rien et n’écrit aucun journal d’e-mail.
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>bonjour</p>" }'Comprenez bien ce que cela prouve et ce que cela ne prouve pas. Le mode bac à sable valide uniquement le format de la requête. Il ne dit rien de l’authentification de l’expéditeur, du rendu des modèles ni de la délivrabilité. Gardez un compte Brevo distinct pour les tests d’intégration de tout ce qui touche aux contacts ou aux données CRM, car le mode bac à sable couvre l’envoi et non le reste de l’API.
Les limites qui façonnent la conception de votre intégration
Voici les contraintes qui n’apparaissent qu’une fois l’intégration confrontée à un vrai compte et à du volume. Plusieurs contredisent ce que l’API dit d’elle-même. Aucune n’est négociable : la seule réaction sensée est de concevoir autour d’elles.
Les entreprises exigent un domaine, et une seule entreprise par domaine
GET /v3/crm/attributes/companies signale chaque attribut comme non obligatoire, et la référence de création d’entreprise ne liste que name comme requis. En pratique, POST /v3/companies sans attribut domain non vide renvoie un code 400 avec un message signalant des attributs par défaut obligatoires manquants. Une chaîne vide échoue exactement comme une omission.
Pire, l’unicité du domaine est imposée. Une seconde entreprise sur un domaine déjà utilisé renvoie un code 409. Pour le commerce B2B, c’est structurel : des filiales qui partagent un même domaine d’e-mail acheteur ne peuvent pas exister toutes comme entreprises distinctes dans Brevo. Synchroniser un contact suffit d’ailleurs à faire apparaître une entreprise sur le domaine d’e-mail de ce contact, si bien qu’une création peut entrer en collision avec une entreprise que personne n’a explicitement créée. Le bon gestionnaire adopte l’entreprise existante sur un 409 au lieu d’échouer ou de réessayer.
Les attributs non déclarés sont écartés en silence
C’est le comportement le plus dangereux de la plateforme, et Brevo le documente sans détour : si un attribut apparaît dans une requête sans avoir été défini au préalable dans le schéma de l’objet, il ne se passe rien. Aucune erreur, aucune création d’attribut, aucun avertissement.
Une réponse 2xx n’est donc pas la preuve que vos données ont atterri. Lisez le schéma avant d’écrire, écartez tout élément non déclaré dans votre propre client, et refusez d’exécuter une synchronisation dont les attributs n’existent pas plutôt que d’écrire une demi-fiche pendant un mois avant que quiconque s’en aperçoive.
Les filtres d’attributs sont acceptés puis ignorés
GET /v3/companies?filters[attributes.domain]=... renvoie un code 200 et ignore le filtre. Deux filtres complètement différents renvoient les mêmes enregistrements. Il n’existe aucun moyen fonctionnel de retrouver une entreprise par attribut via cette route.
Combiné au fait que la liste non filtrée expire avec un 504 sur les gros comptes, quelle que soit la taille de page, une entreprise existante peut devenir réellement introuvable par le chemin documenté. Le contournement consiste à parcourir GET /v3/objects/company/records avec sort=desc, une route rapide, paginée et qui renvoie les attributs, en la bornant à un nombre raisonnable de pages. Une entreprise qui vient de déclencher un 409 a presque toujours été créée quelques instants plus tôt : un parcours du plus récent au plus ancien la retrouve vite.
Un million d’enregistrements par type d’objet, et pas de suppression en masse
POST /v3/objects/{type}/batch/upsert renvoie un code 400 dès qu’un type d’objet contient un million d’enregistrements. Cela bloque les mises à jour autant que les créations : adresser un enregistrement existant par son propre identifiant numérique échoue de la même façon. Tout le chemin d’écriture des objets se ferme d’un coup.
Repasser sous le plafond est lent, car POST /v3/objects/{type}/batch/delete renvoie un code 403 pour les types d’objets standard de Brevo comme company. La seule route est DELETE /v3/companies/{id}, un enregistrement par appel à environ 156 ms. Nettoyer 124 000 enregistrements de cette façon a demandé des heures avec 20 workers en parallèle. Surveillez le nombre d’enregistrements de façon régulière plutôt que de découvrir le plafond par une synchronisation en échec, et faites passer les mises à jour à fort volume par PATCH /v3/companies/{id}, qui n’a pas cette limite.
ext_id est l’identifiant de Brevo, pas le vôtre
Sur les enregistrements d’objets, identifiers.ext_id contient l’identifiant d’entreprise CRM propre à Brevo, une chaîne de style Mongo. Ce n’est pas une clé externe libre. Baser un upsert sur ext_id renseigné avec l’identifiant de votre plateforme crée des doublons au lieu de faire correspondre les fiches. Votre identifiant externe a sa place dans un attribut déclaré à part.
Les upserts d’objets sont asynchrones, les écritures CRM ne le sont pas
batch/upsert renvoie un code 202 et un processId, puis s’applique plus tard. Un identifiant inexistant échoue de façon asynchrone et renvoie quand même un 202 à votre appelant. PATCH /v3/companies/{id} renvoie un code 204 et s’applique de façon synchrone. Si votre synchronisation annonce un succès, seul le chemin synchrone mérite ce mot sans lecture de vérification.
Une courte liste de contrôle d’intégration
- Des clés API distinctes par service et par environnement, remplacées lors des changements d’équipe.
- Toutes les écritures passent par un client unique qui lit les en-têtes de limite de débit et applique un backoff sur 429.
- Le schéma des attributs est vérifié au démarrage, et la synchronisation refuse de s’exécuter si ses attributs manquent.
- Un 409 à la création d’une entreprise signifie adopter, pas réessayer.
- Les chemins de masse utilisent l’API objets pour le débit et les routes CRM pour tout ce qui doit être confirmé.
- Les webhooks sont idempotents, groupés, restreints par IP et porteurs d’un en-tête de secret partagé.
- Les synchronisations incrémentales de contacts utilisent
modifiedSince, pas des parcours de liste complète.
Construire et maintenir cette couche est un vrai travail d’ingénierie : vérification de schéma, backoff, logique d’adoption, réconciliation. Tajo existe pour l’absorber, en gardant les données Shopify et commerce synchronisées avec les contacts, les entreprises et les événements Brevo sans que personne n’écrive à la main la logique de reprise et de déduplication. Si vous préférez le câbler vous-même, le guide de l’intégration Brevo parcourt les choix de modèle de données qui précèdent le code.
Points clés à retenir
- L’API forme une seule surface REST à l’adresse
https://api.brevo.com/v3/, authentifiée par un en-têteapi-keyplutôt que par un jeton bearer. - Les limites de débit sont très inégales : l’envoi est pratiquement illimité, tandis que la plupart des autres endpoints se partagent 100 requêtes par heure sur les forfaits standard.
- Des SDK officiels existent pour sept langages, mais la surface HTTP est assez simple pour être appelée directement quand vous n’avez besoin que de quelques endpoints.
- Le mode bac à sable valide uniquement le format de la requête : gardez un compte distinct pour tester tout ce qui dépasse l’envoi.
- Une réponse 2xx ne prouve pas qu’une écriture s’est appliquée. Les attributs non déclarés sont écartés en silence, et les upserts d’objets sont asynchrones.
- Concevez autour des limites fixes : une entreprise par domaine, un million d’enregistrements par type d’objet, pas de suppression en masse pour les objets standard, et des filtres d’attributs qui ne font discrètement rien.