Перейти к документации
API

Отправка письма

POST /emails: одно сообщение, сейчас или позже.

POSTapi.openemail.uk/emails

Выполняет настоящий запрос в вашем рабочем пространстве, с вашим собственным ключом.

Запрос

from обязателен. В отличие от редактора, запасного отправителя здесь нет, потому что этот запасной вариант — адрес рабочего пространства по умолчанию, а он незаметно меняется по мере того, как адреса появляются и исчезают.

ПолеОбязательноПримечания
fromдаГолый адрес или Name <addr>. Должен быть одним из тех, от имени которых ключ может отправлять.
toдаНе более 50 получателей суммарно в to, cc и bcc.
cc, bccнетПолучатели в bcc никогда не упоминаются в байтах, которые получает кто-либо ещё.
subjectнетПо умолчанию пусто.
html, textодно изМожно оба. Получатели видят HTML.
templateодно из{ id, version?, props?, slots? }. Сохранённое тело письма, по id или по slug. Отклоняется вместе с html, text или draftId. См. Отправка по шаблону.
replyToнетОдин адрес.
headersнетX-*, List-*, Reply-To, Precedence, Auto-Submitted, Importance, Priority, Feedback-Id.
attachmentsнет{ filename, content, contentType } в base64, суммарно 5 MB, либо { fileId } с именем файла, уже имеющегося в рабочем пространстве. 20 файлов.
attachmentDeliveryнетmime, link или auto. auto превращает файлы в ссылки, как только они превышают 2 MB, на домене с активным доменом для файлов. По умолчанию берётся настройка почтового ящика.
threadIdнетОтвет в существующую цепочку.
draftIdнетОтправка существующего черновика.
scheduledAtнетМомент времени или длительность в формате ISO. См. Планирование.
cancellableForSecondsнетОкно отмены от 0 до 900 секунд для немедленной отправки. Отклоняется вместе с scheduledAt, который и так остаётся отменяемым до самой отправки. См. Планирование.
signatureнетfalse оставляет это сообщение без подписи. Иначе оно несёт подпись адреса, с которого отправлено, — собственную подпись этого адреса либо ту, что задана для All addresses.
tagsнетДо 10 ваших собственных меток. Возвращаются обратно, но никогда не интерпретируются.
trackingнет{ opens?, clicks? }. Любое из них переопределяет настройку для этого сообщения; опустите поле — и эта половина откатится к настройке адреса, с которого идёт отправка, либо к All addresses, и она включена, если только одна из них её не выключила.
translateнет{ to, from?, subject?, includeOriginal? }. Отправляет сообщение на языке получателя. Разрешается в момент принятия запроса, отклоняется вместе с draftId.

Неизвестные поля отклоняются, а не игнорируются, поэтому опечатка в имени — это 422 сейчас, а не сюрприз потом. Заголовки, которые подорвали бы аутентификацию отправителя (From, Sender, Bcc, Message-ID, Return-Path и другие), отклоняются с reserved_header.

Ответ

200, когда сообщение уже ушло, 202, когда с ним ещё что-то должно произойти. Вызывающая сторона, ветвящаяся по коду статуса, права в обоих случаях.

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 — это устойчивый идентификатор, который вы храните, и тот, по которому приходит событие доставки, ведь webhook о возврате называет его emailId. messageId — это Message-ID по RFC 5322, и он равен null, пока не существует MIME. Не сопоставляйте по нему: служба отправки переписывает этот заголовок на выходе, поэтому значение отсюда не встречается ни в одном отчёте о возврате или доставке, и сопоставление по нему никогда не срабатывает.

На языке получателя

translate пишет сообщение на чужом языке, прежде чем оно уйдёт. Тело, а также тема, если вы это не отключите, переводятся в момент ПРИНЯТИЯ запроса — по тому же правилу, которому следует template, и оно несущее по тем же причинам: запланированное сообщение несёт те слова, которые были одобрены, а не то, что модель выдаст во вторник, а перевод, который не удалось получить, отклоняет отправку до того, как появится запись. Ничто не доставляется на языке, который отправитель не выбирал.

translate

