События и вебхуки
Отслеживайте статусы писем в журнале отправки и получайте подписанные события доставки, отказов, жалоб, открытий и кликов на собственный эндпоинт.
Каждое отправленное письмо получает идентификатор и проходит через ряд статусов: постановка в очередь, доставка, повторные попытки или отказ. Следить за этим можно в журнале отправки в консоли, запрашивать через API или получать каждое событие на собственный сервер с помощью вебхука.
Статусы писем
У каждой копии для отдельного получателя один статус. Статус меняется только вперёд: например, доставленное письмо ещё может перейти в статус «отказ» или «жалоба», но никогда не вернётся в статус «в очереди».
| Статус | Метка в консоли | Значение |
|---|---|---|
queued | В очереди | Принято и ожидает доставки |
deferred | Отложено | Принимающий сервер вернул временную ошибку. Доставка повторяется в течение до 72 часов. |
delivered | Доставлено | Принимающий сервер принял письмо |
bounced | Отказ | Доставка окончательно не удалась — во время доставки или по отчёту об отказе, полученному позже. См. типы отказов ниже. |
complained | Жалобы | Получатель отметил письмо как спам |
rejected | Отклонено | Отклонено до отправки, например потому что получатель в списке подавления, не разрешён в песочнице или содержимое было отклонено |
failed | Ошибка | Письмо не удалось доставить после 72 часов повторных попыток или не удалось поставить в очередь |
Открытия, клики и отписки не являются статусами. Они записываются в письме (opened_at, clicked_at, unsubscribed_at) и отправляются как события вебхука. В журнале отправки у открытого письма есть дополнительная метка Открыто.
Типы отказов
| Тип | Когда | Последствие |
|---|---|---|
| Жёсткий | Постоянный отказ 5xx, например неизвестный пользователь | Адрес добавляется в глобальный список подавления |
| Мягкий | Переполненный ящик (x.2.2) или отказ по системным, сетевым причинам либо из-за политики (x.3.x–x.7.x, например блокировка по репутации IP) | Адрес не добавляется в список подавления |
Журнал отправки
На вкладке Журнал отправки потока перечислены письма за последние 30 дней с указанием времени, получателя, темы, категории, источника (API или SMTP) и статуса. Можно фильтровать по Получатель, Состояние, Источник, Категория и диапазону дат.
Нажмите на письмо, чтобы открыть Сведения о письме: статус, отправитель, тема, категория, теги, последний ответ принимающего сервера и список событий с кодом SMTP, ответом, типом отказа, MX-хостом и IP каждой попытки доставки.
Вкладка Обзор суммирует те же данные за 7 или 30 дней: отправлено, доля доставки, доля отказов, доля жалоб, открытия и клики.
Вебхуки
Вебхук отправляет каждое событие на ваш HTTPS-эндпоинт в момент, когда оно происходит, чтобы ваше приложение могло, например, пометить адрес пользователя как недействительный после жёсткого отказа.
Настройка вебхука
- Откройте поток и перейдите на вкладку Webhook.
- В поле URL получателя введите URL
https://, доступный из публичного интернета. Частные и локальные адреса не принимаются. - В разделе События выберите нужные события. По умолчанию выбраны все события.
- Нажмите Сохранить. При первом сохранении URL один раз отображается Секрет подписи. Он начинается с
whsec_. Сохраните его на своём сервере. - Нажмите Отправить тест, чтобы отправить событие
testи проверить, что ваш эндпоинт отвечает.
Чтобы получить новый секрет, нажмите Сбросить секрет подписи. Старый секрет сразу перестаёт действовать. Чтобы перестать получать события, очистите URL и сохраните.
События
| Событие | Метка в консоли | Когда отправляется |
|---|---|---|
delivered | Доставлено | Принимающий сервер принял письмо |
deferred | Задержка (tarpit) | Попытка доставки временно не удалась. Отправляется один раз на каждую попытку. |
bounced | Отказ | Окончательная неудача. data.bounce_type равно hard или soft. |
complained | Жалобы | Получатель пожаловался на письмо как на спам |
failed | Ошибка | Доставка прекращена через 72 часа |
rejected | Отклонить | Сервер отправки отклонил получателя |
opened | Открыть | Письмо открыто впервые (требуется отслеживание открытий) |
clicked | Клики | Каждый клик по отслеживаемой ссылке (требуется отслеживание кликов) |
unsubscribed | Отписка | Получатель отписался |
Получатели, отклонённые самим HTTP API, возвращаются в поле rejected ответа API и не порождают вебхук.
Формат запроса
Каждое событие отправляется как POST с телом JSON:
POST /your/webhook HTTP/1.1
Content-Type: application/json
User-Agent: Yeap-TM-Webhook/1
X-Yeap-Signature: t=1760955304,v1=5f2b9c...e81a
{
"id": "evt_01JAQ40B2C3D4E5F6G7H8J9K0M",
"type": "bounced",
"stream_id": 12,
"message_id": "01JAQ3Z9V8K4M2N6P7R5S1T0XW",
"created_at": "2026-10-20T10:15:04+00:00",
"data": {
"rcpt": "alice@example.org",
"smtp_code": 550,
"smtp_response": "5.1.1 The email account that you tried to reach does not exist",
"bounce_type": "hard",
"mx": "mx.example.org"
}
}id уникален для каждого события: используйте его, чтобы отбрасывать дубликаты. Содержимое data зависит от события:
| Событие | Поля data |
|---|---|
delivered, deferred, bounced, complained, failed, rejected | rcpt, а также при наличии smtp_code, smtp_response, bounce_type, mx, at |
opened | rcpt, ip, user_agent |
clicked | rcpt, url, ip, user_agent |
unsubscribed | rcpt, category, via |
test | message |
Проверка подписи
Заголовок X-Yeap-Signature имеет вид t=<timestamp>,v1=<signature>. Подпись — это HMAC-SHA256 в шестнадцатеричном виде от <timestamp>.<raw request body>, где ключом служит полный секрет подписи (включая whsec_). Чтобы проверить запрос:
- Прочитайте необработанное тело точно в том виде, в каком оно получено, до разбора JSON.
- Вычислите HMAC от
t + "." + bodyи сравните его сv1с помощью сравнения за постоянное время. - Отклоняйте запросы, метка времени которых старше 5 минут, чтобы блокировать повторное воспроизведение.
Python:
import hashlib, hmac, time
def verify(secret: str, header: str, body: bytes, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
ts, sig = parts.get("t", ""), parts.get("v1", "")
if not ts.isdigit() or abs(time.time() - int(ts)) > tolerance:
return False
expected = hmac.new(secret.encode(), ts.encode() + b"." + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig)Node.js:
const crypto = require('crypto');
function verify(secret, header, rawBody, tolerance = 300) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const ts = Number(parts.t);
if (!ts || Math.abs(Date.now() / 1000 - ts) > tolerance) return false;
const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1 || '');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Ответы и повторные попытки
- Отвечайте любым статусом
2xxв течение 10 секунд. Всё остальное считается неудачей. - Неудачные доставки повторяются с увеличивающимися интервалами: от 1 минуты до не более 4 часов между попытками. Повторные попытки прекращаются через 24 часа после события.
- События могут приходить более одного раза или не по порядку. Используйте
idсобытия, чтобы пропускать дубликаты, а статус письма — чтобы определить актуальное состояние.
Таблица Последние доставки на вкладке Webhook показывает последние 100 доставок с событием, статусом (В очереди ожидает, Успешно успешно, Повтор повтор, Прекращено прекращено), количеством попыток, последней ошибкой и временем следующей попытки.