Brevo API: en praktisk guide för utvecklare
Brevo API-guide för utvecklare: autentisering, bas-URL, kontakter, transaktionell e-post, kampanjer, CRM-objekt, webhooks, hastighetsgränser och verkliga begränsningar.
Brevo erbjuder ett REST-API som spänner över transaktionella meddelanden, marknadsföringskampanjer, kontaktdata och CRM-poster. Att få det första anropet att svara 201 tar ungefär två minuter. Att bygga en integration i produktion som inte tyst tappar data tar betydligt längre tid, eftersom flera av de begränsningar som betyder mest antingen är odokumenterade eller motsäger det API:et rapporterar om sig självt.
Den här guiden täcker båda halvorna: slutpunkterna, SDK:erna och autentiseringen du behöver dag ett, och plattformsgränserna du måste designa runt innan du släpper något skarpt.
Vad Brevos API täcker
Allt ligger under en enda värd och en enda versionssökväg. Utvecklardokumentationen grupperar ytan i fyra produktområden:
- Meddelanden: transaktionell e-post, SMS och WhatsApp, inklusive batchsändningar, schemaläggning och meddelandeaktivitet.
- Marknadsföringsplattform: kontakter, listor, segment och e-postkampanjer.
- E-handel: produkter, ordrar och spårning av kundhändelser.
- Konversationer: chattwidgeten och programmatisk hantering av konversationer.
Områdena delar ett konto, en kontaktdatabas och en API-nyckel. Det är bekvämt och ibland farligt: ett skript som skrivits utifrån en föreställning om testdata pratar med samma kontakter som dina kampanjer går ut till.
Transaktionellt kontra marknadsföring
De två familjerna beter sig så olika att förväxla dem är det vanligaste designfelet.
| Transaktionellt | Marknadsföring | |
|---|---|---|
| Primär slutpunkt | POST /v3/smtp/email | POST /v3/emailCampaigns |
| Adressering | Explicita mottagare i anropet | listIds eller segmentIds |
| Utlösare | Din applikation, i realtid | Schemalagt eller skickat på begäran |
| Typisk volymform | Kontinuerlig, ett meddelande i taget | Stötvis, en stor sändning |
| Hastighetsgräns | Mycket hög, 1 000 anrop per sekund på standardplaner | Låg, kampanjslutpunkter faller under det generella taket |
Om du fortfarande funderar på om Brevo är rätt plattform över huvud taget täcker plattformsöversikten det området.
Autentisering och nyckelhantering
Brevo använder en vanlig API-nyckel i en egen header. Headern heter api-key, inte Authorization, och det finns inget Bearer-prefix. Det snubblar nästan alla på som har använt ett annat meddelande-API först.
curl https://api.brevo.com/v3/account \ -H "api-key: $BREVO_API_KEY"Nycklar skapas i Brevo-appen under kontoinställningar, i avsnittet SMTP and API, på fliken API keys. Ge varje nyckel ett beskrivande namn kopplat till systemet som använder den. Nyckelvärdet visas exakt en gång när det skapas, så om du tappar bort det skapar du en ny nyckel i stället för att återställa den gamla.
Några praktiska regler:
- Skapa en egen nyckel per driftsmiljö och per tjänst. Att återkalla en komprometterad nyckel ska aldrig slå ut tre orelaterade system.
- Vanliga API-nycklar gäller hela kontot. Behandla varje nyckel som full åtkomst till kontakter, sändning och CRM-data.
- Brevo stöder även OAuth 2.0 för applikationer som agerar för andra Brevo-kontos räkning, beskrivet vid sidan av nyckelflödet i authentication schemes.
- MCP-servern som AI-assistenter använder tar en separat token och använder faktiskt en bearer-header. Den token skapas i samma API keys-vy men går inte att byta mot en REST-nyckel.
Bas-URL, versionshantering och din första skrivning
Bas-URL är https://api.brevo.com/v3/. Versionen ligger i sökvägen i stället för i en header, och v3 är den aktuella generationen. Varje sökväg i den här guiden är relativ till den basen.
En första skrivning säger mer än en första läsning, eftersom den använder de delar av kontot som oftast är felkonfigurerade, särskilt verifierade avsändare:
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": "Första transaktionella sändningen", "htmlContent": "<html><body><p>Det fungerar.</p></body></html>", "tags": ["smoke-test"] }'En lyckad sändning svarar 201 med ett messageId. En schemalagd sändning svarar 202.
Slutpunkterna du faktiskt kommer att använda
Kontakter
POST /v3/contacts skapar en kontakt. Kroppen tar email, en attributes-map för egna fält, listIds, ext_id för din egen externa nyckel, och de två flaggor som betyder mest i praktiken: updateEnabled, som gör anropet till en upsert, och getId, som gör att svaret innehåller kontaktens id.
Läsningar går via GET /v3/contacts, som paginerar med limit (standard 50, max 1000) och offset, och stöder modifiedSince och createdSince i UTC. Inkrementella synkroniseringar bör luta sig mot modifiedSince i stället för att gå igenom hela listan. Observera att parametern filter bara stöder likhetsoperatorn, så allt mer uttrycksfullt hör hemma i ett segment.
För massinläsning tar POST /v3/contacts/import emot fileUrl, fileBody eller jsonBody, riktar sig mot listIds och körs asynkront med ett processId i svaret. Brevo dokumenterar en maxgräns på 10 MB och rekommenderar att du håller dig runt 8 MB, eftersom tolkningen blåser upp nyttolasten. Ange notifyUrl så att du får veta utfallet i stället för att polla.
Transaktionell e-post
POST /v3/smtp/email är arbetshästen. Utöver sender, to, subject och htmlContent är dessa fält värda att känna till:
templateIdmedparams, som byter ut inbäddat innehåll mot en Brevo-mall och dess variabelsubstitutioner. Enskilda versionsparametrar är begränsade till 100 KB, kumulativa parametrar till 1000 KB.messageVersions, som skickar personaliserade varianter i ett anrop, med upp till 99 mottagare per version.tags, som du alltid bör sätta. Taggar följer med tillbaka i webhook-händelser, och de är det enda billiga sättet att koppla en leveranshändelse till kodvägen som skapade den.scheduledAtplusbatchId, för framtida sändningar du kanske vill avbryta som en grupp.headers, i Title-Case, för egna SMTP-headers.
Ett enskilt anrop tar emot högst 2 000 mottagare. För skillnaden mellan den här slutpunkten och kampanjsändning har guiden om transaktionell e-post det strategiska perspektivet.
E-postkampanjer
POST /v3/emailCampaigns kräver name och sender, plus exakt en innehållskälla: htmlContent (minst 10 tecken, under 1 MB), htmlUrl eller templateId. Målgruppen anges i recipients som listIds eller segmentIds, och scheduledAt använder UTC-formatet YYYY-MM-DDTHH:mm:ss.SSSZ. Kompletterande rutter täcker omedelbar sändning, testsändning, statusuppdatering och hämtning av kampanjrapporten.
Företag, affärer och objekt
Brevos CRM har två överlappande skrivvägar, och att välja rätt spelar roll.
CRM-rutterna är POST /v3/companies, PATCH /v3/companies/{id}, DELETE /v3/companies/{id} och motsvarande uppsättning för affärer. De är synkrona. En PATCH svarar 204 när ändringen är tillämpad.
Objekt-API:et är massvägen: POST /v3/objects/{object_type}/batch/upsert tar upp till 1000 poster och 1 MB per anrop, upp till 500 attribut per post och upp till 10 associationsposter per objekttyp och post. Det svarar 202 med ett processId, vilket betyder mottaget snarare än tillämpat.
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" } } ] }'Guiden till Brevo CRM täcker objektmodellen från operatörens sida.
Officiella SDK:er
Brevo underhåller klienter under GitHub-organisationen getbrevo:
| Språk | 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 |
Node-klienten installeras som @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 bekräftad", htmlContent: "<html><body><p>Tack för din order.</p></body></html>", tags: ["order-confirmation"],});
console.log("Message ID:", result.messageId);Python-klienten installeras med pip install brevo-python. Om du hellre slipper bära ett SDK-beroende för två slutpunkter är den råa HTTP-ytan liten nog att anropa direkt, vilket också skyddar dig mot versionsstök i SDK:erna:
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 responseDet finns också en MCP-server på https://mcp.brevo.com/v1/brevo/mcp för AI-assistenter, autentiserad med en bearer-token som skapas i samma inställningsvy. Den är användbar för utforskning och kontofrågor, inte för dataflöden i produktion.
Webhooks
Webhooks är hur du får veta vad som hände efter en sändning. POST /v3/webhooks skapar en, med url, events, type och valfritt channel (email eller sms), batched, egna headers och ett auth-objekt.
Det finns tre typer av webhooks med skilda händelsevokabulärer:
- Transaktionella:
sent,request,delivered,hardBounce,softBounce,blocked,spam,invalid,deferred,click,opened,uniqueOpened,unsubscribed. - Marknadsföring:
spam,opened,click,hardBounce,softBounce,unsubscribed,listAddition,delivered,contactUpdated,contactDeleted. - Inkommande:
inboundEmailProcessedochreply, som dessutom kräver endomain.
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": "Signaler om leveransbarhet" }'Tre saker att få rätt. För det första kan ett konto ha högst 40 webhooks över alla typer, så dirigera efter händelse inne i din hanterare i stället för att registrera en slutpunkt per händelse. För det andra, använd flaggan batched när du väntar dig volym, eftersom ett anrop som bär många händelser är långt billigare att behandla än många anrop. För det tredje, skydda mottagaren: Brevo publicerar sina avsändande IP-intervall, och att begränsa din slutpunkt till dessa intervall är den dokumenterade metoden. Lägg till din egen delade hemlighet via fältet headers som ett andra lager.
Hanterare måste vara idempotenta. Behandla meddelande-id plus händelsetyp plus tidsstämpel som deduplicerings-nyckel.
Hastighetsgränser och felhantering
Brevos hastighetsgränser gäller per slutpunkt och per plannivå, och spridningen mellan slutpunkter är enorm.
| Slutpunkt | Standard | Professional och 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ögre på Enterprise |
GET /v3/smtp/emails | 2 RPS, 7 200 RPH | 3 RPS, 10 800 RPH |
| Allt annat | 100 RPH | 200 RPH |
Den sista raden är den som gör ont. Sändning är i praktiken omätt, medan kampanjhantering, CRM-läsningar och de flesta administrativa anrop delar en budget på 100 anrop per timme på standardplaner. En naiv återfyllnad som läser en företagspost före varje skrivning bränner en timmes kvot på under två minuter.
Varje svar bär x-sib-ratelimit-limit, x-sib-ratelimit-remaining och x-sib-ratelimit-reset. Läs dem vid framgång, inte bara vid fel. Att överskrida en gräns ger 429, och rätt reaktion är att vänta ut intervallet i reset-headern och sedan tillämpa exponentiell backoff med 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;}Gör om anrop vid 429 och 5xx. Gör aldrig om 400 eller 409 blint, eftersom båda oftast betyder att anropet är fel snarare än för tidigt, och en 409 kräver särskilt en annan åtgärd i stället för en upprepning.
Testa utan att skicka e-post
Lägg till headern X-Sib-Sandbox med värdet drop i en transaktionell sändning. Brevo validerar anropet, svarar 201 med ett messageId, levererar ingenting och skriver ingen e-postlogg.
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>hej</p>" }'Förstå vad detta bevisar och inte bevisar. Sandbox-läget validerar bara anropets format. Det säger ingenting om avsändarautentisering, mallrendering eller leveransbarhet. Ha ett separat Brevo-konto för integrationstester av allt som rör kontakter eller CRM-data, eftersom sandbox-läget täcker sändning och inte resten av API:et.
Gränser som formar din integrationsdesign
Det här är begränsningarna som bara dyker upp när en integration körs mot ett riktigt konto i volym. Flera motsäger vad API:et säger om sig självt. Ingen av dem går att förhandla om, så det enda vettiga svaret är att designa runt dem.
Företag kräver en domän, och bara ett företag per domän
GET /v3/crm/attributes/companies rapporterar varje attribut som icke obligatoriskt, och referensen för att skapa ett företag listar bara name som obligatoriskt. I praktiken svarar POST /v3/companies utan ett ifyllt domain-attribut med 400 och ett meddelande om saknade obligatoriska standardattribut. En tom sträng faller på samma sätt som om fältet utelämnas.
Värre är att domänunikhet upprätthålls. Ett andra företag på en domän som redan används ger 409. För B2B-handel är detta strukturellt: dotterbolag som delar en köpares e-postdomän kan inte alla finnas som separata företag i Brevo. Att synkronisera en kontakt räcker också för att ett företag ska dyka upp på den kontaktens e-postdomän, så en skapandeoperation kan krocka med ett företag som ingen uttryckligen skapade. Rätt hanterare tar över det befintliga företaget vid 409 i stället för att fallera eller försöka igen.
Odeklarerade attribut kastas tyst bort
Det här är plattformens farligaste beteende, och Brevo dokumenterar det rakt ut: om ett attribut förekommer i ett anrop men inte tidigare definierats i objektets schema händer ingenting. Inget fel, inget attribut skapas, ingen varning.
Ett 2xx-svar är därför inte bevis för att dina data landade. Läs schemat före skrivning, sortera bort allt odeklarerat i din egen klient, och vägra köra en synkronisering vars attribut inte finns i stället för att skriva halva poster i en månad innan någon märker det.
Attributfilter accepteras och ignoreras
GET /v3/companies?filters[attributes.domain]=... svarar 200 och ignorerar filtret. Två helt olika filter ger samma poster. Det finns inget fungerande sätt att slå upp ett företag på attribut via den rutten.
Kombinerat med att den ofiltrerade listan går ut i timeout med 504 på stora konton oavsett sidstorlek kan ett befintligt företag vara omöjligt att hitta via den dokumenterade vägen. Lösningen är att skanna GET /v3/objects/company/records med sort=desc, vilket är snabbt, paginerat och returnerar attribut, avgränsat till ett rimligt antal sidor. Ett företag som just utlöste en 409 skapades nästan alltid ögonblick tidigare, så en skanning med nyast först hittar det snabbt.
En miljon poster per objekttyp, och ingen massradering
POST /v3/objects/{type}/batch/upsert svarar 400 så snart en objekttyp innehåller en miljon poster. Det blockerar uppdateringar lika mycket som skapanden: att adressera en befintlig post med dess eget numeriska id faller på samma sätt. Hela skrivvägen för objekt stängs på en gång.
Att komma tillbaka under taket går långsamt, eftersom POST /v3/objects/{type}/batch/delete svarar 403 för Brevos standardobjekttyper som company. Enda vägen är DELETE /v3/companies/{id}, en post per anrop på ungefär 156 ms. Att rensa 124 000 poster på det sättet tog timmar med 20 parallella arbetare. Övervaka antalet poster schemalagt i stället för att upptäcka taket genom en misslyckad synkronisering, och dirigera uppdateringar med hög volym via PATCH /v3/companies/{id}, som inte har någon sådan gräns.
ext_id är Brevos id, inte ditt
På objektposter innehåller identifiers.ext_id Brevos eget CRM-företags-id, en sträng i Mongo-stil. Det är inte en fri extern nyckel. Att göra en upsert med ext_id satt till din plattforms identifierare skapar dubbletter i stället för att matcha. Ditt externa id hör hemma i ett eget deklarerat attribut.
Objekt-upserts är asynkrona, CRM-skrivningar är det inte
batch/upsert svarar 202 och ett processId, och tillämpas sedan senare. Ett id som inte finns faller asynkront och ger ändå 202 till anroparen. PATCH /v3/companies/{id} svarar 204 och tillämpas synkront. Om din synkronisering rapporterar framgång är det bara den synkrona vägen som förtjänar ordet utan en uppföljande läsning.
En kort checklista för integration
- Separata API-nycklar per tjänst och per miljö, roterade vid personalförändringar.
- Alla skrivningar går via en klient som läser hastighetsgränsheaders och backar av vid 429.
- Attributschemat verifieras vid start, och synkroniseringen vägrar köra om dess attribut saknas.
- 409 vid skapande av företag betyder ta över, inte försök igen.
- Massvägar använder objekt-API:et för genomströmning och CRM-rutterna för allt som måste bekräftas.
- Webhooks är idempotenta, batchade, IP-begränsade och bär en header med delad hemlighet.
- Inkrementella kontaktsynkroniseringar använder
modifiedSince, inte genomgångar av hela listan.
Att bygga och underhålla det här lagret är riktigt ingenjörsarbete: schemaverifiering, backoff, övertagandelogik, avstämning. Tajo finns för att absorbera det, och håller Shopify- och handelsdata synkroniserade med Brevos kontakter, företag och händelser utan att någon handskriver logik för omförsök och deduplicering. Om du kopplar ihop det själv i stället går guiden till Brevo-integration igenom datamodellvalen som kommer före koden.
Viktigaste slutsatserna
- API:et är en enda REST-yta på
https://api.brevo.com/v3/, autentiserad med enapi-key-header i stället för en bearer-token. - Hastighetsgränserna är vilt ojämna: sändning är i praktiken omätt, medan de flesta andra slutpunkter delar 100 anrop per timme på standardplaner.
- Officiella SDK:er finns för sju språk, men HTTP-ytan är enkel nog att anropa direkt när du bara behöver några få slutpunkter.
- Sandbox-läget validerar bara anropets format, så ha ett separat konto för att testa allt utöver sändningar.
- Ett 2xx-svar bevisar inte att en skrivning tillämpades. Odeklarerade attribut kastas tyst bort, och objekt-upserts är asynkrona.
- Designa runt de fasta gränserna: ett företag per domän, en miljon poster per objekttyp, ingen massradering för standardobjekt, och attributfilter som tyst gör ingenting.