Photo Studio Service — API Contract

Этот документ является единственным источником правды для API Photo Studio Service.

Бэкенд реализует контракт. Фронтенд потребляет контракт. Отклонения запрещены.

URL-префикс

Все пути ниже — через API Gateway: {gateway}/api/v1/gensvc/.... Внутренние пути сервиса без /api/ и /gensvc/ сегментов (/v1/jobs/...). При прямом обращении к сервису использовать внутренние пути. Подробнее — Overview.

Auth

Все endpoints (кроме /healthz, /readyz) требуют:

Authorization: Bearer <JWT>

JWT валидируется через POST /internal/auth/validate Auth Service. Permissions извлекаются из ответа introspection, кэшируются в Redis (TTL 60 сек).

Ошибки авторизации возвращаются в формате RFC 7807 Problem Details:

{
  "type": "about:blank",
  "title": "Unauthorized",
  "status": 401,
  "detail": "missing or invalid token"
}

Содержание

Jobs

Presets

User Presets

Admin Presets


POST /api/v1/gensvc/jobs/photo

Создаёт задание на генерацию фото в выбранном стиле (style transfer).

ПараметрЗначение
AuthBearer JWT
Permissiongensvc.photo.create
Content-Typemultipart/form-data

Request Body (multipart)

ПолеТипRequiredОписание
imagebinaryyesФото блюда — JPEG, PNG или WebP, макс. 10 MB
metastring (JSON)yesJSON-строка с параметрами задания

Структура meta (JSON):

{
  "preset_id": "string, optional — ID системного пресета",
  "user_preset_id": "string, optional — ID личного пресета",
  "extra_prompt": "string, optional, макс. 500 символов — дополнительная инструкция",
  "size": "string, optional — один из: delivery-1x1, story-9x16, banner-16x9, menu-4x3",
  "count": "integer, optional, 1-4 — количество вариантов (default: 1)"
}

Ровно одно из preset_id или user_preset_id обязательно.

Headers

ЗаголовокОписание
Idempotency-KeyUUID, опциональный. Защита от дублирования при ретраях

Response 202

{
  "job_id": "string — ULID с префиксом job_",
  "status_url": "string — URL для polling статуса",
  "stream_url": "string — URL SSE-потока"
}

Errors

CodeHTTPКогда
UNAUTHORIZED401Нет токена или невалидный JWT
FORBIDDEN403Нет permission gensvc.photo.create
VALIDATION_ERROR400Не указан ни preset_id, ни user_preset_id; оба указаны одновременно; extra_prompt > 500 символов
PRESET_NOT_FOUND404Пресет с указанным ID не найден или выключен
IMAGE_TOO_LARGE413Изображение превышает 10 MB
UNSUPPORTED_MEDIA_TYPE415Формат файла не JPEG/PNG/WebP
INTERNAL_ERROR500Внутренняя ошибка

POST /api/v1/gensvc/jobs/enhance

Создаёт задание на улучшение фото — AI сохраняет блюдо, пересоздаёт сцену по текстовому описанию.

ПараметрЗначение
AuthBearer JWT
Permissiongensvc.photo.create
Content-Typemultipart/form-data

Request Body (multipart)

ПолеТипRequiredОписание
imagebinaryyesФото блюда — JPEG, PNG или WebP, макс. 10 MB
metastring (JSON)yesJSON-строка с параметрами

Структура meta (JSON):

{
  "extra_prompt": "string, required, 1-1000 символов — описание желаемой сцены",
  "size": "string, optional — один из: delivery-1x1, story-9x16, banner-16x9, menu-4x3",
  "count": "integer, optional, 1-4 — количество вариантов (default: 1)"
}

Response 202

Та же структура что и у POST /jobs/photo (job_id, status_url, stream_url).

Errors

CodeHTTPКогда
UNAUTHORIZED401Нет токена или невалидный
FORBIDDEN403Нет permission gensvc.photo.create
VALIDATION_ERROR400extra_prompt пустой или > 1000 символов
IMAGE_TOO_LARGE413Изображение > 10 MB
UNSUPPORTED_MEDIA_TYPE415Не JPEG/PNG/WebP
INTERNAL_ERROR500Внутренняя ошибка

