Webhook и жизненный цикл медиа-задач

Регистрация mediaWebhookUrl, доставка POST при завершении задачи, опрос GET /api/media/jobs/{id}, коды ошибок, списание кредитов и ограничения идемпотентности.

Операционное руководство по асинхронным медиа-задачам: регистрация webhook, статусы, опрос, коды ошибок, списания и ограничения идемпотентности — строго по текущей реализации в коде.

Жизненный цикл задачи

  1. POST /api/media/generate — при успехе 202 и { ok: true, jobId, status: "processing" }. Отдельных статусов queued / running в API нет: пока задача не завершена, клиент видит processing.
  2. Upstream может проходить внутренние стадии (waiting, generating) — они не отражаются в публичном API; при опросе задача остаётся processing.
  3. Терминальные статусы: success (есть resultUrls, списание при успехе) или fail (без списания).
  4. Завершение фиксируется один раз: через callback upstream или при ленивом опросе в GET /api/media/jobs/{id}.

Опрос: GET /api/media/jobs/{id}

GEThttps://plusvibeapi.ru/api/media/jobs/{id}
ПараметрТипОписание
statusstringprocessing | success | fail.
resultUrlsstring[]Подписанные URL /api/media/file/{id}/{idx}?sig=… (без API-ключа).
priceRubnumberСумма списания в ₽ (0 при fail или до завершения).
failMsgstring | nullБезопасное сообщение об ошибке при fail.
errorCodestring | 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
ПараметрТипОписание
mediaWebhookUrlHTTPS URLПубличный HTTPS-адрес; приватные/loopback заблокированы (SSRF-защита). Пустая строка или null — сброс.
balanceWebhookUrlHTTPS 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-Typeapplication/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
502media_submit_failed — upstream недоступенНетПовтор с backoff
503priceUnavailable, routing guard, VIDEO_DISABLED, veo без ценыНетДругая модель / позже

Ошибки опроса и терминальные сбои

HTTP / statusСмыслСписание
404job 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).