Вебхуки

.md

Получай результаты генерации без опроса — POST на твой URL при завершении джобы. HMAC-подпись Souz-Signature, повторы доставки, callback_url на один запрос.

Вместо опроса GET /v1/jobs/{id} СОЮЗ может сам прислать POST на твой URL, когда джоба завершится (успехом или ошибкой).

Два способа:

  1. Постоянный вебхук — регистрируешь URL один раз, получаешь события по всем своим джобам.
  2. callback_url на запрос — указываешь URL прямо в теле POST .../generations, событие придёт только по этой джобе.

Требования к URL: только https, публичный адрес (не локальная сеть), без редиректов.

Постоянный вебхук

curl https://api.souz.ai/v1/webhooks \
  -H "Authorization: Bearer sk-souz-..." \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com/souz/hook" }'
{ "id": "wh_1a2b3c", "object": "webhook", "url": "https://example.com/souz/hook", "secret": "whsec_..." }

secret показывается один раз — сохрани, им подписывается каждая доставка.

Список и удаление:

GET    /v1/webhooks
DELETE /v1/webhooks/{id}

callback_url на один запрос

curl https://api.souz.ai/v1/images/generations \
  -H "Authorization: Bearer sk-souz-..." \
  -H "Content-Type: application/json" \
  -d '{
    "model": "souz/nano-banana",
    "prompt": "...",
    "callback_url": "https://example.com/hooks/job-42"
  }'

В ответе 202 появится webhook_secret (один раз) — секрет подписи для этой джобы.

Что приходит

POST с JSON — тот же объект джобы плюс event:

{
  "event": "job.completed",
  "id": "job_a1b2c3",
  "object": "image",
  "model": "souz/nano-banana-pro",
  "status": "completed",
  "price": 20,
  "created_at": "2026-06-12T10:00:00.000Z",
  "data": [{ "url": "https://api.souz.ai/v1/files/res_....png?exp=...&sig=..." }],
  "error": null
}

События: job.completed и job.failed (при сбое data: null, причина в error.code). Заголовки: Souz-Event (тип события), Souz-Delivery-Id (уникальный ID доставки — для дедупликации), Souz-Signature (подпись).

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

Подпись в стиле Stripe. Заголовок:

Souz-Signature: t=1781250000,v1=9f86d081884c7d659a2feaa0c55ad015...

где v1 = HMAC_SHA256(secret, "{t}.{сырое_тело}") (hex). Проверяй по сырому телу запроса (до парсинга JSON) и отклоняй старые t (защита от повтора):

import hmac, hashlib, time

def verify(secret: str, signature_header: str, raw_body: bytes) -> bool:
    parts = dict(p.split("=", 1) for p in signature_header.split(","))
    t, v1 = parts["t"], parts["v1"]
    if abs(time.time() - int(t)) > 300:  # старше 5 минут — отказ
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(secret, signatureHeader, rawBody) {
  const parts = Object.fromEntries(signatureHeader.split(",").map((p) => p.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
  return expected.length === parts.v1.length &&
    timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

Доставка и повторы

  • Ответь 2xx быстро (до 10 секунд) — лучше принять и обработать асинхронно.
  • Не 2xx или таймаут → повторы с нарастающей паузой, до 12 попыток на протяжении ~суток.
  • Дедуплицируй по Souz-Delivery-Id: при повторах одно событие может прийти больше одного раза.
  • Вебхук — это уведомление, а не источник истины: при сомнениях перепроверь статус через GET /v1/jobs/{id}.