Вебхуки
Получай результаты генерации без опроса — POST на твой URL при завершении джобы. HMAC-подпись Souz-Signature, повторы доставки, callback_url на один запрос.
Вместо опроса GET /v1/jobs/{id} СОЮЗ может сам прислать POST на твой URL, когда джоба завершится (успехом или ошибкой).
Два способа:
- Постоянный вебхук — регистрируешь URL один раз, получаешь события по всем своим джобам.
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}.