Вебхуки
Как получать события на свой адрес и проверять, что их отправили мы.
Вебхук — запрос POST на ваш адрес с событием, отправленный сразу, как оно случилось. Вебхуки избавляют от опроса API в ожидании перемен.
Адрес
Адреса заводятся в панели, раздел «API» → «Вебхуки»: до 5 адресов на аккаунт. У адреса задаются:
- адрес — только
https, доступный из интернета, на порту 443 или 8443. Адреса во внутренних сетях не принимаются; - события — все или выбранные типы. «Все» включает и типы, которые появятся позже;
- описание — для себя.
Заведение адреса и смена его подтверждаются кодом из письма; о заведении приходит уведомление. Секрет подписи показывается один раз — при заведении. Сохраните его сразу.
Запрос
POST /webhooks HTTP/1.1
Content-Type: application/json
webhook-id: evt_1024
webhook-timestamp: 1790769600
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
{"id":"evt_1024","type":"run.stopped","object_type":"run","created_at":"2026-09-30T12:00:00.000Z","data":{"object":{...}}}| Заголовок | Что содержит |
|---|---|
webhook-id | Идентификатор события — тот же, что id в теле. У повтора он не меняется |
webhook-timestamp | Время отправки в секундах Unix |
webhook-signature | Подпись: v1, и HMAC-SHA256 в base64. Подписей бывает две через пробел — после смены секрета |
Тело — событие в том же виде, что возвращает GET /events/{id}.
Ответ
Ответьте кодом 2xx в течение 10 секунд — тогда событие доставлено. Любой другой код, тайм-аут или обрыв соединения — неудача. Перенаправления (3xx) не выполняются и тоже считаются неудачей.
Отвечайте сразу, а событие обрабатывайте после ответа: долгая обработка внутри запроса упрётся в тайм-аут, и событие придёт повторно.
Проверка подписи
Подпись — по открытому стандарту Standard Webhooks. Проверять её удобнее готовой библиотекой: они есть для JavaScript, Python, Go, PHP, Java, Ruby и других языков.
import { Webhook } from "standardwebhooks";
const webhook = new Webhook(process.env.WEBHOOK_SECRET); // whsec_…
app.post("/webhooks", express.raw({ type: "application/json" }), (req, res) => {
let event;
try {
event = webhook.verify(req.body, req.headers);
} catch {
return res.sendStatus(400);
}
res.sendStatus(204);
handle(event); // после ответа
});Без библиотеки подпись проверяется так:
- Возьмите тело запроса как пришло, байтами: разобранный и снова собранный JSON даст другую строку.
- Соберите строку
{webhook-id}.{webhook-timestamp}.{тело}. - Посчитайте HMAC-SHA256 от неё. Ключ — часть секрета после
whsec_, раскодированная из base64. - Сравните
v1,<base64 результата>с каждой подписью изwebhook-signatureсравнением постоянного времени. - Отклоните запрос, если
webhook-timestampотличается от текущего времени больше чем на 5 минут: так перехваченный запрос нельзя отправить повторно.
import crypto from "node:crypto";
function verify(secret, headers, rawBody) {
const id = headers["webhook-id"];
const timestamp = headers["webhook-timestamp"];
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const expected = crypto
.createHmac("sha256", key)
.update(`${id}.${timestamp}.${rawBody}`)
.digest("base64");
return headers["webhook-signature"].split(" ").some((signature) => {
const [version, value] = signature.split(",");
return (
version === "v1" &&
value.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(value), Buffer.from(expected))
);
});
}Повторы
Неудачная доставка повторяется: всего 8 попыток, с паузами сразу, 5 с, 5 мин, 30 мин, 2 ч, 5 ч, 10 ч, 10 ч — чуть больше суток. Если все попытки неудачны, доставка отмечается «не доставлено»; событие остаётся в GET /events.
- Одно событие может прийти больше одного раза: например, если ваш ответ не дошёл до нас. Отличайте повтор по
webhook-idи не обрабатывайте одно событие дважды. - Порядок доставки не гарантирован: повтор старого события может прийти после нового. Время события — в
created_at. - Если ваш адрес был недоступен долго, прочитайте пропущенное через
GET /events.
Сбои адреса
Сбои адреса считаются полосой: от первой неудачи до первой успешной доставки. Если сутки не было ни одной попытки — например, все повторы уже исчерпаны, — полоса закончилась, и следующая неудача начинает новую.
- Если полоса длится 24 часа, при следующей неудаче приходит уведомление.
- Если 3 суток — адрес выключается, события на него больше не отправляются, и об этом тоже приходит уведомление. Включите адрес в панели, когда он заработает.
В панели у каждого адреса видны его доставки: что отправлено, что ответил адрес и когда следующая попытка. Доставку «не доставлено» можно повторить: она встаёт в очередь и уходит в течение нескольких секунд. Кнопка «Проверить» отправляет событие webhook.test — удобно, чтобы проверить приём до первых настоящих событий; на счёт сбоев адреса проверка не влияет.
Секрет
Секрет меняется в панели кнопкой «Новый секрет» — с подтверждением кодом. Прежний секрет действует ещё 24 часа: всё это время запросы подписываются обоими, и вы успеваете заменить секрет в интеграции без потери событий.
Секрет, как и ключ API, храните на сервере. Если он утёк, смените его.