Уведомления: Callback и JSON-RPC события
Как получать уведомления о ходе и результате генерации — простой webhook или JSON-RPC-события, без ответа с вашей стороны.
Что это
Два независимых и более простых канала, чем полная доставка статей (см.
«Приём статей на своём сайте»):
Octopus сам стучится к вам, когда что-то происходит, но не ждёт от вас
осмысленного ответа — только код 2xx как подтверждение получения.
- Callback URL — один простой
POSTпо завершении всей задачи. Без конверта JSON-RPC; подписывается тем же JSON-RPC secret, если он задан. - JSON-RPC события —
task.started/article.completed/task.finished, каждое можно включить или выключить отдельно. Опционально подписаны HMAC.
Нужна не просто галочка «получено», а решение принять/отклонить конкретную статью —
это уже другой механизм, article.deliver из гайда выше.
Шаг 1. Настройте нужный канал (или оба) в проекте
Всё — на странице проекта → блок «Интеграция».
- Callback URL — публичный HTTPS-адрес, куда прилетит один POST по завершении задачи. Пусто — колбэк не планируется вовсе.
-
JSON-RPC endpoint — публичный HTTPS-адрес для событий (тот же, что использует
article.deliver, если он тоже включён). JSON-RPC secret — опционален, но если задан, каждое событие подписывается (см. Шаг 3). - Отметьте чекбоксы «JSON-RPC события»: какие из трёх присылать. Ничего не отмечено при том, что вы вообще не трогали этот блок — по умолчанию присылается только
task.finished. Если явно снять все галочки и сохранить форму — не придёт вообще ничего, это тоже валидный осознанный выбор.
Оба адреса в приватных сетях (localhost, 127.0.0.1, серые IP) отбрасываются защитой от SSRF — endpoint должен быть доступен из интернета.
Шаг 2. Примите Callback URL (если настроили)
По завершении задачи (успех, провал или частичный провал) на ваш URL придёт:
{
"event": "task.completed",
"task_id": 45,
"status": "partially_failed",
"total_articles": 10,
"completed_articles": 8,
"failed_articles": 2,
"total_tokens": 152340,
"total_cost": "3.841200",
"timestamp": "2026-08-28T09:12:00+00:00"
}
| Поле | Значение |
|---|---|
| status | completed / partially_failed / failed. |
| total_cost | Строка (не число) — денежная точность, не подставляйте напрямую во float-арифметику. |
Ни текста статей, ни article_id здесь нет — это сводка по всей задаче целиком.
Подписи нет: любой, кто узнает URL, может дёрнуть его с произвольным телом — не доверяйте payload'у
без дополнительной проверки, если это важно для вашей логики.
Шаг 3. Примите JSON-RPC события (если настроили)
Каждое событие — отдельный POST на JSON-RPC endpoint с телом вида:
{
"jsonrpc": "2.0",
"method": "<событие>",
"params": { ... }
}
Обратите внимание — здесь нет id. Это осознанно: по JSON-RPC 2.0 отсутствие
id и означает «notification», а не «запрос». Octopus не разбирает тело вашего
ответа — только код 2xx как подтверждение.
task.started — задачу взял воркер
{"task_id": 45, "type": "generate", "total_articles": 10, "mode": "pipeline"}
type — generate или process. mode всегда "pipeline" — оставлено в контракте для старых интеграций.
article.completed — одна статья готова
{
"task_id": 45, "article_id": 123, "title": "Заголовок статьи",
"rubric": "Технологии", "model": "polza/gpt-4o-mini", "prompt_tokens": 1200,
"completion_tokens": 2400, "total_tokens": 3600, "cost": "0.048600"
}
Только метаданные — текста статьи здесь нет. rubric определяется AI на
стадии Reviewer и может быть null у пайплайнов без этой стадии. Нужен
текст сразу с решением принять/отклонить — см. article.deliver.
task.finished — все статьи задачи завершены
{
"task_id": 45, "status": "partially_failed", "total_articles": 10,
"completed_articles": 8, "failed_articles": 2,
"total_tokens": 152340, "total_cost": "3.841200",
"finished_at": "2026-08-28T09:12:00+00:00"
}
По сути то же самое, что Callback URL из Шага 2 (событие task.completed там), но в конверте JSON-RPC.
Проверка подписи
Если задан JSON-RPC secret, событие приходит подписанным — тот же принцип, что и у
article.deliver: X-Timestamp
плюс X-Octopus-Signature-V2
(HMAC от «<X-Timestamp>.<тело>»). Старый
X-Octopus-Signature (HMAC только от тела)
продолжает приходить для обратной совместимости, но от повторной отправки перехваченного запроса не защищает.
$rawBody = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_TIMESTAMP'] ?? '';
// Окно ±5 минут: без него подпись вечная и запрос можно проиграть заново.
if ($ts === '' || abs(time() - (int) $ts) > 300) {
http_response_code(400);
exit;
}
$expected = 'sha256=' . hash_hmac('sha256', $ts . '.' . $rawBody, $secret);
if (!hash_equals($expected, $_SERVER['HTTP_X_OCTOPUS_SIGNATURE_V2'] ?? '')) {
http_response_code(401);
exit;
}
Без secret заголовка не будет вовсе — тогда endpoint принимает события от кого угодно, кто узнает его адрес.
Повторные попытки и отладка
- Оба канала — best-effort и вне транзакции генерации: недоступный endpoint не ломает задачу.
- Callback URL: до 7 попыток, интервалы 10 мин → 30 мин → 2 ч → далее раз в 2 ч. Очередь и история — страница
/callbacks. - JSON-RPC события: до 5 попыток, тот же ритм (10 мин → 30 мин → 2 ч → 2 ч). Своей UI-страницы с историей у них нет — смотрите логи задачи (запись
jsonrpc.scheduled). - В обоих случаях любой код вне диапазона
2xx, сетевая ошибка или таймаут (30 сек) — повод для повтора; тело вашего ответа не разбирается ни там, ни там.