Перейти к основному содержимому

Вебхуки

Вместо поллинга статуса API может сам постучаться на твой URL, когда с заказом что-то случилось.

Регистрация​

POST /webhooks:

curl -X POST https://api.piratepress.fun/public/v1/webhooks \
-H "X-API-Key: pp_..." \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/piratepress/webhook"}'

Ответ 201:

{
"id": "wh_...",
"url": "https://example.com/piratepress/webhook",
"active": true,
"created_at": "2026-09-08T12:00:00Z",
"secret": "whs_..."
}

Один эндпоинт на ключ: повторный POST /webhooks заменяет URL и перевыпускает секрет. secret показывается один раз — сохрани его, им проверяется подпись.

Управление: GET /webhooks — список, DELETE /webhooks/{id} — удалить (204).

События​

  • video.done — ролик готов, в data есть result_url;
  • video.error — генерация упала без списания дублонов (триал или заказ, оплаченный извне);
  • video.refunded — генерация упала, дублоны за заказ уже возвращены на баланс;
  • video.awaiting_review — режиссёрский режим: сценарий ждёт решения (POST /videos/{id}/review);
  • clip.done — нарезка готова, в data клипы с подписанными ссылками и zip_url;
  • clip.error — нарезка упала, оценка возвращена на баланс.

Видео-события приходят для заказов с каналом создания api/agent (созданных через публичный API), события нарезок — тоже только для заказов с origin api/agent.

Формат тела​

Конверт — {"event", "data"}: тип события в поле event, а data — тот же JSON, что отдаёт GET /videos/{id} (для нарезок — GET /clips/{id}):

{
"event": "video.done",
"data": {
"id": "gen_a1b2c3",
"status": "done",
"result_url": "https://api.piratepress.fun/public/v1/files/eyJ...",
"cost": 100,
"created_at": "2026-10-19T12:00:00Z"
}
}

Тип события определяй по полю event, а не по data.status: одна и та же ручка статуса обслуживает все статусы заказа, а событие приходит на конкретный переход.

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

Каждый запрос подписан заголовком:

X-PiratePress-Signature: <hex HMAC-SHA256(secret, сырой body)>

Проверяй подпись до парсинга JSON и по сырым байтам тела — пере-serialизация JSON меняет байты и ломает подпись. Сравнение — только constant-time.

import hashlib
import hmac
import json

from fastapi import FastAPI, HTTPException, Request

app = FastAPI()
WEBHOOK_SECRET = "whs_..." # из ответа на регистрацию вебхука


def verify_signature(secret: str, body: bytes, signature: str) -> bool:
expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)


@app.post("/piratepress/webhook")
async def piratepress_webhook(request: Request) -> dict:
body = await request.body() # сырые байты, не request.json()!
signature = request.headers.get("X-PiratePress-Signature", "")
if not verify_signature(WEBHOOK_SECRET, body, signature):
raise HTTPException(status_code=401)

event = json.loads(body)
if event["event"] == "video.done":
download(event["data"]["result_url"]) # своя логика — в фоне/очереди
return {"ok": True}

Доставка и ретраи​

  • Отвечай 2xx быстро (пара секунд): скачивание mp4 и прочую тяжесть клади в свою очередь. Таймаут или не-2xx = неуспешная доставка.
  • Неуспешная доставка ретраится: 5 попыток (паузы 10 с, 1 мин, 5 мин, 30 мин). После пятой событие не пропадает — заказ всегда можно дополлить через GET /videos/{id} или GET /clips/{id}.
  • Повторная доставка возможна (ретрай после таймаута, когда ты на самом деле ответил 200). Делай обработчик идемпотентным: ключ идемпотентности — event + data.id.
  • URL должен быть публичным HTTPS — самоподписанный сертификат не прокатит, для локальной отладки бери туннель (ngrok и т.п.).