Saltar al contenido principal

Enviar un correo

POST/v1/mails/send

Descripción

Encola un email para su entrega — transaccional o con plantilla guardada.

Autenticación

Authorizationbearer tokenheaderobligatorio
API key con scope mail:send. Formato: Bearer FD.<key_id>.<token>.
X-Instance-Slugstringheaderobligatorio

Request

Body (Content-Type: application/json):

{
"sender_email": "newsletter@example.com",
"to": [{ "email": "alice@example.com", "name": "Alice" }],
"subject": "Hello from Fidelizador",
"html": "<p>Hello, <strong>Alice</strong>!</p>",
"text": "Hello, Alice!",
"category": "transactional",
"reply_to": { "email": "soporte@example.com", "name": "Soporte" },
"headers": { "X-Order-Ref": "order-123" }
}
sender_emailstring (email)obligatorio
Dirección de un remitente ya registrado (ver `GET /v1/senders`; el alta es exclusiva del panel web).
toarray de objeto `{email, name?}`, mínimo 1 elementoobligatorio
Destinatarios.
template_idinteger | null
ID de una plantilla guardada. *Si se omite, subject y html pasan a ser requeridos.
subjectstring | null
Asunto del email, máx. 254 caracteres. No admite string vacío. *Requerido cuando no se envía template_id.
htmlstring | null
Cuerpo HTML. No admite string vacío. Sin cota propia — ver Límites de tamaño. *Requerido cuando no se envía template_id.
textstring | null
Versión en texto plano, usada como fallback si el cliente de correo no soporta HTML. No admite string vacío ni tiene cota propia.
ccarray de objeto `{email, name?}` | null
Destinatarios en copia, visibles para el resto.
bccarray de objeto `{email, name?}` | null
Destinatarios en copia oculta.
headersobject (string→string) | null
Headers propios, incluidos en el mensaje que recibe el destinatario. Un valor largo se plega y puede quedar codificado (RFC 2047). Máx. 15; nombre ≤ 126 caracteres ASCII imprimibles, valor ≤ 995, y ≤ 996 entre ambos. Los headers que administra la plataforma se rechazan — ver Headers propios.
attachmentsarray de objeto `{type, content, filename}` | null
Adjuntos en base64. type = MIME type con formato tipo/subtipo, content = base64 estricto, filename = nombre del archivo. Ninguno de los tres admite string vacío. Peso total limitado — ver Límites de tamaño.
categorystring | null
Categoría que clasifica el email, una por mensaje, máx. 255 caracteres. Se normaliza a slug: los acentos se pliegan, las mayúsculas pasan a minúsculas y cualquier otro carácter se colapsa a -, así que Envíos Masivos queda registrada como envios-masivos. Se registra en su primer uso; no hace falta crearla antes.
reply_toobjeto `{email, name?}` | null
Dirección a la que deben ir las respuestas. Si se omite, las respuestas van al remitente.
test_modeboolean
Si es true, el email se entrega igual, pero queda marcado como prueba y los reportes lo cuentan como test en vez de tráfico real. Es el único modo en que se admite una plantilla en borrador. Default: false.
custom_msg_idstring | null
Identificador propio del mensaje, máx. 254 caracteres. Se guarda tal cual y se devuelve en los reportes de actividad.
custom_group_codestring | null
Etiqueta de grupo arbitraria (campaña, batch), máx. 254 caracteres.

Response

200 OKDevuelve un objeto con los campos que siguen.
message_idstringobligatorio
Identificador del mensaje encolado.

Errores

