Джобы и результаты
Жизненный цикл async-задач, polling, подписанные ссылки и честная оговорка — доступность и фактическое удаление результатов пока не гарантированы.
Картинки и видео создаются асинхронно: POST .../generations сразу возвращает джобу, а результат появляется в ней позже.
Что возвращает создание
| Ответ | Когда |
|---|---|
202 + джоба queued |
Новая джоба принята в работу |
200 + джоба |
Повтор с тем же Idempotency-Key, а существующая джоба не failed — вернём её как есть, в любом статусе (queued, in_progress, completed) |
400 + джоба failed |
Либо повтор ключа для уже упавшей джобы, либо запрос отклонён сразу (модель отключена, неподдержанное поле, не хватило кредитов) |
400 + короткий конверт invalid_request |
Ошибка на границе: модели не существует, тело кривое, плохой референс |
Разбор всех случаев и что при этом происходит с деньгами — в Ошибках.
Статусы
queued → in_progress → completed
↘ failed
| Статус | Значение |
|---|---|
queued |
Принята, ждёт обработки |
in_progress |
Генерируется у провайдера |
completed |
Готово — ссылка в data[0].url |
failed |
Не получилось — причина в error.code; деньги не списаны: резерв возвращён либо не создавался |
Опрос
GET /v1/jobs/{id}
curl https://api.souz.ai/v1/jobs/job_a1b2c3 \
-H "Authorization: Bearer sk-souz-..."
Ответ — всегда полный объект джобы:
{
"id": "job_a1b2c3",
"object": "image",
"model": "souz/nano-banana-pro",
"status": "completed",
"price": 121500,
"created_at": 1781250000,
"parameters": {
"schema": "souz.parameters/v1",
"requested": { "resolution": "2K" },
"effective": { "aspect_ratio": "1:1", "resolution": "2K" },
"degradations": []
},
"data": [{ "url": "https://api.souz.ai/v1/files/res_....png?exp=...&sig=..." }]
}
price — целые микро-USD, как все денежные поля API (121 500 µUSD ≈ 12.15 кредита).
parameters есть у новых normalized job и одинаково выглядит в create/replay, polling и
webhook. requested содержит только source-free канонический intent; URL/data URL, digest,
object key и provider locator в него не попадают. У старых legacy job поле отсутствует.
effective остаётся null, пока attempt-owned outcome не зафиксирован вместе с durable send
intent; при этом degradations пуст. После этого поля проецируются из immutable snapshot
текущей/terminal попытки. При allow_degradation:true нетерминальный price остаётся maximum
reserve, а terminal success показывает фактический capture. Если резерв был создан и затем
возвращён, terminal failed показывает 0; pre-reserve capability/validation failure также имеет
price: 0. Отдельное исключение — insufficient_credits: резерва не было, а price сохраняет
рассчитанную maximum charge.
При failed вместо data приходит error:
{ "status": "failed", "error": { "code": "content_blocked", "message": "content_blocked", "type": "content_blocked" } }
Для capability_mismatch тот же error дополнен details.schema = "souz.capability-mismatch/v1", отсортированным blocking[] и
available_degradations[]. Это provider-opaque описание: имена/модели провайдера, стоимость,
попытки, секреты и locator-ы туда не попадают.
Рекомендуемая частота опроса: картинки — раз в 2–5 сек, видео — раз в 5–10 сек.
Вебхуки добавляют push-уведомление, но до RSA-109 не заменяют polling fallback.
Результат апстрима не публикуется вслепую. Перед сохранением Souz требует ровно один ожидаемый
артефакт, ограничивает объём ответа и проверяет URL, формат и полную декодируемость изображения.
Если уже оплаченный результат небезопасен или неоднозначен, джоба остаётся in_progress, а не
переключается на другого провайдера и не возвращает резерв без доказательства. Сырые URL и детали
ошибок провайдера клиенту не выдаются. Видео в первом сертифицированном срезе выключено:
запрос получает model_unavailable до резерва кредитов и вызова провайдера.
Файлы результатов
data[0].url — подписанная ссылка (?exp=...&sig=...): её можно открывать в браузере и отдавать куда угодно без API-ключа, пока не истекла подпись. Если подпись истекла — запроси джобу ещё раз и получишь свежую ссылку.
Retention пока не подтверждён. Не гарантируются ни минимальный срок доступности, ни срок
фактического удаления объекта: планируй скачивание сразу после completed, но не считай
недоступную ссылку доказательством удаления.
Что означают ответы по ссылке на файл и что делать:
| Ответ | Что случилось | Что делать |
|---|---|---|
403 forbidden (наш JSON) |
Подпись ссылки невалидна или истекла; сам файл может быть на месте | Шаг 1 ниже |
403 / 404 без нашего тела |
Непрозрачный ответ файлового хоста при прямой раздаче: это может быть отсутствие объекта, а может — права доступа, конфигурация или временный сбой хранилища. Мы такой ответ не формируем и не проверяли объект перед редиректом | Шаги 1–3 ниже |
404 not_found (наш JSON) |
Локальная ветка сейчас сводит любую ошибку чтения (ENOENT, права, I/O, конфигурацию) к этому ответу |
Не считать доказательством удаления: свежая ссылка → повтор с паузой → поддержка |
410 file_expired (наш JSON) |
Наш структурированный ответ: мы отдавали файл через свой прокси и объекта в хранилище нет | Отсутствие подтверждено; если результат нужен — генерировать заново |
502 storage_unavailable |
Временная проблема хранилища | Повторить с паузой — это не удаление |
Порядок восстановления для неоднозначных 403/404 (не начинай с перегенерации — она платная):
- Обнови ссылку. Запроси
GET /v1/jobs/{id}— там свежая подпись. Часто этого достаточно. - Повтори с нарастающей паузой. Временные сбои хранилища и CDN выглядят так же, как отсутствие файла, но проходят сами.
- Не помогло — напиши в поддержку. Устойчивые
403/404от файлового хоста могут быть проблемой конфигурации на нашей стороне; перегенерация её не вылечит.
Генерировать заново имеет смысл после структурированного 410 — или когда отсутствие
подтверждено отдельно. Локальный структурированный 404 пока тоже неоднозначен (RSA-92).
И учти: если причина была в сбое хранилища, новая генерация может упереться в него же.
⚠️ В тексте
messageу410пока встречается упоминание «~24 часа» — это устаревшая строка в коде, а не обещание. Никакого гарантированного срока хранения сейчас нет; ориентируйся наerror.code.
Правило простое: скачивай результат сразу после completed и храни у себя.
Идемпотентность
Передай непустой Idempotency-Key: <твой-уникальный-id> (до 255 символов) при создании
image/video-джобы. Souz claim'ит ключ в пределах аккаунта до проверки текущего каталога,
callback DNS, загрузки референсов, резерва и enqueue. Поэтому повтор того же смыслового запроса
вернёт исходную job даже если source URL уже протух или модель позже отключили; исходные URL
заново не читаются. Модель и webhook_secret берутся из исходного admission, а не из retry.
Тот же ключ с другим смысловым payload получает 409 idempotency_conflict. Если первый
одинаковый запрос ещё готовит job, конкурентный retry может получить retryable
409 idempotency_in_progress и Retry-After; повтори его позже с тем же телом — после bind он
вернёт исходную job. Порядок JSON-полей значения не имеет, порядок элементов массивов имеет;
omitted optional и null там, где API трактует их одинаково, нормализуются одинаково.
Без заголовка каждый вызов остаётся новой генерацией.
curl https://api.souz.ai/v1/images/generations \
-H "Authorization: Bearer sk-souz-..." \
-H "Idempotency-Key: order-42-avatar" \
-H "Content-Type: application/json" \
-d '{ "model": "souz/nano-banana-2", "prompt": "..." }'
Дедлайн
Provider-processing deadline с запасом — 15 минут для картинки и 90 минут для видео.
По его истечении джоба остаётся публично in_progress, пока система разбирает неоднозначный
результат. Примерно через 10 минут grace начинается proof-only сверка, но это не таймер
возврата: подтверждённый результат даёт completed, доказанное отсутствие работы —
failed: model_unavailable и возврат резерва, а pending/неработающий lookup/отсутствие
механизма оставляют джобу in_progress с удержанным резервом. Worker повторяет сверку и
эскалирует размер и возраст backlog оператору.
Тот же режим действует, когда связь с провайдером оборвалась после отправки запроса (таймаут,
разрыв, 408 или любой 5xx от шлюза): раз мы не знаем, взял он работу или нет, джоба не отдаётся другому
провайдеру и её резерв не возвращается сразу. Она остаётся in_progress, пока сверка не выяснит
судьбу: подтверждённый результат даёт completed с обычным списанием, а failed + возврат
появляется только после доказательства, что работа не была принята или закончилась без
результата. Неподтверждённая судьба остаётся in_progress; оператор видит такой backlog.
Legacy-исключение из «джоба всегда придёт к терминальному статусу»: если обнаружена старая или
противоречивая пара «списание есть, доставляемого результата нет», джоба замораживается и остаётся
in_progress до разбора. Объявлять успех нечем, а реальный расход не отменяется выдуманной
проводкой. Для новых async-job manifest результата, списание и terminal status коммитятся одной
транзакцией.
⚠️ Намерение отправки коммитится до provider call, поэтому рестарт не превращает одну логическую попытку во второй submit. Принятый async-результат восстанавливается по тому же provider task и immutable manifest; sync body-only вариант без такого механизма не принимается в production-контур. Подробности про деньги — в Кредитах.