> ## Documentation Index
> Fetch the complete documentation index at: https://docs.toapis.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook асинхронных задач

> Подписанные события ToAPIs об успехе и окончательной ошибке

> [!NOTE]
>
> **Для пользователей из материкового Китая:** используйте `https://toapis.cn` в качестве эндпоинта (Base URL). Замените `https://toapis.com` на `https://toapis.cn` во всех примерах этого документа.

Для асинхронных image и video ToAPIs отправляет `generation.completed` и `generation.failed`; тест — `endpoint.test`. Доставка at-least-once: обрабатывайте повторный event ID идемпотентно.

В настройках Token задайте HTTPS URL, создайте 32-byte secret (показывается один раз), включите Webhook и отправьте тест. Без `callback_url` используется URL Token. Переопределение может менять только path/query при том же scheme/host/port. Без настройки: `400 webhook_not_configured`.

```json theme={null}
{"id":"evt_xxx","type":"generation.completed","api_version":"v1","created_at":1783953302,"data":{"task_id":"tsk_xxx","client_business_id":"order_123","task_type":"video","model":"model-name","status":"SUCCESS","progress":"100%","result":{"type":"video","data":[{"url":"https://cdn.example/result.mp4","format":"mp4"}]},"usage":{"charge_type":"per_request","requests":1},"billing":{"status":"settled","credits":"100","cost_usd":"0.5"},"error":null,"created_at":1783953000,"finished_at":1783953300,"completed_at":1783953300,"expires_at":1784039700}}
```

Окончательная ошибка отправляется только после завершения внутренних повторов и возврата средств или финального биллинга.

