Документация
Webhook
Канал webhook шлёт JSON-запрос на ваш HTTPS-адрес при каждом событии срабатывания. Доступен на Pro и выше. Как подключить канал, см. Уведомления.
На этой странице
Запрос
POST на адрес канала, Content-Type: application/json, User-Agent: SmartAlert-Webhook/1. Таймаут 5 секунд, редиректы не выполняются.
| Заголовок | Значение |
|---|---|
X-SmartAlert-Timestamp | Время отправки, Unix-секунды, например 1790000000 |
X-SmartAlert-Signature | sha256=<hex>, подпись |
X-SmartAlert-Delivery | UUID доставки. Повтор той же доставки несёт тот же id: по нему отбрасывайте дубли |
X-SmartAlert-Event | alert.firing, alert.resolved, alert.no_data или test |
X-SmartAlert-Timestamp1790000000X-SmartAlert-Signaturesha256=<hex>, подписьX-SmartAlert-DeliveryX-SmartAlert-Eventalert.firing, alert.resolved, alert.no_data или testСобытия
| Событие | Когда |
|---|---|
alert.firing | Правило сработало |
alert.resolved | Значение вернулось в норму. Тот же alert.id, что у alert.firing |
alert.no_data | Серия замолчала: данных нет |
test | Кнопка Отправить тест. Поле alert равно null |
alert.firingalert.resolvedalert.id, что у alert.firingalert.no_datatestalert равно nullТело
{
"version": 1,
"event": "alert.firing",
"deliveryId": "6f1c…",
"sentAt": "2026-10-17T12:00:00Z",
"alert": {
"id": "0b8e…",
"url": "https://…/alerts/0b8e…",
"severity": "critical",
"ruleId": "c2d1…",
"ruleName": "Отток ликвидности",
"project": { "id": "4a7f…", "name": "Vault" },
"contract": { "network": "ethereum", "address": "0xabc…", "name": "Pool" },
"labels": { "token": "USDC" },
"condition": "gt",
"value": 1250000,
"threshold": 1000000,
"band": null,
"conditions": null,
"startedAt": "2026-10-17T11:58:00Z",
"resolvedAt": null
}
}| Поле | Смысл |
|---|---|
version | Всегда 1 |
event | То же, что в X-SmartAlert-Event |
deliveryId | То же, что в X-SmartAlert-Delivery |
sentAt | Время отправки, RFC 3339 UTC |
alert | null у test, иначе срабатывание |
alert.id | Инцидент: firing и resolved одного инцидента несут один id |
alert.url | Страница срабатывания в приложении |
alert.severity | critical, warning или info |
alert.labels | Серия (значения группировки правила). {}, если группировки нет |
alert.condition | Оператор условия правила |
alert.value, alert.threshold | Значение и порог |
alert.band | {"norm": n, "edge": e} у правила с нормой: норма и пересечённая граница. Тогда threshold равен null |
alert.conditions | У правила из двух условий — [{"met": true, "text": "Не было пополнения за 1 ч"}, …]: по элементу на условие, met — выполнено ли, текст на языке канала. У правила из одного условия null |
alert.unit | "usd" у правила с порогом в $ (Правила): тогда value и threshold — в $ |
alert.transfer | У правила с порогом в $ — перевод, давший значение: {"txHash", "blockNumber", "amount", "symbol", "price", "priceSource", "priceAt"}. У других правил null |
alert.call | У правила состояния контракта (Контракты) — ответ функции: {"signature", "args", "raw", "previous", "reverted"}. value у bool — 1 или 0, у address — null. У других правил null |
alert.startedAt | Начало инцидента |
alert.resolvedAt | Конец инцидента у alert.resolved, иначе null |
text | Свой текст уведомления правила (Уведомления), null, если его нет |
version1eventX-SmartAlert-EventdeliveryIdX-SmartAlert-DeliverysentAtalertnull у test, иначе срабатываниеalert.idfiring и resolved одного инцидента несут один idalert.urlalert.severitycritical, warning или infoalert.labels{}, если группировки нетalert.conditionalert.value, alert.thresholdalert.band{"norm": n, "edge": e} у правила с нормой: норма и пересечённая граница. Тогда threshold равен nullalert.conditions[{"met": true, "text": "Не было пополнения за 1 ч"}, …]: по элементу на условие, met — выполнено ли, текст на языке канала. У правила из одного условия nullalert.unit"usd" у правила с порогом в $ (Правила): тогда value и threshold — в $alert.transfer{"txHash", "blockNumber", "amount", "symbol", "price", "priceSource", "priceAt"}. У других правил nullalert.call{"signature", "args", "raw", "previous", "reverted"}. value у bool — 1 или 0, у address — null. У других правил nullalert.startedAtalert.resolvedAtalert.resolved, иначе nulltextnull, если его нетОтсутствующее значение всегда null, поле не пропадает. В версии 1 могут появиться новые поля: игнорируйте незнакомые.
Проверка подписи
Секрет (whsec_…) SmartAlert показывает один раз: при создании канала и при смене секрета. Ключ HMAC — вся строка секрета, вместе с префиксом.
signature = hex( HMAC-SHA256( secret, timestamp + "." + rawBody ) )
X-SmartAlert-Signature: sha256=<signature>rawBody — байты тела как пришли, до разбора JSON. Метка времени входит в подпись, поэтому перехваченный запрос нельзя повторить с новой.
- Отклоните запрос, если
X-SmartAlert-Timestampотличается от ваших часов больше чем на 300 секунд. - Посчитайте подпись и сравните с заголовком за постоянное время.
- Отбрасывайте дубли по
X-SmartAlert-Delivery.
Node
Нужно сырое тело: в Express подключите express.raw, не express.json.
import crypto from "node:crypto";
import express from "express";
const SECRET = process.env.SMARTALERT_SECRET; // whsec_…
function verify(secret, headers, rawBody) {
const ts = headers["x-smartalert-timestamp"];
const got = headers["x-smartalert-signature"];
if (!ts || !got || !Number.isFinite(Number(ts))) return false;
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const mac = crypto.createHmac("sha256", secret).update(`${ts}.`).update(rawBody).digest("hex");
const a = Buffer.from(got);
const b = Buffer.from(`sha256=${mac}`);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
const app = express();
app.post("/smartalert", express.raw({ type: "application/json" }), (req, res) => {
if (!verify(SECRET, req.headers, req.body)) return res.sendStatus(401);
const event = JSON.parse(req.body.toString("utf8"));
// Дубли отбрасывайте по event.deliveryId, обработку делайте асинхронно.
res.sendStatus(200);
});
app.listen(3000);Python
import hashlib
import hmac
import time
def verify(secret: str, headers, raw_body: bytes) -> bool:
ts = headers.get("X-SmartAlert-Timestamp", "")
got = headers.get("X-SmartAlert-Signature", "")
try:
if abs(time.time() - int(ts)) > 300:
return False
except ValueError:
return False
mac = hmac.new(secret.encode(), ts.encode() + b"." + raw_body, hashlib.sha256)
return hmac.compare_digest("sha256=" + mac.hexdigest(), got)Во Flask raw_body — это request.get_data(), в FastAPI — await request.body(). Берите байты до разбора JSON.
Смена секрета
Кнопка Сменить секрет выдаёт новый секрет. Он действует сразу, перекрытия нет: старый секрет не проходит проверку с первого запроса после смены. Обновите секрет у получателя до смены или сразу после неё.
Ответ и повторы
| Ответ получателя | Что делает SmartAlert |
|---|---|
| 2xx | Доставлено. Тело ответа не читается дальше 200 символов, они видны в журнале доставок |
| 408, 429, 5xx, таймаут, обрыв | До 5 попыток с паузами 1, 4, 16, 64 и 256 секунд. Retry-After на 429 соблюдается, до 5 минут |
| 404, 410 | Вебхук удалён: доставка в статусе «ошибка», канал «отключён», владельцу уходит письмо. Только для боевой доставки, тест канал не отключает |
| Остальное, включая 3xx | «Ошибка», без повторов |
Retry-After на 429 соблюдается, до 5 минутОтвечайте быстро, до 5 секунд, и обрабатывайте асинхронно. Повтор несёт тот же X-SmartAlert-Delivery.
Тест
Отправить тест шлёт событие test с alert: null, подписанное тем же секретом. Ответ 2xx включает канал. По тестовому запросу удобно писать и отлаживать обработчик.
PagerDuty через webhook
Для PagerDuty есть отдельный канал: ключ интеграции, без посредника — см. Уведомления → PagerDuty. Этот рецепт нужен, только если нужен свой формат инцидента.
PagerDuty Events API v2 принимает свой формат, а не этот, поэтому между webhook и PagerDuty ставится функция-посредник (облачная функция, Zapier, n8n). Она проверяет подпись, перекладывает поля и отправляет запрос в API PagerDuty.
| PagerDuty | Из webhook |
|---|---|
dedup_key | alert.id |
event_action: trigger или resolve | event: alert.firing или alert.resolved |
payload.summary | alert.ruleName и alert.contract.name |
payload.severity | alert.severity |
links[].href | alert.url |
dedup_keyalert.idevent_action: trigger или resolveevent: alert.firing или alert.resolvedpayload.summaryalert.ruleName и alert.contract.namepayload.severityalert.severitylinks[].hrefalert.urlОдин и тот же alert.id у alert.firing и alert.resolved позволяет закрыть тот же инцидент в PagerDuty. События alert.no_data и test в его формат не переводите, если они вам не нужны: функция может ответить на них 200 и ничего не делать.
Нашли ошибку или не нашли ответ? Напишите