GET /api/v1/gensvc/jobs

Список заданий с keyset-пагинацией.

ПараметрЗначение
AuthBearer JWT
Permissiongensvc.history.read (свои) или gensvc.history.read.all (все франшизы)

Query Parameters

ParamTypeRequiredDescription
cursorstringnoНепрозрачный cursor из поля next_cursor предыдущего ответа
limitintegerno1–100, default: 20
statusstringnoФильтр по статусу: pending, running, succeeded, failed, cancelled

Response 200

{
  "items": [
    {
      "id": "string — ULID job_...",
      "kind": "string — photo-studio | enhance",
      "status": "string — pending | running | succeeded | failed | cancelled",
      "user_id": "string",
      "franchise_id": "string",
      "preset_id": "string | null",
      "user_preset_id": "string | null",
      "extra_prompt": "string | null",
      "input_asset_id": "string | null",
      "output_asset_id": "string | null",
      "size": "string | null",
      "count": "integer",
      "retry_of": "string | null",
      "error_code": "string | null",
      "error_message": "string | null",
      "created_at": "datetime",
      "started_at": "datetime | null",
      "finished_at": "datetime | null"
    }
  ],
  "next_cursor": "string — пустая строка если страниц больше нет"
}

Фильтрация по scope

Без gensvc.history.read.all возвращаются только задания текущего пользователя (user_id = me). С gensvc.history.read.all — все задания франшизы (franchise_id = my_franchise).

Errors

CodeHTTPКогда
UNAUTHORIZED401Нет токена
FORBIDDEN403Нет ни gensvc.history.read, ни gensvc.history.read.all

GET /api/v1/gensvc/jobs/{id}

Детали конкретного задания.

ПараметрЗначение
AuthBearer JWT
Permissiongensvc.history.read (только своё) или gensvc.history.read.all (любое в рамках франшизы)

Path Parameters

ParamTypeDescription
idstringULID задания (job_...)

Response 200

Полный объект Job (та же структура что и в items[] списка).

Errors

CodeHTTPКогда
UNAUTHORIZED401Нет токена
FORBIDDEN403Задание чужое, а gensvc.history.read.all нет
NOT_FOUND404Задание не найдено или принадлежит другой франшизе

GET /api/v1/gensvc/jobs/{id}/stream

SSE-поток статусных событий задания. Клиент подключается и получает обновления в реальном времени.

ПараметрЗначение
AuthBearer JWT
Content-Type ответаtext/event-stream

Path Parameters

ParamTypeDescription
idstringULID задания

SSE Events

Каждое событие — JSON в поле data::

data: {"status":"running"}\n\n
data: {"status":"succeeded","output_asset_id":"ast_01HXYZ..."}\n\n
data: {"status":"failed","error":"timeout from AI provider"}\n\n

Поток закрывается при переходе в терминальный статус (succeeded, failed, cancelled).

Errors

CodeHTTPКогда
UNAUTHORIZED401Нет токена
NOT_FOUND404Задание не найдено

POST /api/v1/gensvc/jobs/{id}/retry

Создаёт новое задание с теми же параметрами. Новый Job содержит retry_of = id.

ПараметрЗначение
AuthBearer JWT
Permissiongensvc.photo.create

Path Parameters

ParamTypeDescription
idstringULID исходного задания

Response 202

{
  "job_id": "string",
  "status_url": "string",
  "stream_url": "string"
}

Errors

CodeHTTPКогда
UNAUTHORIZED401Нет токена
FORBIDDEN403Нет gensvc.photo.create или задание чужое
NOT_FOUND404Исходное задание не найдено
BUSINESS_RULE_VIOLATION422Исходное задание ещё не завершено (статус не терминальный)

GET /api/v1/gensvc/presets

Каталог системных пресетов (только включённые).

ПараметрЗначение
AuthBearer JWT
Permissiongensvc.access

Query Parameters

ParamTypeRequiredDescription
categorystringnoФильтр по категории: bar-lounge, delivery-and-takeout, fine-dining, lifestyle, menu, pastry-bakery, studio
qstringnoПолнотекстовый поиск по name_en и slug
limitintegerno1–500, default: 50
offsetintegernodefault: 0

