---
title: Вебхуки
description: Получайте POST-уведомление о завершении генерации с проверкой подписи HMAC-SHA256 и автоматическими ретраями.
---

Передайте `webhook_url` при постановке генерации — и по её завершении Clipia отправит `POST` на этот URL с результатом. Это избавляет от опроса статуса. Каждая доставка подписана HMAC-SHA256 (заголовок `X-Clipia-Signature`), а при сбое повторяется до 6 раз с экспоненциальной задержкой.

## Payload

При успехе:

```json
{
  "request_id": "764cabcf-b745-4b3e-ae38-1200304cf45b",
  "status": "OK",
  "payload": {
    "model": "nano-banana-2",
    "output": { "images": [{ "url": "https://media.clipia.ai/works/result.png" }] },
    "cost": 12
  }
}
```

При ошибке:

```json
{
  "request_id": "764cabcf-b745-4b3e-ae38-1200304cf45b",
  "status": "ERROR",
  "error": { "code": "GENERATION_FAILED", "message": "..." }
}
```

Поле `output` принимает одну из трёх форм — в зависимости от типа модели: `images: [{ url }]`, `video: { url }` либо `audio: { url }` для синтеза речи.

<Callout type="warn" title="Payload вебхука компактнее ответа REST">
Два отличия, о которые чаще всего спотыкаются интеграции:

1. `original_url` (оригинал в полном качестве) в вебхук **не кладётся** — если он нужен, запросите результат по `GET /v1/requests/:id`.
2. При мульти-выдаче изображений в вебхук попадает **только первый** URL; полный список — там же, по `request_id`.
</Callout>

### Поля payload

<TypeTable
  type={{
    request_id: {
      type: 'string (UUID)',
      description: 'Идентификатор генерации — совпадает с request_id из ответа submit.',
      required: true,
    },
    status: {
      type: '"OK" | "ERROR"',
      description: 'Терминальный исход доставки.',
      required: true,
    },
    'payload.model': {
      type: 'string',
      description: 'Slug модели (только при status="OK").',
    },
    'payload.output': {
      type: 'object',
      description: 'Результат генерации со ссылками на медиа (только при status="OK").',
    },
    'payload.cost': {
      type: 'number',
      description: 'Списанная стоимость в кредитах (только при status="OK").',
    },
    error: {
      type: 'object',
      description: 'Поля code и message (только при status="ERROR").',
    },
  }}
/>

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

Каждая доставка несёт три заголовка:

```http
X-Clipia-Webhook-Id: 1f2e3d4c-5b6a-7890-abcd-ef0123456789
X-Clipia-Timestamp: 1717243200
X-Clipia-Signature: t=1717243200,v1=5257a869e7...
```

Подпись считается как `HMAC_SHA256(secret, "{timestamp}.{raw_body}")`, где `secret` — webhook signing secret из консоли разработчика. Сравнивайте подпись над **сырым** телом запроса (до парсинга JSON) и используйте constant-time сравнение.

<Tabs items={['Node.js', 'Python']}>

<Tab value="Node.js">

```js
import crypto from "node:crypto";

const secret = process.env.CLIPIA_WEBHOOK_SECRET;

function verify(req) {
  const sig = req.headers["x-clipia-signature"];        // "t=...,v1=..."
  const parts = Object.fromEntries(sig.split(",").map(p => p.split("=")));
  const signed = `${parts.t}.${req.rawBody}`;
  const expected = crypto.createHmac("sha256", secret).update(signed).digest("hex");
  const ok = crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300; // окно 5 мин
  return ok && fresh;
}
```

</Tab>

<Tab value="Python">

```python
import hashlib
import hmac
import os
import time

SECRET = os.environ["CLIPIA_WEBHOOK_SECRET"].encode()

def verify(headers: dict, raw_body: bytes) -> bool:
    sig = headers["X-Clipia-Signature"]               # "t=...,v1=..."
    parts = dict(p.split("=", 1) for p in sig.split(","))
    signed = f"{parts['t']}.".encode() + raw_body
    expected = hmac.new(SECRET, signed, hashlib.sha256).hexdigest()
    ok = hmac.compare_digest(parts["v1"], expected)
    fresh = abs(time.time() - int(parts["t"])) < 300   # 5-минутное окно
    return ok and fresh
```

</Tab>

</Tabs>

<Callout type="warn" title="Проверяйте свежесть">
Отвергайте доставку, если `X-Clipia-Timestamp` отличается от текущего времени более чем на 5 минут — это защищает от повторного воспроизведения старых запросов.
</Callout>

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

- Отвечайте кодом `2xx` в течение 10 секунд.
- При не-`2xx` или таймауте доставка повторяется с экспоненциальной задержкой, до 6 попыток.
- История доставок видна в консоли разработчика — удобно для отладки.

<Callout type="info" title="Обрабатывайте идемпотентно">
Одна и та же доставка может прийти повторно. Дедуплицируйте по `request_id` (или `X-Clipia-Webhook-Id`), прежде чем выполнять побочные эффекты.
</Callout>
