Задачи и долгие операции
Всё, кроме ответа чата, — задача с одним объектом (схема Job): его отдают ответ на
запрос, GET /v1/jobs/{id}, поток событий и вебхук. status — queued, in_progress,
completed или failed; data[] — файлы результата; price — null до завершения;
error — той же формы, что тело ошибки.
Ответ сразу или позже
- Картинки, музыка, транскрибация и речь отвечают результатом сразу —
200с готовой задачей. Не успели за 9 минут от приёма запроса (загрузка тела входит в это время) —202с задачей в работе. - Видео — всегда
202. Prefer: respond-async— не ждать:202сразу у любой задачи.- У любого
202— заголовкиLocation: /v1/jobs/{id}иRetry-After(секунды до следующего опроса). - Провал во время ожидания — статус по причине (
400,402,502,503) и тот же объект задачи с полемerror. - Отменить принятую задачу нельзя — метода отмены нет,
DELETE /v1/jobs/{id}отвечает405. Задача доделается и при успехе спишется; обрыв соединения и прекращение опроса её не останавливают.
Как забрать результат после 202
- Опрос —
GET /v1/jobs/{id}с паузой вRetry-After, покаstatusне станетcompletedилиfailed. - Поток событий —
GET /v1/jobs/{id}/events,text/event-stream: первым событием — текущее состояние, затем новый снимок при каждом изменении иdata: [DONE]в конце; раз в 15 секунд — комментарий: ping. Одно соединение живёт до 35 минут; переподключаться можно в любой момент. Поток закрылся без[DONE](истёк срок соединения, обновление сервиса, обрыв сети) — переподключитесь: первым событием придёт текущее состояние. - Вебхук —
callback_urlв запросе: вебхук по завершении задачи.
// EventSource не умеет заголовок Authorization — читаем поток через fetch.
// Поток закрылся без [DONE] — переподключаемся: первым придёт текущее состояние.
async function* jobEvents(apiKey: string, jobId: string) {
for (;;) {
let res: Response;
try {
res = await fetch(`https://api.souz.ai/v1/jobs/${jobId}/events`, { headers: { Authorization: `Bearer ${apiKey}` } });
} catch {
await new Promise((r) => setTimeout(r, 1000)); // сеть — пауза и снова
continue;
}
if (!res.ok) throw new Error(`events: HTTP ${res.status}`);
const reader = res.body!.getReader();
const decoder = new TextDecoder();
let buffer = "";
try {
for (;;) {
const { done, value } = await reader.read();
if (done) break; // без [DONE] — переподключиться
buffer += decoder.decode(value, { stream: true });
let cut: number;
while ((cut = buffer.indexOf("\n\n")) !== -1) {
const frame = buffer.slice(0, cut);
buffer = buffer.slice(cut + 2);
for (const line of frame.split("\n")) {
if (!line.startsWith("data: ")) continue;
const payload = line.slice(6);
if (payload === "[DONE]") return;
yield JSON.parse(payload);
}
}
}
} catch {
// обрыв посреди потока — переподключиться
}
await new Promise((r) => setTimeout(r, 1000));
}
}import json, os, time, requests
def job_events(job_id: str):
url = f"https://api.souz.ai/v1/jobs/{job_id}/events"
headers = {"Authorization": f"Bearer {os.environ['SOUZ_API_KEY']}"}
while True: # поток закрылся без [DONE] — переподключаемся
try:
with requests.get(url, headers=headers, stream=True, timeout=(10, 60)) as res:
res.raise_for_status()
for line in res.iter_lines(decode_unicode=True):
if not line or not line.startswith("data: "):
continue
payload = line[6:]
if payload == "[DONE]":
return
yield json.loads(payload)
except (requests.ConnectionError, requests.Timeout):
pass # обрыв — переподключиться
time.sleep(1)Повтор без второй задачи — Idempotency-Key
Обрыв соединения задачу не отменяет. Повтор с тем же Idempotency-Key (строка до 255
символов, уникальная в аккаунте) вернёт ту же задачу с текущим статусом, без второй задачи и
второго списания.
Повтор с тем же ключом отдаёт исходное задание независимо от изменений настроек аккаунта;
сравниваются только поля запроса. Пресет, потолок цены и настройки каналов, с которыми задача
принята, — в её полях routing, max_price_multiplier и routing_options.
- Ключ длиннее 255 символов —
400 invalid_requestсdetail[].reason: too_long. - Отказ до приёма — ответ с одним
error, без объекта задачи (400,401,402,403,429,503): задачи нет, ключ не занят — исправьте запрос и повторите с тем же ключом. Ответ с объектом задачи, дажеfailed, ключ занимает. - Тот же ключ с другим запросом —
409 idempotency_key_conflict. Это ошибка в коде клиента: новый ключ автоматически не берите, найдите, почему запрос изменился. Сравниваются в том числеrouting,routing_optionsиmax_price_multiplier— как они указаны в запросе: поле, которого не было, и поле с тем же значением, что в настройках, — разные запросы. Поля доставки (store,callback_url) не сравниваются. У картинок, видео и музыки не сравниваетсяresponse_format, у речи формат звука сравнивается, у транскрибации — только «с таймкодами или без». - Повтор задачи со статусом
failedвозвращает её же; для новой попытки — новый ключ. - Работает для картинок, видео, музыки, транскрибации, речи и чата. Ключ хранится вместе с задачей и не продлевает срок файлов.
- У чата сравнивается тело запроса целиком. Повтор во время вызова дожидается его и получает
тот же ответ, после вызова — сразу. Ответ хранится сутки; позже —
502со ссылкой на задачу, итог — вGET /v1/jobs/{id}.
Долгие модели и тяжёлые картинки — один путь кода
Модель, которой бывает мало 9 минут (видео, сбой канала), отвечает то 200, то 202. Отправляйте такие
запросы с Prefer: respond-async и Idempotency-Key: ответ всегда 202, результат —
вебхуком или потоком событий. Тяжёлые референсы — заранее через POST /v1/files,
в запрос — file_….
curl -i https://api.souz.ai/v1/images/generations \
-H "Authorization: Bearer $SOUZ_API_KEY" \
-H "Content-Type: application/json" \
-H "Prefer: respond-async" \
-H "Idempotency-Key: order-1842-cover" \
-d '{"model": "nano-banana-pro", "prompt": "кот-космонавт на Луне"}'HTTP/1.1 202 Accepted
Location: /v1/jobs/job_1a2b3c4d5e6f
Retry-After: 5{
"id": "job_1a2b3c4d5e6f",
"object": "job",
"created": 1758801600,
"created_at": "2026-09-25T12:00:00Z",
"completed_at": null,
"model": "nano-banana-pro",
"modality": "image",
"status": "queued",
"routing": "balanced",
"max_price_multiplier": null,
"routing_options": null,
"attempt_count": 0,
"price": null,
"currency": "RUB",
"usage": null,
"data": [],
"error": null
}