tostringобязательно
Язык, на котором писать: код BCP-47 (`de`), английское название («German») или самоназвание языка («Deutsch»), от 2 до 60 символов. Все три формы нормализуются к табличному коду прежде всего остального, поэтому это один и тот же запрос — а это важно, потому что отпечаток Idempotency-Key берётся с разобранного запроса. Псевдонимы тоже разрешаются: `zh-TW` становится `zh-Hant`. Форма, которая не разрешается ни во что, даёт 422 по `translate.to`.
fromstring
Язык, на котором вы написали, в любой из тех же трёх форм. Чисто оптимизация. Если опустить, тело будет прочитано и язык определён, что стоит одного короткого обращения к модели. Стоит указывать на высоконагруженном пути, а также когда тело состоит в основном из имён, чисел и ссылок: определение скорее воздержится, чем угадает, а неустановленный исходный язык не стоит вам ничего, кроме названия языка в подписи над вашим оригиналом. Это не поле `from` верхнего уровня, которое является адресом.
subjectboolean
Переводить также строку темы. По умолчанию true; при false тема отправляется ровно так, как вы её написали.
includeOriginalboolean
Поместить то, что вы написали на самом деле, под переводом, за разделителем и с подписью на языке получателя. По умолчанию true, и это стоит оставить включённым. Только так читающий может проверить странно звучащую фразу, а не полагаться на модель, вывод которой ни один из вас не видит.
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 добавляется сверху и появляется только у переведённого сообщения: в этом ответе и в GET /emails/{id}, но никогда в строке списка, потому что список не подтягивает сохранённый запрос и его молчание там не говорит ни о чём. Он несёт коды, а не целые строки языков: это запись о том, что было сделано, а самоназвание живёт в GET /languages. subject в ответе — уже переведённый, поэтому консоль никогда не покажет сообщение под строкой, которой получатель не видел.

  • Работает с template, и это как раз полезный случай: переводится ОТРЕНДЕРЕННЫЙ результат, поэтому одно сохранённое тело обслуживает все языки, на которых читают ваши клиенты. Шаблон, рендерящий целый документ, сначала разбирается: до модели доходит только то, что внутри <body>, а doctype, блоки <style> и правила @font-face возвращаются вокруг ответа. По этой же причине ограничение в 30 000 символов измеряет текст, а не документ: сообщение из двух строк, завёрнутое в фирменную таблицу стилей, остаётся сообщением из двух строк.
  • Единственная часть шаблона, остающаяся без перевода, — его <title>, который не отображает ни один почтовый клиент. <Preview> из react-email рендерится в тело и переводится вместе со всем остальным.
  • Отклоняется вместе с draftId: 422 по translate с текстом «A draft is sent as it was written; translate a body or send a draft, not both». Черновик написан человеком и отправляется таким, каким он его оставил.
  • Намеренно не входит в отпечаток идемпотентности. Хешируется отправленный вами запрос, включая translate; то, что выдала модель, — нет. Поэтому повтор оставшейся без ответа отправки с тем же Idempotency-Key воспроизводит исходный результат. Возвращается уже существующее сообщение, без второй отправки и без второго перевода. Если бы хешировался сам текст перевода, честный повтор каждый раз давал бы другой отпечаток — а это прямой путь к тому, что одно и то же сообщение уйдёт дважды.
  • Переведённое сообщение в очереди или по расписанию заморожено от изменений формулировок. Его можно перенести или отменить; изменить сказанное — значит отменить и отправить заново, на глазах у того, кто может прочитать новый текст.
  • Язык с письмом справа налево получается справа налево: перевод обёрнут в dir="rtl", а ваш оригинал ниже ориентирован сам по себе. Атрибут переживает исходящий санитайзер, который разрешает dir именно по этой причине, поэтому сообщение в сети несёт то же направление письма, что показал предпросмотр.
КодСтатусКогда
`invalid_parameter`422translate.to или translate.from называет язык, который мы не можем определить. Сообщение перечисляет, какие три формы принимаются, и указывает на GET /languages.
`unknown_language`422Тот же сбой, пойманный шагом позже — сервисом, а не схемой. Подстраховка, по translate.to.
`translation_too_long`422Более 30 000 символов на любом конце обращения к модели. Отказ, а не усечение: у половины переведённого сообщения нет шва, показывающего, где оно оборвалось, и читающий действует по той половине, которую получил.
`translation_not_configured`409У рабочего пространства нет ключа AI, а платформенный AI выключен. 409, а не 503, потому что повтор завершится точно так же. Ничего не отправлено. Отправьте без translate, если хотели отправить как написано.
`translation_failed`503Провайдер не ответил или ответил чем-то непригодным. Ничего не отправлено; сообщение никогда не уходит без перевода в качестве запасного варианта. Эта ошибка наша, и её стоит повторить.
`unknown_parameter`422Нераспознанный ключ внутри translate, который является строгим объектом, как и остальная часть запроса.

При отправке из кода перевод никто не читает заранее. POST /emails/translate — тот же цикл, остановленный на шаг раньше, чтобы показать человеку, что он собирается отправить. Затем отправьте одобренное им как обычные html/subject, вообще без translate в запросе.