Вебхук — это HTTP-запрос, который сервис отправляет вашему приложению, когда с письмом что-то происходит: оно доставлено, отклонено, отложено, открыто или получатель отписался. Так приложение узнаёт о судьбе письма, не опрашивая API.
POST с JSON на ваш URL и подписывает запрос секретом эндпоинта.2xx и обрабатывает событие.whsec_… создаётся вместе с эндпоинтом и показывается только один раз. Скопируйте его сразу и храните как пароль. | Событие | Когда приходит |
|---|---|
delivered | сервер получателя принял письмо |
deferred | временный отказ, доставка будет повторена |
bounced | постоянный отказ, доставка невозможна |
complained | получатель пожаловался на спам |
dropped | письмо не отправлено — например, адрес в списке исключений |
opened | письмо открыли |
unsubscribed | получатель отписался |
{
"event_id": "a1b2c3d4-0001-4f3a-9c2e-7b6d5e4f3a21",
"event": "delivered",
"domain": "mail.example.ru",
"recipient": "client@example.ru",
"message_id": "01JN4K9F2A",
"occurred_at": "2026-06-24T09:41:13.512000+00:00"
}Заголовки:
X-Webhook-Event: delivered
X-Webhook-Delivery: a1b2c3d4-0001-4f3a-9c2e-7b6d5e4f3a21
X-Webhook-Timestamp: 1782639673
X-Webhook-Signature: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08 Подпись — HMAC-SHA256 от строки {timestamp}.{тело запроса}, где тело берётся в исходном виде, до разбора JSON. Ключ — секрет эндпоинта.
import hmac
import hashlib
WEBHOOK_SECRET = "whsec_..." # секрет эндпоинта из панели
def is_valid(headers, raw_body: bytes) -> bool:
signature = headers["X-Webhook-Signature"]
timestamp = headers["X-Webhook-Timestamp"]
# 1. Отбросьте запросы со старой меткой времени — защита от повторов.
# (сравните timestamp с текущим временем, допуск в несколько минут)
# 2. Пересчитайте подпись по строке "{timestamp}.{raw_body}".
signed = f"{timestamp}.".encode() + raw_body
expected = hmac.new(
WEBHOOK_SECRET.encode(), signed, hashlib.sha256
).hexdigest()
# 3. Сравнивайте за постоянное время.
return hmac.compare_digest(expected, signature)2xx сразу, а обработку выполняйте асинхронно — долгий ответ считается неудачей.2xx, доставка повторяется; время следующей попытки видно в панели.event_id для защиты от повторной обработки.