CódigoCuándo
402Se alcanzó el límite de envíos del plan para el período, o la instancia no tiene una suscripción activa (plan_send_limit_exceeded / subscription_inactive).
404El sender_email no corresponde a un remitente registrado (sender_not_found), o el template_id no existe (mail_template_not_found).
409Un remitente de prueba (*.sandbox.test) intentó enviar a destinatarios fuera de sandbox (sandbox_relay_restricted); el dominio del remitente no está verificado (domain_sender_not_verified); o la plantilla referenciada no está publicada (template_not_public, se publica desde el panel).
413El cuerpo del request excede el tamaño permitido, el contenido comprimido excede el tamaño permitido al descomprimirlo, un adjunto individual de multipart/form-data supera su límite, o el mensaje final excede el tamaño máximo aceptado para envío (request_too_large).
422El request no cumple el contrato: falta subject o html sin template_id, se enviaron junto a template_id, el subject supera los 254 caracteres, se superó el máximo de destinatarios, de adjuntos o de peso de html/text/adjuntos, un header está reservado o excede sus cotas, una dirección de email es inválida, un adjunto no está en base64 válido o su type no tiene formato tipo/subtipo, se envió attachments como campo de texto en vez de parte de archivo, o una categoría no tiene ninguna letra ni dígito con que armar un slug (validation_error).
429Se superó el límite de tasa. La respuesta incluye Retry-After.
503El envío no pudo aceptarse de forma transitoria; reintentar (service_unavailable).

Detalle completo en Errores de negocio, Errores de negocio, Errores genéricos y Errores genéricos.

Headers propios

Los headers de headers se incluyen en el mensaje que recibe el destinatario. Es el mecanismo para correlacionar (X-Order-Ref), hilar conversaciones (In-Reply-To, References) o pasar cualquier metadato que el cliente de correo deba ver.

Un valor largo se plega y, si no tiene dónde plegarse, se codifica según RFC 2047 (=?utf-8?q?...?=): un consumidor que lea los bytes crudos debe decodificarlo.

La plataforma administra un conjunto que no se puede fijar y que se rechaza con 422: los que definen la estructura del mensaje (To, From, Cc, Bcc, Subject, Reply-To, Content-Type, Content-Transfer-Encoding, Content-Disposition, MIME-Version) y los que la plataforma escribe al entregar (Message-ID, Date, Return-Path, Received, DKIM-Signature, List-Unsubscribe, List-Unsubscribe-Post, Bounce-To, Sender, X-Mailer, Priority). El prefijo x-fd- también está reservado; si se necesita una de esas capacidades, hay que usar el campo JSON correspondiente (el mensaje de error lo indica).

Para hilar una conversación se usan In-Reply-To y References, no Message-ID: ese lo asigna la plataforma.

Plantillas y contenido

Con template_id, el contenido lo aporta la plantilla. Enviar además subject, html o text se rechaza con 422 en lugar de descartarse en silencio, para que un error del lado del integrador no se confunda con un envío correcto.

Una plantilla recién editada o eliminada toma efecto de inmediato sobre los envíos siguientes; si la invalidación interna de la copia en caché no llega a aplicarse, puede tardar unos segundos. Editar una plantilla no altera los correos ya aceptados: el contenido se materializa al momento del envío.

Límites de tamaño

