Webhook-уведомления: 6 типов событий, payload, HMAC-SHA256

7 мин чтения
Обновлено 12 мая 2026

Webhook-уведомления — это HTTP POST-запросы от Tracker.ru на ваш endpoint при наступлении события мониторинга. В отличие от Telegram/MAX/Email — каналов с человекочитаемыми сообщениями, webhook предназначен для машинной интеграции: Slack/Discord через адаптер, PagerDuty, OpsGenie, ваш собственный инцидент-менеджер, CI/CD-пайплайн.

Каждый запрос подписан HMAC-SHA256 в заголовке X-Tracker-Signature, защищён от SSRF, ограничен rate-лимитом и поставляется с автоматическим retry при ошибках.

Настройка

  1. Откройте Настройки сайта → раздел Вебхуки (или общий профиль вебхуков для всех URL).
  2. Укажите URL для получения уведомлений. HTTPS обязателен на проде — HTTP отклоняется валидацией. Внутренние IP (10.x, 172.16-31.x, 192.168.x, 127.x, ::1, fe80::, fd00::) блокируются — это защита от SSRF (см. ниже).
  3. Сгенерируйте секретный ключ — он используется для HMAC-SHA256 подписи. Сохраните секрет в безопасном месте, повторно не показывается.
  4. Выберите события, на которые подписываетесь — от 1 до 6 из перечисленных ниже. Для каждого события можно создать отдельный webhook или один общий для всех.
  5. Опционально: настройте 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 прозондировать внутреннюю сеть нашей инфраструктуры.

Блокируется на двух уровнях:

  1. При создании webhook — Laravel-валидация BlacklistPrivateIp (см. app/Rules/BlacklistPrivateIp.php) резолвит DNS для указанного URL и отклоняет, если хост — RFC 1918 / loopback / link-local.
  2. При каждой отправке — Go-воркер пере-резолвит DNS перед запросом и проверяет финальный IP. Это закрывает атаку через DNS rebinding (когда DNS-запись меняется между валидацией и доставкой).

Заблокированные диапазоны:

  • 127.0.0.0/8 (loopback) и ::1
  • 169.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.

Связанные