База знаний
Вебхуки
Сообщает вашему эндпоинту о приходе почты, вместо того чтобы заставлять вас опрашивать.
Подробности
- Доступны уже сегодня из «Настройки → Вебхуки» и через API: зарегистрируйте https-эндпоинт, выберите, какие из двадцати событий ему нужны, и скопируйте подписывающий секрет whsec_, который показывается при создании и при ротации и больше никогда. Доставки — это настоящие подписанные POST-запросы, которые поднимает сам почтовый ящик, а не какой-либо вызов API, поэтому они срабатывают на входящую почту, а также на открытия и клики, чем бы письмо ни было отправлено. Отправка срабатывает со всех поверхностей, а раньше срабатывала лишь с некоторых: отправка через API, MCP, шаблон или правило поднимала email.sent, а письмо, отправленное из окна написания в самом приложении, — нет, потому что это окно пишет в почтовый ящик напрямую, а не через сервис отправки, который поднимал событие. Теперь событие поднимается на самом почтовом ящике, там, где все они сходятся, поэтому написать в приложении, запланировать на вторник и отправить через API — это три способа вызвать один и тот же вебхук. Отложенная отправка сообщает о себе дважды: email.scheduled или email.queued при приёме, email.sent, когда она действительно уходит, и email.cancelled, если вы успели её забрать. Десять эндпоинтов на почтовый ящик, и это соблюдается везде, где эндпоинт регистрируется, а не только на этом экране.
- События делятся на три семейства. Пятнадцать относятся к одному письму: email.received, email.replied, email.sent, email.delivered, email.failed, email.cancelled, email.scheduled, email.queued (собрат scheduled со стороны отмены отправки), email.delivery_delayed, email.bounced, email.complained, email.suppressed, email.opened, email.clicked и email.downloaded. email.sent означает, что письмо принял сервис отправки, email.delivered — что его принял получающий сервер, а email.delivery_delayed — что оно ещё не дошло и попытки продолжаются. email.replied срабатывает вместе с email.received, когда пришедшее письмо отвечает на то, что уже есть в ящике, так что потребитель, которому нужны оба, получает оба. email.downloaded срабатывает, когда человек забирает файл, ушедший ссылкой на скачивание, причём тот же классификатор не пускает в счёт сканеры и генераторы превью ссылок, и оно не называет получателя, потому что ссылка одинакова для всех, кому ушло письмо. Три относятся к домену: domain.verified, когда он начинает принимать почту, domain.sending_changed, когда меняется вердикт по его отправке, и domain.deleted, когда он удалён — по вашей просьбе или потому, что семидневный сборщик убрал неподтверждённый. Два относятся к самому списку подавления, а это не то же самое, что email.suppressed: suppression.added, когда адрес туда попадает, и suppression.removed, когда его снова разрешают. Не подписаться ни на одно означает все события о письмах, кроме email.replied, сегодня четырнадцать, и никогда не семейство, добавленное позже, а API читает это обратно как ["*"]. Перечислите нужные события, если предпочитаете явность. Каждая доставка несёт X-OpenEmail-Signature в виде t=<unix>,v1=<hex> — HMAC-SHA-256 по отметке времени, точке и сырому телу, — а также X-OpenEmail-Event и X-OpenEmail-Delivery. Проверяйте по байтам в том виде, в каком они пришли: разбор и повторная сериализация переставляют ключи и ломают подпись. Окно защиты от повторов в 300 секунд обеспечивает получатель, и верификатор в SDK использует его по умолчанию.
- Регистрация отклоняется для всего, что не https или не маршрутизируемо в публичной сети (loopback, RFC1918, link-local, CGNAT и их эквиваленты в IPv6), а редиректы не выполняются, поэтому 3xx записывается как неудачная доставка, а не преследуется куда-то ещё. Получателю даётся 5 секунд, эндпоинты обслуживаются параллельно, так что десять из них всё равно стоят 5 секунд, а не 50, а все попытки перечислены постранично на странице этого эндпоинта с кодом ответа и затраченным временем.
- Доставка предпринимается до 8 раз. Первая уходит в момент события; сбой, который правдоподобно может пройти сам, повторяется через 1 минуту, затем через 5, затем через 30, затем через 2 часа, 5 часов, 10 часов и ещё через 10 часов, что растягивает одно событие примерно на 27 с половиной часов. Каждое ожидание отклоняется не более чем на десятую часть, чтобы тысяча событий, упавших вместе, не вернулась в одну и ту же секунду, а Retry-After от эндпоинта, просящий подождать дольше, соблюдается, но не дольше 6 часов. Повторы хранятся как надёжная работа, а не в памяти, поэтому деплой посреди этого окна их не теряет. Повторяются только те сбои, которые стоит повторять: таймаут, отказ в соединении, 408, 425, 429 или любой 5xx. Любой другой 4xx — это намеренный отказ эндпоинта от полезной нагрузки, и спросить ещё семь раз значило бы всемеро увеличить нагрузку ради того же ответа. Идентификатор события и его createdAt фиксируются один раз, и каждая попытка несёт их, а идентификатор ещё и в X-OpenEmail-Delivery, так что получатель, увидевший один и тот же идентификатор дважды, может отбросить второй, а не сработать дважды. Когда эндпоинт починят, неудавшуюся доставку можно отправить повторно из журнала доставок в приложении или через API, по одному событию, и повторная отправка несёт тот же идентификатор. На время отправки она приостанавливает автоматические повторы этого события и отклоняется, если один из них уже отправляется, так что получатель никогда не получит две копии одновременно. После 100 событий подряд, у которых провалились все попытки, эндпоинт отключается, рабочему пространству отправляется письмо, а причину можно прочитать на самом эндпоинте. Эндпоинт, отвечающий 410 Gone, отключается сразу.
- Эндпоинт, провалившийся 100 раз подряд, выключается, а не обзванивается вечно, и всем, у кого есть доступ к вебхукам, отправляется письмо об этом: какой именно, что сообщила последняя попытка и что во время сбоев ничего не ставилось в очередь. Счётчик считает ПОДРЯД, и любая успешная попытка его сбрасывает, поэтому неудачный день в марте не может сложиться в отключённый эндпоинт сегодня. Включение обратно сбрасывает счётчик вместе с собой. Консоль различает эти два состояния, а не показывает один переключатель: эндпоинт, который выключили вы, выглядит иначе, чем тот, который выключили мы.
- Управление эндпоинтами — одна задача с двумя входными дверями. Через API это POST /webhooks, патч, удаление, ротация секрета, тест, журнал доставок и повторная отправка, с методом на каждое в SDK; в приложении это «Настройки → Вебхуки», работающие с тем же реестром, а не со вторым. Чтение закрыто скоупом webhooks:read, так что любой, кто строит интеграцию, может видеть эндпоинты и историю их доставок (какой сработал, что ответил получатель, сколько это заняло), не будучи владельцем. Регистрация, изменение, тестирование, ротация, повторная отправка и удаление требуют webhooks:write И владения почтовым ящиком, на обеих поверхностях, и вторая половина сделана намеренно: эндпоинт узнаёт обо всех адресах рабочего пространства, если только его собственные списки разрешённых не сужают его, с темами и получателями, и отсутствие разрешения означает «ему можно всё это отправлять». Роль, которая строит интеграции и не читает почту, управляет этим через ключ рабочего пространства. Открыть доставку, чтобы прочитать отправленное тело и ответ целиком, тоже может только владелец: в этом теле те же темы и получатели.
- Журналы везде читаются одинаково. GET /webhooks/deliveries читает журнал доставок всех эндпоинтов сразу, а GET /webhooks/{id}/deliveries одного, оба с фильтром по статусу, переключателем «только неудачные», и по периоду, а GET /webhooks/activity и GET /webhooks/{id}/activity показывают, кто что создал, изменил, выключил или включил, сменил секрет, проверил, переотправил или удалил, как @username или как API-ключ, который это сделал. В SDK есть метод для каждого, а у MCP-сервера есть listWebhookDeliveries, getWebhookDelivery, listWebhookActivity и replayWebhookDelivery, который всегда спрашивает перед отправкой. Читать тело доставки на любой поверхности может только владелец, а ключ, ограниченный одним адресом домена, не может читать доставки эндпоинта, который покрывает весь домен.