Brevo API: een praktische gids voor ontwikkelaars
Brevo API-gids voor ontwikkelaars: authenticatie, base URL, contacten, transactionele e-mail, campagnes, CRM-objecten, webhooks, rate limits en de echte grenzen.
Brevo biedt één REST API die transactionele berichten, marketingcampagnes, contactgegevens en CRM-records omspant. Het eerste verzoek een 201 laten teruggeven kost ongeveer twee minuten. Een productie-integratie bouwen die niet stilletjes gegevens verliest kost aanzienlijk langer, want een aantal van de beperkingen die er het meest toe doen is ofwel niet gedocumenteerd, ofwel in tegenspraak met wat de API over zichzelf rapporteert.
Deze gids behandelt beide helften: de endpoints, SDK’s en authenticatie die je op dag één nodig hebt, en de platformlimieten waar je omheen moet ontwerpen voordat je live gaat.
Wat de Brevo API omvat
Alles staat onder één host en één versiepad. De ontwikkelaarsdocumentatie verdeelt dat oppervlak in vier productgebieden:
- Messaging: transactionele e-mail, SMS en WhatsApp, inclusief batchverzendingen, planning en berichtactiviteit.
- Marketingplatform: contacten, lijsten, segmenten en e-mailcampagnes.
- eCommerce: producten, bestellingen en het volgen van klantgebeurtenissen.
- Conversations: de chatwidget en programmatisch gespreksbeheer.
Die gebieden delen één account, één contactdatabase en één API-sleutel. Dat is handig en af en toe gevaarlijk: een script dat geschreven is vanuit een staging-idee van de data praat met dezelfde contacten waar je campagnes naartoe gaan.
Transactioneel versus marketing
De twee families gedragen zich verschillend genoeg dat ze door elkaar halen de meest gemaakte ontwerpfout is.
| Transactioneel | Marketing | |
|---|---|---|
| Belangrijkste endpoint | POST /v3/smtp/email | POST /v3/emailCampaigns |
| Adressering | Expliciete ontvangers in het verzoek | listIds of segmentIds |
| Trigger | Je applicatie, in realtime | Ingepland of op aanvraag verzonden |
| Typisch volumepatroon | Continu, één bericht tegelijk | Piekerig, één grote verzending |
| Houding rond rate limits | Zeer hoog, 1.000 verzoeken per seconde op standaardabonnementen | Laag, campagne-endpoints vallen onder de algemene limiet |
Ben je nog aan het bepalen of Brevo überhaupt het juiste platform is, dan behandelt het platformoverzicht die vraag.
Authenticatie en sleutelbeheer
Brevo gebruikt een gewone API-sleutel in een eigen header. Die header heet api-key, niet Authorization, en er is geen Bearer-prefix. Daar struikelt bijna iedereen over die eerst een andere messaging-API heeft gebruikt.
curl https://api.brevo.com/v3/account \ -H "api-key: $BREVO_API_KEY"Sleutels maak je aan in de Brevo-app onder de accountinstellingen, in de sectie SMTP and API, op het tabblad API keys. Geef elke sleutel een beschrijvende naam die verwijst naar het systeem dat hem gebruikt. De waarde van de sleutel wordt precies één keer getoond bij het aanmaken, dus raak je hem kwijt, dan maak je een nieuwe aan in plaats van de oude terug te halen.
Een paar praktische regels:
- Geef een aparte sleutel uit per deployomgeving en per dienst. Het intrekken van een gelekte sleutel mag nooit drie ongerelateerde systemen platleggen.
- Standaard API-sleutels gelden accountbreed. Behandel elke sleutel als volledige toegang tot contacten, verzending en CRM-gegevens.
- Brevo ondersteunt ook OAuth 2.0 voor applicaties die namens andere Brevo-accounts handelen, beschreven naast de sleutelflow in authentication schemes.
- De MCP-server die AI-assistenten gebruiken werkt met een apart token en gebruikt wél een bearer-header. Dat token maak je aan in hetzelfde API keys-scherm, maar het is niet uitwisselbaar met een REST-sleutel.
Base URL, versies en je eerste schrijfactie
De base URL is https://api.brevo.com/v3/. De versie zit in het pad in plaats van in een header, en v3 is de huidige generatie. Elk pad in deze gids is relatief aan die basis.
Een eerste schrijfactie zegt meer dan een eerste leesactie, want die raakt de onderdelen van het account die meestal verkeerd staan (geverifieerde afzenders in het bijzonder):
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": "First transactional send", "htmlContent": "<html><body><p>It works.</p></body></html>", "tags": ["smoke-test"] }'Een geslaagde verzending geeft 201 terug met een messageId. Een ingeplande verzending geeft 202.
De endpoints die je echt gaat gebruiken
Contacten
POST /v3/contacts maakt een contact aan. De body neemt email, een attributes-map voor eigen velden, listIds, ext_id voor je eigen externe sleutel, en de twee vlaggen die in de praktijk het meest uitmaken: updateEnabled, dat de aanroep in een upsert verandert, en getId, dat de respons het contact-id laat teruggeven.
Lezen gaat via GET /v3/contacts, dat pagineert met limit (standaard 50, maximaal 1000) en offset, en modifiedSince en createdSince in UTC ondersteunt. Incrementele synchronisaties leunen beter op modifiedSince dan op het aflopen van de volledige lijst. Let op: de parameter filter ondersteunt alleen een gelijkheidsoperator, dus alles wat expressiever is hoort in een segment thuis.
Voor bulkladen accepteert POST /v3/contacts/import een fileUrl, fileBody of jsonBody, richt zich op listIds en draait asynchroon, met een processId als resultaat. Brevo documenteert een maximum van 10 MB per body en raadt aan rond 8 MB te blijven, omdat het parsen de payload opblaast. Geef notifyUrl mee zodat je de uitkomst te horen krijgt in plaats van te moeten pollen.
Transactionele e-mail
POST /v3/smtp/email is het werkpaard. Naast sender, to, subject en htmlContent zijn dit de velden die je moet kennen:
templateIdmetparams, waarmee inline content wordt vervangen door een Brevo-template en de bijbehorende variabelen. Params per versie zijn begrensd op 100 KB, cumulatief op 1000 KB.messageVersions, waarmee je gepersonaliseerde varianten in één aanroep verstuurt, met maximaal 99 ontvangers per versie.tags, die je altijd zou moeten zetten. Tags komen terug op webhookgebeurtenissen en zijn de enige goedkope manier om een bezorggebeurtenis te koppelen aan het codepad dat hem veroorzaakte.scheduledAtplusbatchId, voor toekomstige verzendingen die je als groep wilt kunnen annuleren.headers, in Title-Case, voor eigen SMTP-headers.
Eén verzoek accepteert maximaal 2.000 ontvangers. Voor het verschil tussen dit endpoint en campagneverzending geeft de gids over transactionele e-mail de strategische kant.
E-mailcampagnes
POST /v3/emailCampaigns vereist name en sender, plus precies één contentbron: htmlContent (minimaal 10 tekens, onder 1 MB), htmlUrl of templateId. De doelgroep gaat in recipients als listIds of segmentIds, en scheduledAt gebruikt het UTC-formaat YYYY-MM-DDTHH:mm:ss.SSSZ. Bijbehorende routes dekken direct verzenden, een test versturen, de status bijwerken en het campagnerapport ophalen.
Bedrijven, deals en objecten
Het CRM van Brevo heeft twee overlappende schrijfpaden, en het juiste kiezen doet ertoe.
De CRM-routes zijn POST /v3/companies, PATCH /v3/companies/{id}, DELETE /v3/companies/{id} en de vergelijkbare set voor deals. Die zijn synchroon. Een PATCH geeft 204 terug zodra de wijziging is doorgevoerd.
De objects-API is het bulkpad: POST /v3/objects/{object_type}/batch/upsert neemt tot 1000 records en 1 MB per verzoek, tot 500 attributen per record en tot 10 associatierecords per objecttype per record. Hij geeft 202 terug met een processId, wat geaccepteerd betekent en niet doorgevoerd.
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" } } ] }'De Brevo CRM-gids behandelt het objectmodel vanuit de kant van de operator.
Officiële SDK’s
Brevo onderhoudt clients onder de GitHub-organisatie getbrevo:
| Taal | 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 |
De Node-client installeer je als @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: "Order confirmed", htmlContent: "<html><body><p>Thanks for your order.</p></body></html>", tags: ["order-confirmation"],});
console.log("Message ID:", result.messageId);De Python-client installeer je met pip install brevo-python. Wil je liever geen SDK-afhankelijkheid meeslepen voor twee endpoints, dan is het ruwe HTTP-oppervlak klein genoeg om direct aan te roepen, wat je bovendien beschermt tegen versiegedoe in de 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 responseEr is ook een MCP-server op https://mcp.brevo.com/v1/brevo/mcp voor AI-assistenten, geauthenticeerd met een bearer-token dat je in hetzelfde instellingenscherm aanmaakt. Handig om te verkennen en accountvragen te stellen, niet voor productiedatapaden.
Webhooks
Webhooks zijn hoe je te weten komt wat er na een verzending gebeurt. POST /v3/webhooks maakt er een aan, met url, events, type en optioneel channel (email of sms), batched, eigen headers en een auth-object.
Er zijn drie webhooktypes met elk een eigen gebeurtenissenwoordenboek:
- Transactional:
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:
inboundEmailProcessedenreply, die daarnaast eendomainvereisen.
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": "Deliverability signals" }'Drie dingen die goed moeten. Ten eerste kan een account maximaal 40 webhooks bevatten over alle types heen, dus routeer op gebeurtenis binnen je handler in plaats van één endpoint per gebeurtenis te registreren. Ten tweede: gebruik de vlag batched als je volume verwacht, want één verzoek met veel gebeurtenissen is veel goedkoper te verwerken dan veel verzoeken. Ten derde: bescherm de ontvanger. Brevo publiceert zijn verzend-IP-reeksen, en je endpoint tot die reeksen beperken is de gedocumenteerde aanpak. Voeg via het veld headers je eigen gedeelde geheim toe als tweede laag.
Handlers moeten idempotent zijn. Behandel het bericht-id plus het gebeurtenistype plus het tijdstempel als de deduplicatiesleutel.
Rate limits en foutafhandeling
De rate limits van Brevo gelden per endpoint en per abonnementsniveau, en de spreiding tussen endpoints is enorm.
| Endpoint | Standard | Professional en 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 | hoger op Enterprise |
GET /v3/smtp/emails | 2 RPS, 7.200 RPH | 3 RPS, 10.800 RPH |
| Al het overige | 100 RPH | 200 RPH |
Die laatste rij is degene die pijn doet. Verzenden is praktisch ongemeten, terwijl campagnebeheer, CRM-leesacties en de meeste administratieve aanroepen samen een budget van 100 verzoeken per uur delen op standaardabonnementen. Een naïeve backfill die vóór elke schrijfactie een bedrijfsrecord leest, put een uur quotum uit in minder dan twee minuten.
Elke respons draagt x-sib-ratelimit-limit, x-sib-ratelimit-remaining en x-sib-ratelimit-reset. Lees ze bij succes, niet alleen bij falen. Een limiet overschrijden geeft 429, en de juiste reactie is wachten op het interval in de reset-header en daarna exponentiële backoff met jitter toepassen.
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;}Probeer 429 en 5xx opnieuw. Probeer 400 of 409 nooit blind opnieuw, want beide betekenen meestal dat het verzoek verkeerd is en niet te vroeg, en een 409 vraagt bovendien om een andere actie in plaats van een herhaling.
Testen zonder e-mail te versturen
Voeg de header X-Sib-Sandbox met de waarde drop toe aan een transactionele verzending. Brevo valideert het verzoek, geeft 201 terug met een messageId, bezorgt niets en schrijft geen e-maillog weg.
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>hi</p>" }'Begrijp goed wat dit wel en niet bewijst. Sandboxmodus valideert alleen het formaat van het verzoek. Het zegt niets over afzenderauthenticatie, templaterendering of bezorgbaarheid. Houd een apart Brevo-account aan voor integratietests van alles wat contacten of CRM-gegevens raakt, want sandboxmodus dekt verzending en niet de rest van de API.
Limieten die je integratieontwerp bepalen
Dit zijn de beperkingen die pas opduiken zodra een integratie op volume tegen een echt account draait. Verschillende ervan spreken tegen wat de API over zichzelf zegt. Geen ervan is onderhandelbaar, dus de enige zinnige reactie is eromheen ontwerpen.
Bedrijven vereisen een domein, en er kan maar één bedrijf per domein zijn
GET /v3/crm/attributes/companies rapporteert elk attribuut als niet verplicht, en de referentie voor het aanmaken van een bedrijf noemt alleen name als verplicht. In de praktijk geeft POST /v3/companies zonder een niet-leeg domain-attribuut een 400 terug met een melding over ontbrekende verplichte standaardattributen. Een lege string faalt op precies dezelfde manier als weglaten.
Erger nog: domeinuniciteit wordt afgedwongen. Een tweede bedrijf op een domein dat al in gebruik is geeft 409. Voor B2B-commerce is dat structureel: dochterondernemingen die één e-maildomein van de inkoper delen kunnen niet allemaal als aparte bedrijven in Brevo bestaan. Een contact synchroniseren is bovendien al genoeg om een bedrijf te laten verschijnen op het e-maildomein van dat contact, dus een create kan botsen met een bedrijf dat niemand expliciet heeft aangemaakt. De juiste handler neemt bij een 409 het bestaande bedrijf over in plaats van te falen of opnieuw te proberen.
Niet-gedeclareerde attributen worden stilletjes weggegooid
Dit is het gevaarlijkste gedrag in het platform, en Brevo documenteert het onomwonden: verschijnt een attribuut in een verzoek terwijl het niet eerder in het objectschema is gedefinieerd, dan gebeurt er niets. Geen fout, geen aanmaak van het attribuut, geen waarschuwing.
Een 2xx-respons is dus geen bewijs dat je data is geland. Lees het schema voordat je schrijft, laat alles wat niet gedeclareerd is in je eigen client vallen, en weiger een synchronisatie te draaien waarvan de attributen niet bestaan, in plaats van een maand lang halve records weg te schrijven voordat iemand het merkt.
Attribuutfilters worden geaccepteerd en genegeerd
GET /v3/companies?filters[attributes.domain]=... geeft 200 terug en negeert het filter. Twee volstrekt verschillende filters leveren dezelfde records op. Er is via die route geen werkende manier om een bedrijf op attribuut op te zoeken.
Gecombineerd met het feit dat de ongefilterde lijst op grote accounts bij elke paginagrootte afbreekt met een 504, kan een bestaand bedrijf via het gedocumenteerde pad werkelijk onvindbaar zijn. De omweg is GET /v3/objects/company/records scannen met sort=desc, wat snel is, gepagineerd en attributen teruggeeft, begrensd tot een verstandig aantal pagina’s. Een bedrijf dat net een 409 veroorzaakte is bijna altijd kort daarvoor aangemaakt, dus scannen met de nieuwste eerst vindt het snel.
Eén miljoen records per objecttype, en geen bulk delete
POST /v3/objects/{type}/batch/upsert geeft 400 terug zodra een objecttype een miljoen records bevat. Het blokkeert updates net zo goed als creates: een bestaand record aanspreken op zijn eigen numerieke id faalt identiek. Het hele objectschrijfpad sluit in één klap.
Weer onder het plafond komen gaat langzaam, want POST /v3/objects/{type}/batch/delete geeft 403 terug voor Brevo-standaardobjecttypes zoals company. De enige route is DELETE /v3/companies/{id}, één record per aanroep op ruwweg 156 ms. 124.000 records op die manier opruimen kostte uren met 20 parallelle workers. Bewaak het aantal records volgens een schema in plaats van het plafond te ontdekken via een mislukte synchronisatie, en routeer updates met hoog volume via PATCH /v3/companies/{id}, dat zo’n limiet niet kent.
ext_id is het id van Brevo, niet dat van jou
Op objectrecords bevat identifiers.ext_id het eigen CRM-bedrijfs-id van Brevo, een string in Mongo-stijl. Het is geen vrije externe sleutel. Een upsert sleutelen op ext_id met de identificatie van jouw platform maakt duplicaten in plaats van te matchen. Jouw externe id hoort in een eigen, gedeclareerd attribuut.
Object-upserts zijn asynchroon, CRM-schrijfacties niet
batch/upsert geeft 202 en een processId terug en past de wijziging later toe. Een niet-bestaand id faalt asynchroon en geeft je aanroeper alsnog 202. PATCH /v3/companies/{id} geeft 204 en wordt synchroon toegepast. Als je synchronisatie succes meldt, verdient alleen het synchrone pad dat woord zonder een controlerende leesactie.
Een korte integratiechecklist
- Aparte API-sleutels per dienst en per omgeving, geroteerd bij personeelswisselingen.
- Alle schrijfacties lopen via één client die de rate limit-headers leest en terugschakelt bij 429.
- Het attribuutschema wordt bij het opstarten geverifieerd, en de synchronisatie weigert te draaien als attributen ontbreken.
- Een 409 bij het aanmaken van een bedrijf betekent overnemen, niet opnieuw proberen.
- Bulkpaden gebruiken de objects-API voor doorvoer en de CRM-routes voor alles wat bevestigd moet worden.
- Webhooks zijn idempotent, gebatcht, IP-beperkt en dragen een header met een gedeeld geheim.
- Incrementele contactsynchronisaties gebruiken
modifiedSince, geen volledige lijstdoorloop.
Deze laag bouwen en onderhouden is echt engineeringwerk: schemaverificatie, backoff, overnamelogica, reconciliatie. Tajo bestaat om dat werk op te vangen en houdt Shopify- en commercegegevens in sync met Brevo-contacten, bedrijven en gebeurtenissen zonder dat iemand de retry- en dedupelogica met de hand schrijft. Bedraad je het liever zelf, dan loopt de Brevo-integratiegids de keuzes in het datamodel langs die vóór de code komen.
Belangrijkste punten
- De API is één REST-oppervlak op
https://api.brevo.com/v3/, geauthenticeerd met eenapi-key-header in plaats van een bearer-token. - De rate limits zijn extreem ongelijk: verzenden is praktisch ongemeten, terwijl de meeste andere endpoints samen 100 verzoeken per uur delen op standaardabonnementen.
- Er zijn officiële SDK’s voor zeven talen, maar het HTTP-oppervlak is eenvoudig genoeg om direct aan te roepen als je maar een paar endpoints nodig hebt.
- Sandboxmodus valideert alleen het formaat van het verzoek, dus houd een apart account aan om alles buiten verzending te testen.
- Een 2xx-respons bewijst niet dat een schrijfactie is doorgevoerd. Niet-gedeclareerde attributen worden stilletjes weggegooid en object-upserts zijn asynchroon.
- Ontwerp om de vaste limieten heen: één bedrijf per domein, één miljoen records per objecttype, geen bulk delete voor standaardobjecten, en attribuutfilters die stilletjes niets doen.