Enviar un lote de correos
/v1/mails/send_bulkPara desarrolladores
Descripción
Envía un lote de hasta 100 mensajes de remitentes masivos. Cada mensaje tiene los mismos campos y los mismos límites que un envío por POST /v1/mails/send, y cada uno genera un correo por destinatario.
El lote se valida completo antes de enviar el primer mensaje: si un mensaje se rechaza, no sale ninguno. Cada llamada lleva una Idempotency-Key, de modo que un reintento con la misma clave no vuelve a enviar lo que ya salió.
Qué endpoint usar
El camino de envío depende del tipo de remitente:
| Remitente | Cómo envía |
|---|---|
| Transaccional | POST /v1/mails/send o SMTP. Este endpoint lo rechaza con 409 sender_not_bulk. |
| Masivo | Solo este endpoint. POST /v1/mails/send lo rechaza con 409 sender_is_bulk, y el relay SMTP con 550 5.7.1. |
El tipo de un remitente se elige al crearlo, en el panel (Configuración > Remitentes), y no se puede cambiar después: para enviar del otro tipo, cree otro remitente. Las respuestas de GET /v1/senders y GET /v1/senders/{id} lo informan en is_bulk.
Guía relacionada
- Enviar su primer correo — Haga el primer envío desde su sistema, por la API o por SMTP, con un ejemplo para copiar.
Autenticación
Authorizationbearer tokenheaderobligatorio- API key con scope
mail:send_bulk. Formato:Bearer <API_KEY>. X-Instance-Slugstringheaderobligatorio- Identificador de su instancia. Ver Autenticación.
Es un permiso distinto de mail:send: una API key que solo tiene uno de los dos recibe un 403 en el otro endpoint.
Request
Headers obligatorios:
Idempotency-Keyheader- Cadena que identifica este lote, de hasta 255 caracteres. Sin ella, el lote se rechaza con
400 idempotency_key_required(ver Idempotencia). Content-Typeheaderapplication/json. El mismo body puede enviarse comprimido conContent-Encoding: gzip. No se aceptamultipart/form-data: los adjuntos van en base64 dentro de cada mensaje.
Body:
{
"messages": [
{
"sender_email": "novedades@example.com",
"to": [{ "email": "alice@example.com", "name": "Alice" }],
"template_id": 42,
"custom_group_code": "newsletter-octubre"
},
{
"sender_email": "novedades@example.com",
"to": [{ "email": "bob@example.com" }],
"subject": "Novedades de octubre",
"html": "<p>Hola, Bob.</p>",
"custom_group_code": "newsletter-octubre"
}
]
}
messagesarray de objeto, entre 1 y 100 elementosobligatorio- Los mensajes del lote, en el orden en que se informan en la respuesta. Cada elemento tiene los campos del body de
POST /v1/mails/send, con sus mismas reglas:sender_emailytoobligatorios,template_ido biensubjectyhtml, y los campos opcionales (cc,bcc,text,vars,headers,attachments,category,reply_to,test_mode,custom_msg_id,custom_group_code). Los valores de las etiquetas de cada mensaje valen para todos sus destinatarios. Para que cada persona reciba sus propios datos, use un mensaje por persona. Todos los remitentes del lote tienen que ser masivos.
Los mensajes de un lote son independientes entre sí: pueden usar remitentes, plantillas o destinatarios distintos. Para reconocer después los correos de un mismo lote o de una misma campaña, use custom_group_code.
Response
messagesarray de objetosiempre presente- Un elemento por mensaje, en el orden del request.
message_idstringsiempre presente- Identificador del mensaje, el mismo que devuelve
POST /v1/mails/send. Sirve para filtrarGET /v1/mails?msg_id=<message_id>. recipientsarray de objetosiempre presente- Un elemento por destinatario al que sale el mensaje. Una dirección repetida en
to,ccobccaparece una sola vez, en el primer campo donde figura.emailstringsiempre presente- Dirección del destinatario, normalizada.
typestring, enum: `to | cc | bcc`siempre presente- Campo del mensaje en que figura el destinatario.
mail_idstringsiempre presente- Identificador del correo de ese destinatario. Sirve para consultar su estado con
GET /v1/mails/{mail_id}sin buscarlo antes.
- Un reintento con la misma
Idempotency-Keyy el mismo lote responde también202, con el mismo body y el headerIdempotent-Replayed: true. - La respuesta lleva además los headers
X-RateLimit-Limit,X-RateLimit-RemainingyX-RateLimit-Resetdel límite que esté más cerca de agotarse (el cupo de la API key o la tasa masiva de su plan), con el estado después del último mensaje enviado. Un reintento que devuelve la respuesta original no los lleva. 202significa que el lote quedó aceptado para su entrega, no que los correos ya se entregaron. El estado de cada correo se consulta por sumail_id.
Idempotencia
Un lote puede llevar miles de destinatarios, y reintentar una llamada cuya respuesta no se recibió los enviaría todos de nuevo. Por eso la Idempotency-Key es obligatoria en este endpoint.
- Un reintento del mismo lote no vuelve a enviar. Si el lote ya se aceptó, el reintento responde
202con la respuesta original y el headerIdempotent-Replayed: true. - Un lote interrumpido se completa con la misma clave. Si la llamada se interrumpe a mitad del envío (un
503, un429o un corte de red), los mensajes que ya salieron quedan registrados. Un reintento con la misma clave y el mismo lote envía solo los que faltan y responde con el lote completo. Repita la llamada sin cambiar el body. - La misma clave con otro lote se rechaza con
422 idempotency_key_reused. Use una clave nueva para cada lote. - Mientras la llamada original sigue en curso, un reintento con la misma clave recibe
409 idempotency_key_in_progresscon un headerRetry-Afteren segundos. Espere ese plazo y repita la llamada. Si la llamada original se interrumpió sin llegar a terminar, el reintento puede recibir este código durante hasta un minuto, y después completa el lote. - Un lote rechazado no consume la clave. Si la llamada responde un error antes de enviar el primer mensaje (un
4xxde validación, de remitente o de cupo), corrija el lote y repita la llamada con la misma clave. - La clave vale 24 horas desde el primer intento. Pasado ese plazo queda libre, y una llamada con ella se trata como un lote nuevo: se envía otra vez.
- La clave es única en la instancia, y distingue mayúsculas de minúsculas. Si varios sistemas envían a la misma instancia, use claves que no puedan coincidir entre ellos, como un UUID o el identificador del lote en su sistema.
Dos lotes son el mismo si sus mensajes son iguales, en el mismo orden. El orden de las claves dentro de cada objeto JSON no importa.
Entrega
Los correos de un lote salen al menos 15 segundos después de aceptados. Esa espera mínima separa el envío masivo del transaccional, de modo que un lote grande no demore los correos operativos de la instancia. Un mensaje con test_mode: true sale sin esa espera.
El compromiso de latencia del envío transaccional no cubre el envío masivo.
Límites de tamaño
| Qué | Límite | Qué pasa al excederlo |
|---|---|---|
| Mensajes por lote | Entre 1 y 100 | 422 (validation_error). Para enviar más, divida el envío en varios lotes, cada uno con su propia Idempotency-Key. |
| Body del request | 20 MB, el lote completo | 413 (request_too_large) antes de procesar el contenido. Aplica al tamaño transferido y, si se envía comprimido, también al resultado de descomprimirlo. Con adjuntos o HTML pesado, el lote admite menos mensajes que el máximo. |
| Cada mensaje | Los mismos de POST /v1/mails/send: 50 destinatarios, asunto de 254 caracteres, html y text de 5 MB, 20 adjuntos que suman 10 MB, y 15 MB por mensaje armado | Un mensaje fuera de límite rechaza el lote completo (ver Límites de tamaño de POST /v1/mails/send). |
Errores
Un error que corresponde a un mensaje del lote indica cuál en el detail, que empieza con la posición del mensaje (/messages/3: ..., contando desde 0). En un 422, el pointer de cada elemento de errors[] la incluye del mismo modo (/messages/3/subject).
| Código | Cuándo |
|---|---|
| 400 | Falta el header Idempotency-Key (idempotency_key_required). |
| 402 | El lote supera el límite de envíos del plan para el período más su margen de tolerancia, contando todos sus destinatarios (plan_send_limit_exceeded, ver Límite de envíos del plan), o la instancia no tiene una suscripción activa (subscription_inactive). No se envía ningún mensaje. |
| 403 | La API key no tiene el scope mail:send_bulk, o la IP no está en la lista de acceso permitido. |
| 404 | El sender_email de un mensaje no corresponde a un remitente registrado (sender_not_found), o su template_id no existe (mail_template_not_found). |
| 409 | El remitente de un mensaje no es masivo (sender_not_bulk). Un remitente de prueba (*.sandbox.test) intentó enviar a destinatarios fuera de sandbox (sandbox_relay_restricted). El dominio de un remitente no está verificado (domain_sender_not_verified). Una plantilla no está publicada y el mensaje no lleva test_mode: true (template_not_public). La misma Idempotency-Key se está enviando en otra llamada (idempotency_key_in_progress, con Retry-After). |
| 413 | El body supera los 20 MB, el contenido comprimido los supera al descomprimirlo, o un mensaje ya armado excede el tamaño máximo aceptado para envío (request_too_large). |
| 422 | La Idempotency-Key ya se usó con un lote distinto (idempotency_key_reused). El request no cumple el contrato (validation_error): messages vacío o con más de 100 elementos, un mensaje que no cumple las reglas de POST /v1/mails/send o un body que no es JSON. |
| 422 | Un mensaje del lote tiene más destinatarios que el cupo de envío entero de la API key (rate_limit_cost_exceeds_quota); el detail indica cuál. No se envía ningún mensaje. |
| 429 | Se agotó por ahora el cupo de envío de la API key o la tasa masiva de su plan (rate_limit_exceeded; limit_scope indica cuál). La respuesta incluye Retry-After. Si ocurre a mitad del lote, lo enviado queda registrado: repita la llamada con la misma clave después de esa espera. |
| 503 | El envío se interrumpió de forma transitoria (service_unavailable). Lo enviado queda registrado: repita la llamada con la misma clave y el mismo lote para enviar el resto. |
Salvo 429, 503 e idempotency_key_in_progress, los errores se producen antes de enviar el primer mensaje, así que no sale ninguno.
idempotency_key_reused tiene código propio, distinto del validation_error de los demás 422, porque la solución es otra: el lote no tiene nada que corregir, y hay que enviarlo con una clave nueva.
Formato del cuerpo de error y detalle por código: Respuestas de error.
- El cupo del plan se descuenta una vez por lote, por la suma de los destinatarios de todos sus mensajes. Si el lote no cabe completo dentro del límite y su margen de tolerancia, se rechaza entero con
402; si su plan permite exceder el límite, el lote se acepta hasta el tope del plan, sin indicarlo en la respuesta (ver Límite de envíos del plan). Al completar un lote interrumpido, se descuentan solo los mensajes que faltaban. - Cada correo se cobra por separado, igual que en
POST /v1/mails/send: un mensaje con tres destinatarios son tres correos. - El resultado por destinatario se consulta con
GET /v1/mails/{mail_id}, o en conjunto conGET /v1/mailsfiltrando pormsg_ido porcustom_group_code.
curl -X POST "https://$API_HOST/v1/mails/send_bulk" \
-H "Authorization: Bearer $API_KEY" \
-H "X-Instance-Slug: $SLUG" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: newsletter-octubre-lote-1" \
-d '{
"messages": [
{
"sender_email": "novedades@example.com",
"to": [
{
"email": "alice@example.com",
"name": "Alice"
}
],
"template_id": 42,
"vars": {
"NOMBRE": "Alice",
"PUNTOS": 1200
},
"custom_group_code": "newsletter-octubre"
},
{
"sender_email": "novedades@example.com",
"to": [
{
"email": "bob@example.com"
}
],
"template_id": 42,
"vars": {
"NOMBRE": "Bob",
"PUNTOS": 350
},
"custom_group_code": "newsletter-octubre"
}
]
}'
{
"messages": [
{
"message_id": "01HXYZ...",
"recipients": [
{
"email": "alice@example.com",
"type": "to",
"mail_id": "01HXYZ..."
}
]
},
{
"message_id": "01HXZA...",
"recipients": [
{
"email": "bob@example.com",
"type": "to",
"mail_id": "01HXZA..."
}
]
}
]
}