Вебхуки: доставка и проверка подписи
Вебхук — подписанный HTTP POST от O·LESA вашему серверу после завершения агента. Получатель действует на весь аккаунт, а не на один API-ключ или одно расписание.
Подключение
Откройте Настройки → API → Получатели вебхуков либо вызовите:
curl https://api.o-lesa.ru/v1/webhooks \
-H "Authorization: Bearer $OLESA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://your-service.example/hooks/olesa"}'
Нужен ключ с правом agents. Замените пример адресом своего сервера. Разрешён публичный HTTPS на порту 443, без логина, пароля и фрагмента URL. Loopback, частные и служебные IP запрещены; DNS проверяется при каждой отправке, соединение закрепляется за проверенным IP. Редиректы не выполняются, TLS-сертификат проверяется.
В ответе создания возвращается secret с префиксом whsec_. Сохраните его: повторно он не показывается. Это не API-ключ, а отдельный секрет для проверки подписи. На стороне O·LESA он хранится зашифрованным.
События
После подключения приходят новые терминальные запуски: agent.run.succeeded, agent.run.failed, agent.run.no_signal. Старые запуски не отправляются задним числом.
{
"id": "UUID-события",
"type": "agent.run.succeeded",
"created_at": "2026-09-22T09:00:00+00:00",
"data": {
"run_id": "UUID-запуска",
"kind": "seo_audit",
"status": "succeeded",
"output": "Текст отчёта",
"output_truncated": false,
"error": null,
"cost": "0.0123"
}
}
Это пример формата, не реальный результат. cost — строка в валюте баланса аккаунта. Отчёт ограничен 32 000 символов; если output_truncated: true, полный результат забирается через API запуска. Для ошибки передаётся общий код agent_failed, без внутреннего стека. Не передавайте payload в логи без учёта конфиденциальности: он содержит ваш отчёт.
Проверка подписи
Заголовки: X-Olesa-Event-Id, X-Olesa-Timestamp (Unix seconds), X-Olesa-Signature (v1=<hex>).
Вычислите HMAC-SHA256 с секретом whsec_… над последовательностью: timestamp в ASCII, точка и исходные байты HTTP-тела. Нельзя сначала разобрать JSON и сериализовать его заново. Сравнивайте подписи constant-time и проверяйте отклонение времени не более пяти минут.
Минимальный пример приёмника с надёжным сохранением события в SQLite перед подтверждением:
import hashlib
import hmac
import json
import os
import sqlite3
import time
from fastapi import FastAPI, HTTPException, Request, Response
app = FastAPI()
secret = os.environ["OLESA_WEBHOOK_SECRET"].encode()
db = sqlite3.connect("webhook-inbox.sqlite", check_same_thread=False)
db.execute("CREATE TABLE IF NOT EXISTS inbox (id TEXT PRIMARY KEY, body BLOB NOT NULL)")
db.commit()
@app.post("/hooks/olesa")
async def receive(request: Request):
body = await request.body()
stamp = request.headers.get("x-olesa-timestamp", "")
try:
fresh = abs(time.time() - int(stamp)) <= 300
except ValueError:
fresh = False
expected = "v1=" + hmac.new(secret, stamp.encode() + b"." + body, hashlib.sha256).hexdigest()
if not fresh or not hmac.compare_digest(expected, request.headers.get("x-olesa-signature", "")):
raise HTTPException(401)
event = json.loads(body)
if event["id"] != request.headers.get("x-olesa-event-id"):
raise HTTPException(400)
with db:
db.execute("INSERT OR IGNORE INTO inbox(id, body) VALUES (?, ?)", (event["id"], body))
return Response(status_code=204)
В production ограничьте размер входящего тела на reverse proxy, настройте HTTPS и обработчик сохранённых событий. Используйте общую транзакционную БД для нескольких экземпляров. Дедупликация по id обязательна: один event может прийти несколько раз. Отдельный обработчик должен выполнять бизнес-операцию идемпотентно.
Повторы и журнал
Успех — любой статус 2xx. Первая попытка обычно выполняется ближайшим минутным тиком фонового сервиса. Для остальных ответов и сетевых ошибок — до шести попыток всего, с паузами минимум 1, 2, 4, 8 и 16 минут. При нагрузке или простое воркера доставка может задержаться. Идентификатор и тело события сохраняются; timestamp и подпись обновляются для каждой попытки. Порядок доставки не гарантируется.
GET /v1/webhooks— список получателей.GET /v1/webhooks/{id}/deliveries?limit=50&offset=0— журнал: статус, число попыток, HTTP-код, общая причина ошибки.DELETE /v1/webhooks/{id}— отключить получателя. Новые отправки останавливаются; уже выполняющаяся отправка может завершиться.
Для замены секрета подключите нового получателя и отключите старого. Подписки не пропадают при отзыве API-ключа: при необходимости отключайте их отдельно. После шести неудач событие получает статус failed; результат остаётся доступен через API агента.