Response 200

{
  "items": [
    {
      "id": "string",
      "slug": "string",
      "name_en": "string",
      "name_ru": "string | null",
      "category": "string — основная категория (legacy)",
      "reference_object_key": "string",
      "enabled": true,
      "created_at": "datetime"
    }
  ],
  "limit": 50,
  "offset": 0
}

Errors

CodeHTTPКогда
UNAUTHORIZED401Нет токена
FORBIDDEN403Нет gensvc.access

GET /api/v1/gensvc/presets/{id}

Детали пресета с presigned URL на reference-изображение.

ПараметрЗначение
AuthBearer JWT
Permissiongensvc.access

Path Parameters

ParamTypeDescription
idstringID пресета (prs_...)

Response 200

{
  "id": "string",
  "slug": "string",
  "name_en": "string",
  "name_ru": "string | null",
  "category": "string",
  "reference_object_key": "string",
  "reference_url": "string — presigned URL, действителен 24 часа",
  "enabled": true,
  "created_at": "datetime"
}

Errors

CodeHTTPКогда
UNAUTHORIZED401Нет токена
FORBIDDEN403Нет gensvc.access
NOT_FOUND404Пресет не найден или выключен

GET /api/v1/gensvc/user-presets

Список личных пресетов текущего пользователя.

ПараметрЗначение
AuthBearer JWT
Permissiongensvc.access

Query Parameters

ParamTypeRequiredDescription
limitintegerno1–50, default: 50
offsetintegernodefault: 0

Response 200

{
  "items": [
    {
      "id": "string — ULID upr_...",
      "name": "string",
      "reference_url": "string — presigned URL, действителен 1 час",
      "created_at": "datetime"
    }
  ],
  "total": "integer — всего личных пресетов у пользователя",
  "quota": 50
}

Errors

CodeHTTPКогда
UNAUTHORIZED401Нет токена
FORBIDDEN403Нет gensvc.access

POST /api/v1/gensvc/user-presets

Загружает reference-изображение как личный пресет.

ПараметрЗначение
AuthBearer JWT
Permissiongensvc.access
Content-Typemultipart/form-data

Request Body (multipart)

ПолеТипRequiredОписание
imagebinaryyesReference-изображение — JPEG, PNG или WebP, макс. 10 MB
namestringyesНазвание (1–100 символов)

Response 201

{
  "id": "string",
  "name": "string",
  "reference_url": "string",
  "created_at": "datetime"
}

Errors

CodeHTTPКогда
UNAUTHORIZED401Нет токена
FORBIDDEN403Нет gensvc.access
VALIDATION_ERROR400Пустое имя или > 100 символов
QUOTA_EXCEEDED422Превышена квота 50 личных пресетов
IMAGE_TOO_LARGE413> 10 MB
UNSUPPORTED_MEDIA_TYPE415Не JPEG/PNG/WebP

DELETE /api/v1/gensvc/user-presets/{id}

Удаляет личный пресет.

ПараметрЗначение
AuthBearer JWT
Permissiongensvc.access

Path Parameters

ParamTypeDescription
idstringULID личного пресета (upr_...)

Response 204

Нет тела ответа.

Errors

CodeHTTPКогда
UNAUTHORIZED401Нет токена
NOT_FOUND404Пресет не найден или не принадлежит текущему пользователю

GET /api/v1/gensvc/admin/presets

Список всех системных пресетов (включая выключенные). Admin view.

ПараметрЗначение
AuthBearer JWT
Permissiongensvc.preset.admin

Query Parameters

ParamTypeRequiredDescription
limitintegerno1–200, default: 50
offsetintegernodefault: 0
enabledbooleannoФильтр по флагу enabled

Response 200

{
  "items": [
    {
      "id": "string",
      "slug": "string",
      "name_en": "string",
      "name_ru": "string | null",
      "category": "string — legacy основная категория",
      "categories": ["string — полный список категорий"],
      "reference_object_key": "string",
      "reference_url": "string — presigned URL 24ч",
      "enabled": true,
      "created_at": "datetime",
      "updated_at": "datetime"
    }
  ],
  "limit": 50,
  "offset": 0,
  "total": 282
}