QuéLímiteQué pasa al excederlo
subject254 caracteres422 al recibir el request. Es el largo que admite la columna donde se reporta el asunto, así que un asunto más largo llegaría completo al destinatario pero aparecería truncado en los reportes.
Destinatarios (to + cc + bcc)50 en total, sumando los tres campos422. Para llegar a más destinatarios, dividir el envío en varias llamadas.
Cuerpo del request20 MB413 (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.
html, text5 MB cada uno422. Se mide en bytes UTF-8, no en caracteres: un texto con acentos o emoji ocupa más de un byte por carácter.
html (recomendado)bajo 100 KBNo es un límite y no se rechaza nada. Por encima de unos 102 KB de HTML, Gmail no muestra el mensaje completo: lo corta con un aviso de "Mensaje recortado" y un enlace para ver el resto, de modo que el destinatario necesita un click extra. El correo se entrega igual. El umbral es empírico y Google no lo publica, así que la recomendación habitual del sector es quedarse bajo 80 KB. Cuentan el marcado y los estilos, no el peso de las imágenes enlazadas.
Adjuntos (cantidad)20422.
Adjuntos (peso técnico)10 MB sumados, ya decodificados422 al recibir el request. En multipart/form-data, además, cada archivo individual está acotado a 10 MB y se rechaza con 413.
Adjuntos (peso funcional)1 MB por defecto, configurable por instanciaEl correo se acepta con 200 y se descarta después: termina en estado dropped y no se cobra. Para diagnosticarlo, consultar GET /v1/mails/{mail_id}/logs. Solicitar un ajuste del límite a soporte.
Mensaje final15 MB413. Es el tamaño del correo ya armado, con los adjuntos codificados. Rige igual para el envío por SMTP.

Los dos límites de adjuntos son distintos y conviven: el técnico es fijo y rechaza el request de entrada; el funcional se ajusta por instancia y opera después, descartando el correo.

Los límites de la tabla no son simultáneos. Cada campo puede llegar a su propio máximo, pero el conjunto comparte el presupuesto del mensaje final: un html de 5 MB junto con 10 MB de adjuntos supera los 15 MB del mensaje y se rechaza con 413. La cota que manda en la práctica es siempre la del mensaje.

Por la misma razón, un adjunto por encima de ~10 MB no es entregable: la codificación del mensaje lo agranda alrededor de un 37%, así que supera el máximo del mensaje antes de salir. Solicitar un ajuste del límite funcional por encima de ese valor no cambia el resultado.

Los mismos límites rigen para el envío por SMTP: un asunto de más de 254 caracteres se rechaza en la sesión SMTP con 554 5.6.0, un mensaje de más de 15 MB se rechaza en la sesión con 552 5.3.4, y el exceso de adjuntos termina igualmente en dropped.

Consentimiento

Los adjuntos no son el único motivo por el que un correo aceptado con 200 puede descartarse después. Si el remitente está configurado para exigir consentimiento, cada destinatario se evalúa por separado y el correo no sale hacia quien no lo tenga: queda en estado dropped, con el motivo visible en GET /v1/mails/{mail_id}/logs.

Son dos motivos distintos: falta un consentimiento que el remitente exige, o el destinatario lo revocó explícitamente.

Los envíos con test_mode no pasan por esta evaluación. Para saber de antemano si un destinatario está habilitado, consultar GET /v1/consent-rules/resolve; el panorama completo está en la suite de consentimiento.

Normalización de direcciones

Las direcciones de sender_email, to, cc, bcc y reply_to se normalizan antes de guardarse (se aplica la forma canónica del dominio). Una dirección con sintaxis inválida se rechaza con 422.

Reintentos

El endpoint no es idempotente y no hay forma de pedir que lo sea: no existe un header Idempotency-Key ni ningún otro mecanismo de deduplicación. Un reintento tras un timeout de red produce un segundo envío, y custom_msg_id no cambia eso — se guarda tal cual, sirve para correlacionar, y dos mensajes con el mismo valor se envían los dos.

Si su sistema reintenta de forma automática, el control de duplicados es suyo. Dos enfoques habituales:

  • Registrar del lado del cliente qué mensajes ya se enviaron con éxito, antes de reintentar.
  • Tratar un timeout como resultado desconocido: consultar GET /v1/mails filtrando por custom_msg_id para averiguar si el envío llegó a registrarse, y recién ahí decidir si reintentar.
Notas
  • Además de application/json, el endpoint acepta el mismo body con Content-Encoding: gzip (recomendado para HTML grande o múltiples adjuntos) y multipart/form-data (adjuntos como binario crudo, sin overhead de base64; en ese formato los campos array/objeto como to, cc, headers van serializados como string JSON dentro del form). En multipart/form-data, attachments debe enviarse como parte de archivo, con su filename: una parte de texto en ese campo se rechaza con 422. Enviar el contenido en base64 como string es la forma del cuerpo JSON, no la de multipart.
  • Consultar el resultado del envío por message_id vía GET /v1/mails/{mail_id} o GET /v1/mails/{mail_id}/logs — usar el mail_id que devuelven GET /v1/mails al filtrar por msg_id=<message_id>.
Request
curl -X POST "https://$API_HOST/v1/mails/send" \
-H "Authorization: Bearer $API_KEY" \
-H "X-Instance-Slug: $SLUG" \
-H "Content-Type: application/json" \
-d '{
"sender_email": "newsletter@example.com",
"to": [
{
"email": "alice@example.com",
"name": "Alice"
}
],
"subject": "Hello from Fidelizador",
"html": "<p>Hello, <strong>Alice</strong>!</p>"
}'
Response
{
"message_id": "01HXYZ..."
}