Saltar al contenido principal

Enviar un lote de correos

POST/v1/mails/send_bulk

Para 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:

RemitenteCómo envía
TransaccionalPOST /v1/mails/send o SMTP. Este endpoint lo rechaza con 409 sender_not_bulk.
MasivoSolo 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-Typeheader
application/json. El mismo body puede enviarse comprimido con Content-Encoding: gzip. No se acepta multipart/form-data: los adjuntos van en base64 dentro de cada mensaje.

Body:

{
"messages": [
{
"sender_email": "[email protected]",
"to": [{ "email": "[email protected]", "name": "Alice" }],
"template_id": 42,
"custom_group_code": "newsletter-octubre"
},
{
"sender_email": "[email protected]",
"to": [{ "email": "[email protected]" }],
"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_email y to obligatorios, template_id o bien subject y html, 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​

202 AcceptedEl lote se aceptó.
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 filtrar GET /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, cc o bcc aparece 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.
Notas
  • Un reintento con la misma Idempotency-Key y el mismo lote responde también 202, con el mismo body y el header Idempotent-Replayed: true.
  • La respuesta lleva además los headers X-RateLimit-Limit, X-RateLimit-Remaining y X-RateLimit-Reset del 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.
  • 202 significa que el lote quedó aceptado para su entrega, no que los correos ya se entregaron. El estado de cada correo se consulta por su mail_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 202 con la respuesta original y el header Idempotent-Replayed: true.
  • Un lote interrumpido se completa con la misma clave. Si la llamada se interrumpe a mitad del envío (un 503, un 429 o 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_progress con un header Retry-After en 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 4xx de 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.

Importante

El compromiso de latencia del envío transaccional no cubre el envío masivo.

Límites de tamaño​

QuéLímiteQué pasa al excederlo
Mensajes por loteEntre 1 y 100422 (validation_error). Para enviar más, divida el envío en varios lotes, cada uno con su propia Idempotency-Key.
Body del request20 MB, el lote completo413 (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 mensajeLos 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 armadoUn 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ódigoCuándo
400Falta el header Idempotency-Key (idempotency_key_required).
402El 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.
403La API key no tiene el scope mail:send_bulk, o la IP no está en la lista de acceso permitido.
404El sender_email de un mensaje no corresponde a un remitente registrado (sender_not_found), o su template_id no existe (mail_template_not_found).
409El 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).
413El 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).
422La 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.
422Un 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.
429Se 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.
503El 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.

Notas
  • 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 con GET /v1/mails filtrando por msg_id o por custom_group_code.
Request
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": "[email protected]",
"to": [
{
"email": "[email protected]",
"name": "Alice"
}
],
"template_id": 42,
"vars": {
"NOMBRE": "Alice",
"PUNTOS": 1200
},
"custom_group_code": "newsletter-octubre"
},
{
"sender_email": "[email protected]",
"to": [
{
"email": "[email protected]"
}
],
"template_id": 42,
"vars": {
"NOMBRE": "Bob",
"PUNTOS": 350
},
"custom_group_code": "newsletter-octubre"
}
]
}'
Response
{
"messages": [
{
"message_id": "01HXYZ...",
"recipients": [
{
"email": "[email protected]",
"type": "to",
"mail_id": "01HXYZ..."
}
]
},
{
"message_id": "01HXZA...",
"recipients": [
{
"email": "[email protected]",
"type": "to",
"mail_id": "01HXZA..."
}
]
}
]
}