Enviar un correo
POST /emails: un mensaje, ahora o más tarde.
Ejecuta la llamada real contra tu espacio de trabajo, con tu propia clave.
La solicitud
from es obligatorio. A diferencia del redactor no hay remitente de reserva, porque esa reserva es la dirección predeterminada del espacio de trabajo y cambia de forma invisible según entran y salen direcciones.
| Campo | Obligatorio | Notas |
|---|---|---|
| from | sí | Una dirección a secas o Name <addr>. Debe ser una con la que la clave pueda enviar. |
| to | sí | Hasta 50 destinatarios entre to, cc y bcc juntos. |
| cc, bcc | no | Los destinatarios en bcc nunca se nombran en los bytes que recibe nadie más. |
| subject | no | Por defecto, vacío. |
| html, text | uno de los dos | Ambos vale. El HTML es lo que ven los destinatarios. |
| template | uno de los dos | { id, version?, props?, slots? }. Un cuerpo almacenado, por id o por slug. Se rechaza junto con html, text o draftId. Ver Enviar con una plantilla. |
| replyTo | no | Una sola dirección. |
| headers | no | X-*, List-*, Reply-To, Precedence, Auto-Submitted, Importance, Priority, Feedback-Id. |
| attachments | no | { filename, content, contentType } en base64, 5 MB en total, o { fileId } nombrando un archivo que ya está en el espacio de trabajo. 20 archivos. |
| attachmentDelivery | no | mime, link o auto. auto enlaza los archivos cuando pasan de 2 MB en un dominio con un dominio de archivos activo. Por defecto, el ajuste del buzón. |
| threadId | no | Responder dentro de un hilo existente. |
| draftId | no | Enviar un borrador existente. |
| scheduledAt | no | Instante ISO o duración. Ver Programación. |
| cancellableForSeconds | no | Una ventana de deshacer de 0 a 900 segundos en un envío inmediato. Se rechaza junto con scheduledAt, que se puede cancelar hasta que se envía. Ver Programación. |
| signature | no | false deja este mensaje sin firma. Si no, lleva la firma de la dirección desde la que se envía, que es la suya propia o bien la configurada para Todas las direcciones. |
| tags | no | Hasta 10 etiquetas tuyas. Se devuelven tal cual, nunca se interpretan. |
| tracking | no | { opens?, clicks? }. Cualquiera de los dos anula el ajuste para este mensaje; omite un campo y esa mitad recae en el ajuste de la dirección desde la que se envía, o si no en Todas las direcciones, y está activado salvo que uno de esos lo haya desactivado. |
| translate | no | { to, from?, subject?, includeOriginal? }. Lo envía en el idioma del destinatario. Se resuelve cuando se acepta la solicitud, y se rechaza junto con draftId. |
Los campos desconocidos se rechazan en lugar de ignorarse, así que un nombre mal escrito es un 422 ahora en vez de una sorpresa después. Los encabezados que anularían la autorización del remitente (From, Sender, Bcc, Message-ID, Return-Path y otros) se rechazan con reserved_header.
La respuesta
200 cuando el mensaje ya ha salido, 202 cuando todavía tiene que pasarle algo. Quien ramifica según el código de estado acierta en ambos casos.
{ "object": "email", "id": "msg_c5f21cc6bfec4e848caf905b", "status": "sent", "mode": "live", "from": "[email protected]", "subject": "Your September invoice", "messageId": "<2598…@acme.com>", "transport": "ses", "sentAt": "2026-08-29T08:19:08.000Z", "source": "api", "replayed": false}id es el identificador duradero que conservas, y con el que vuelve un evento de entrega, ya que un webhook de rebote lo nombra como emailId. messageId es el Message-ID de RFC 5322 y es null hasta que existe el MIME. No correlaciones por él: el servicio de envío reescribe ese encabezado al salir, así que el valor de aquí no aparece en ningún informe de rebote ni de entrega y una coincidencia por él nunca se produce.
En el idioma del destinatario
translate escribe el mensaje en el idioma de otra persona antes de que salga. El cuerpo, y el asunto salvo que lo desactives, se traduce en el momento en que la solicitud se ACEPTA, que es la misma regla que sigue template y es igual de determinante por los mismos motivos: un mensaje programado lleva las palabras que se aprobaron y no lo que un modelo produzca el martes, y una traducción que no se pudo producir rechaza el envío antes de que exista una fila. Nada se entrega en un idioma que su remitente no eligió.
translate
tostringobligatorio- El idioma en el que escribir: un código BCP-47 (`de`), un nombre en inglés («German») o el nombre propio del idioma («Deutsch»), de 2 a 60 caracteres. Las tres formas se normalizan al código de la tabla antes de cualquier otra cosa, así que son una misma solicitud, lo que importa porque la huella de Idempotency-Key se toma sobre la solicitud ya analizada. Los alias también se resuelven: `zh-TW` pasa a ser `zh-Hant`. Uno que no resuelva a nada es un 422 sobre `translate.to`.
fromstring- En qué lo escribiste, en cualquiera de esas tres formas. Es puramente una optimización. Si lo omites, se lee el cuerpo y se deduce el idioma, lo que cuesta una llamada corta al modelo. Vale la pena indicarlo en una ruta de mucho volumen, y vale la pena indicarlo cuando el cuerpo son sobre todo nombres, números y enlaces: la detección se abstiene en vez de adivinar, y un origen indeterminado no te cuesta más que el idioma nombrado en el pie sobre tu original. No es el `from` de nivel superior, que es una dirección.
subjectboolean- Traducir también la línea de asunto. Por defecto true; false envía el asunto exactamente como lo escribiste.
includeOriginalboolean- Poner lo que escribiste de verdad debajo de la traducción, tras un separador y con un pie en el idioma del destinatario. Por defecto true, y conviene dejarlo activado. Es lo único que permite a quien lee comprobar una frase que suena rara en lugar de pedirle que confíe en un modelo cuya salida ninguno de los dos puede ver.
curl -X POST "$OE/emails" -H "$AUTH" -H "Content-Type: application/json" \ -d '{ "from": "[email protected]", "to": ["[email protected]"], "subject": "Your September invoice", "html": "<p>Invoice attached. Payment is due on the 14th.</p>", "translate": { "to": "de" } }'{ "object": "email", "id": "msg_c5f21cc6bfec4e848caf905b", "status": "sent", "from": "[email protected]", "subject": "Ihre Rechnung für September", "translation": { "language": "de", "languageName": "German", "detectedSourceLanguage": "en", "subject": true, "includeOriginal": true }}translation es aditivo y solo aparece en un mensaje que se tradujo: en esta respuesta y en GET /emails/{id}, nunca en una fila de lista, porque una lista no recupera la solicitud almacenada y su silencio ahí no dice nada en ningún sentido. Lleva códigos y no filas de idioma enteras: es un registro de lo que se hizo, y GET /languages es donde vive el endónimo. El subject de la respuesta es el traducido, así que una consola nunca lista un mensaje bajo una cadena que el destinatario no vio.
- Funciona con
template, y ese es el caso útil: lo que se traduce es la salida RENDERIZADA, así que un solo cuerpo almacenado sirve para todos los idiomas en los que leen tus clientes. Una plantilla que renderiza un documento completo se desmonta antes: al modelo solo llega lo que está dentro de<body>, y el doctype, los bloques<style>y las reglas@font-facese vuelven a poner alrededor de la respuesta. También es la razón de que el límite de 30.000 caracteres mida la prosa y no el documento: un mensaje de dos líneas envuelto en una hoja de estilos de marca es un mensaje de dos líneas. - La única parte de una plantilla que queda sin traducir es su
<title>, que ningún cliente de correo muestra. Un<Preview>de react-email se renderiza dentro del cuerpo y se traduce con el resto. - Se rechaza con
draftId: un 422 sobretranslate, que dice «Un borrador se envía tal como se escribió; traduce un cuerpo o envía un borrador, no ambas cosas». Un borrador lo escribió una persona y se envía tal como lo dejó. - Deliberadamente no forma parte de la huella de idempotencia. Lo que se somete a hash es la solicitud que enviaste,
translateincluido; lo que produjo el modelo, no. Así que reintentar un envío sin respuesta con la mismaIdempotency-Keyreproduce el original. Vuelve el mensaje que ya existe, sin un segundo envío y sin una segunda traducción. Someter a hash el texto traducido haría que un reintento honesto tuviera una huella distinta cada vez, que es como el mismo mensaje sale dos veces. - Un mensaje traducido que está en cola o programado queda congelado frente a cambios de texto. Muévelo o cancélalo; alterar lo que dice significa cancelar y volver a enviar, delante de alguien que pueda leer las palabras nuevas.
- Un idioma de destino de derecha a izquierda se produce de derecha a izquierda: la traducción envuelta en
dir="rtl", con tu original debajo orientado por su cuenta. El atributo sobrevive al saneador de salida, que permitedirjusto por eso, así que el mensaje que va por el cable lleva la dirección que mostró la vista previa.
| Código | Estado | Cuándo |
|---|---|---|
| `invalid_parameter` | 422 | translate.to o translate.from nombra un idioma que no podemos situar. El mensaje dice cuáles son las tres formas aceptadas y apunta a GET /languages. |
| `unknown_language` | 422 | El mismo fallo detectado un paso más tarde, por el servicio en vez de por el esquema. Una red de seguridad, sobre translate.to. |
| `translation_too_long` | 422 | Más de 30.000 caracteres en cualquiera de los dos extremos de la llamada al modelo. Un rechazo en lugar de un recorte: medio mensaje traducido no tiene una costura que muestre dónde se paró, y quien lo lee actúa sobre la mitad que recibió. |
| `translation_not_configured` | 409 | El espacio de trabajo no tiene clave de IA y la IA de la plataforma está desactivada. Un 409 en lugar de un 503 porque el reintento falla igual. No se envió nada. Envía sin translate si lo que querías era enviarlo tal como está escrito. |
| `translation_failed` | 503 | El proveedor no respondió, o respondió con algo inservible. No se envió nada; el mensaje nunca se publica sin traducir como plan B. Este es nuestro y vale la pena reintentarlo. |
| `unknown_parameter` | 422 | Una clave no reconocida dentro de translate, que es un objeto estricto como el resto de la solicitud. |
En un envío desde código no hay nadie leyendo la traducción antes. POST /emails/translate es el mismo recorrido detenido un paso antes, para mostrar a una persona lo que está a punto de enviar. Después envía lo que aprobó como un html/subject corriente, sin ningún translate en la solicitud.