Документация / REST API

REST API

REST API подходит, когда нужны шаблоны, защита от повторной отправки и разбор ответа в коде. База: https://api.fmailer.ru. Все запросы — POST с телом в JSON.

Логин и пароль можно получить на странице управления токенами домена.

Аутентификация

Отдельного заголовка авторизации нет: логин и пароль токена передаются в теле запроса, в объекте auth. Используется тот же токен, что и для SMTP.

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

POST/external/send_email_simple/

Тело запроса:

json
{
    "recipient": "client@example.ru",
    "subject": "Подтверждение заказа №4417",
    "body": "<p>Заказ принят. Доставка — завтра до 18:00.</p>",
    "text": "Заказ принят. Доставка — завтра до 18:00.",
    "sender": "Магазин <noreply@mail.example.ru>",
    "idempotency_key": "order-4417",
    "mass_mail": false,
    "tags": {
        "stream": "order",
        "lang": "ru"
    },
    "auth": {
        "username": "<логин токена>",
        "password": "<пароль токена>"
    }
}
ПолеОписание
recipientадрес получателя (один)
to, cc, bccмассивы адресов вместо recipient; каждый адрес — отдельное письмо
subjectтема письма
bodyHTML-версия письма
textтекстовая версия; необязательна — если не передать, соберём её из HTML
senderотправитель; домен должен быть подтверждён
idempotency_keyключ защиты от повторной отправки
mass_mailtrue для рассылок — добавляет заголовки отписки в один клик
attachmentsмассив файлов: filename и base64 в content
tagsваши метки: {"stream": "receipt"}; в письмо не попадают, но по ним можно фильтровать
authлогин и пароль токена

Текстовая версия

Письмо всегда уходит как multipart/alternative: HTML-часть и текстовая. body — это HTML, text — текстовая альтернатива. Если text не передан, мы соберём его из HTML сами: уберём теги, раскодируем сущности, вынесем адреса ссылок после их текста.

Передавайте text, когда важны формулировки. Автоматическая версия не знает, что в вёрстке было оформлением, и не увидит фразы, которую вы написали бы для читателя без стилей. Обе части уходят в любом случае, а почтовые провайдеры сравнивают их между собой при оценке письма. Пустая строка в text не отправляется — сработает версия из HTML.

Рассылки

Для маркетинговых писем ставьте mass_mail: true. Это добавляет заголовки отписки в один клик (List-Unsubscribe и List-Unsubscribe-Post), которых Apple, Gmail и Yahoo требуют от массовых отправителей: без них рассылку могут отклонить или отправить в спам.

Без флага мы определяем тип письма сами, но это эвристика — она может принять рассылку за транзакционное письмо. Флаг работает в одну сторону: true помечает письмо рассылкой, а false просто оставляет решение за нами и не отключает заголовки отписки.

Отправка по шаблону

POST/external/send_email_tpl/

Вместо темы и текста передаётся tpl — slug шаблона, созданного в панели, язык и параметры для подстановки:

json
{
    "recipient": "client@example.ru",
    "tpl": "order-confirmed",
    "lang": "ru",
    "sender": "Магазин <noreply@mail.example.ru>",
    "idempotency_key": "order-4417",
    "mass_mail": false,
    "params": {
        "order": "4417",
        "delivery": "завтра до 18:00"
    },
    "auth": {
        "username": "<логин токена>",
        "password": "<пароль токена>"
    }
}

Отправка до подтверждения домена

Чтобы увидеть первое письмо, DNS не нужен. У каждого аккаунта есть песочница — учётные данные на общем домене, которым владеем мы: настоящий DKIM, настоящая доставка, тот же API и тот же SMTP. Интеграцию можно собрать и проверить, пока кто-то ещё ищет доступ к вашей DNS-панели.

Отправлять можно только на адрес своего аккаунта — и это не ограничение, а смысл: общий домен, доступный любому зарегистрировавшемуся, иначе был бы открытым релеем. Адрес аккаунта должен быть подтверждён, и есть небольшой суточный лимит. Всё остальное работает как обычно: те же вебхуки, тот же журнал событий, та же история доставки.

Когда понадобится писать кому-то ещё, добавьте свой домен и подтвердите его. В интеграции изменятся только учётные данные и адрес в From.

Несколько получателей

recipient — это один адрес. to, cc и bcc принимают массивы и являются той же самой формой во множественном числе: используйте либо одно написание, либо другое, но не оба сразу. Они работают во всех методах на этой странице, включая каждый элемент пакета.

Каждый адрес — отдельное письмо. Отправка на двух получателей в to, одного в cc и одного в bcc — это четыре записи в логе, четыре Message-ID, четыре вебхука доставки и четыре письма в счёт лимитов тарифа. Ровно так же всегда работала отправка по SMTP с несколькими получателями в одной транзакции, и именно это делает возможными отписки, повторы и статистику по каждому получателю отдельно.

