Отправка письма
POST /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, когда с ним ещё что-то должно произойти. Вызывающая сторона, ветвящаяся по коду статуса, права в обоих случаях.
{ "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 -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 добавляется сверху и появляется только у переведённого сообщения: в этом ответе и в 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` | 422 | translate.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 в запросе.