API и интеграции

Вебхуки: доставка и проверка подписи

Вебхук — подписанный 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 агента.