Заголовки при этом одинаковые во всех копиях: весь список to, весь список cc и никогда — Bcc. Скрытый получатель — это адрес, которого нет ни в одном заголовке письма; прочитать его можно только в массиве emails ответа, поэтому там и есть поля recipient и kind. Адрес, названный дважды — в to и в cc, — это одно письмо: побеждает первое упоминание.

До 100 получателей на письмо — тот же потолок, что и на SMTP. Если задан idempotency_key, первый получатель сохраняет ваш ключ как есть, а каждая следующая копия получает производный (ваш-ключ#…), поэтому повтор запроса воспроизводит все копии, а не только первую. Производные ключи строим мы — вам их составлять не нужно.

Вложения

attachments — массив файлов: filename и байты в base64 в поле content. content_type необязателен: мы определим его по имени файла, иначе поставим application/octet-stream. Переносы строк внутри content допустимы — именно так их расставляет любой кодировщик base64. Письмо уходит как multipart/mixed, внутри которого остаётся обычная пара HTML + текст, поэтому читатель получает и письмо, и файл.

До 20 файлов, по 10 МБ каждый и 10 МБ суммарно на письмо; всё тело запроса вместе с base64 — не больше 16 МБ. Помните, что base64 добавляет примерно треть к реальному размеру файла.

Вложение хранится и отправляется отдельно для каждого получателя — одна запись, одно письмо, одна копия файла. PDF на 5 МБ для двадцати человек — это 100 МБ, и на это произведение тоже есть потолок. Если упёрлись в него, дайте ссылку: письма такого размера всё равно отклоняет или обрезает изрядная часть почтовых служб.

Исполняемые типы файлов мы отклоняем — и по расширению, и по заявленному content_type: .exe, .msi, .jar, .js, .vbs, .bat, .scr, .lnk, .iso и остальные из опубликованного списка Gmail. Именно этот список и применяется к вашей почте на приёме, поэтому отказ при отправке — это разница между понятной ошибкой и отказом, который засчитается домену. Архивы разрешены: заглянуть внутрь мы не можем, а получатель — может.

Приложить файл по URL нельзя. Это означало бы, что наш сервер открывает соединение к хосту, который назвал держатель отправляющего токена, — и сделать это безопасно куда сложнее, чем кажется. Если файл у вас есть, отправьте его.

Метки

tags — объект ваших собственных меток на письме: {"stream": "password-reset", "lang": "en"}. Они отвечают на вопрос, на который не отвечает ничто другое в письме: к какому потоку оно относится. method говорит, как письмо отправлено, mass_mail — рассылка это или нет; ни то, ни другое не отличит сброс пароля от чека и от счёта, потому что вашу классификацию знаете только вы.

Имя метки — от 1 до 40 символов: латиница, цифры, подчёркивание и дефис. Значение — произвольный текст до 256 символов, двоеточия допустимы. До 10 меток на письмо.

В письмо метки не попадают. Это не заголовок: показывать вашу внутреннюю классификацию вашим же получателям, класть её под подпись DKIM и под оценку принимающей стороны — не то, о чём вы просили. Если метка нужна именно в письме, для этого остаются заголовки X-.

На нашей стороне их никто не читает. Доставка, throttling, репутация и антиспам к меткам слепы — и это намеренно: значение, которое выбирает отправитель, не должно влиять на то, как обходятся с его почтой.

Нужны они для чтения почты обратно:

  • GET /api/emails/emails/?tag=stream:password-reset и тот же параметр у /api/emails/logs/. Повторите параметр, чтобы добавить условие — ?tag=stream:receipt&tag=lang:en это и, а не или; ?tag=stream без значения означает «есть такая метка».
  • tags в каждом вебхуке доставки — обработчик маршрутизирует событие, не запрашивая письмо.
  • Массив tags в отчёте аналитики и фильтр tags в запросе к нему, сужающий все числа на странице. Так и задаётся вопрос «как за неделю отработал поток password-reset».

Пакетная отправка

POST/external/send_email_batch/

До 100 писем в одном запросе. Объект auth переезжает на верхний уровень и читается один раз на всю пачку, всё остальное — по письму, в массиве emails. Письмо в пачке — это либо обычное (subject и body), либо шаблонное (tpl), и их можно смешивать: рассылка на получателей с разными языками — это свой tpl и lang в каждом элементе.

json
{
    "auth": {
        "username": "<логин токена>",
        "password": "<пароль токена>"
    },
    "emails": [
        {
            "recipient": "first@example.ru",
            "subject": "Заказ принят",
            "body": "<p>Доставка — завтра до 18:00.</p>",
            "sender": "Магазин <noreply@mail.example.ru>",
            "idempotency_key": "order-4417"
        },
        {
            "recipient": "second@example.ru",
            "tpl": "order-confirmed",
            "lang": "ru",
            "params": {
                "order": "4418"
            },
            "sender": "Магазин <noreply@mail.example.ru>",
            "idempotency_key": "order-4418"
        }
    ]
}

Ошибка в одном письме отклоняет всю пачкуHTTP 400 с индексом проблемного элемента. Так же отклоняется пачка, которая не помещается в остаток часового или месячного лимита тарифа: тот же Hourly limit exceeded / Monthly limit exceeded, что и при обычной отправке, вместо наполовину ушедшей рассылки.

Ответ плоский и упорядоченный: по одному элементу на письмо, в том порядке, в котором перечислены элементы и их получатели. В каждом — message_id, который потом придёт в вебхуках доставки. Элемент с cc или bcc — это несколько писем, поэтому и элементов ответа будет несколько: различайте их по recipient и kind. Потолок пачки считает письма, а не элементы — по той же причине, по которой их считает тариф.

json
{
    "ok": true,
    "accepted": 2,
    "rejected": 0,
    "emails": [
        {
            "ok": true,
            "message_id": "<uuid@mail.example.ru>",
            "uuid": "uuid",
            "recipient": "first@example.ru",
            "kind": "to",
            "idempotency_key": "order-4417",
            "replayed": false
        },
        {
            "ok": true,
            "message_id": "<uuid@mail.example.ru>",
            "uuid": "uuid",
            "recipient": "second@example.ru",
            "kind": "to",
            "idempotency_key": "order-4418",
            "replayed": false
        }
    ]
}

Ставьте idempotency_key каждому письму. Это единственный способ безопасно повторить пачку: ключи привязаны к письму, поэтому повтор всего запроса после обрыва связи вернёт уже принятые письма (replayed: true, без второй отправки и без повторного расхода лимитов) и поставит в очередь только те, что не дошли. Два письма в одной пачке не могут иметь одинаковый ключ — иначе вместо двух писем молча ушло бы одно.

Ответ

Успешная отправка:

json
{
    "ok": true,
    "emails": [
        {
            "ok": true,
            "message_id": "<uuid@mail.example.ru>",
            "uuid": "uuid",
            "recipient": "client@example.ru",
            "kind": "to",
            "idempotency_key": "order-4417",
            "replayed": false
        }
    ]
}

Ошибка — ok будет false, а причина в полях ошибки:

json
{
    "ok": false,
    "error_code": "код ошибки",
    "error": "описание ошибки"
}

Чтение отправленного

Тот же токен, которым вы отправляете, умеет читать. Пять GET-методов, все ограничены одним доменом — тем, которому принадлежит токен. Параметра domain здесь нет и быть не может.

МетодНа какой вопрос отвечает
GET /external/emails/Что я отправил и чем каждое письмо закончилось.
GET /external/emails/<uuid>/Одно письмо, вместе с ушедшим HTML.
GET /external/emails/<uuid>/events/Всё, что с ним происходило, от старого к новому.
GET /external/events/Весь поток событий домена — то, что опрашивают вместо вебхуков.
GET /external/stats/Итоги, доли и разбивка по меткам за период.

Аутентификация — HTTP Basic, тем же логином и паролем токена, что и SMTP:

bash
curl https://api.fmailer.ru/external/emails/ \
  -u "$TOKEN_USERNAME:$TOKEN_PASSWORD"

curl "https://api.fmailer.ru/external/stats/?start=2026-08-01&end=2026-08-28" \
  -u "$TOKEN_USERNAME:$TOKEN_PASSWORD"

Basic, а не bearer, потому что это ровно то, чем credential и является — логин и пароль. Отсюда и следствие: этот токен открывает /external/ и больше ничего. Это не ключ от аккаунта: домены, биллинг и команда ему недоступны.

Списки постраничные и принимают ожидаемые фильтры: status, method, message_id, recipient, subject, created_at_after / created_at_before и те же ?tag=. У /external/stats/start, end и снова ?tag=.

Два ограничения. Методы событий — платная возможность тарифа, та же, что показывает отложенные доставки в панели; без неё ответ HTTP 403, при этом список писем не ограничен. И история хранится ровно столько, сколько предусмотрено тарифом: старые записи удаляются, а /external/stats/ не отказывает в слишком длинном периоде, а обрезает его и возвращает использованный в window_days.

Настройки подписки и темы

Ссылка отписки в письме теперь открывает центр настроек на вашем собственном хосте stats. — с вашим логотипом, названием компании и фирменным цветом. Ни страница, ни её адрес нас не называют. Особенно это важно для white-label доменов, где наш хост в теле письма был ровно той утечкой, ради предотвращения которой всё и делается.

Заведите темы — «Новости продукта», «Статусы заказов» — в настройках отписки домена и укажите одну при отправке: topic: "product-news". Получатель, отказавшийся от темы, перестаёт получать её и продолжает получать всё остальное, включая письма со сбросом пароля. Если тем нет, страница остаётся такой же, какой была: одна кнопка, отключающая всё.

Две особенности, которые стоит учесть:

  • Письмо без topic останавливает только полная отписка. Именно так работают все письма, отправленные до сегодняшнего дня, и это не меняется.
  • Неизвестный slug темы — это ошибка 400, а не тихое игнорирование. Иначе рассылка ушла бы ровно тем, кто просил её не присылать.

Slug темы нельзя изменить после создания — на него уже ссылаются записанные отказы. Чтобы вывести тему из обращения, отключите её: отказы, записанные по ней, продолжают действовать. А отписка в один клик, которую показывают Gmail и Apple, всегда означает отказ от всего: интерфейса у неё нет, и ничего более узкого она выразить не может.

Отслеживание кликов

Если включить для домена отслеживание ссылок, каждый <a href> в отправляемом HTML переписывается через ваш собственный хост stats.. Переход по такой ссылке записывается как клик, после чего идёт редирект на настоящий адрес.

По умолчанию выключено, в отличие от отслеживания открытий. Пиксель добавляет невидимую картинку, а это меняет то, что видит получатель при наведении на ссылку, и то, что запрашивает корпоративный сканер ссылок, — такое включают сами, а не за вас.

Ссылку отписки мы не переписываем никогда. Это не мелочь: шлюз безопасности, который заранее открывает все ссылки в письме, иначе отписал бы получателя до того, как тот его прочитал, — а поскольку подавление действует на адрес целиком, вместе с рассылками перестали бы приходить и письма со сбросом пароля. mailto:, tel: и якоря внутри страницы тоже не трогаем.

Машинный запрос — сканер, прокси приватности — получает редирект, но не считается кликом: используется та же классификация, что и для пикселя открытий. Клик при этом отмечает письмо как открытое: это более веское доказательство, чем пиксель, и так статистика открытий продолжает работать у клиентов, блокирующих картинки.

Что вы получаете:

  • clicked_at у письма — момент первого перехода.
  • Событие clicked в журнале на каждый клик, с адресом ссылки: повторы тоже, ведь вопрос звучит как «по какой ссылке и сколько раз».
  • Вебхук clicked с адресом в data.
  • Отчёт по кликнутым ссылкам в разбивке аналитики.

Одно ограничение: переписывание ссылок работает для писем, отправленных через этот API, а не через SMTP. По SMTP приходит уже готовый MIME, части которого могут быть закодированы, и правка адресов внутри них — верный способ испортить тело письма.

Ссылка на одно письмо

Когда спрашивают «а что вы мне вообще прислали», откройте письмо в панели и создайте ссылку на письмо: публичный адрес, по которому открывается ровно это письмо — его можно вставить в ответ клиенту.

Ссылку нельзя подобрать, у неё есть срок (по умолчанию 7 дней, максимум 30) и её можно отозвать в любой момент; запись о том, кто её создал и сколько раз открывали, остаётся и после отзыва. Сама страница нарочно простая: письмо, кому и когда оно ушло, и никакого брендирования.

Адрес ссылки — на вашем собственном хосте stats., том же, что использует пиксель и ссылка отписки, так что пересылаемая ссылка не несёт наших названий. Отсюда и единственное условие: если DNS-запись stats так и не опубликована, страницу негде обслуживать, и мы откажем в создании ссылки, а не выдадим неработающую.

Считайте живую ссылку публичной: пока не истёк срок, письмо целиком прочитает любой, у кого она есть. Это особенно важно для писем с одноразовыми ссылками и кодами — такие лучше отзывать сразу после разговора.

Лимиты и отказы

Два отказа стоит обрабатывать отдельно. При превышении часового или месячного объёма тарифа запрос отклоняется с HTTP 400 и текстом Hourly limit exceeded либо Monthly limit exceeded — такой запрос имеет смысл повторить с выдержкой. Если отправка с домена остановлена, ответ будет HTTP 403 с постоянным полем code = domain_banned и причиной в reason; повторы здесь не помогут.

Ни то ни другое не происходит, когда вы отправляете быстрее разгона домена: такие письма принимаются как обычно и уходят чуть позже. См. лимиты отправки и защиту от спама.

Идемпотентность

idempotency_key — ваш собственный идентификатор события, например номер заказа. Повторный запрос с тем же ключом не создаёт второе письмо, поэтому ретраи при обрыве связи безопасны. Ключ должен быть уникальным для каждого письма, которое действительно нужно отправить.