Транзакционные и массовые письма из вашего кода плюс верификация адресов. Все запросы — JSON поверх HTTPS, ответы — JSON.
Базовый адрес всех методов:
https://api.mailermail.ru/v1
Контракт построен на привычной модели транзакционных Email API (from/to/subject/html/text/attachments) — миграция с типового провайдера во многом сводится к смене хоста и токена; отличия, которые легко пропустить, собраны в разделе «Переезд с UniOne». Машиночитаемая спецификация — openapi.yaml (OpenAPI 3.0), интерактивная версия — Swagger UI.
Метод GET /health не требует токена и отвечает {"ok":true} — удобно для мониторинга.
curl https://api.mailermail.ru/v1/health{
"ok": true,
"ts": "2026-08-03T12:00:00+03:00"
}Все методы, кроме /health и /unsubscribe, требуют токен. Передайте его в одном из заголовков:
Api-Token: mm_live_…Authorization: Bearer mm_live_…Если переданы оба заголовка, используется Api-Token.
Формат токена — mm_live_ или mm_test_ и затем от 16 до 64 латинских букв и цифр (регулярное выражение ^mm_(live|test)_[A-Za-z0-9]{16,64}$). Токен другого вида отбивается 401 ещё до поиска в базе.
Токены выпускаются в кабинете (Настройки → API). Каждый токен несёт scope:
verify — методы верификации (/verify, /verify/batch);send — всё остальное (отправка, шаблоны, стоп-лист, логи, вебхуки, домены).401 | Токена нет, он не того формата или неизвестен. |
403 | Токен отозван либо у него нет нужного scope. |
Токен с префиксом mm_test_ работает в песочнице — письма не уходят, подробности в разделе «Песочница».
curl https://api.mailermail.ru/v1/stats \
-H "Api-Token: mm_live_9f3k…"Успех — HTTP 2xx и "success": true. Ошибка — соответствующий код и тело {"success": false, "errors": ["…"]}. Массив errors содержит человекочитаемые сообщения (в основном на английском); разбирать их текст в коде не стоит — он может меняться, опирайтесь на HTTP-код.
Ошибка валидации отклоняет запрос целиком: если в /send один адрес из to[] некорректен, не уходит ни одно письмо.
| Код | Значение |
|---|---|
| 200 | Запрос обработан. Для /send неудачи по отдельным получателям — в skipped/failed, код всё равно 200 (подробно — «Ответ /send»). |
| 201 | Создан шаблон или вебхук. |
| 400 | Тело не является JSON-объектом. |
| 401 | Токена нет, он не того формата или неизвестен. |
| 403 | Токен отозван либо у него нет нужного scope. |
| 404 | Нет такого эндпоинта или ресурса (шаблон, сообщение, вебхук, домен). |
| 405 | Метод не поддерживается этим адресом; допустимые перечислены в заголовке Allow. |
| 409 | Запрос с этим Idempotency-Key ещё выполняется; слаг шаблона занят; шаблон занят идущей рассылкой. |
| 413 | Тело или вложения превышают лимит. |
| 422 | Ошибка валидации полей; в том числе Idempotency-Key длиннее 200 символов или уже использованный на другом эндпоинте. |
| 429 | Превышен лимит запросов. Заголовок Retry-After — секунд до сброса. |
| 500 | Внутренняя ошибка записи (шаблон сохранён, но не опубликован; вебхук не создан). Текст ошибки говорит, что повторить. |
| 502 | /send: не ушло ни одно письмо и нет пропущенных адресов. Повтор уместен, если причина временная (таймаут); при постоянном отказе реле он ничего не даст. |
| 503 | БД недоступна, нет активного SMTP-реле или часть API ещё не развёрнута. |
{
"success": false,
"errors": [
"Field 'to' (non-empty array) is required."
]
}На каждый токен — 120 запросов в минуту. Минута календарная (с :00 до :59 по часам сервера), а не скользящая: 120 запросов в конце одной минуты и 120 в начале следующей пройдут оба.
Считается каждый запрос, прошедший проверку токена и scope, — включая ответы 4xx на ошибки валидации и сами ответы 429. Не считаются /health, /unsubscribe, трекинг и запросы с неверным токеном. При превышении метод отвечает 429 с заголовком Retry-After — число секунд до конца текущей минуты (от 1 до 60).
Лимит рассчитан на пакетную работу, а не на поток по одному письму: 120 запросов в минуту — это до 6000 писем через /send (по 50 получателей) и до 120 000 через /batch.
Отдельные лимиты содержимого:
/send — до 50 получателей, вложения ≤10 МБ суммарно, тело ≤15 МБ;/batch — до 500 request'ов, до 1000 писем суммарно;/verify/batch — до 100 адресов.Retry-After: 17
{
"success": false,
"errors": [
"Rate limit exceeded: 120 requests/minute."
]
}Методы /send и /batch принимают заголовок Idempotency-Key (произвольная строка ≤200 символов, длиннее — 422). Это защищает от двойной отправки при сетевых ретраях. Ключ действует в пределах токена: у двух токенов одинаковые ключи не пересекаются.
Idempotency-Replayed: true. Тело повтора с первым не сравнивается.4xx: если первый запрос получил 422, исправленный запрос с тем же ключом снова получит тот же 422. Исправили тело — берите новый ключ. Исключения — ответы, которые отбиваются раньше, чем ключ запоминается: 400 (тело не JSON), 413 за тело больше 15 МБ и 429.5xx не сохраняются, включая 502 «не ушло ни одно письмо»: повтор с тем же ключом действительно повторит отправку.409. Запрос, зависший больше 5 минут, считается брошенным, и ключ можно использовать снова./send, потом /batch) — 422.failed, повтор с тем же ключом вернёт тот же ответ, ничего не отправив, — дошлите эти три адреса новым запросом с новым ключом.curl -X POST https://api.mailermail.ru/v1/send \
-H "Api-Token: mm_live_9f3k…" \
-H "Idempotency-Key: order-4211-confirm" \
-H "Content-Type: application/json" \
-d '{ … }'Idempotency-Replayed: true
{
"success": true,
"message_ids": ["0b6f7a52-3c1e-4d8a-9f2b-6e1d0c4a7b93"]
}Токен с префиксом mm_test_ прогоняет /send и /batch через весь конвейер, но письма физически не уходят. Остальные методы (шаблоны, стоп-лист, вебхуки, домены) с таким токеном работают по-настоящему — это те же данные аккаунта.
422 в /send и ошибка в responses[i] в /batch.skipped, как на бою.message_id и попадают в лог со статусом sandbox (GET /messages?status=sandbox)."sandbox": true.email.sent, ни открытий, ни переходов: вебхуки песочницей не проверить.GET /stats → usage.sends) не идёт.Idempotency-Key действуют как обычно.{
"success": true,
"message_ids": ["5d2c9e10-7a4b-4f3e-8c21-0e9b6a1d4f77"],
"sandbox": true,
"skipped": [
{"email": "blocked@example.ru", "reason": "suppressed"}
]
}Синхронная транзакционная отправка: письмо уходит за секунды, у каждого получателя — своё отдельное письмо и свой message_id. Требует scope send. Домен отправителя должен быть подтверждён в аккаунте (иначе 422) — он же даёт DKIM-подпись.
Задайте subject + html и/или text, либо шаблон через template_id — тогда тему и тело берёт шаблон. Переменные {{var}} подставляются из template_variables и to[].substitutions — правила в разделе «Переменные».
| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
from | object | да | Отправитель: {email, name?}. Домен — verified. name ≤128 символов. |
to | array | да | Получатели: email, name?, substitutions?; ≤50. Дубли адресов (без учёта регистра) отбрасываются молча — остаётся первое вхождение, но в лимит 50 дубли считаются. |
reply_to | object | нет | Адрес для ответа {email, name?}. |
subject | string | — | Тема, ≤255 (длиннее обрезается, переводы строк заменяются пробелами). Обязательна, когда шаблон не задан. |
html | string | — | HTML-тело. Нужно html и/или text. |
text | string | — | Текстовое тело. Если не задано — генерируется из HTML. |
category | string | нет | Метка для группировки в статистике и в событиях, ≤255. С шаблоном и без category берётся категория шаблона. |
attachments | array | нет | Поля: content (base64, ≤10 МБ суммарно после декодирования), filename, type? (вида application/pdf, иначе application/octet-stream), disposition?, content_id?. Значение inline в disposition требует content_id (иначе 422) — в теле на картинку ссылаются через cid:, например src="cid:logo". |
template_id | string | нет | Шаблон по UUID или по слагу. См. «Шаблон» ниже. |
template_uuid | string | нет | То же, но строго UUID. Поле оставлено для действующих интеграций. |
template_variables | object | нет | Переменные {{var}} для всех получателей. Значения — скаляры. |
headers | object | нет | Свои заголовки письма, до 20. См. «Заголовки и метаданные». |
custom_variables | object | нет | Метаданные письма, до 10 ключей. В письмо не попадают — возвращаются в событиях вебхука. См. «Заголовки и метаданные». |
options | object | нет | Флаги отправки, см. таблицу ниже. |
| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
list_unsubscribe | bool | нет | Заголовки One-Click отписки со ссылкой MailerMail. По умолчанию выключено. См. «Отписка в письме». |
unsubscribe_url | string | нет | Свой https-адрес отписки в заголовках письма, ≤300 символов после https://. |
track_read | bool | нет | Пиксель открытия. На /send по умолчанию выключен. См. «Трекинг». |
track_links | bool | нет | Оборачивать ссылки для учёта переходов. На /send по умолчанию выключено. |
template_id принимает UUID или слаг шаблона (слаг — [a-z0-9-], до 64 символов, строчными). template_uuid — только UUID.template_id и template_uuid вместе — 422.subject, html и text — 422: тему и тело даёт шаблон.422 с просьбой опубликовать; неизвестный шаблон — тоже 422. У опубликованной версии нет темы — 422.category в запросе берётся категория шаблона.{{…}} шаблона уйдут получателю как есть — см. «Переменные».cc, bcc, metadata и опечатки в именах полей не вызовут 422 — просто ничего не сделают.cc/bcc нет: каждый адрес из to[] получает отдельное письмо, где в To стоит только он. Заголовки Cc и Bcc через headers задать нельзя.options.send_at на /send — 422: отложенная отправка есть только в /batch.curl -X POST https://api.mailermail.ru/v1/send \
-H "Api-Token: mm_live_9f3k…" \
-H "Content-Type: application/json" \
-d '{
"from": {"email": "no-reply@shop.ru", "name": "Shop"},
"to": [{"email": "ivan@example.ru"}],
"subject": "Заказ №4211 собран",
"html": "<h1>Спасибо за заказ!</h1>"
}'{
"success": true,
"message_ids": ["0b6f7a52-3c1e-4d8a-9f2b-6e1d0c4a7b93"]
}{
"from": {"email": "robot@shop.ru", "name": "Shop"},
"to": [
{"email": "ivan@example.ru",
"substitutions": {"name": "Иван"}}
],
"subject": "Заказ №{{order}} собран",
"html": "<p>{{name}}, заказ №{{order}} ждёт вас.</p>",
"template_variables": {"order": "4211"},
"category": "orders",
"custom_variables": {"order_id": "4211", "user_id": "77"},
"headers": {"X-Order-Id": "4211"},
"options": {"list_unsubscribe": true}
}Каждый уникальный адрес из to[] получает ровно один исход:
| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
message_ids | array | — | UUID писем, которые ушли (или прошли песочницу). Всегда присутствует, может быть пустым. |
skipped | array | — | {email, reason} — адреса, на которые письмо не отправлялось. Ключа нет, если пропущенных нет. |
failed | array | — | {email, reason} — адреса, которые отправить не удалось. Ключа нет, если неудач нет. |
sandbox | bool | — | Только у токенов mm_test_, всегда true. |
message_id получают только ушедшие письма; пропущенные и неудачные адреса его не получают. Поэтому message_ids[i] — это не to[i], как только хоть один адрес попал в skipped или failed или в to[] был дубль.
Порядок ушедших при этом сохраняется. Сопоставить можно так: уберите из to[] дубли (без учёта регистра, остаётся первое вхождение), затем адреса из skipped[] и failed[] — оставшиеся адреса по порядку соответствуют message_ids. Если нужен надёжный ключ «письмо ↔ ваша запись», передайте его в custom_variables — он вернётся в каждом событии письма — или отправляйте по одному получателю на запрос.
suppressed | Адрес в стоп-листе вашего аккаунта. Настоящую причину (отписка, жалоба, отказ, ручное добавление) покажет GET /suppressions?email=…; снять — DELETE /suppressions. |
known_bounce | Адрес в общем реестре недоставляемых MailerMail: за последние 24 месяца почтовый сервер отвечал, что такого ящика нет. Через API не снимается; если адрес снова жив, верните его в кабинете: Контакты → Заблокированные. Сейчас такой возврат действует только на /send: /batch продолжает пропускать адрес с known_bounce. |
Если адрес подходит под обе причины, в ответе будет suppressed.
reason — произвольный текст для человека, не перечисление:
rendered body over 3 MB — после подстановки переменных HTML и текст вместе больше 3 МБ;timeout: send budget exceeded, retry these recipients — на весь запрос отводится 60 секунд, и на медленных реле до части адресов очередь не дошла: письма им не отправлялись, их можно слать повторно;SMTP Error: data not accepted. Это ответ нашего реле, а не сервера получателя: /send передаёт письмо реле, а отказ почтового ящика (переполнен, не существует) приходит позже и в failed[] не попадает.| HTTP | success | Исход |
|---|---|---|
| 200 | true | все письма ушли |
| 200 | true | часть ушла, часть в skipped/failed |
| 200 | true | все адреса пропущены: пустой message_ids и skipped |
| 200 | true | ничего не ушло, но есть и failed, и skipped |
| 502 | false | ничего не ушло, все адреса в failed, пропущенных нет |
| 200 | true | песочница, в ответе есть sandbox |
| 4xx | false | ошибка валидации — не ушло ничего, текст в errors |
| 503 | false | нет активного SMTP-реле |
"success": true значит «запрос обработан», а не «все письма ушли»: проверяйте failed и skipped всегда. После 502 и 503 письма не ушли и ответ не сохранён под Idempotency-Key, поэтому весь запрос можно повторить как есть, с тем же ключом. Если в 502 причина — постоянный отказ реле (код 5xx в reason), повтор не поможет.
message_id ушедшего письма совпадает с message_id в его событиях вебхука и в GET /messages/{uuid}. У пропущенных и неудачных адресов тоже есть свои записи в логе и события email.suppressed/email.failed, но их id в ответе /send не приходит.
{
"success": true,
"message_ids": [
"0b6f7a52-3c1e-4d8a-9f2b-6e1d0c4a7b93",
"8e3a1f04-52d7-4b6c-a0e9-3f7c2d19b845"
],
"skipped": [
{"email": "old@dead.ru", "reason": "known_bounce"},
{"email": "gone@example.ru", "reason": "suppressed"}
],
"failed": [
{"email": "full@example.ru",
"reason": "SMTP Error: data not accepted."}
]
}{
"success": true,
"message_ids": [],
"skipped": [
{"email": "gone@example.ru", "reason": "suppressed"}
]
}{
"success": false,
"message_ids": [],
"failed": [
{"email": "ivan@example.ru",
"reason": "timeout: send budget exceeded, retry these recipients"}
]
}Плейсхолдер {{name}} в subject, html и text заменяется значением переменной. Работает одинаково в письме из шаблона и в письме, собранном прямо в запросе: шаблон не обязателен.
[A-Za-z0-9_.], от 1 до 64 символов; пробелы внутри скобок допустимы ({{ name }}). Плейсхолдер с другим именем ({{Имя}}, {{user-name}}) не распознаётся и остаётся в письме как есть.template_variables — для всех получателей, to[].substitutions — для одного. Значение получателя перекрывает общее; null в substitutions не перекрывает.true/false, null. Объект или массив — 422. true подставится как 1, false и null — как пустая строка. Значение ≤64 КБ, все значения одного словаря вместе ≤256 КБ.htmlspecialchars), иначе < и & сломают вёрстку.Плейсхолдеры обрабатываются, только если в запросе есть хотя бы одна переменная — непустой template_variables или непустой substitutions у кого-то из получателей. Без переменных все {{…}} уходят получателю дословно, в том числе из шаблона: это сделано, чтобы не портить тела, где двойные скобки — часть текста.
Если переменные переданы, а для какого-то плейсхолдера значения нет, он превращается в пустую строку, без ошибки и без предупреждения. Опечатка в имени ({{nmae}}) даёт «Здравствуйте, !». Проверяйте, что передаёте все переменные шаблона.
При переезде с UniOne это ловушка: там неизвестная переменная остаётся в письме как есть, и опечатку видно в тестовом письме; здесь она молча исчезает.
Служебных переменных нет. {{unsubscribe_url}}, {{email}} и подобные — обычные имена: без своего значения они станут пустой строкой. Ссылку отписки в тело нужно вставлять самим — см. «Отписка в письме».
{
"from": {"email": "robot@shop.ru"},
"to": [
{"email": "anna@example.ru",
"substitutions": {"name": "Анна", "bonus": 500}},
{"email": "boris@example.ru",
"substitutions": {"name": "Борис"}}
],
"subject": "{{name}}, у вас {{bonus}} бонусов",
"text": "Бонусы сгорают {{date}}. {{promo}}",
"template_variables": {"bonus": 100, "date": "01.10"}
}anna@example.ru
Тема: Анна, у вас 500 бонусов
Текст: Бонусы сгорают 01.10.
boris@example.ru
Тема: Борис, у вас 100 бонусов
Текст: Бонусы сгорают 01.10.
{{promo}} не передан ни разу — пустая строка.Произвольные пары «ключ — значение», которые не попадают в само письмо, а возвращаются в data.custom_variables событий вебхука. Это главный способ связать событие с записью в вашей системе: положите сюда id заказа или пользователя.
_, ., -, от 1 до 64 символов; ключ из одних цифр ("123") не принимается.42 → "42", true → "true", false → "false", null → "". Объект или массив — 422./send метаданные одинаковые. Разные метаданные на разных получателей — разными запросами или через requests[] в /batch.Возвращаются в событиях email.sent, email.failed, email.suppressed, email.opened, email.clicked. Не возвращаются в email.bounced и email.unsubscribed: эти события подбирают письмо по адресу получателя, а не по самому письму (см. «Типы событий»).
true/false тоже примутся и станут "1"/""; null — 422), без переводов строк, до 900 байт. Все заголовки вместе в виде JSON — не больше ~1900 байт.422 (регистр не важен):
From, To, Cc, Bcc, Subject, Reply-To, Sender, Return-Path, Message-ID, Date, MIME-Version, Content-Type, Content-Transfer-Encoding, Content-Disposition, DKIM-Signature, DomainKey-Signature, Received, X-Mailer, List-Unsubscribe, List-Unsubscribe-Post, List-Id, List-Post, List-Help, List-Owner, List-Archive, Feedback-ID, Disposition-Notification-To, Return-Receipt-To, Errors-To, Read-Receipt-To, Resent-From, Resent-To, Resent-Cc, Resent-Sender, Resent-Date, Resent-Message-ID.Отписку в заголовках включают не через headers, а через options.list_unsubscribe или options.unsubscribe_url.
{
"…": "…",
"custom_variables": {
"order_id": 4211,
"vip": true,
"coupon": null
},
"headers": {
"X-Order-Id": "4211",
"X-Entity-Ref-ID": "order-4211"
}
}"data": {
"custom_variables": {
"order_id": "4211",
"vip": "true",
"coupon": ""
}
}{
"success": false,
"errors": ["header 'List-Id' is reserved."]
}Кнопку «Отписаться» почтовые клиенты (Gmail, Яндекс, Mail.ru) рисуют по заголовкам List-Unsubscribe и List-Unsubscribe-Post (RFC 8058, One-Click). На /send заголовки по умолчанию не ставятся; включить их можно двумя способами — они по-разному решают, кто узнаёт об отписке.
https://mailermail.ru/api/v1/u/{token}. Токен один на пару «аккаунт — адрес»: у всех писем одному человеку ссылка одинаковая.POST от почтового клиента) обрабатывает MailerMail: адрес уходит в стоп-лист с причиной unsubscribed, приходит событие email.unsubscribed, а контакт с этим адресом в кабинете помечается отписавшимся./send и /batch пропускают этот адрес (skipped, suppressed). Вернуть — DELETE /suppressions.GET по этой ссылке (человек открыл её в браузере) показывает страницу с кнопкой подтверждения и сам по себе ничего не меняет: по ссылкам ходят сканеры почтовых шлюзов.List-Unsubscribe: <ваш URL> и List-Unsubscribe-Post: List-Unsubscribe=One-Click. Почтовый клиент шлёт POST на ваш URL с телом List-Unsubscribe=One-Click — ваш обработчик должен его принять.POST /suppressions с reason: "unsubscribed").list_unsubscribe при этом передавать не нужно — unsubscribe_url включает заголовки сам.https://, после схемы — от 5 до 300 байт без пробельных символов, двойных кавычек и угловых скобок, иначе 422. mailto: не поддерживается. Используйте только ASCII: кириллицу в адресе кодируйте (%D0%…), иначе заголовок уйдёт закодированным и почтовики его не распознают.Оба способа ставят только заголовки. Ссылки отписки в тексте письма MailerMail не добавляет, плейсхолдера {{unsubscribe_url}} нет — если передаёте переменные, он превратится в пустую строку, если нет — останется в письме дословно. Для массовых писем ссылку в теле ставьте сами, на свою страницу.
В /batch заголовки со ссылкой MailerMail включены по умолчанию и выключаются options.skip_unsubscribe: true; своего unsubscribe_url там нет.
List-Unsubscribe: <https://mailermail.ru/api/v1/u/3fa9c2e17b04d6a8>
List-Unsubscribe-Post: List-Unsubscribe=One-ClickList-Unsubscribe: <https://shop.ru/unsub?u=77&sig=9c1e>
List-Unsubscribe-Post: List-Unsubscribe=One-ClickPOST /unsub?u=77&sig=9c1e HTTP/1.1
Host: shop.ru
Content-Type: application/x-www-form-urlencoded
List-Unsubscribe=One-ClickПостановка партии писем в очередь: message_ids выдаются сразу, реально отправляет фоновый воркер (латентность до ~5 минут). Объект base задаёт общие поля, поля каждого request их перекрывают. Вложения не поддерживаются — используйте /send.
HTTP всегда 200 (кроме структурных ошибок тела): пер-письмо статусы — в массиве responses[] в порядке requests.
| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
base | object | нет | Общие поля письма (как в /send, без attachments). |
requests | array | да | Массив писем (поля /send), ≤500; суммарно ≤1000 писем. |
options.send_at | string | нет | Отложить: YYYY-MM-DD HH:MM:SS UTC, окно ≤72 ч. |
options.skip_unsubscribe | bool | нет | Отключить List-Unsubscribe (в bulk он включён по умолчанию). |
curl -X POST https://api.mailermail.ru/v1/batch \
-H "Api-Token: mm_live_9f3k…" \
-H "Content-Type: application/json" \
-d '{
"base": {
"from": {"email": "news@shop.ru"},
"subject": "Скидки недели",
"html": "<p>Привет, {{name}}!</p>"
},
"requests": [
{"to": [{"email": "a@ex.ru", "substitutions": {"name": "Анна"}}]},
{"to": [{"email": "b@ex.ru", "substitutions": {"name": "Борис"}}]}
]
}'{
"success": true,
"responses": [
{"success": true, "message_ids": ["…"]},
{"success": true, "message_ids": ["…"]}
]
}Отправлять можно только с подтверждённого домена: если домен из from не проверен, /send отвечает 422, а /batch — ошибкой в responses[i] для каждого такого письма. Это защита от подделки отправителя и требование почтовых провайдеров — без SPF и DKIM письма уходят в спам.
Добавление и проверка живут в кабинете намеренно: это разовое действие человека с доступом к DNS, а не то, что автоматизируют из кода. Через API домены доступны только на чтение — чтобы интеграция могла убедиться, что домен готов, до отправки, а не ловить 422 в бою.
| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
verified | статус | — | Все три записи на месте — можно отправлять. |
pending_dns | статус | — | Домен добавлен, записи ещё не найдены. |
failed | статус | — | Записи были, но пропали: домен перестал проходить суточную перепроверку. Отправка с него не пойдёт, пока записи не вернутся. |
Поля spf, dkim, dmarc показывают, какая именно запись не найдена. checked_at — время последней проверки; у домена, который ещё ни разу не проверялся, он равен null. Раз в сутки мы сами перепроверяем только домены в статусе verified и failed. Домен в pending_dns сам в verified не перейдёт — после публикации записей нажмите «Проверить DNS» в кабинете; опрашивать API в ожидании бесполезно. Домен, простоявший в pending_dns больше 7 дней, удаляется, и GET /domains/{domain} начинает отвечать 404. Метки времени здесь — ISO 8601 со смещением.
GET /domains/{domain} дополнительно отдаёт dns_records — три TXT-записи, которые нужно опубликовать, с подсказкой note к каждой. Записи приходят и для уже подтверждённого домена: пригодится при переезде к другому DNS-провайдеру. Длинный ключ DKIM разбит на части по 255 символов в кавычках — так его принимает большинство DNS-панелей. Метод читает сохранённый результат проверки и сам DNS не опрашивает. Домена нет в аккаунте — 404.
Рекомендуем отдельный поддомен для рассылок (например mail.brand.ru): он изолирует репутацию рассылок от корпоративной почты — проблемы с одним потоком не утянут другой.
Кириллические домены можно запрашивать как есть — /domains/почта.рф и /domains/xn--80a1acny.xn--p1ai вернут одно и то же.
curl https://api.mailermail.ru/v1/domains \
-H "Api-Token: mm_live_9f3k…"{
"success": true,
"total": 1,
"domains": [
{
"domain": "mail.brand.ru",
"status": "verified",
"spf": true,
"dkim": true,
"dmarc": true,
"is_default": true,
"checked_at": "2026-08-03T06:00:11+03:00",
"created_at": "2026-07-20T12:31:00+03:00"
}
]
}curl https://api.mailermail.ru/v1/domains/mail.brand.ru \
-H "Api-Token: mm_live_9f3k…"{
"success": true,
"domain": {
"domain": "mail.brand.ru",
"status": "pending_dns",
"spf": true,
"dkim": false,
"dmarc": false,
"is_default": false,
"checked_at": null,
"created_at": "2026-09-18T11:02:40+03:00",
"dns_records": [
{"type": "TXT", "name": "@",
"value": "v=spf1 include:_spf.mailermail.ru ~all",
"note": "SPF. Если запись v=spf1 уже есть — допишите include в неё, второй SPF недопустим."},
{"type": "TXT", "name": "mm1._domainkey",
"value": "\"v=DKIM1; k=rsa; p=MIIBIjANBg…\" \"…IDAQAB\"",
"note": "DKIM. Часть провайдеров требует значение без кавычек — тогда склейте чанки в одну строку."},
{"type": "TXT", "name": "_dmarc",
"value": "v=DMARC1; p=none; rua=mailto:dmarc@feedback.mailermail.ru; adkim=r; aspf=r",
"note": "DMARC. p=none — режим наблюдения, доставку не блокирует; политику можно ужесточить позже."}
]
}
}{
"success": false,
"errors": [
"Domain not found. Add it in the cabinet: Настройки → Домены отправителя."
]
}Проверка адреса без отправки письма: синтаксис, MX/A домена, ролевые и одноразовые адреса, глобальный реестр недоставляемых, подсказка опечаток. Требует scope verify. Пакетом — до 100 адресов.
| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
email | string | да | Один адрес (для /verify). |
emails | array | да | Массив адресов ≤100 (для /verify/batch). |
deliverable | Домен принимает почту, признаков риска нет. |
risky | Ролевой/одноразовый адрес либо вероятная опечатка домена. |
undeliverable | Синтаксис неверен, домен мёртв или адрес в реестре отказов. |
unknown | Домен не удалось проверить (временно). |
curl -X POST https://api.mailermail.ru/v1/verify \
-H "Api-Token: mm_live_9f3k…" \
-H "Content-Type: application/json" \
-d '{"email": "ivan@example.ru"}'{
"success": true,
"email": "ivan@example.ru",
"status": "deliverable",
"checks": {
"syntax": true,
"domain": "ok",
"role": false,
"disposable": false,
"known_bounce": false,
"did_you_mean": null
}
}Хранимые шаблоны письма с переменными {{var}} в теме и теле. При отправке подставляются через template_id (или template_uuid) + template_variables/substitutions. Требует scope send. Лимит — 5000 шаблонов на аккаунт.
Хранилище общее с личным кабинетом. Шаблон, созданный по API, виден и редактируется в разделе «Шаблоны», а письмо, свёрстанное в кабинете, доступно по API — поле source показывает, откуда строка. Поэтому у шаблона есть версии: send и batch отправляют опубликованную версию, а не текущий черновик. Через API шаблон публикуется сам — при создании и при правке subject/html/text; публиковать вручную нужно только то, что правили в кабинете.
| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
name | string | да | Название, ≤128. Обязательно при создании. |
subject | string | нет | Тема (с {{var}}), ≤255. |
html | string | — | HTML-тело, ≤2 МБ. Нужно html и/или text. |
text | string | — | Текстовое тело, ≤2 МБ. |
category | string | нет | Метка по умолчанию, ≤255. |
slug | string | нет | Читаемый алиас для template_id: [a-z0-9-], ≤64. Уникален внутри аккаунта (занятый — 409), в форме UUID запрещён. Переименование ломает интеграции — постоянный ключ шаблона это uuid. |
publish | bool | нет | По умолчанию true. false оставит черновик — отправить им нельзя. |
GET /templates — список, новые первыми: ?limit= от 1 до 100 (по умолчанию 50), ?offset=. Тел в списке нет. uuid может быть null — у старого письма из кабинета, которому не удалось выдать ключ; такой шаблон по API не адресуется.GET /templates/{uuid} — шаблон целиком, с html/text черновика и признаком has_unpublished_changes.POST /templates — создать, ответ 201 с объектом template.PATCH /templates/{uuid} — обновить любое подмножество полей; после правки обязано остаться хотя бы одно тело. Ответ без обёртки template и без uuid — только состояние после правки: status, version, has_unpublished_changes.POST /templates/{uuid}/publish — опубликовать текущий черновик, тело не нужно.DELETE /templates/{uuid} — удалить, ответ {"success": true}.В пути — только UUID. Слаг адресует шаблон лишь в template_id при отправке: GET /templates/order-ok вернёт 404. Храните у себя uuid.
subject, html или text публикуется сама, если не передать "publish": false. Если новое содержимое совпадает с опубликованным, новой версии не будет.name, category или slug не публикуется: иначе переименование по API отправило бы подписчикам недописанный в кабинете черновик.PATCH отвечает 500 — повторите POST /templates/{uuid}/publish. До этого уходит прежняя версия.422; имя, тему, категорию и слаг — можно.Шаблон, за который держится рассылка, запущенная до появления версий, править и удалять нельзя (409): она читает его на каждом письме. Занятый слаг — тоже 409. Метки created_at/updated_at у шаблонов — в формате YYYY-MM-DD HH:MM:SS по московскому времени, без смещения.
html/text в ответе — это черновик, а отправляется опубликованная версия. Их видно расходящимися, когда письмо правят в кабинете и не публикуют. Признак — has_unpublished_changes в GET и в ответе PATCH: если он true, уйдёт не то тело, что вы прочитали, — опубликуйте (POST /templates/{uuid}/publish). Само по себе status: "published" говорит лишь о том, что опубликованная версия существует.
curl -X POST https://api.mailermail.ru/v1/templates \
-H "Api-Token: mm_live_9f3k…" \
-H "Content-Type: application/json" \
-d '{
"name": "Подтверждение заказа",
"subject": "Заказ №{{order}} принят",
"html": "<p>{{name}}, спасибо!</p>",
"slug": "order-ok"
}'{
"success": true,
"template": {
"uuid": "7c9e6a3b-1f2d-4c5e-8a9b-0d1e2f3a4b5c",
"name": "Подтверждение заказа",
"subject": "Заказ №{{order}} принят",
"category": "",
"created_at": "2026-09-18 12:00:00",
"updated_at": "2026-09-18 12:00:00",
"slug": "order-ok",
"source": "api",
"status": "published",
"version": 1
}
}{
"success": true,
"total": 2267,
"templates": [
{
"uuid": "7c9e6a3b-1f2d-4c5e-8a9b-0d1e2f3a4b5c",
"name": "Подтверждение заказа",
"subject": "Заказ №{{order}} принят",
"category": "",
"created_at": "2026-09-18 12:00:00",
"updated_at": "2026-09-18 12:00:00",
"slug": "order-ok",
"source": "api",
"status": "published",
"version": 1
},
{
"uuid": null,
"name": "Рассылка 2019",
"subject": "",
"category": "",
"created_at": "2019-03-01 10:00:00",
"updated_at": "2019-03-01 10:00:00",
"slug": null,
"source": "ui",
"status": "draft",
"version": 0
}
]
}{
"success": true,
"template": {
"uuid": "7c9e6a3b-1f2d-4c5e-8a9b-0d1e2f3a4b5c",
"name": "Подтверждение заказа",
"subject": "Заказ №{{order}} принят",
"category": "",
"created_at": "2026-09-18 12:00:00",
"updated_at": "2026-09-18 12:30:00",
"slug": "order-ok",
"source": "api",
"status": "published",
"version": 1,
"has_unpublished_changes": false,
"html": "<p>{{name}}, спасибо!</p>",
"text": ""
}
}PATCH /v1/templates/7c9e6a3b-…
{"html": "<p>{{name}}, заказ принят!</p>"}{
"success": true,
"status": "published",
"version": 2,
"has_unpublished_changes": false
}{
"success": true,
"status": "published",
"version": 3
}# отправка по слагу: на стенде и на бою он одинаковый
curl -X POST https://api.mailermail.ru/v1/send \
-H "Api-Token: mm_live_9f3k…" \
-H "Content-Type: application/json" \
-d '{
"from": {"email": "robot@example.ru"},
"to": [{"email": "user@example.ru"}],
"template_id": "order-ok",
"template_variables": {"order": "1042", "name": "Иван"}
}'curl -X DELETE \
https://api.mailermail.ru/v1/templates/7c9e6a… \
-H "Api-Token: mm_live_9f3k…"{
"success": true
}Адреса, на которые сервис не отправит письмо: /send и /batch отдают их в skipped с причиной suppressed. Пополняется автоматически (отказы, жалобы, отписки) и вручную через API. Требует scope send. Адреса хранятся в нижнем регистре.
GET /suppressions — список, новые первыми: ?limit= от 1 до 1000 (по умолчанию 100), ?offset=, ?reason=. Ответ — {success, total, suppressions: [{email, reason, created_at}]}.GET /suppressions?email=x@y.ru — проверка одного адреса: {success, email, suppressed, reason, created_at}; если адреса в списке нет — suppressed: false, reason и created_at равны null. Некорректный адрес — 422.POST — добавить {emails: […≤1000], reason?}. Ответ — {success, added, reason, invalid?}. added — число обработанных корректных адресов, включая уже бывшие в списке; причина у них перезаписывается переданной (по умолчанию manual).DELETE — убрать (снять блокировку) {emails: […≤1000]}. Ответ — {success, removed, invalid?}, где removed — сколько адресов действительно было в списке.Некорректные адреса в POST/DELETE не прерывают запрос — они перечислены в invalid. Если корректных нет вовсе — 422 с тем же invalid. Дубли внутри запроса схлопываются. created_at — формат YYYY-MM-DD HH:MM:SS, московское время.
DELETE снимает только стоп-лист аккаунта. Причину known_bounce (общий реестр недоставляемых) через API не снять — только в кабинете: Контакты → Заблокированные, и сейчас это действует только на /send.
manual | Добавлен вручную (по умолчанию при POST). |
hard_bounce | Почтовый сервер ответил, что ящика или домена нет. Пишется автоматически, но сейчас — только по отказам рассылок из кабинета, а не писем Email API (см. email.bounced в «Типах событий»). |
complained | Жалоба на спам через FBL почтового провайдера. Пишется автоматически. Фильтр ?reason=complained сейчас отвечает 422 — такие строки видны в общем списке и в проверке ?email=. |
complaint | Принимается в POST и в фильтре ?reason=; автоматически сейчас не пишется — жалобы приходят с причиной complained. |
unsubscribed | Отписка по One-Click-ссылке MailerMail (options.list_unsubscribe или /batch). |
curl -X POST https://api.mailermail.ru/v1/suppressions \
-H "Api-Token: mm_live_9f3k…" \
-H "Content-Type: application/json" \
-d '{"emails": ["a@ex.ru", "b@ex.ru"], "reason": "manual"}'{
"success": true,
"added": 2,
"reason": "manual",
"invalid": ["not-an-email"]
}{
"success": true,
"email": "a@ex.ru",
"suppressed": true,
"reason": "unsubscribed",
"created_at": "2026-09-17 21:14:05"
}{
"success": true,
"email": "new@ex.ru",
"suppressed": false,
"reason": null,
"created_at": null
}{
"success": true,
"total": 3,
"suppressions": [
{"email": "a@ex.ru", "reason": "unsubscribed",
"created_at": "2026-09-17 21:14:05"},
{"email": "spam@ex.ru", "reason": "complained",
"created_at": "2026-09-10 08:00:12"},
{"email": "b@ex.ru", "reason": "manual",
"created_at": "2026-09-01 12:00:00"}
]
}{
"success": true,
"removed": 1
}Лог отправок хранится 30 дней. Требует scope send.
GET /messages| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
limit / offset | int | нет | Пагинация (limit ≤200, по умолч. 50). |
status | enum | нет | sent, sandbox, skipped, failed, queued. |
category | string | нет | Фильтр по метке. |
email | string | нет | Фильтр по получателю. |
from / to | string | нет | YYYY-MM-DD или с временем. |
GET /messages/{uuid} возвращает одно сообщение, а для писем из /batch — ещё и состояние очереди (попытки, время отправки). GET /stats?days=7|30 отдаёт агрегаты по статусам, дням и категориям + суммарный объём проверок и отправок.
curl "https://api.mailermail.ru/v1/messages?status=sent&limit=20" \
-H "Api-Token: mm_live_9f3k…"{
"success": true,
"total": 128,
"messages": [
{
"message_id": "a1b2c3-…",
"to": "ivan@example.ru",
"from": "no-reply@shop.ru",
"subject": "Заказ №4211 собран",
"category": "orders",
"status": "sent",
"reason": "",
"sandbox": false,
"created_at": "2026-08-03 12:00:01"
}
]
}curl "https://api.mailermail.ru/v1/stats?days=30" \
-H "Api-Token: mm_live_9f3k…"Публичный endpoint по стандарту RFC 8058 (One-Click). Токен не нужен: длинную ссылку /unsubscribe?t=&e=&s= санкционирует HMAC-подпись, короткую /u/{token}, которая стоит в заголовках, — случайный токен, выданный на пару «аккаунт — адрес». Его вызывают почтовые клиенты (кнопка «Отписаться» в Gmail/Yandex) и получатели.
Ссылка автоматически проставляется в заголовок List-Unsubscribe при массовой отправке (в /batch — по умолчанию; в /send — по options.list_unsubscribe). POST добавляет адрес в стоп-лист (reason=unsubscribed), снимает подписку в кабинете и порождает событие email.unsubscribed; GET отдаёт человеку страницу подтверждения и ничего не меняет. Когда отписку ведёт ваш собственный unsubscribe_url, этот endpoint не участвует — см. «Отписка в письме».
Самостоятельно формировать эти ссылки не нужно — сервис вставляет их сам. В письмах они ведут на основной домен (https://mailermail.ru/api/v1/u/…), а не на api.: получателю привычнее видеть знакомый адрес, и репутация этих ссылок уже набрана.
{
"success": true,
"unsubscribed": true
}Мы шлём POST на ваш адрес пачками до 500 событий, очередь разбирается каждую минуту. Требует scope send; до 20 вебхуков на аккаунт (21-й — 422). Управлять ими можно и в кабинете: Настройки → Вебхуки.
| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
url | string | да | Адрес приёмника, ≤512 символов. Схемы http/https, порты 80, 443, 8080 и 8443, без логина и пароля в адресе. Приватные и зарезервированные адреса (127.0.0.1, 10.x, 192.168.x, облачная метадата), localhost, *.local, *.internal и домены самого MailerMail отклоняются — и при создании, и перед каждой доставкой. В ответе адрес нормализован: без порта по умолчанию, без #фрагмента, пустой путь заменён на /. |
events | array | нет | Типы событий (см. «Типы событий»). Поле не передано — подписка на все типы, включая те, что появятся позже; перечислить все восемь — то же самое. Пустой массив или неизвестный тип — 422. |
enabled | bool | нет | false — создать выключенным. Передавайте JSON-boolean: строка "false" будет прочитана как «включить». |
POST /webhooks → 201: объект webhook, а секрет — на верхнем уровне, в поле secret. Идентификатор вебхука — webhook.webhook_id (не uuid); его и подставляйте в путь /webhooks/{uuid}.GET /webhooks → {success, total, webhooks: […]}, новые первыми.GET /webhooks/{uuid} → {success, webhook, queue}; queue — счётчики строк очереди доставки: pending (ждут доставки), sent и failed (эти два хранятся 7 дней).PATCH /webhooks/{uuid} — любое из url, events, enabled; ответ {success, webhook}. Ни одного из трёх полей — 422. Смена url обнуляет счётчик отказов и снимает авто-паузу (но не ручное выключение); enabled: true включает вебхук и обнуляет счётчик.DELETE /webhooks/{uuid} → {success, deleted: "<webhook_id>"}; недоставленные события удаляются вместе с ним.POST /webhooks/{uuid}/rotate_secret → {success, webhook, secret, note}, тело запроса не нужно. Старый секрет перестаёт действовать сразу, в том числе для событий, которые уже ждут повторной доставки.POST /webhooks/{uuid} → 405: для правки — PATCH.Секрет показывается один раз — при создании и при rotate_secret; дальше в ответах только маска secret_hint (первые 8 символов). Потеряли — перевыпустите.
| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
webhook_id | string | — | UUID вебхука. |
url | string | — | Нормализованный адрес приёмника. |
events | array | — | Типы событий. При подписке «на все» — полный список. |
status | string | — | active или paused. |
paused_reason | string | — | Почему на паузе: «создан выключенным», «выключен вручную», «5 неудачных доставок подряд». У активного — пустая строка. |
secret_hint | string | — | Маска секрета: первые 8 символов и восемь точек. |
fail_count | int | — | Неудачных доставок подряд; успешная обнуляет. |
last_error | string | — | Последняя ошибка доставки, например HTTP 500, «connection error: …», «dns: host does not resolve». Тело ответа вашего приёмника сюда не попадает. |
last_delivery_at | string | — | Последняя успешная доставка; null, если её ещё не было. |
created_at | string | — | Дата создания. |
created_at и last_delivery_at здесь — в формате MySQL YYYY-MM-DD HH:MM:SS по московскому времени, без смещения, а не ISO 8601, как в событиях.
curl -X POST https://api.mailermail.ru/v1/webhooks \
-H "Api-Token: mm_live_9f3k…" \
-H "Content-Type: application/json" \
-d '{
"url": "https://shop.ru/hooks/mail",
"events": ["email.bounced", "email.opened"]
}'{
"success": true,
"webhook": {
"webhook_id": "7f1c2d3e-4b5a-4c6d-9e8f-0a1b2c3d4e5f",
"url": "https://shop.ru/hooks/mail",
"events": ["email.bounced", "email.opened"],
"status": "active",
"paused_reason": "",
"secret_hint": "3f9a1c2e••••••••",
"fail_count": 0,
"last_error": "",
"last_delivery_at": null,
"created_at": "2026-09-18 12:00:00"
},
"secret": "3f9a1c2e5b7d9f0a1c3e5b7d9f0a2c4e6b8d0f1a3c5e7b9d1f3a5c7e9b0d2f4a",
"note": "Store the secret now — it is shown only once. Signature: MailerMail-Signature = hex(HMAC-SHA256(raw body, secret))."
}{
"success": true,
"total": 1,
"webhooks": [
{
"webhook_id": "7f1c2d3e-4b5a-4c6d-9e8f-0a1b2c3d4e5f",
"url": "https://shop.ru/hooks/mail",
"events": ["email.bounced", "email.opened"],
"status": "paused",
"paused_reason": "5 неудачных доставок подряд",
"secret_hint": "3f9a1c2e••••••••",
"fail_count": 5,
"last_error": "HTTP 502",
"last_delivery_at": "2026-09-18 12:04:01",
"created_at": "2026-09-18 12:00:00"
}
]
}{
"success": true,
"webhook": { "webhook_id": "7f1c2d3e-…", "…": "…" },
"queue": {"pending": 12, "sent": 3480, "failed": 0}
}PATCH /v1/webhooks/7f1c2d3e-…
{"enabled": true}{
"success": true,
"webhook": {
"webhook_id": "7f1c2d3e-4b5a-4c6d-9e8f-0a1b2c3d4e5f",
"status": "active",
"paused_reason": "",
"fail_count": 0,
"…": "…"
}
}{
"success": true,
"deleted": "7f1c2d3e-4b5a-4c6d-9e8f-0a1b2c3d4e5f"
}{
"success": true,
"webhook": { "webhook_id": "7f1c2d3e-…", "secret_hint": "b41e07d9••••••••", "…": "…" },
"secret": "b41e07d9…(64 hex-символа)",
"note": "Store the secret now — it is shown only once."
}Каждая доставка — POST с JSON-телом {"events": [ … ]}, в пачке от 1 до 500 событий одного вебхука.
| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
Content-Type | header | — | application/json |
MailerMail-Signature | header | — | Подпись тела: 64 символа, hex в нижнем регистре. |
MailerMail-Delivery | header | — | UUID попытки: при повторе той же пачки он новый. Для дедупликации не годится. |
MailerMail-Event-Count | header | — | Число событий в пачке. |
User-Agent | header | — | MailerMail-Webhooks/1.0 и ссылка на описание Email API в скобках. |
MailerMail-Signature = HMAC-SHA256 от сырого тела запроса, в hex нижним регистром. Ключ — строка секрета как есть, все 64 символа: не декодируйте её из hex в байты. Префикса вроде sha256= нет, метки времени в подписи нет.
hash_equals, crypto.timingSafeEqual).event_id.2xx. Тело ответа игнорируется. Редиректы (3xx) не выполняются и считаются неудачей.MailerMail-Delivery.event_id: одно событие может прийти дважды, если ответ вашего приёмника до нас не дошёл.paused_reason: "5 неудачных доставок подряд"). Считаются доставки, а не события: пока события идут потоком, попытка случается каждую минуту, и недоступный приёмник уходит на паузу примерно через 5 минут; одиночное событие — примерно через полчаса. Включить обратно — PATCH {"enabled": true}, смена url или кнопка в кабинете.Пока вебхук на паузе, новые события в его очередь не попадают — восстановить их потом нельзя. События, уже стоявшие в очереди, ждут включения, но не больше суток с момента самого события (а не паузы); после этого они помечаются недоставленными. История событий письма всегда доступна через GET /messages/{uuid}/events (30 дней) независимо от подписок.
POST /hooks/mail HTTP/1.1
Host: shop.ru
Content-Type: application/json
MailerMail-Signature: 5f0c3b7e0d1a…(64 hex)
MailerMail-Delivery: 9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d
MailerMail-Event-Count: 1
User-Agent: MailerMail-Webhooks/1.0 (+https://mailermail.ru/email-api/)
{"events":[{"event_id":"3b9c1d2e-…","event":"email.sent","message_id":"0b6f7a52-…","email":"ivan@example.ru","timestamp":"2026-09-18T12:00:02+03:00","category":"orders","data":{"custom_variables":{"order_id":"4211"}}}]}// приёмник на вашей стороне
$SECRET = getenv('MM_WEBHOOK_SECRET'); // 64 символа как есть
$raw = file_get_contents('php://input');
$sig = $_SERVER['HTTP_MAILERMAIL_SIGNATURE'] ?? '';
$calc = hash_hmac('sha256', $raw, $SECRET);
if (!hash_equals($calc, $sig)) {
http_response_code(403);
exit;
}
http_response_code(200); // подтверждаем СРАЗУ
fastcgi_finish_request(); // обработка — после ответа
foreach (json_decode($raw, true)['events'] as $e) {
if (already_seen($e['event_id'])) continue; // дедупликация
// $e['event'], $e['message_id'], $e['email'],
// $e['data']['custom_variables'] ?? []
}| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
event_id | string | — | UUID события. Ключ дедупликации. |
event | string | — | Тип события, см. таблицу ниже. |
message_id | string|null | — | UUID письма. Бывает null у email.unsubscribed. |
email | string | — | Адрес получателя, в нижнем регистре. |
timestamp | string | — | Когда событие записано у нас (для email.bounced — время разбора журналов, а не самого отказа): ISO 8601 со смещением сервера, 2026-09-18T12:04:11+03:00. |
category | string | — | Категория письма. Ключа нет, если категория пустая. |
data | object | — | Подробности по типу события. Ключа нет, если подробностей нет. |
| Событие | Когда и что в data |
|---|---|
email.sent | SMTP-реле MailerMail приняло письмо к доставке (это не «доставлено в ящик» — такого события нет). У /batch — в момент фактической отправки воркером, а не постановки в очередь. data — только custom_variables; без них ключа data нет. |
email.failed | Отправить не удалось. reason — текст, как в failed[] ответа /send; у /batch ещё enqueue failed и sender domain no longer verified (домен перестал быть подтверждённым, пока письмо ждало в очереди). Плюс custom_variables?. |
email.suppressed | Адрес пропущен, письмо не отправлялось. reason: suppressed или known_bounce. Плюс custom_variables?. |
email.opened | Загрузили пиксель открытия. ip, user_agent, client (подпись почтового клиента для человека: Gmail, Яндекс.Почта, Apple Mail (iOS), Другое — не перечисление, список пополняется), country?, city?, custom_variables?. Открытия чаще всего приходят через прокси почтовика (Gmail, Яндекс), поэтому IP и гео — прокси, а не человека. |
email.clicked | Переход по обёрнутой ссылке. То же, что у открытия, плюс url — исходный адрес ссылки. |
email.unsubscribed | Отписка по One-Click-ссылке MailerMail. method: "one-click"; attribution: "by-address", если удалось подобрать письмо. custom_variables нет. |
email.bounced | Жёсткий отказ. bounce_type, smtp_code?, attribution: "by-address". custom_variables нет. См. ограничения ниже. |
email.complained | Жалоба на спам. Сейчас не отправляется, см. ниже. |
HB | Hard bounce: ящика или домена нет, ящик закрыт, либо сервер отклонил письмо адресно (в том числе по своей политике: «Access denied», «Sender verify failed»). В стоп-лист с причиной hard_bounce попадают только ответы вида «такого ящика нет». |
MBSD | Отклонено как спам: блоклист (DNSBL), контентный фильтр, плохая репутация отправителя. Адрес при этом может быть живым — в стоп-лист не попадает. |
MBKS | «Known SPAM source»: сервер получателя счёл IP отправителя известным источником спама. Тоже про репутацию, а не про адрес; в стоп-лист не попадает. |
smtp_code — трёхзначный код ответа сервера строкой ("550", не расширенный "5.1.1"); если кода в ответе не нашлось, ключа нет. Текста ответа почтового сервера в событии нет намеренно: в нём имена наших серверов и внутренние идентификаторы.
email.sent, email.opened, email.clicked — тот же message_id, что вернул /send или /batch. У /batch так же и email.failed, если письмо не ушло из очереди.email.suppressed и email.failed по адресам из skipped[]/failed[] несут новый UUID, которого в ответе не было: сопоставляйте их по email и custom_variables.email.bounced и email.unsubscribed приходят не из письма, а из внешних источников, поэтому письмо подбирается по адресу: последнее успешно отправленное через API на этот адрес за 7 дней (отказ) или за 30 дней (отписка). Отсюда attribution: "by-address": это догадка, а не факт. Для отписки письма может не найтись — тогда message_id: null, а category нет.email.bounced для писем Email API сейчас почти не приходит. Отказы разбираются по журналам рассылок кабинета, и только для контактов их списков. Событие появится, лишь если адрес, получивший ваше письмо через API за последние 7 дней, отказал в рассылке из кабинета того же аккаунта. Отказ самого API-письма события не порождает.email.complained сейчас не отправляется никогда. Жалобы через FBL обрабатываются: адрес попадает в стоп-лист с причиной complained и дальше пропускается. Следить за жалобами можно через GET /suppressions.mm_test_) событий не порождает вовсе.История событий одного письма — GET /messages/{uuid}/events, до 500 событий за 30 дней. Поля там называются иначе: type вместо event и created_at вместо timestamp, а пустой data приходит как {}.
{
"events": [
{
"event_id": "3b9c1d2e-5f60-4a71-8b92-c3d4e5f60718",
"event": "email.opened",
"message_id": "0b6f7a52-3c1e-4d8a-9f2b-6e1d0c4a7b93",
"email": "ivan@example.ru",
"timestamp": "2026-09-18T12:04:11+03:00",
"category": "orders",
"data": {
"ip": "66.249.84.12",
"user_agent": "Mozilla/5.0 … GoogleImageProxy",
"client": "Gmail",
"country": "US",
"custom_variables": {"order_id": "4211"}
}
},
{
"event_id": "a0b1c2d3-e4f5-4061-8273-94a5b6c7d8e9",
"event": "email.clicked",
"message_id": "0b6f7a52-3c1e-4d8a-9f2b-6e1d0c4a7b93",
"email": "ivan@example.ru",
"timestamp": "2026-09-18T12:05:40+03:00",
"category": "orders",
"data": {
"ip": "95.108.213.7",
"user_agent": "Mozilla/5.0 (iPhone; …)",
"url": "https://shop.ru/orders/4211",
"client": "Apple Mail (iOS)",
"country": "RU",
"city": "Москва",
"custom_variables": {"order_id": "4211"}
}
}
]
}{"event": "email.sent", "…": "…",
"data": {"custom_variables": {"order_id": "4211"}}}
{"event": "email.failed", "…": "…",
"data": {"reason": "SMTP Error: data not accepted.",
"custom_variables": {"order_id": "4211"}}}
{"event": "email.suppressed", "…": "…",
"data": {"reason": "known_bounce"}}
{"event": "email.unsubscribed", "message_id": null, "…": "…",
"data": {"method": "one-click"}}
{"event": "email.bounced", "…": "…",
"data": {"bounce_type": "HB", "attribution": "by-address",
"smtp_code": "550"}}curl "https://api.mailermail.ru/v1/messages/0b6f7a52-…/events" \
-H "Api-Token: mm_live_9f3k…"{
"success": true,
"message_id": "0b6f7a52-3c1e-4d8a-9f2b-6e1d0c4a7b93",
"total": 1,
"events": [
{
"event_id": "3b9c1d2e-5f60-4a71-8b92-c3d4e5f60718",
"type": "email.sent",
"email": "ivan@example.ru",
"data": {},
"created_at": "2026-09-18T12:00:02+03:00"
}
]
}В HTML-письмо можно вшить пиксель открытия и обернуть ссылки редиректом — тогда приходят события email.opened и email.clicked. Управляют этим два флага в options:
| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
options.track_read | bool | нет | Пиксель открытия. В /batch включён по умолчанию, в /send — выключен. |
options.track_links | bool | нет | Оборачивать ссылки. Значения по умолчанию те же. |
Транзакционному письму трекинг обычно не нужен — код подтверждения и счёт незачем считать пикселем, — поэтому в /send он выключен. В массовой рассылке наоборот: без статистики открытий она бессмысленна.
Трекинг работает только в HTML-версии: ссылки в текстовой версии не оборачиваются, а письмо без html остаётся без пикселя. Пиксель ставится перед </body>, а если его нет — в конец HTML. Обёрнутая ссылка ведёт на https://mailermail.ru/api/v1/t/c/… и отвечает редиректом 302 на исходный адрес; подпись в ссылке покрывает и сам адрес, подменить его нельзя.
data-mm-no-track у тега <a>;http:// и не https://: mailto:, tel:, якоря #…, относительные адреса;{{ в адресе — неподставленная переменная (так бывает, когда переменные не переданы вовсе);https://mailermail.ru/api/v1/u/… и уже обёрнутые https://mailermail.ru/api/v1/t/…;<a> без href.Известная проблема: адрес с пробелом, ", < или > внутри (например, неэкранированный пробел в параметре) оборачивается, но по клику отвечает 400 — получатель увидит битую ссылку. Кодируйте такие символы в адресах (%20) или ставьте на ссылку data-mm-no-track.
Открытия и переходы дедуплицируются на нашей стороне: повторное открытие того же письма в течение 10 минут и повторный переход в течение 1 минуты событий не порождают. Окно считается на письмо, а не на ссылку: второй переход по другой ссылке в ту же минуту тоже не придёт. На одно письмо приходится не более 200 событий — предзагрузка картинок почтовиком и антивирусные сканеры не превращаются в поток дублей.
События трекинга приходят, пока письмо хранится в логе, — 30 дней после отправки. Письма песочницы событий не порождают.
curl -X POST https://api.mailermail.ru/v1/send \
-H "Api-Token: mm_live_9f3k…" \
-d '{
"from": {"email": "news@shop.ru"},
"to": [{"email": "ivan@example.ru"}],
"subject": "Новинки недели",
"html": "<a href=\"https://shop.ru/new\">Смотреть</a> <a href=\"https://shop.ru/help\" data-mm-no-track>Помощь</a>",
"options": {"track_read": true, "track_links": true}
}'Модель та же — JSON поверх HTTPS, переменные {{var}}, вебхуки о событиях писем, — но в нескольких местах поведение отличается так, что ошибка не видна сразу. Проверьте эти пункты до переключения.
| Что | В MailerMail |
|---|---|
| метаданные письма | custom_variables — одно значение на весь запрос, не на получателя; значения приводятся к строке. Поле metadata игнорируется молча, как любое неизвестное поле, и в событиях ничего не будет. |
| неизвестная переменная | Становится пустой строкой, а не остаётся в письме как есть. Опечатку в имени переменной в тестовом письме не видно. |
| ссылка отписки в теле | Автоматически не вставляется, служебного {{unsubscribe_url}} нет. Отписка — только заголовками (options.list_unsubscribe или свой unsubscribe_url), ссылку в теле ставьте сами. |
трекинг в /send | Открытия и переходы по умолчанию выключены; включаются options.track_read и options.track_links. |
| события о жалобах | email.complained сейчас не приходит; жалобы видны в стоп-листе с причиной complained. |
| события об отказах | email.bounced для писем Email API сейчас почти не приходит (см. «Типы событий»). События «доставлено в ящик» нет: email.sent означает, что письмо принял наш SMTP-сервер. |
| лимит запросов | 120 запросов в минуту на токен — примерно в 50 раз меньше привычных ~100 в секунду. Отправляйте до 50 получателей в одном /send или пачками через /batch, а не по запросу на письмо. |
| отложенная отправка | Только в /batch (options.send_at, время в UTC, не дальше 72 часов); в /send — 422. |
| подпись вебхука | HMAC-SHA256 от тела в заголовке MailerMail-Signature (см. «Доставка и подпись»). |
{
"from": {"email": "robot@school.ru", "name": "Школа"},
"to": [
{"email": "student@example.ru",
"substitutions": {"name": "Мария"}}
],
"template_id": "lesson-reminder",
"template_variables": {"lesson": "Урок 3"},
"custom_variables": {"user_id": "77", "lesson_id": "3"},
"options": {
"list_unsubscribe": true,
"track_read": true,
"track_links": true
}
}