API de Brevo: guía práctica para desarrolladores
Guía de la API de Brevo para desarrolladores: autenticación, URL base, contactos, email transaccional, campañas, objetos de CRM, webhooks, límites de tasa y límites reales.
Brevo expone una sola API REST que abarca mensajería transaccional, campañas de marketing, datos de contacto y registros de CRM. Conseguir que la primera solicitud devuelva 201 lleva unos dos minutos. Conseguir una integración de producción que no pierda datos en silencio lleva bastante más, porque varias de las restricciones que más importan están sin documentar o contradicen lo que la propia API dice de sí misma.
Esta guía cubre las dos mitades: los endpoints, los SDK y la autenticación que necesitas el primer día, y los límites de plataforma alrededor de los que tienes que diseñar antes de pasar a producción.
Qué cubre la API de Brevo
Todo vive bajo un único host y una única ruta de versión. La documentación para desarrolladores agrupa la superficie en cuatro áreas de producto:
- Mensajería: email transaccional, SMS y WhatsApp, incluidos los envíos por lotes, la programación y la actividad de los mensajes.
- Plataforma de marketing: contactos, listas, segmentos y campañas de email.
- eCommerce: productos, pedidos y seguimiento de eventos de cliente.
- Conversaciones: el widget de chat y la gestión programática de conversaciones.
Esas áreas comparten una cuenta, una base de datos de contactos y una clave de API. Eso es cómodo y de vez en cuando peligroso: un script escrito pensando en datos de staging está hablando con los mismos contactos a los que envían tus campañas.
Transaccional frente a marketing
Las dos familias se comportan de forma lo bastante distinta como para que confundirlas sea el error de diseño más habitual.
| Transaccional | Marketing | |
|---|---|---|
| Endpoint principal | POST /v3/smtp/email | POST /v3/emailCampaigns |
| Destinatarios | Explícitos en la solicitud | listIds o segmentIds |
| Disparador | Tu aplicación, en tiempo real | Programado o enviado a demanda |
| Forma habitual del volumen | Continuo, un mensaje cada vez | A ráfagas, un envío grande |
| Postura ante los límites de tasa | Muy alta, 1.000 solicitudes por segundo en planes estándar | Baja, los endpoints de campaña caen bajo el límite general |
Si todavía estás decidiendo si Brevo es la plataforma adecuada, la visión general de la plataforma cubre ese terreno.
Autenticación y gestión de claves
Brevo usa una clave de API simple en una cabecera propia. La cabecera se llama api-key, no Authorization, y no lleva prefijo Bearer. Esto despista a casi todo el mundo que ha usado antes otra API de mensajería.
curl https://api.brevo.com/v3/account \ -H "api-key: $BREVO_API_KEY"Las claves se generan en la aplicación de Brevo, dentro de los ajustes de la cuenta, en la sección SMTP y API, en la pestaña de claves de API. Dale a cada clave un nombre descriptivo ligado al sistema que la usa. El valor de la clave se muestra exactamente una vez cuando se genera, así que si la pierdes generas una nueva en lugar de recuperar la antigua.
Unas cuantas reglas prácticas:
- Emite una clave separada por entorno de despliegue y por servicio. Revocar una clave comprometida nunca debería tumbar tres sistemas que no tienen nada que ver.
- Las claves de API estándar son de ámbito de cuenta. Trata cualquier clave como acceso total a contactos, envíos y datos de CRM.
- Brevo también admite OAuth 2.0 para aplicaciones que actúan en nombre de otras cuentas de Brevo, descrito junto al flujo de claves en authentication schemes.
- El servidor MCP que usan los asistentes de AI toma un token aparte y ese sí usa una cabecera bearer. Ese token se genera en la misma pantalla de claves de API, pero no es intercambiable con una clave REST.
URL base, versionado y tu primera escritura
La URL base es https://api.brevo.com/v3/. La versión va en la ruta y no en una cabecera, y v3 es la generación actual. Todas las rutas de esta guía son relativas a esa base.
Una primera escritura es más informativa que una primera lectura, porque ejercita las partes de la cuenta que suelen estar mal configuradas, en particular los remitentes verificados:
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": "Primer envío transaccional", "htmlContent": "<html><body><p>Funciona.</p></body></html>", "tags": ["smoke-test"] }'Un envío correcto devuelve 201 con un messageId. Un envío programado devuelve 202.
Los endpoints que vas a usar de verdad
Contactos
POST /v3/contacts crea un contacto. El cuerpo acepta email, un mapa attributes para campos personalizados, listIds, ext_id para tu propia clave externa y las dos banderas que más importan en la práctica: updateEnabled, que convierte la llamada en un upsert, y getId, que hace que la respuesta devuelva el id del contacto.
Las lecturas pasan por GET /v3/contacts, que pagina con limit (50 por defecto, 1000 como máximo) y offset, y admite modifiedSince y createdSince en UTC. Las sincronizaciones incrementales deberían apoyarse en modifiedSince en lugar de recorrer la lista completa. Ten en cuenta que el parámetro filter solo admite un operador de igualdad, así que cualquier cosa más expresiva pertenece a un segmento.
Para carga masiva, POST /v3/contacts/import acepta fileUrl, fileBody o jsonBody, apunta a listIds y se ejecuta de forma asíncrona devolviendo un processId. Brevo documenta un máximo de 10 MB por cuerpo y recomienda quedarse cerca de 8 MB porque el parseo infla la carga útil. Facilita notifyUrl para enterarte del resultado en lugar de hacer polling.
Email transaccional
POST /v3/smtp/email es el caballo de batalla. Más allá de sender, to, subject y htmlContent, los campos que merece la pena conocer son:
templateIdconparams, que sustituye el contenido en línea por una plantilla de Brevo y sus sustituciones de variables. Los params de cada versión están limitados a 100 KB y los acumulados a 1000 KB.messageVersions, que envía variantes personalizadas en una sola llamada, con hasta 99 destinatarios por versión.tags, que deberías fijar siempre. Las etiquetas vuelven en los eventos de webhook y son la única forma barata de correlacionar un evento de entrega con la ruta de código que lo produjo.scheduledAtjunto conbatchId, para envíos futuros que quizá quieras cancelar en grupo.headers, en Title-Case, para cabeceras SMTP personalizadas.
Una sola solicitud acepta como máximo 2.000 destinatarios. Para la diferencia entre este endpoint y el envío de campañas, la guía de email transaccional ofrece la visión de estrategia de mensajería.
Campañas de email
POST /v3/emailCampaigns requiere name y sender, además de exactamente una fuente de contenido: htmlContent (mínimo 10 caracteres, menos de 1 MB), htmlUrl o templateId. La audiencia va en recipients como listIds o segmentIds, y scheduledAt usa el formato UTC YYYY-MM-DDTHH:mm:ss.SSSZ. Las rutas complementarias cubren el envío inmediato, el envío de una prueba, la actualización del estado y la descarga del informe de campaña.
Empresas, negocios y objetos
El CRM de Brevo tiene dos rutas de escritura solapadas, y elegir bien importa.
Las rutas de CRM son POST /v3/companies, PATCH /v3/companies/{id}, DELETE /v3/companies/{id} y el conjunto equivalente para los negocios. Son síncronas. Un PATCH devuelve 204 en cuanto se aplica el cambio.
La API de objetos es la vía masiva: POST /v3/objects/{object_type}/batch/upsert acepta hasta 1000 registros y 1 MB por solicitud, hasta 500 atributos por registro y hasta 10 registros de asociación por tipo de objeto y registro. Devuelve 202 con un processId, lo que significa aceptado y no aplicado.
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 guía de Brevo CRM cubre el modelo de objetos desde el lado del operador.
SDK oficiales
Brevo mantiene clientes en la organización de GitHub getbrevo:
| Lenguaje | Repositorio |
|---|---|
| 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 |
El cliente de Node se instala como @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: "Pedido confirmado", htmlContent: "<html><body><p>Gracias por tu pedido.</p></body></html>", tags: ["order-confirmation"],});
console.log("Message ID:", result.messageId);El cliente de Python se instala con pip install brevo-python. Si prefieres no arrastrar una dependencia de SDK para dos endpoints, la superficie HTTP en crudo es lo bastante pequeña como para llamarla directamente, lo que además te aísla de los cambios de versión del 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 responseTambién hay un servidor MCP en https://mcp.brevo.com/v1/brevo/mcp para asistentes de AI, autenticado con un token bearer generado en la misma pantalla de ajustes. Es útil para exploración y preguntas sobre la cuenta, no para rutas de datos en producción.
Webhooks
Los webhooks son la forma de enterarte de lo que pasó después de un envío. POST /v3/webhooks crea uno, con url, events, type y, de forma opcional, channel (email o sms), batched, headers personalizadas y un objeto auth.
Hay tres tipos de webhook con vocabularios de evento distintos:
- Transaccional:
sent,request,delivered,hardBounce,softBounce,blocked,spam,invalid,deferred,click,opened,uniqueOpened,unsubscribed. - Marketing:
spam,opened,click,hardBounce,softBounce,unsubscribed,listAddition,delivered,contactUpdated,contactDeleted. - Entrante:
inboundEmailProcessedyreply, que además requieren 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": "Señales de entregabilidad" }'Hay tres cosas que conviene hacer bien. Primero, una cuenta admite como máximo 40 webhooks entre todos los tipos, así que enruta por evento dentro de tu manejador en lugar de registrar un endpoint por evento. Segundo, usa la bandera batched cuando esperes volumen, porque una solicitud que lleva muchos eventos es mucho más barata de procesar que muchas solicitudes. Tercero, protege el receptor: Brevo publica sus rangos de IP de envío, y restringir tu endpoint a esos rangos es el enfoque documentado. Añade tu propio secreto compartido a través del campo headers como segunda capa.
Los manejadores deben ser idempotentes. Trata el id del mensaje más el tipo de evento más la marca temporal como clave de deduplicación.
Límites de tasa y gestión de errores
Los límites de tasa de Brevo son por endpoint y por nivel de plan, y la diferencia entre endpoints es enorme.
| Endpoint | Estándar | Professional y 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 | más alto en Enterprise |
GET /v3/smtp/emails | 2 RPS, 7.200 RPH | 3 RPS, 10.800 RPH |
| Todo lo demás | 100 RPH | 200 RPH |
Esa última fila es la que duele. Enviar es prácticamente ilimitado, mientras que la gestión de campañas, las lecturas de CRM y la mayoría de las llamadas administrativas comparten un presupuesto de 100 solicitudes por hora en los planes estándar. Un backfill ingenuo que lea un registro de empresa antes de cada escritura agotará una hora de cuota en menos de dos minutos.
Cada respuesta lleva x-sib-ratelimit-limit, x-sib-ratelimit-remaining y x-sib-ratelimit-reset. Léelos cuando la llamada va bien, no solo cuando falla. Superar un límite devuelve 429, y la respuesta correcta es esperar el intervalo de la cabecera de reinicio y después aplicar backoff exponencial 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;}Reintenta los 429 y los 5xx. Nunca reintentes un 400 o un 409 a ciegas, porque los dos suelen significar que la solicitud está mal y no que llega pronto, y un 409 en particular necesita una acción distinta y no una repetición.
Probar sin enviar correo
Añade la cabecera X-Sib-Sandbox con el valor drop a un envío transaccional. Brevo valida la solicitud, devuelve 201 con un messageId, no entrega nada y no escribe ningún registro de 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>hola</p>" }'Entiende qué demuestra esto y qué no. El modo sandbox valida solo el formato de la solicitud. No dice nada sobre la autenticación del remitente, el renderizado de plantillas ni la entregabilidad. Mantén una cuenta de Brevo aparte para las pruebas de integración de todo lo que toque contactos o datos de CRM, porque el modo sandbox cubre el envío y no el resto de la API.
Límites que condicionan el diseño de tu integración
Estas son las restricciones que solo aparecen cuando una integración se ejecuta contra una cuenta real con volumen. Varias contradicen lo que la API dice de sí misma. Ninguna es negociable, así que la única respuesta sensata es diseñar a su alrededor.
Las empresas necesitan un dominio, y solo una empresa por dominio
GET /v3/crm/attributes/companies informa de que ningún atributo es obligatorio, y la referencia de creación de empresa lista solo name como obligatorio. En la práctica, POST /v3/companies sin un atributo domain no vacío devuelve 400 con un mensaje sobre atributos por defecto obligatorios que faltan. Una cadena vacía falla igual que omitirlo.
Peor aún, la unicidad de dominio se aplica de verdad. Una segunda empresa en un dominio ya en uso devuelve 409. Para el comercio B2B esto es estructural: las filiales que comparten un dominio de correo de comprador no pueden existir todas como empresas separadas en Brevo. Sincronizar un contacto también basta para que aparezca una empresa con el dominio de correo de ese contacto, así que una creación puede chocar con una empresa que nadie creó de forma explícita. El manejador correcto adopta la empresa existente ante un 409 en lugar de fallar o reintentar.
Los atributos no declarados se descartan en silencio
Este es el comportamiento más peligroso de la plataforma, y Brevo lo documenta con claridad: si un atributo aparece en una solicitud pero no estaba definido antes en el esquema del objeto, no pasa nada. Ni error, ni creación del atributo, ni aviso.
Por tanto, una respuesta 2xx no es prueba de que tus datos hayan aterrizado. Lee el esquema antes de escribir, descarta en tu propio cliente todo lo que no esté declarado y niégate a ejecutar una sincronización cuyos atributos no existen, en lugar de escribir medio registro durante un mes hasta que alguien se dé cuenta.
Los filtros por atributo se aceptan y se ignoran
GET /v3/companies?filters[attributes.domain]=... devuelve 200 e ignora el filtro. Dos filtros totalmente distintos devuelven los mismos registros. No hay forma de buscar una empresa por atributo a través de esa ruta.
Sumado a que el listado sin filtrar caduca con 504 en cuentas grandes con cualquier tamaño de página, una empresa existente puede resultar literalmente imposible de encontrar por la vía documentada. La solución es recorrer GET /v3/objects/company/records con sort=desc, que es rápido, paginado y devuelve atributos, acotado a un número razonable de páginas. Una empresa que acaba de provocar un 409 casi siempre se creó momentos antes, así que un recorrido de más nuevo a más antiguo la encuentra rápido.
Un millón de registros por tipo de objeto, y sin borrado masivo
POST /v3/objects/{type}/batch/upsert devuelve 400 en cuanto un tipo de objeto llega al millón de registros. Bloquea las actualizaciones además de las creaciones: dirigirse a un registro existente por su propio id numérico falla igual. Toda la ruta de escritura de objetos se cierra de golpe.
Volver por debajo del techo es lento, porque POST /v3/objects/{type}/batch/delete devuelve 403 para los tipos de objeto estándar de Brevo como company. La única vía es DELETE /v3/companies/{id}, un registro por llamada a unos 156 ms. Limpiar 124.000 registros así llevó horas con 20 trabajadores en paralelo. Vigila el recuento de registros de forma programada en lugar de descubrir el techo con una sincronización fallida, y enruta las actualizaciones de alto volumen por PATCH /v3/companies/{id}, que no tiene ese límite.
ext_id es el id de Brevo, no el tuyo
En los registros de objeto, identifiers.ext_id contiene el propio id de empresa del CRM de Brevo, una cadena al estilo de Mongo. No es una clave externa libre. Basar un upsert en ext_id con el identificador de tu plataforma crea duplicados en lugar de emparejar. Tu id externo va en un atributo declarado propio.
Los upserts de objetos son asíncronos, las escrituras de CRM no
batch/upsert devuelve 202 y un processId, y se aplica más tarde. Un id inexistente falla de forma asíncrona y aun así devuelve 202 a quien llama. PATCH /v3/companies/{id} devuelve 204 y se aplica de forma síncrona. Si tu sincronización informa de éxito, solo la vía síncrona se gana esa palabra sin una lectura de confirmación.
Una lista de comprobación breve para la integración
- Claves de API separadas por servicio y por entorno, rotadas cuando cambia el equipo.
- Todas las escrituras pasan por un único cliente que lee las cabeceras de límite de tasa y aplica backoff ante un 429.
- El esquema de atributos se verifica al arrancar, y la sincronización se niega a ejecutarse si faltan sus atributos.
- Un 409 al crear una empresa significa adoptar, no reintentar.
- Las vías masivas usan la API de objetos para el rendimiento y las rutas de CRM para todo lo que deba confirmarse.
- Los webhooks son idempotentes, agrupados, restringidos por IP y llevan una cabecera con un secreto compartido.
- Las sincronizaciones incrementales de contactos usan
modifiedSince, no recorridos de la lista completa.
Construir y mantener esta capa es trabajo de ingeniería de verdad: verificación de esquema, backoff, lógica de adopción, reconciliación. Tajo existe para absorberlo, manteniendo los datos de Shopify y de comercio sincronizados con los contactos, empresas y eventos de Brevo sin que nadie escriba a mano la lógica de reintentos y deduplicación. Si vas a cablearlo tú, la guía de integración de Brevo recorre las decisiones de modelo de datos que van antes del código.
Conclusiones clave
- La API es una sola superficie REST en
https://api.brevo.com/v3/, autenticada con una cabeceraapi-keyen lugar de un token bearer. - Los límites de tasa son tremendamente desiguales: enviar es prácticamente ilimitado, mientras que la mayoría de los demás endpoints comparten 100 solicitudes por hora en los planes estándar.
- Hay SDK oficiales para siete lenguajes, pero la superficie HTTP es lo bastante simple como para llamarla directamente cuando solo necesitas unos pocos endpoints.
- El modo sandbox valida solo el formato de la solicitud, así que mantén una cuenta aparte para probar cualquier cosa que vaya más allá de los envíos.
- Una respuesta 2xx no demuestra que una escritura se haya aplicado. Los atributos no declarados se descartan en silencio y los upserts de objetos son asíncronos.
- Diseña alrededor de los límites fijos: una empresa por dominio, un millón de registros por tipo de objeto, sin borrado masivo para los objetos estándar y filtros por atributo que no hacen nada.