Webhook-уведомления — это HTTP POST-запросы от Tracker.ru на ваш endpoint при наступлении события мониторинга. В отличие от Telegram/MAX/Email — каналов с человекочитаемыми сообщениями, webhook предназначен для машинной интеграции: Slack/Discord через адаптер, PagerDuty, OpsGenie, ваш собственный инцидент-менеджер, CI/CD-пайплайн.
Каждый запрос подписан HMAC-SHA256 в заголовке X-Tracker-Signature, защищён от SSRF, ограничен rate-лимитом и поставляется с автоматическим retry при ошибках.
Настройка
- Откройте Настройки сайта → раздел Вебхуки (или общий профиль вебхуков для всех URL).
- Укажите URL для получения уведомлений. HTTPS обязателен на проде — HTTP отклоняется валидацией. Внутренние IP (
10.x,172.16-31.x,192.168.x,127.x,::1,fe80::,fd00::) блокируются — это защита от SSRF (см. ниже). - Сгенерируйте секретный ключ — он используется для HMAC-SHA256 подписи. Сохраните секрет в безопасном месте, повторно не показывается.
- Выберите события, на которые подписываетесь — от 1 до 6 из перечисленных ниже. Для каждого события можно создать отдельный webhook или один общий для всех.
- Опционально: настройте
retry_count(0–5, default 0),retry_delayв секундах (1–60, default 5),timeoutответа (5–60 сек, default 30).
6 типов событий
| Событие | Когда триггерится | Подавляется в maintenance |
|---|---|---|
url.down |
URL стал недоступен (HTTP-ошибка, timeout, SSL-handshake fail) | Да |
url.recovery |
URL восстановился после down |
Нет |
ssl.expiring |
SSL-сертификат скоро истечёт (за 30/14/7 дней) | Да |
ssl.renewed |
SSL-сертификат обновлён (новая дата notAfter позже старой) |
Нет |
apdex.degraded |
Apdex score упал ниже порога удовлетворённости | Да |
apdex.recovered |
Apdex score восстановился до приемлемого | Нет |
«Подавляется в maintenance» означает, что во время плановых работ (см. /docs/features/maintenance-windows) уведомления-«проблемы» не отправляются — а уведомления-«восстановления» приходят всегда, потому что это успешное событие.
Payload-примеры по событиям
Все payload — JSON, Content-Type: application/json. Поля с omitempty могут отсутствовать, если не релевантны для события (например, regions есть только для multi-region URL).
Общие поля
manage_url(string, omitempty) — абсолютный URL на страницу управления монитором в личном кабинете (например,https://tracker.ru/my/urls/42). Удобно для one-tap-навигации из webhook-обработчика: можно положить ссылку прямо в Slack/Discord-сообщение или incident-карточку, чтобы дежурный одним кликом попал на страницу монитора и увидел контекст инцидента. Поле может отсутствовать, если на стороне сервера не заданHOSTв окружении Go-воркеров — в этом случае ссылку в кабинет приходится строить вручную изurl_id.
url.down
Шлётся при первом обнаружении недоступности URL.
{
"event": "url.down",
"url": "https://example.com",
"url_id": 42,
"manage_url": "https://tracker.ru/my/urls/42",
"status": 500,
"status_text": "500 Internal Server Error",
"error_type": "server_error",
"downtime_since": "2026-05-01T19:15:09Z",
"monitor_type": "http",
"timestamp": "2026-05-01T19:15:11Z",
"regions": [
{"code": "msk", "name": "Москва", "flag_emoji": "🇷🇺", "is_error": true, "http_status": 500},
{"code": "eu", "name": "Франкфурт", "flag_emoji": "🇩🇪", "is_error": true, "http_status": 500},
{"code": "us", "name": "Алматы", "flag_emoji": "🇰🇿", "is_error": false, "http_status": 200}
]
}
Для TCP-мониторов monitor_type=tcp, port указывает порт, status_text содержит описание ошибки (Connection refused, i/o timeout).
url.recovery
Шлётся при восстановлении URL после down.
{
"event": "url.recovery",
"url": "https://example.com",
"url_id": 42,
"manage_url": "https://tracker.ru/my/urls/42",
"status": 200,
"status_text": "200 OK",
"downtime_since": "2026-05-01T19:15:09Z",
"downtime_duration": "2 ч 15 мин",
"previous_error": "Internal Server Error",
"monitor_type": "http",
"timestamp": "2026-05-01T21:30:23Z"
}
downtime_duration — человекочитаемая длительность downtime (X сек, X мин, X ч Y мин, X д, X д Y ч). previous_error — текст ошибки, которая привела к падению.
ssl.expiring
Шлётся за 30, 14 и 7 дней до истечения SSL-сертификата (см. /docs/features/ssl-expiring-alert). Каждый порог триггерит webhook отдельно (один раз).
{
"event": "ssl.expiring",
"url": "https://example.com",
"url_id": 42,
"manage_url": "https://tracker.ru/my/urls/42",
"ssl_expired_at": "2026-05-30T10:30:00Z",
"ssl_issuer": "Let's Encrypt Authority X3",
"days_left": 14,
"timestamp": "2026-05-16T10:30:11Z"
}
ssl.renewed
Шлётся при обновлении SSL-сертификата (новая дата notAfter позже старой).
{
"event": "ssl.renewed",
"url": "https://example.com",
"url_id": 42,
"manage_url": "https://tracker.ru/my/urls/42",
"ssl_expired_at": "2027-05-30T10:30:00Z",
"ssl_issuer": "Let's Encrypt Authority X3",
"timestamp": "2026-05-16T11:00:00Z"
}
days_left для ssl.renewed не отправляется (поле omitempty).
apdex.degraded
Шлётся, когда Apdex-score URL упал ниже настроенного порога удовлетворённости.
{
"event": "apdex.degraded",
"url_id": 42,
"manage_url": "https://tracker.ru/my/urls/42",
"url": "https://example.com",
"apdex_score": 0.62,
"threshold": 0.85,
"apdex_level": "fair",
"avg_ttfb_ms": 1850,
"degraded_at": "2026-05-01T18:00:00Z",
"timestamp": "2026-05-01T18:00:11Z"
}
apdex_level — текстовая категория (excellent/good/fair/poor/unacceptable). avg_ttfb_ms может быть null, если данных недостаточно.
apdex.recovered
Шлётся при возвращении Apdex-score выше порога.
{
"event": "apdex.recovered",
"url_id": 42,
"manage_url": "https://tracker.ru/my/urls/42",
"url": "https://example.com",
"apdex_score": 0.92,
"threshold": 0.85,
"apdex_level": "good",
"avg_ttfb_ms": 420,
"degraded_at": "2026-05-01T18:00:00Z",
"recovered_at": "2026-05-01T19:30:00Z",
"duration_seconds": 5400,
"timestamp": "2026-05-01T19:30:11Z"
}
Для apdex.recovered дополнительно отправляются recovered_at и duration_seconds (длительность инцидента в секундах).
HMAC-SHA256 подпись
Каждый запрос подписан HMAC-SHA256 от тела запроса с вашим секретом. Подпись передаётся в заголовке X-Tracker-Signature, дополнительно отправляется X-Tracker-Timestamp (Unix epoch) для защиты от replay-атак.
Заголовки запроса:
Content-Type: application/json
User-Agent: Tracker.ru-Webhook/1.0
X-Tracker-Signature: sha256=<hex>
X-Tracker-Timestamp: 1746123009
Верификация на PHP
$payload = file_get_contents('php://input');
$secret = getenv('TRACKER_WEBHOOK_SECRET');
$expected = 'sha256=' . hash_hmac('sha256', $payload, $secret);
$received = $_SERVER['HTTP_X_TRACKER_SIGNATURE'] ?? '';
if (!hash_equals($expected, $received)) {
http_response_code(401);
exit('Invalid signature');
}
$event = json_decode($payload, true);
// ... обработка
hash_equals использует constant-time сравнение — это защита от тайминговых атак. Не сравнивайте через ==.
Верификация на Node.js
const crypto = require('crypto');
app.post('/tracker-webhook', express.raw({type: 'application/json'}), (req, res) => {
const signature = req.header('X-Tracker-Signature') || '';
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.TRACKER_WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
const sigBuf = Buffer.from(signature);
const expBuf = Buffer.from(expected);
if (sigBuf.length !== expBuf.length || !crypto.timingSafeEqual(sigBuf, expBuf)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
// ... обработка
res.sendStatus(200);
});
Принципиально важно подписывать сырое тело запроса (req.body как Buffer), а не результат JSON.parse + JSON.stringify — последовательность ключей и пробелы могут отличаться от исходных, и подпись не сойдётся.
Верификация на Python
import hmac, hashlib, os
from flask import Flask, request, abort
app = Flask(__name__)
@app.post('/tracker-webhook')
def webhook():
signature = request.headers.get('X-Tracker-Signature', '')
expected = 'sha256=' + hmac.new(
os.environ['TRACKER_WEBHOOK_SECRET'].encode(),
request.get_data(),
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(signature, expected):
abort(401)
event = request.get_json()
# ... обработка
return '', 200
hmac.compare_digest — constant-time сравнение, аналог hash_equals в PHP.
Retry-стратегия
При ошибке доставки (timeout, не-2xx ответ, сетевая ошибка) Tracker.ru повторяет запрос с экспоненциальной задержкой:
- Количество попыток:
retry_count + 1(одна основная + доretry_countповторов). По умолчаниюretry_count=0(без повторов), максимум5повторов (итого 6 попыток). - Задержка между попытками:
retry_delay × 2^(attempt − 1)секунд, но не больше 60 секунд (cap). Приretry_delay=5интервалы будут: 5, 10, 20, 40, 60, 60 секунд между попытками. - Timeout каждой попытки:
timeoutсекунд (5–60, default 30). - Что считается успехом: HTTP-статус
2xx. Любой4xx/5xx, timeout, DNS-ошибка, TCP RST — считаются неудачей и триггерят retry. - Логирование: каждая попытка пишется в
webhook_logs(response_status, response_body до 10 KB, error). Видно в/my/webhooks/{id}/logs.
Идемпотентность на стороне приёмника
Из-за retry один и тот же event может прийти несколько раз. Сделайте обработчик идемпотентным: используйте связку (event, url_id, timestamp) как ключ дедупликации, или проверяйте пришёл ли этот же url_id + timestamp за последние N минут перед applying side-effect.
Rate limit
30 доставок в час на один webhook. Защита от шквала уведомлений при флапах URL и от случайной перегрузки вашего endpoint.
Если лимит превышен, Tracker.ru пропускает доставку — событие записывается в webhook_logs с error="rate limited", retry не запускается. Сам монитор не блокируется, следующее событие через час доставится нормально.
Если ваш сценарий требует больше 30/час (например, большой парк URL с частыми инцидентами) — создайте несколько webhook'ов и распределите URL по ним.
Дополнительно: между однотипными уведомлениями для одного URL действует anti-flap cooldown 5 минут на уровне статус-воркера. Это означает, что если URL мигает down→up→down с интервалом меньше 5 минут — отправится только первое событие. Защита включена для всех каналов, не только webhook.
SSRF-protection
Tracker.ru не позволяет отправлять webhook на внутренние/служебные IP-адреса — это защита от SSRF (Server-Side Request Forgery), когда злоумышленник пытается через webhook прозондировать внутреннюю сеть нашей инфраструктуры.
Блокируется на двух уровнях:
- При создании webhook — Laravel-валидация
BlacklistPrivateIp(см.app/Rules/BlacklistPrivateIp.php) резолвит DNS для указанного URL и отклоняет, если хост — RFC 1918 / loopback / link-local. - При каждой отправке — Go-воркер пере-резолвит DNS перед запросом и проверяет финальный IP. Это закрывает атаку через DNS rebinding (когда DNS-запись меняется между валидацией и доставкой).
Заблокированные диапазоны:
127.0.0.0/8(loopback) и::1169.254.0.0/16иfe80::/10(link-local)0.0.0.0и::(unspecified)10.0.0.0/8,172.16.0.0/12,192.168.0.0/16(RFC 1918 private)fc00::/7(IPv6 unique local, включаяfd00::)
Дополнительно: HTTP-клиент Go-воркера не следует редиректам (CheckRedirect: ErrUseLastResponse) — это защита от обхода SSRF через 301 → http://10.0.0.1.
Если ваш endpoint находится на корпоративной сети без публичного IP — используйте reverse-tunnel (ngrok, cloudflared) или поднимите промежуточный публичный прокси.
Webhooks и совместный доступ
Webhook-уведомления отправляются только владельцу монитора (owner). Даже если вы расшарили монитор другим пользователям, webhook-запросы продолжат приходить только на ваш URL — shared-пользователи получают только Telegram, MAX и Email-уведомления (если настроены).
Подробнее в /docs/features/sharing.
Тарификация
Webhook-уведомления доступны на тарифах Basic и Pro. На плане Free webhook'и недоступны — есть только Telegram и Email. Подробнее — на /#pricing.
Связанные
- /docs/notifications/telegram — Telegram-уведомления.
- /docs/notifications/email — email-уведомления.
- /docs/notifications/max — уведомления в MAX-мессенджере.
- /docs/features/maintenance-windows — окна обслуживания (подавляют
url.down/ssl.expiring/apdex.degraded). - /docs/features/ssl-expiring-alert — детали алертов 30/14/7.
- /docs/features/sharing — webhook'и и shared-пользователи.