Brevo API: Ein praxisnaher Leitfaden für Entwickler:innen
Brevo-API-Leitfaden für Entwickler:innen: Authentifizierung, Basis-URL, Kontakte, transaktionale E-Mails, Kampagnen, CRM-Objekte, Webhooks, Rate Limits und echte Grenzen.
Brevo stellt eine REST-API bereit, die transaktionales Messaging, Marketing-Kampagnen, Kontaktdaten und CRM-Datensätze umfasst. Bis die erste Anfrage 201 zurückgibt, vergehen etwa zwei Minuten. Bis eine produktive Integration steht, die keine Daten stillschweigend verliert, dauert es deutlich länger, denn mehrere der wichtigsten Einschränkungen sind entweder nicht dokumentiert oder widersprechen dem, was die API über sich selbst meldet.
Dieser Leitfaden deckt beide Hälften ab: die Endpunkte, SDKs und die Authentifizierung, die du am ersten Tag brauchst, und die Plattformgrenzen, um die du dein Design herumbauen musst, bevor du live gehst.
Was die Brevo API abdeckt
Alles liegt unter einem Host und einem Versionspfad. Die Entwicklerdokumentation gliedert die Oberfläche in vier Produktbereiche:
- Messaging: transaktionale E-Mails, SMS und WhatsApp, inklusive Batch-Versand, Planung und Nachrichtenaktivität.
- Marketing-Plattform: Kontakte, Listen, Segmente und E-Mail-Kampagnen.
- eCommerce: Produkte, Bestellungen und Tracking von Kundenereignissen.
- Conversations: das Chat-Widget und programmatische Verwaltung von Konversationen.
Diese Bereiche teilen sich ein Konto, eine Kontaktdatenbank und einen API-Schlüssel. Das ist praktisch und gelegentlich gefährlich: Ein Skript, das gegen eine Staging-Vorstellung der Daten geschrieben wurde, spricht mit genau den Kontakten, an die deine Kampagnen gehen.
Transaktional versus Marketing
Die beiden Familien verhalten sich unterschiedlich genug, dass sie zu verwechseln der häufigste Designfehler ist.
| Transaktional | Marketing | |
|---|---|---|
| Primärer Endpunkt | POST /v3/smtp/email | POST /v3/emailCampaigns |
| Adressierung | Explizite Empfänger:innen in der Anfrage | listIds oder segmentIds |
| Auslöser | Deine Anwendung, in Echtzeit | Geplant oder auf Abruf gesendet |
| Typisches Volumenprofil | Kontinuierlich, eine Nachricht nach der anderen | Stoßweise, ein großer Versand |
| Haltung beim Rate Limit | Sehr hoch, 1.000 Anfragen pro Sekunde in Standardtarifen | Niedrig, Kampagnen-Endpunkte fallen unter das allgemeine Limit |
Falls du noch überlegst, ob Brevo überhaupt die richtige Plattform ist, deckt die Plattform-Übersicht dieses Feld ab.
Authentifizierung und Schlüsselverwaltung
Brevo nutzt einen einfachen API-Schlüssel in einem eigenen Header. Der Header heißt api-key, nicht Authorization, und es gibt kein Bearer-Präfix. Darüber stolpern fast alle, die vorher mit einer anderen Messaging-API gearbeitet haben.
curl https://api.brevo.com/v3/account \ -H "api-key: $BREVO_API_KEY"Schlüssel erzeugst du in der Brevo-App in den Kontoeinstellungen, im Bereich SMTP and API, auf dem Tab API keys. Gib jedem Schlüssel einen sprechenden Namen, der auf das System verweist, das ihn nutzt. Der Wert wird genau einmal beim Erzeugen angezeigt. Wenn du ihn verlierst, erzeugst du einen neuen, statt den alten wiederherzustellen.
Ein paar praktische Regeln:
- Vergib pro Deployment-Ziel und pro Dienst einen eigenen Schlüssel. Das Widerrufen eines kompromittierten Schlüssels sollte nie drei unbeteiligte Systeme lahmlegen.
- Normale API-Schlüssel gelten kontoweit. Behandle jeden Schlüssel als Vollzugriff auf Kontakte, Versand und CRM-Daten.
- Brevo unterstützt außerdem OAuth 2.0 für Anwendungen, die im Namen anderer Brevo-Konten handeln, beschrieben neben dem Schlüsselverfahren in den Authentication schemes.
- Der MCP-Server, den KI-Assistenten nutzen, verlangt ein eigenes Token und arbeitet tatsächlich mit einem Bearer-Header. Dieses Token erzeugst du im selben Bildschirm für API-Schlüssel, es ist aber nicht mit einem REST-Schlüssel austauschbar.
Basis-URL, Versionierung und dein erster Schreibvorgang
Die Basis-URL ist https://api.brevo.com/v3/. Die Version steht im Pfad statt in einem Header, und v3 ist die aktuelle Generation. Jeder Pfad in diesem Leitfaden ist relativ zu dieser Basis.
Ein erster Schreibvorgang sagt mehr aus als ein erster Lesevorgang, weil er genau die Teile des Kontos beansprucht, die üblicherweise falsch konfiguriert sind (allen voran verifizierte Absender:innen):
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": "Erster transaktionaler Versand", "htmlContent": "<html><body><p>Es funktioniert.</p></body></html>", "tags": ["smoke-test"] }'Ein erfolgreicher Versand liefert 201 mit einer messageId. Ein geplanter Versand liefert 202.
Die Endpunkte, die du wirklich nutzen wirst
Kontakte
POST /v3/contacts legt einen Kontakt an. Der Body nimmt email, eine attributes-Map für eigene Felder, listIds, ext_id für deinen eigenen externen Schlüssel und die beiden Flags, die in der Praxis am meisten zählen: updateEnabled, das den Aufruf zu einem Upsert macht, und getId, das die Kontakt-ID in der Antwort zurückgibt.
Gelesen wird über GET /v3/contacts, das mit limit (Standard 50, Maximum 1000) und offset blättert und modifiedSince sowie createdSince in UTC unterstützt. Inkrementelle Syncs sollten sich auf modifiedSince stützen, statt die gesamte Liste durchzugehen. Beachte, dass der Parameter filter nur einen Gleichheitsoperator unterstützt. Alles Ausdrucksstärkere gehört in ein Segment.
Für das massenhafte Laden nimmt POST /v3/contacts/import entweder fileUrl, fileBody oder jsonBody entgegen, adressiert listIds, läuft asynchron und gibt eine processId zurück. Brevo dokumentiert maximal 10 MB Body und empfiehlt, nahe bei 8 MB zu bleiben, weil das Parsen die Nutzlast aufbläht. Gib notifyUrl an, damit du das Ergebnis erfährst, statt zu pollen.
Transaktionale E-Mails
POST /v3/smtp/email ist das Arbeitspferd. Neben sender, to, subject und htmlContent lohnen sich diese Felder:
templateIdmitparams, das Inline-Inhalte durch eine Brevo-Vorlage samt Variablenersetzung austauscht. Params pro Version sind auf 100 KB begrenzt, kumulierte Params auf 1000 KB.messageVersions, das personalisierte Varianten in einem Aufruf verschickt, mit bis zu 99 Empfänger:innen pro Version.tags, die du immer setzen solltest. Tags kommen in Webhook-Events zurück und sind der einzige günstige Weg, ein Zustellereignis mit dem Codepfad zu verknüpfen, der es erzeugt hat.scheduledAtplusbatchId, für künftige Versendungen, die du als Gruppe abbrechen möchtest.headers, in Title-Case, für eigene SMTP-Header.
Eine einzelne Anfrage akzeptiert höchstens 2.000 Empfänger:innen. Zum Unterschied zwischen diesem Endpunkt und dem Kampagnenversand liefert der Leitfaden zu transaktionalen E-Mails die strategische Sicht.
E-Mail-Kampagnen
POST /v3/emailCampaigns verlangt name und sender plus genau eine Inhaltsquelle: htmlContent (mindestens 10 Zeichen, unter 1 MB), htmlUrl oder templateId. Die Zielgruppe steht in recipients als listIds oder segmentIds, und scheduledAt nutzt das UTC-Format YYYY-MM-DDTHH:mm:ss.SSSZ. Begleitende Routen decken sofortiges Senden, Testversand, Statusänderung und den Abruf des Kampagnenberichts ab.
Unternehmen, Deals und Objekte
Das CRM von Brevo hat zwei sich überschneidende Schreibpfade, und die richtige Wahl ist wichtig.
Die CRM-Routen sind POST /v3/companies, PATCH /v3/companies/{id}, DELETE /v3/companies/{id} und das entsprechende Set für Deals. Sie arbeiten synchron. Ein PATCH liefert 204, sobald die Änderung angewendet ist.
Die Objects-API ist der Massenpfad: POST /v3/objects/{object_type}/batch/upsert nimmt bis zu 1000 Datensätze und 1 MB pro Anfrage, bis zu 500 Attribute pro Datensatz und bis zu 10 Verknüpfungsdatensätze pro Objekttyp und Datensatz. Sie liefert 202 mit einer processId, was angenommen heißt und nicht angewendet.
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" } } ] }'Der Brevo-CRM-Leitfaden behandelt das Objektmodell aus der Sicht der Operator:innen.
Offizielle SDKs
Brevo pflegt Clients unter der GitHub-Organisation getbrevo:
| Sprache | 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 |
Der Node-Client wird als @getbrevo/brevo installiert:
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: "Bestellung bestätigt", htmlContent: "<html><body><p>Danke für deine Bestellung.</p></body></html>", tags: ["order-confirmation"],});
console.log("Message ID:", result.messageId);Der Python-Client wird mit pip install brevo-python installiert. Wenn du für zwei Endpunkte lieber keine SDK-Abhängigkeit mitschleppen möchtest: Die rohe HTTP-Oberfläche ist klein genug, um sie direkt anzusprechen, und das hält dich zusätzlich von SDK-Versionswechseln fern:
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 responseEs gibt außerdem einen MCP-Server unter https://mcp.brevo.com/v1/brevo/mcp für KI-Assistenten, authentifiziert mit einem Bearer-Token, das im selben Einstellungsbildschirm erzeugt wird. Er ist nützlich zum Erkunden und für Kontofragen, nicht für produktive Datenpfade.
Webhooks
Über Webhooks erfährst du, was nach einem Versand passiert ist. POST /v3/webhooks legt einen an, mit url, events, type und optional channel (email oder sms), batched, eigenen headers und einem auth-Objekt.
Es gibt drei Webhook-Typen mit jeweils eigenem Ereignis-Vokabular:
- Transaktional:
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:
inboundEmailProcessedundreply, die zusätzlich einedomainverlangen.
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": "Signale zur Zustellbarkeit" }'Drei Dinge musst du richtig machen. Erstens hält ein Konto höchstens 40 Webhooks über alle Typen hinweg, also verteile die Ereignisse innerhalb deines Handlers, statt pro Ereignis einen eigenen Endpunkt zu registrieren. Zweitens nutze das batched-Flag, wenn du Volumen erwartest, denn eine Anfrage mit vielen Ereignissen ist weit günstiger zu verarbeiten als viele Anfragen. Drittens schütze den Empfänger: Brevo veröffentlicht seine sendenden IP-Bereiche, und deinen Endpunkt auf diese Bereiche zu beschränken ist der dokumentierte Weg. Ergänze über das Feld headers dein eigenes Shared Secret als zweite Ebene.
Handler müssen idempotent sein. Nimm Message-ID plus Ereignistyp plus Zeitstempel als Schlüssel zur Deduplizierung.
Rate Limits und Fehlerbehandlung
Die Rate Limits von Brevo gelten pro Endpunkt und pro Tarifstufe, und die Spanne zwischen den Endpunkten ist enorm.
| Endpunkt | Standard | Professional und 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 | höher bei Enterprise |
GET /v3/smtp/emails | 2 RPS, 7.200 RPH | 3 RPS, 10.800 RPH |
| Alles andere | 100 RPH | 200 RPH |
Die letzte Zeile ist die schmerzhafte. Versenden ist praktisch ungedrosselt, während Kampagnenverwaltung, CRM-Lesevorgänge und die meisten administrativen Aufrufe sich in Standardtarifen ein Budget von 100 Anfragen pro Stunde teilen. Ein naiver Backfill, der vor jedem Schreibvorgang einen Unternehmensdatensatz liest, verbraucht das Stundenkontingent in unter zwei Minuten.
Jede Antwort trägt x-sib-ratelimit-limit, x-sib-ratelimit-remaining und x-sib-ratelimit-reset. Lies sie bei Erfolg, nicht nur bei Fehlern. Ein überschrittenes Limit liefert 429, und die richtige Reaktion ist, das Intervall aus dem Reset-Header abzuwarten und dann exponentielles Backoff mit Jitter anzuwenden.
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;}Wiederhole 429 und 5xx. Wiederhole 400 oder 409 niemals blind, denn beide bedeuten meist, dass die Anfrage falsch statt zu früh ist, und ein 409 verlangt insbesondere eine andere Aktion statt einer Wiederholung.
Testen ohne echten Mailversand
Füge einem transaktionalen Versand den Header X-Sib-Sandbox mit dem Wert drop hinzu. Brevo validiert die Anfrage, liefert 201 mit einer messageId, stellt nichts zu und schreibt kein E-Mail-Log.
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>" }'Sei dir klar darüber, was das beweist und was nicht. Der Sandbox-Modus validiert ausschließlich das Format der Anfrage. Über Absenderauthentifizierung, Vorlagen-Rendering oder Zustellbarkeit sagt er nichts. Halte ein separates Brevo-Konto für Integrationstests bereit, sobald Kontakt- oder CRM-Daten im Spiel sind, denn der Sandbox-Modus deckt den Versand ab und nicht den Rest der API.
Grenzen, die dein Integrationsdesign prägen
Das sind die Einschränkungen, die erst auftauchen, wenn eine Integration mit echtem Volumen gegen ein echtes Konto läuft. Mehrere widersprechen dem, was die API über sich selbst sagt. Keine davon ist verhandelbar, also bleibt nur, das Design darum herumzubauen.
Unternehmen brauchen eine Domain, und pro Domain nur ein Unternehmen
GET /v3/crm/attributes/companies meldet jedes Attribut als nicht erforderlich, und die Referenz zum Anlegen eines Unternehmens führt nur name als Pflichtfeld. In der Praxis liefert POST /v3/companies ohne ein nicht leeres domain-Attribut ein 400 mit einer Meldung über fehlende verpflichtende Standardattribute. Ein leerer String scheitert genauso wie das Weglassen.
Schlimmer noch: Die Eindeutigkeit der Domain wird erzwungen. Ein zweites Unternehmen auf einer bereits belegten Domain liefert 409. Für den B2B-Handel ist das strukturell: Tochtergesellschaften, die sich eine Käufer-E-Mail-Domain teilen, können nicht alle als eigene Unternehmen in Brevo existieren. Schon das Synchronisieren eines Kontakts genügt, damit ein Unternehmen auf der E-Mail-Domain dieses Kontakts auftaucht, sodass ein Anlegen mit einem Unternehmen kollidieren kann, das niemand ausdrücklich erzeugt hat. Der richtige Handler übernimmt bei 409 das bestehende Unternehmen, statt zu scheitern oder es erneut zu versuchen.
Nicht deklarierte Attribute werden stillschweigend verworfen
Das ist das gefährlichste Verhalten der Plattform, und Brevo dokumentiert es unmissverständlich: Taucht ein Attribut in einer Anfrage auf, war es aber vorher nicht im Objektschema definiert, passiert nichts. Kein Fehler, kein neues Attribut, keine Warnung.
Eine 2xx-Antwort ist also kein Beleg dafür, dass deine Daten angekommen sind. Lies das Schema vor dem Schreiben, verwirf alles Nichtdeklarierte schon in deinem eigenen Client und verweigere einen Sync, dessen Attribute nicht existieren, statt einen Monat lang halbe Datensätze zu schreiben, bis es jemandem auffällt.
Attributfilter werden angenommen und ignoriert
GET /v3/companies?filters[attributes.domain]=... liefert 200 und ignoriert den Filter. Zwei völlig verschiedene Filter liefern dieselben Datensätze. Über diese Route gibt es keinen funktionierenden Weg, ein Unternehmen per Attribut nachzuschlagen.
Zusammen mit der Tatsache, dass die ungefilterte Liste auf großen Konten bei jeder Seitengröße mit 504 wegläuft, kann ein bestehendes Unternehmen über den dokumentierten Pfad tatsächlich unauffindbar sein. Der Workaround ist, GET /v3/objects/company/records mit sort=desc zu scannen, was schnell ist, paginiert und Attribute zurückgibt, begrenzt auf eine vernünftige Seitenzahl. Ein Unternehmen, das gerade ein 409 ausgelöst hat, wurde fast immer Momente vorher angelegt, also findet ein Scan nach Neuestem zuerst es schnell.
Eine Million Datensätze pro Objekttyp, und kein Bulk-Delete
POST /v3/objects/{type}/batch/upsert liefert 400, sobald ein Objekttyp eine Million Datensätze hält. Es blockiert Aktualisierungen ebenso wie Neuanlagen: Einen bestehenden Datensatz über seine eigene numerische ID anzusprechen scheitert identisch. Der gesamte Objekt-Schreibpfad schließt sich auf einen Schlag.
Wieder unter die Grenze zu kommen ist langsam, denn POST /v3/objects/{type}/batch/delete liefert 403 für Brevo-Standardobjekttypen wie company. Der einzige Weg ist DELETE /v3/companies/{id}, ein Datensatz pro Aufruf bei etwa 156 ms. 124.000 Datensätze auf diese Weise zu löschen hat mit 20 parallelen Workern Stunden gedauert. Überwache die Datensatzzahl planmäßig, statt die Grenze über einen fehlgeschlagenen Sync zu entdecken, und leite Aktualisierungen mit hohem Volumen über PATCH /v3/companies/{id}, das kein solches Limit hat.
ext_id ist Brevos ID, nicht deine
Auf Objektdatensätzen hält identifiers.ext_id Brevos eigene CRM-Unternehmens-ID, einen String im Mongo-Stil. Es ist kein freier externer Schlüssel. Einen Upsert auf ext_id mit der Kennung deiner Plattform zu schlüsseln erzeugt Duplikate statt Treffer. Deine externe ID gehört in ein eigenes, deklariertes Attribut.
Objekt-Upserts sind asynchron, CRM-Schreibvorgänge nicht
batch/upsert liefert 202 und eine processId und wendet die Änderung später an. Eine nicht existierende ID scheitert asynchron und liefert deinem Aufrufer trotzdem 202. PATCH /v3/companies/{id} liefert 204 und wird synchron angewendet. Wenn dein Sync Erfolg meldet, verdient nur der synchrone Pfad dieses Wort ohne nachgelagerten Lesevorgang.
Eine kurze Checkliste für die Integration
- Getrennte API-Schlüssel pro Dienst und pro Umgebung, rotiert bei Personalwechseln.
- Alle Schreibvorgänge laufen über einen Client, der Rate-Limit-Header liest und bei 429 zurücksteckt.
- Das Attributschema wird beim Start geprüft, und der Sync verweigert den Lauf, wenn seine Attribute fehlen.
- 409 beim Anlegen eines Unternehmens heißt übernehmen, nicht wiederholen.
- Massenpfade nutzen die Objects-API für Durchsatz und die CRM-Routen für alles, was bestätigt sein muss.
- Webhooks sind idempotent, gebündelt, IP-beschränkt und tragen einen Shared-Secret-Header.
- Inkrementelle Kontakt-Syncs nutzen
modifiedSince, keine Durchläufe der kompletten Liste.
Diese Schicht zu bauen und zu pflegen ist echte Ingenieursarbeit: Schemaprüfung, Backoff, Übernahmelogik, Abgleich. Tajo existiert, um das abzunehmen, und hält Shopify- und Commerce-Daten mit Brevo-Kontakten, -Unternehmen und -Ereignissen synchron, ohne dass jemand die Retry- und Dedupe-Logik von Hand schreibt. Wenn du es stattdessen selbst verdrahtest, führt der Brevo-Integrationsleitfaden durch die Datenmodell-Entscheidungen, die vor dem Code kommen.
Die wichtigsten Erkenntnisse
- Die API ist eine REST-Oberfläche unter
https://api.brevo.com/v3/, authentifiziert mit einemapi-key-Header statt mit einem Bearer-Token. - Die Rate Limits sind wild ungleich verteilt: Versenden ist praktisch ungedrosselt, während sich die meisten anderen Endpunkte in Standardtarifen 100 Anfragen pro Stunde teilen.
- Offizielle SDKs gibt es für sieben Sprachen, aber die HTTP-Oberfläche ist einfach genug, um sie direkt anzusprechen, wenn du nur wenige Endpunkte brauchst.
- Der Sandbox-Modus validiert nur das Format der Anfrage, halte also ein eigenes Konto bereit, um alles jenseits des Versands zu testen.
- Eine 2xx-Antwort beweist nicht, dass ein Schreibvorgang angewendet wurde. Nicht deklarierte Attribute werden stillschweigend verworfen, und Objekt-Upserts sind asynchron.
- Baue dein Design um die festen Grenzen: ein Unternehmen pro Domain, eine Million Datensätze pro Objekttyp, kein Bulk-Delete für Standardobjekte und Attributfilter, die klammheimlich nichts tun.