Errors

CodeHTTPКогда
UNAUTHORIZED401Нет токена
FORBIDDEN403Нет gensvc.preset.admin

POST /api/v1/gensvc/admin/presets

Создаёт новый системный пресет. Изображение загружается отдельно через PUT .../reference.

ПараметрЗначение
AuthBearer JWT
Permissiongensvc.preset.admin
Content-Typeapplication/json

Request Body

{
  "slug": "string, required — уникальный kebab-case идентификатор",
  "name_en": "string, required, макс. 100 символов",
  "name_ru": "string, optional, макс. 100 символов",
  "categories": ["string, required, минимум 1 — список категорий"],
  "enabled": "boolean, optional, default: true"
}

Response 201

Объект AdminPreset (полная структура, как в списке).

Errors

CodeHTTPКогда
UNAUTHORIZED401Нет токена
FORBIDDEN403Нет gensvc.preset.admin
VALIDATION_ERROR400Пустой slug, пустой name_en, пустой список categories
CONFLICT409Пресет с таким slug уже существует

GET /api/v1/gensvc/admin/presets/{id}

Admin-детали пресета.

ПараметрЗначение
AuthBearer JWT
Permissiongensvc.preset.admin

Path Parameters

ParamTypeDescription
idstringID пресета

Response 200

Объект AdminPreset с полем reference_url.

Errors

CodeHTTPКогда
UNAUTHORIZED401Нет токена
FORBIDDEN403Нет gensvc.preset.admin
NOT_FOUND404Пресет не найден

PATCH /api/v1/gensvc/admin/presets/{id}

Частичное обновление метаданных пресета (PATCH semantics — только переданные поля).

ПараметрЗначение
AuthBearer JWT
Permissiongensvc.preset.admin
Content-Typeapplication/json

Path Parameters

ParamTypeDescription
idstringID пресета

Request Body

{
  "name_en": "string, optional",
  "name_ru": "string | null, optional",
  "categories": ["string, optional — минимум 1 элемент если передан"],
  "enabled": "boolean, optional"
}

Response 200

Обновлённый объект AdminPreset.

Errors

CodeHTTPКогда
UNAUTHORIZED401Нет токена
FORBIDDEN403Нет gensvc.preset.admin
NOT_FOUND404Пресет не найден
VALIDATION_ERROR400Передан пустой массив categories

PUT /api/v1/gensvc/admin/presets/{id}/reference

Заменяет reference-изображение пресета.

ПараметрЗначение
AuthBearer JWT
Permissiongensvc.preset.admin
Content-Typemultipart/form-data

Path Parameters

ParamTypeDescription
idstringID пресета

Request Body (multipart)

ПолеТипRequiredОписание
imagebinaryyesНовое reference-изображение — WebP рекомендован, макс. 10 MB

Response 200

Обновлённый объект AdminPreset с новым reference_object_key.

Errors

CodeHTTPКогда
UNAUTHORIZED401Нет токена
FORBIDDEN403Нет gensvc.preset.admin
NOT_FOUND404Пресет не найден
IMAGE_TOO_LARGE413> 10 MB

DELETE /api/v1/gensvc/admin/presets/{id}

Soft delete системного пресета (флаг enabled = false, запись сохраняется).

ПараметрЗначение
AuthBearer JWT
Permissiongensvc.preset.admin

Path Parameters

ParamTypeDescription
idstringID пресета

Response 204

Нет тела ответа.

Errors

CodeHTTPКогда
UNAUTHORIZED401Нет токена
FORBIDDEN403Нет gensvc.preset.admin
NOT_FOUND404Пресет не найден

Общий формат ошибок

Photo Studio Service возвращает ошибки в формате RFC 7807 Problem Details:

{
  "type": "about:blank",
  "title": "string — HTTP reason phrase",
  "status": 400,
  "detail": "string — человекочитаемое описание",
  "instance": "string — путь запроса (optional)"
}

Отличие от ERP-конвенции

Остальные ERP-сервисы используют { "error": { "code", "message", "details" } }. Photo Studio Service (Go-прототип) использует RFC 7807. При интеграции через API Gateway возможна нормализация формата — см. декомпозицию.