Значения и действия описаны в [справочнике ошибок](https://docs.toapis.com/docs/ru/api-reference/errors.md). Webhook `data.error` не гарантирует поле `reason`, доступное в некоторых проверках задач.

```json theme={null}
{"id":"evt_failure","type":"generation.failed","api_version":"v1","created_at":1783953302,"data":{"task_id":"tsk_failure","client_business_id":"order_456","task_type":"video","model":"model-name","status":"FAILURE","progress":"100%","result":null,"billing":{"status":"refunded","credits":"0","cost_usd":"0"},"error":{"code":"generation_failed","message":"Generation failed after retries"},"created_at":1783953000,"finished_at":1783953300}}
```

```json theme={null}
{"id":"evt_test","type":"endpoint.test","api_version":"v1","created_at":1783953302,"data":{"task_id":"endpoint_test","task_type":"image","status":"SUCCESS","progress":"100%","result":{"message":"ToAPIs webhook endpoint test"},"error":null,"created_at":0}}
```

`task_type`: `image|video`. Result соответствует публичному query API. Channel ID, upstream task ID/keys, credentials и PrivateData не передаются.

## Использование, оплата и отличия от запроса задачи

Успешные события изображений и видео могут содержать `data.usage`: оплачиваемые количества, доступные токены модели и счётчики инструментов. Определения совпадают с запросами задач; см. [Поля использования задачи](https://docs.toapis.com/docs/ru/api-reference/account/pricing.md). Видеособытия также могут содержать `data.generation`; метаданные Seedance и поле совместимости описаны в [запросе видеозадачи](https://docs.toapis.com/docs/ru/api-reference/tasks/video-status.md).

`data.billing` может содержать `status`, `credits`, `cost_usd`. Суммы являются десятичными строками и опускаются при `pending`; `refunded` означает подтверждённую нулевую итоговую плату. Неподтверждённый объект опускается, что не означает бесплатное использование. Webhook и запрос задачи используют одинаковые условия подтверждения: если списание подтверждено, в том числе для задач с фиксированной ценой, оплаченных сразу при отправке, оба возвращают `settled` и фактическую сумму. Если `data.usage` содержит количества для тарификации, например `charge_type`, `data.billing` тоже всегда имеет статус `settled`.

### Значения полей `data.billing`

| Поле | Тип | Значения |
| - | - | - |
| `status` | string | `pending`: расчёт или возврат ещё не подтверждён; суммы пропускаются. `settled`: итоговое списание подтверждено и возвращается фактическая сумма; для бесплатных задач сумма равна `"0"`. `refunded`: средства возвращены или после сбоя подтверждено отсутствие итогового списания; обе суммы равны `"0"` |
| `credits` | string | Фактически списанные ToAPIs Credits в виде десятичной строки, например `"100"`. Возвращается только для `settled` и `refunded` |
| `cost_usd` | string | Сумма того же списания в USD в виде десятичной строки, например `"0.5"`. Возвращается при тех же условиях, что и `credits`; включает скидки и множители и не является себестоимостью у поставщика |

Успешные события получают `settled`, а `refunded` - если при расчёте возвращено всё предварительно зарезервированное. Событие о сбое отправляется после завершения возврата и получает `refunded`. Если сумму подтвердить нельзя, объект `data.billing` пропускается целиком.

### Значения полей `data.usage`

| Поле | Тип | Значения |
| - | - | - |
| `charge_type` | string | Способ тарификации: `per_request` - за запрос, `per_second` - за секунды, `per_token` - за токены |
| `requests` | integer | Количество оплачиваемых запросов, возвращается для `per_request`. Параметр запроса изображения `n` напрямую не умножается |
| `billed_tokens` | integer | Токены, использованные для расчёта цены, возвращаются для `per_token` |
| `output_seconds` | integer | Оплачиваемая длительность выходного видео в целых секундах, возвращается для `per_second` |
| `input_seconds` | integer | Оплачиваемая длительность входного видео в целых секундах; при нуле пропускается |
| `input_images` | integer | Оплачиваемые входные изображения после вычета бесплатной квоты; при нуле пропускается |
| `input_tokens`, `output_tokens`, `total_tokens` | integer | Входные, выходные и общее число токенов, сообщённые моделью |
| `input_tokens_details` | object | Детализация входа изображения: `text_tokens`, `image_tokens`, `cached_tokens`, а также `cached_tokens_details.text_tokens`, `cached_tokens_details.image_tokens`. Кэшированные токены уже входят в общее число входных токенов |
| `output_tokens_details` | object | Детализация выхода изображения: `text_tokens`, `image_tokens`; `text_or_reasoning_tokens` добавляется, когда текст и рассуждение тарифицируются по одной цене и не разделены, и не входит в `text_tokens` |
| `prompt_tokens`, `completion_tokens` | integer | Токены, сообщённые видеомоделями |
| `tool_usage` | object | Счётчики инструментов: `web_search`, `image_search`, `grounding_queries`; возвращаются при фактическом использовании |

Поля от `charge_type` до `input_images` - это оплачиваемые количества, они возвращаются только вместе с `data.billing.status=settled`. Детализация токенов и счётчики инструментов возвращаются независимо, в том виде, как их предоставила вышестоящая модель. Не возвращённые поля пропускаются и не заменяются нулями.

Webhook содержит снимок на момент создания события, а запрос задачи читает последнее подтверждённое состояние. Доставка не ждёт отсутствующих количеств или сумм; правила повторов не меняются. Созданные события не дополняются задним числом, повторы используют исходное тело события.

Структура события и `api_version: "v1"` не меняются. Webhook использует `data.task_id`, `SUCCESS` / `FAILURE` и строковый прогресс `"100%"`; запрос задачи - `id`, статусы в нижнем регистре и целочисленный прогресс. Поля времени совпадают по именам и смыслу с публичным запросом `generation.task`, все значения в Unix-секундах: `data.created_at` - время отправки задачи, `data.finished_at` - время завершения, а успешные события также содержат `data.completed_at` и `data.expires_at` (срок действия результата, время завершения плюс 24 часа). Поля без значения опускаются. Прежнее поле `data.submitted_at` заменено на `data.created_at` и больше не передаётся. Верхнее поле события `created_at` означает время создания события, а не время отправки задачи. URL медиа могут отличаться из-за разных политик доступа.

### Пример успешного видеособытия Seedance

Примеры показывают поля, а не текущие цены модели или постоянный способ тарификации. Используйте поля и суммы, возвращённые для конкретной задачи.

Успешные события Seedance также могут содержать `fallback_url` и `fallback_last_frame_url` в `data.result.data[]`. Сначала используйте `url` и `last_frame_url`, а резервные адреса только при недоступности исходных. Отсутствие резервных полей не означает ошибку задачи. Событие сохраняет URL на момент создания: последующие изменения копий не меняют существующее событие и его повторные отправки. Определения полей и правила использования см. в разделе [Статус видеозадачи](https://docs.toapis.com/docs/ru/api-reference/tasks/video-status.md).

```json theme={null}
{
  "id": "evt_seedance_example",
  "type": "generation.completed",
  "api_version": "v1",
  "created_at": 1792400061,
  "data": {
    "task_id": "task_seedance_example",
    "client_business_id": "order_123",
    "task_type": "video",
    "model": "seedance-2-global",
    "status": "SUCCESS",
    "progress": "100%",
    "result": {
      "type": "video",
      "data": [
        {
          "url": "https://cdn.example/result.mp4",
          "fallback_url": "https://files.toapis.com/videos/task_seedance_example/result.mp4",
          "format": "mp4",
          "last_frame_url": "https://cdn.example/last-frame.jpg",
          "fallback_last_frame_url": "https://files.toapis.com/videos/task_seedance_example/last-frame.jpg"
        }
      ]
    },
    "usage": {
      "charge_type": "per_token",
      "billed_tokens": 87300,
      "completion_tokens": 87300,
      "total_tokens": 87300
    },
    "billing": {
      "status": "settled",
      "credits": "87.3",
      "cost_usd": "0.4365"
    },
    "generation": {
      "duration": 5,
      "resolution": "720p",
      "ratio": "16:9",
      "generate_audio": true,
      "usage": {
        "completion_tokens": 87300,
        "total_tokens": 87300
      }
    },
    "error": null,
    "created_at": 1792400000,
    "finished_at": 1792400060,
    "completed_at": 1792400060,
    "expires_at": 1792486460
  }
}
```

```text theme={null}
User-Agent: ToAPIs-Webhook/1.0
X-ToAPIs-Webhook-Id
X-ToAPIs-Webhook-Timestamp
X-ToAPIs-Webhook-Signature: v1=<current>[,v1=<previous>]
```

Строка подписи: `event_id + "." + timestamp + "." + raw_request_body`, HMAC-SHA256. Для первой доставки отклоняйте timestamp старше 5 минут. 24 часа после ротации принимайте новую или предыдущую подпись.

```python Python theme={null}
def verify(headers, raw_body, secrets):
    event_id=headers["X-ToAPIs-Webhook-Id"]; timestamp=int(headers["X-ToAPIs-Webhook-Timestamp"])
    if abs(int(time.time())-timestamp)>300: return False
    signed=f"{event_id}.{timestamp}.".encode()+raw_body
    received=[v.strip()[3:] for v in headers["X-ToAPIs-Webhook-Signature"].split(",") if v.strip().startswith("v1=")]
    return any(hmac.compare_digest(hmac.new(s.encode(),signed,hashlib.sha256).hexdigest(),r) for s in secrets for r in received)
```

```javascript Node.js theme={null}
function verify(headers,rawBody,secrets){const id=headers['x-toapis-webhook-id'];const ts=Number(headers['x-toapis-webhook-timestamp']);if(!Number.isFinite(ts)||Math.abs(Date.now()/1000-ts)>300)return false;const signed=Buffer.concat([Buffer.from(`${id}.${ts}.`),rawBody]);const received=headers['x-toapis-webhook-signature'].split(',').filter(v=>v.trim().startsWith('v1=')).map(v=>v.trim().slice(3));return secrets.some(secret=>received.some(sig=>{const expected=crypto.createHmac('sha256',secret).update(signed).digest('hex');return expected.length===sig.length&&crypto.timingSafeEqual(Buffer.from(expected),Buffer.from(sig));}));}
```

```go Go theme={null}
func Verify(id,ts,sig string,body []byte,secrets []string) bool { unix,err:=strconv.ParseInt(ts,10,64);now:=time.Now().Unix();if err!=nil||now-unix>300||unix-now>300{return false};signed:=append([]byte(id+"."+ts+"."),body...);for _,secret:=range secrets{mac:=hmac.New(sha256.New,[]byte(secret));mac.Write(signed);expected:=hex.EncodeToString(mac.Sum(nil));for _,v:=range strings.Split(sig,","){if hmac.Equal([]byte(expected),[]byte(strings.TrimPrefix(strings.TrimSpace(v),"v1="))){return true}}};return false }
```

Первая доставка выполняется сразу; затем повторы через 10с, 30с, 2м, 10м, 1ч, 6ч и 24ч. Успех — только `2xx`; `3xx` не перенаправляются, `4xx/5xx` повторяются, timeout 10 секунд. HTTPS, userinfo, fragment, недопустимый port, SSRF, DNS/IP и DNS rebinding проверяются при каждой доставке. Используйте query API как резерв и быстро сохраняйте временные result URL. Real-avatar, платежи и OAuth callback не меняются. [Лимиты](https://docs.toapis.com/docs/ru/api-reference/rate-limits/async-tasks.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.