SOROKA Документация API

Вебхуки

Подписка с delivery: webhook присылает новые события на ваш https-адрес пачками до 100 штук. Каждая пачка подписана: проверяйте подпись, а не адрес отправителя.

Запрос

POST https://example.ru/hooks/tg
Content-Type: application/json
X-Webhook-Subscription: 5b0c7e1a-…
X-Webhook-Delivery: 7f1a…
X-Webhook-Timestamp: 1790000000
X-Webhook-Signature: sha256=<hex>[, sha256=<hex старым секретом>]

{"events": [{"id": "18446", "kind": "post", "chat": {…}, "message": {…}, "received_at": "…"}]}
заголовоксмысл
X-Webhook-Subscriptionsubscription_id подписки
X-Webhook-Deliveryid попытки: входит в подпись и виден в журнале. Для отсева дублей не годится — после неудачи пачка собирается заново и id может смениться
X-Webhook-Timestampвремя отправки, секунды Unix
X-Webhook-Signatureодна или две подписи через запятую (две — сутки после смены секрета)

Проверка подписи

Подпись — HMAC-SHA256(секрет, timestamp + "." + delivery_id + "." + тело) в hex. Пачка подлинная, если:

  1. хотя бы одна подпись из заголовка совпала (сравнение — за постоянное время);
  2. timestamp отличается от вашего времени не больше чем на 5 минут.

Дубли отсеивайте по id события внутри events[]: после неудачи пачка собирается заново, в неё могут войти и новые события, так что повтор может прийти с другим delivery_id и другим составом. Событие, которое вы уже обработали, пропустите.

Тело берите как пришло, байтами — до разбора JSON: после разбора и обратной сборки подпись не сойдётся. Секрет (whs_…) приходит один раз в ответе на создание подписки или на rotate_webhook_secret: true.

Python
import hashlib
import hmac
import time


def verify_webhook(secret: str, headers, body: bytes) -> bool:
    """headers — заголовки запроса (без учёта регистра), body — тело как пришло, байтами."""
    ts = headers.get("X-Webhook-Timestamp", "")
    delivery = headers.get("X-Webhook-Delivery", "")
    signature = headers.get("X-Webhook-Signature", "")
    if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
        return False
    want = hmac.new(secret.encode(), f"{ts}.{delivery}.".encode() + body, hashlib.sha256).hexdigest()
    return any(part.strip().startswith("sha256=") and hmac.compare_digest(part.strip()[7:], want)
               for part in signature.split(","))
JavaScript
const crypto = require("crypto");

// headers — заголовки запроса (в Node имена в нижнем регистре), rawBody — Buffer с телом как пришло
function verifyWebhook(secret, headers, rawBody) {
  const ts = headers["x-webhook-timestamp"] || "";
  const delivery = headers["x-webhook-delivery"] || "";
  const signature = headers["x-webhook-signature"] || "";
  if (!/^\d+$/.test(ts) || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const want = crypto.createHmac("sha256", secret)
    .update(`${ts}.${delivery}.`).update(rawBody).digest("hex");
  return signature.split(",").map((s) => s.trim()).some((s) =>
    s.startsWith("sha256=") && s.length === want.length + 7 &&
    crypto.timingSafeEqual(Buffer.from(s.slice(7)), Buffer.from(want)));
}

// Express: app.post("/hooks/tg", express.raw({ type: "application/json" }), (req, res) => {
//   if (!verifyWebhook(process.env.WEBHOOK_SECRET, req.headers, req.body)) return res.sendStatus(401);
//   const { events } = JSON.parse(req.body);
//   res.sendStatus(200);
// });
PHP
<?php
// $rawBody = file_get_contents('php://input'); — тело как пришло, до json_decode
function verify_webhook(string $secret, string $rawBody): bool {
    $ts = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';
    $delivery = $_SERVER['HTTP_X_WEBHOOK_DELIVERY'] ?? '';
    $signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
    if (!ctype_digit($ts) || abs(time() - (int)$ts) > 300) {
        return false;
    }
    $want = hash_hmac('sha256', $ts . '.' . $delivery . '.' . $rawBody, $secret);
    foreach (explode(',', $signature) as $part) {
        $part = trim($part);
        if (str_starts_with($part, 'sha256=') && hash_equals($want, substr($part, 7))) {
            return true;
        }
    }
    return false;
}

Ответ и повторы

Какой адрес подойдёт

Смена секрета

PATCH /v1/tg/subscriptions/{sub_id} с {"rotate_webhook_secret": true} вернёт новый секрет. Сутки после смены пачки подписываются обоими секретами — успейте обновить секрет у себя, повторы уже отправленных пачек при этом проходят проверку.

Вебхуки событий организации

Отдельно от подписок организация может получать уведомления о своих делах: готовность задач, расход кредитов, счета, совпадения мониторингов, сроки ключей. Адреса ведутся путями /v1/webhooks (право webhooks, см. справочник) или в кабинете, в разделе «Вебхуки». Адрес получает события, случившиеся после его создания.

событиекогда
task.doneЗадача готова
task.partialЗадача готова частично
task.failedЗадача не получилась
quota.thresholdИзрасходовано 50, 80 или 100 % кредитов месяца
invoice.issuedВыставлен счёт
monitor.hitНовые совпадения мониторинга (не чаще раза в минуту)
key.expiringСкоро истекает срок ключа
webhook.testТестовое событие (служебное, приходит всегда)
webhook.disabledАдрес выключен после неудач (служебное, приходит всегда)
POST https://example.ru/hooks/events
Content-Type: application/json
X-Webhook-Id: evt_5f1c…
X-Webhook-Event: task.done
X-Webhook-Timestamp: 1790000000
X-Webhook-Signature: sha256=<hex>[, sha256=<hex старым секретом>]

{"id": "evt_5f1c…", "type": "task.done", "created_at": "2026-09-23T10:00:00+00:00", "data": {"task": {…}}}