Ir a la documentación
API

Enviar un correo

POST /emails: un mensaje, ahora o más tarde.

POSTapi.openemail.uk/emails

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.

CampoObligatorioNotas
fromUna dirección a secas o Name <addr>. Debe ser una con la que la clave pueda enviar.
toHasta 50 destinatarios entre to, cc y bcc juntos.
cc, bccnoLos destinatarios en bcc nunca se nombran en los bytes que recibe nadie más.
subjectnoPor defecto, vacío.
html, textuno de los dosAmbos vale. El HTML es lo que ven los destinatarios.
templateuno 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.
replyTonoUna sola dirección.
headersnoX-*, List-*, Reply-To, Precedence, Auto-Submitted, Importance, Priority, Feedback-Id.
attachmentsno{ filename, content, contentType } en base64, 5 MB en total, o { fileId } nombrando un archivo que ya está en el espacio de trabajo. 20 archivos.
attachmentDeliverynomime, 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.
threadIdnoResponder dentro de un hilo existente.
draftIdnoEnviar un borrador existente.
scheduledAtnoInstante ISO o duración. Ver Programación.
cancellableForSecondsnoUna 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.
signaturenofalse 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.
tagsnoHasta 10 etiquetas tuyas. Se devuelven tal cual, nunca se interpretan.
trackingno{ 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.
translateno{ 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.

200 OK
{  "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
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" }  }'
200 OK
{  "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-face se 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 sobre translate, 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, translate incluido; lo que produjo el modelo, no. Así que reintentar un envío sin respuesta con la misma Idempotency-Key reproduce 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 permite dir justo por eso, así que el mensaje que va por el cable lleva la dirección que mostró la vista previa.
CódigoEstadoCuándo
`invalid_parameter`422translate.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`422El 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`422Má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`409El 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`503El 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`422Una 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.