Webhook и жизненный цикл медиа-задач
Регистрация mediaWebhookUrl, доставка POST при завершении задачи, опрос GET /api/media/jobs/{id}, коды ошибок, списание кредитов и ограничения идемпотентности.
Операционное руководство по асинхронным медиа-задачам: регистрация webhook, статусы, опрос, коды ошибок, списания и ограничения идемпотентности — строго по текущей реализации в коде.
Жизненный цикл задачи
- POST /api/media/generate — при успехе
202и{ ok: true, jobId, status: "processing" }. Отдельных статусовqueued/runningв API нет: пока задача не завершена, клиент видитprocessing. - Upstream может проходить внутренние стадии (waiting, generating) — они не отражаются в публичном API; при опросе задача остаётся
processing. - Терминальные статусы:
success(естьresultUrls, списание при успехе) илиfail(без списания). - Завершение фиксируется один раз: через callback upstream или при ленивом опросе в
GET /api/media/jobs/{id}.
Опрос: GET /api/media/jobs/{id}
GEThttps://plusvibeapi.ru/api/media/jobs/{id}
| Параметр | Тип | Описание |
|---|---|---|
status | string | processing | success | fail. |
resultUrls | string[] | Подписанные URL /api/media/file/{id}/{idx}?sig=… (без API-ключа). |
priceRub | number | Сумма списания в ₽ (0 при fail или до завершения). |
failMsg | string | null | Безопасное сообщение об ошибке при fail. |
errorCode | string | null | Код ошибки при fail (например media_generation_failed). |
# 2. опросить задачу до готовности
curl https://plusvibeapi.ru/api/media/jobs/JOB_ID \
-H "Authorization: Bearer $PLUSVIBE_API_KEY"
# → {"status":"success","resultUrls":["https://..."],"priceRub":...}Рекомендуемый интервал опроса: 3–5 с, с экспоненциальной задержкой до 30 с. Таймаут на стороне клиента — по SLA вашего приложения (генерация видео может занимать минуты).
Регистрация webhook
GEThttps://plusvibeapi.ru/api/webhooks
PATCHhttps://plusvibeapi.ru/api/webhooks
Один URL на аккаунт: mediaWebhookUrl. Настраивается в личном кабинете → Webhooks или через API с сессией кабинета (cookie после входа). Эндпоинт не принимает sk-pv-… Bearer-ключ — только авторизованную сессию.
# Пример (нужна сессия кабинета, не API-ключ)
curl -X PATCH https://plusvibeapi.ru/api/webhooks \
-H "Content-Type: application/json" \
-b "session=ВАША_СЕССИЯ" \
-d '{"mediaWebhookUrl":"https://example.com/plusvibe/media"}'
# Очистить URL
curl -X PATCH https://plusvibeapi.ru/api/webhooks \
-H "Content-Type: application/json" \
-b "session=ВАША_СЕССИЯ" \
-d '{"mediaWebhookUrl":null}'Требования к URL
| Параметр | Тип | Описание |
|---|---|---|
mediaWebhookUrl | HTTPS URL | Публичный HTTPS-адрес; приватные/loopback заблокированы (SSRF-защита). Пустая строка или null — сброс. |
balanceWebhookUrl | HTTPS URL | Отдельный webhook при низком балансе (не медиа). Порог — balanceWebhookThresholdRub. |
Доставка webhook при завершении задачи
Когда задача переходит в терминальный статус, PlusVibe один раз отправляет POST на зарегистрированный mediaWebhookUrl:
// POST на ваш mediaWebhookUrl при завершении задачи
{
"jobId": "...",
"status": "success", // или "fail"
"model": "veo-3.1",
"kind": "video", // image | video | audio | music | 3d
"resultUrls": ["https://plusvibeapi.ru/api/media/file/..."],
"failMsg": null,
"createdAt": "2026-06-17T10:00:00.000Z"
}Поведение доставки (как в коде)
| Параметр | Тип | Описание |
|---|---|---|
Подпись / auth | — | Исходящий webhook не подписывается. Проверяйте jobId через GET /api/media/jobs/{id} с вашим API-ключом. |
Повторы | нет | Одна попытка POST; при ошибке сети или 5xx повторов нет. Держите опрос как запасной канал. |
Таймаут | системный | fetch без явного AbortSignal — зависит от сетевого стека Node.js. |
Content-Type | application/json | Тело — JSON с полями jobId, status, model, kind, resultUrls, failMsg, createdAt. |
Входящий callback от upstream (kie/fal) — отдельный server-to-server путь
/api/media/callback с опциональным секретом KIE_CALLBACK_SECRET. Это не ваш клиентский webhook.Ошибки POST /api/media/generate
| HTTP | Когда | Списание | Повтор |
|---|---|---|---|
| 202 | Задача принята (ok: true, jobId) | Нет (только после success) | — |
| 400 | Невалидный JSON / тело запроса | Нет | Исправить запрос |
| 401 | Нет или неверный API-ключ | Нет | Проверить Authorization |
| 402 | Недостаточно средств (admission) | Нет | Пополнить баланс |
| 422 | Неизвестная модель, невалидные opts, ok: false от facade | Нет | Исправить model/opts |
| 502 | media_submit_failed — upstream недоступен | Нет | Повтор с backoff |
| 503 | priceUnavailable, routing guard, VIDEO_DISABLED, veo без цены | Нет | Другая модель / позже |
Ошибки опроса и терминальные сбои
| HTTP / status | Смысл | Списание |
|---|---|---|
| 404 | job not found или чужой jobId | Нет |
| processing | Задача ещё выполняется | Нет |
| success | Готово, resultUrls заполнены | Да — priceRub в ответе |
| fail | Генерация не удалась | Нет (priceRub = 0) |
Биллинг и идемпотентность
- Списание один раз при success. В
finalizeJobпереходprocessing → success|failатомарен: повторный callback или опрос не создают второе списание. - fail — бесплатно. При статусе fail дебет не выполняется.
- Admission quote. Если при создании сохранена котировка, при success списывается ровно она (не больше фактического upstream-metering).
- Идемпотентность create — отсутствует. Повторный POST /api/media/generate с тем же телом создаёт новую задачу и новое списание при успехе. Заголовка
Idempotency-Keyнет — при retry после таймаута дедуплицируйте на своей стороне (например, храните jobId). - Webhook клиенту — максимум один раз на jobId после успешного claim терминального статуса.
См. также: Генерация медиа, Каталог медиа-моделей, Ошибки и лимиты (общие коды 401/402/429 для /v1).