openapi: 3.1.0 info: title: banan.wtf Integration API version: "1.0.0" summary: Фото товара и сцена на входе — готовые кадры по постоянным ссылкам, вебхуки и XML-фид Авито. description: | REST API banan.wtf для учётных программ, витрин и CRM. Задача — одно исходное фото и сцена каталога (или своё описание); результат — кадр по постоянной публичной ссылке `https://banan.wtf/f/.jpg`, вебхук `job.completed` / `job.failed` с подписью HMAC-SHA256 и XML-фид Авито Автозагрузки (formatVersion 3). Боевой ключ `bnk_live_…` списывает рублёвый баланс API по цене за кадр; упавшая задача возвращается на баланс. Остаток — `GET /balance` и поле `balance` в ответе `POST /jobs`. Тестовый ключ `bnk_test_…` бесплатный и модель не вызывает: задача сразу завершается тестовым кадром с меткой и вашим `Idempotency-Key`; вебхук, ссылки и фид работают. Дневной лимит тестовых задач — `test.jobsPerDay`. Цены и лимиты — `GET /pricing` и https://banan.wtf/integraciya/pricing.json. Ответ всегда JSON: `{ "success": true, … }` или `{ "success": false, "code": "…", "message": "…" }`. Документация на русском: https://banan.wtf/integraciya/docs/ termsOfService: https://banan.wtf/oferta/ contact: name: banan.wtf email: support@banan.wtf url: https://banan.wtf/integraciya/ externalDocs: description: Документация API на русском url: https://banan.wtf/integraciya/docs/ servers: - url: https://banan.wtf/api/integration/v1 security: - bearerAuth: [] tags: - name: account description: Ключ, аккаунт и цены - name: scenes description: Каталог сцен - name: jobs description: Задачи и кадры - name: feeds description: Фид Авито Автозагрузки paths: /pricing: get: tags: [account] operationId: getPricing summary: Цены за кадр, пополнение, бонусы, тестовый ключ и правила списания description: Ключ не нужен. Содержимое совпадает с https://banan.wtf/integraciya/pricing.json. security: [] responses: "200": description: Цены API content: application/json: schema: type: object required: [success, pricing] properties: success: { type: boolean, const: true } pricing: { $ref: "#/components/schemas/Pricing" } examples: pricing: externalValue: https://banan.wtf/integraciya/pricing.json "429": { $ref: "#/components/responses/RateLimited" } /me: get: tags: [account] operationId: getMe summary: Аккаунт, режим ключа, баланс API, тестовый ключ, цены responses: "200": description: Сведения о ключе и аккаунте content: application/json: schema: { $ref: "#/components/schemas/Me" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "429": { $ref: "#/components/responses/RateLimited" } /balance: get: tags: [account] operationId: getBalance summary: Остаток баланса API и сколько кадров на него выйдет description: | Вызывайте перед пачкой задач и по расписанию, чтобы заранее узнать о низком остатке. `framesLeft` — сколько кадров выйдет на остаток в каждом качестве по текущим ценам. responses: "200": description: Остаток баланса API content: application/json: schema: type: object required: [success, balance, framesLeft, topupUrl] properties: success: { type: boolean, const: true } balance: { $ref: "#/components/schemas/Balance" } framesLeft: { $ref: "#/components/schemas/FramesLeft" } topupUrl: { type: string, format: uri, description: Где пополнить баланс API } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "429": { $ref: "#/components/responses/RateLimited" } /scenes: get: tags: [scenes] operationId: listScenes summary: Сцены каталога description: | Группы аудитории аккаунта идут первыми и помечены `audience: true`. Внутренние инструкции модели не отдаются. Статическая копия каталога — https://banan.wtf/integraciya/scenes.json. responses: "200": description: Группы сцен content: application/json: schema: type: object required: [success, groups] properties: success: { type: boolean, const: true } audience: { type: string, description: Аудитория аккаунта, examples: [resale] } groups: type: array items: { $ref: "#/components/schemas/SceneGroup" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "429": { $ref: "#/components/responses/RateLimited" } /jobs: post: tags: [jobs] operationId: createJob summary: Создать задачу description: | Одно исходное фото — один кадр. Порядок проверок: ключ → тело → идемпотентность → деньги или дневной лимит тестового ключа → скачивание фото → резерв и постановка в очередь. В ответе 202 — остаток баланса после резерва. Тестовый ключ модель не вызывает: задача сразу `completed` с одним тестовым кадром, баланс не меняется. Повтор с тем же `Idempotency-Key` и тем же телом возвращает ту же задачу (200) без второго списания; тот же ключ с другим телом — 422 `idempotency_key_reused`. parameters: - $ref: "#/components/parameters/IdempotencyKey" requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/JobCreate" } examples: resale: summary: Вещь для Авито с полями объявления value: imageUrl: https://cdn.example.com/items/123.jpg sceneId: resale_clean_asis quality: standard fidelity: preserve_condition branch: Ленина, 12 externalId: INV-2026-000123 webhookUrl: https://erp.example.com/hooks/banan listing: Title: Кольцо 585, 3,2 г Price: "12000" Category: Часы и украшения marketplace: summary: Главное фото 3:4 на белом для WB и Ozon value: imageUrl: https://cdn.example.com/sku/4412.jpg sceneId: white_main externalId: SKU-4412 responses: "202": description: Задача создана и поставлена в очередь (тестовый ключ — сразу completed) content: application/json: schema: { $ref: "#/components/schemas/JobCreated" } "200": description: Повтор с тем же Idempotency-Key — та же задача headers: Idempotent-Replayed: schema: { type: string, const: "true" } content: application/json: schema: allOf: - $ref: "#/components/schemas/JobEnvelope" - type: object properties: replayed: { type: boolean, const: true } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "402": { $ref: "#/components/responses/PaymentRequired" } "403": { $ref: "#/components/responses/Forbidden" } "422": { $ref: "#/components/responses/IdempotencyConflict" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/RetryLater" } get: tags: [jobs] operationId: listJobs summary: Список задач API в режиме ключа parameters: - name: externalId in: query schema: { type: string, maxLength: 120 } - name: status in: query schema: { $ref: "#/components/schemas/JobStatus" } - name: since in: query description: Задачи, созданные с этого момента (ISO 8601) schema: { type: string, format: date-time } - name: limit in: query schema: { type: integer, minimum: 1, maximum: 100, default: 50 } - name: cursor in: query description: Значение nextCursor из прошлого ответа schema: { type: string } responses: "200": description: Задачи, новые первыми content: application/json: schema: type: object required: [success, jobs] properties: success: { type: boolean, const: true } jobs: type: array items: { $ref: "#/components/schemas/Job" } nextCursor: { type: [string, "null"] } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "429": { $ref: "#/components/responses/RateLimited" } /jobs/{id}: get: tags: [jobs] operationId: getJob summary: Статус задачи и кадры description: Видны только задачи API этого аккаунта в режиме ключа. parameters: - name: id in: path required: true schema: { type: string } responses: "200": description: Задача content: application/json: schema: { $ref: "#/components/schemas/JobEnvelope" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } /feeds/avito.xml: get: tags: [feeds] operationId: getAvitoFeed summary: Фид Авито Автозагрузки (formatVersion 3) description: | Готовые задачи с `listing` в режиме ключа фида. Задачи с одним `externalId` — одно ``: `` = `externalId` (без него — id задачи), поля — из самой свежей задачи серии, `` — кадры всех готовых задач серии по времени, до 10. Последние 500 задач, кеш 5 минут. Авторизация — токен фида только для чтения в `?token=`; боевой ключ в адресе не принимается. security: [] parameters: - name: token in: query required: true description: Токен фида только для чтения (выдаётся с ключом, виден в настройках студии) schema: { type: string } - name: limit in: query schema: { type: integer, minimum: 1, maximum: 1000, default: 500 } responses: "200": description: XML-фид Авито Автозагрузки content: application/xml: schema: { type: string } example: | INV-2026-000123 Кольцо 585, 3,2 г 12000 "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } webhooks: jobEvent: post: summary: Задача завершилась или упала description: | Уходит на `webhookUrl` задачи. Подпись — `X-Banan-Signature: sha256=`, HMAC-SHA256 от сырого тела с секретом подписи ключа; сравнивайте за постоянное время. Ответьте 2xx в пределах 10 секунд, иначе повторы через 30 с, 2 мин, 10 мин и 1 ч. Редиректы не выполняются. Возможны дубли — отбрасывайте по `job.id` + `event`. parameters: - name: X-Banan-Event in: header required: true schema: { type: string, enum: [job.completed, job.failed] } - name: X-Banan-Signature in: header required: true schema: { type: string, pattern: "^sha256=[0-9a-f]{64}$" } requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/WebhookEvent" } responses: "2XX": description: Доставлено components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: bnk_live_… | bnk_test_… description: Ключ выпускается в студии, «Настройки» → «API для интеграции». Показывается один раз. parameters: IdempotencyKey: name: Idempotency-Key in: header required: false description: До 128 символов A-Za-z0-9_.:- ; повтор с тем же ключом и телом не списывает второй раз. schema: { type: string, pattern: "^[A-Za-z0-9_.:-]{1,128}$" } responses: BadRequest: description: Ошибка в теле запроса content: application/json: schema: { $ref: "#/components/schemas/Error" } Unauthorized: description: Нет ключа или ключ отозван content: application/json: schema: { $ref: "#/components/schemas/Error" } PaymentRequired: description: Недостаточно денег на балансе API content: application/json: schema: { $ref: "#/components/schemas/InsufficientBalanceError" } Forbidden: description: Аккаунт удалён или ограничен, либо ключ участника организации content: application/json: schema: { $ref: "#/components/schemas/Error" } NotFound: description: Задача или фид не найдены content: application/json: schema: { $ref: "#/components/schemas/Error" } IdempotencyConflict: description: Тот же Idempotency-Key с другим телом content: application/json: schema: { $ref: "#/components/schemas/Error" } RateLimited: description: Превышен лимит запросов (`rate_limited`) или дневной лимит тестового ключа (`test_daily_limit`) headers: Retry-After: description: Через сколько секунд повторить schema: { type: integer } content: application/json: schema: { $ref: "#/components/schemas/Error" } RetryLater: description: Временный конфликт записи — повторите запрос headers: Retry-After: description: Через сколько секунд повторить schema: { type: integer } content: application/json: schema: { $ref: "#/components/schemas/Error" } schemas: ErrorCode: description: Код ошибки; `remote_image_*` — семейство ошибок скачивания фото по ссылке. anyOf: - type: string enum: - api_key_required - api_key_invalid - account_deleted - account_restricted - api_key_member_forbidden - scene_unknown - prompt_required - image_required - quality_invalid - idempotency_key_invalid - webhook_url_invalid - insufficient_balance - test_daily_limit - job_not_found - feed_not_found - idempotency_key_reused - status_invalid - since_invalid - limit_invalid - cursor_invalid - job_create_failed - upload_failed - route_not_found - body_invalid - body_too_large - rate_limited - retry_later - internal_error - type: string pattern: "^remote_image_[a-z_]+$" examples: [remote_image_url_invalid, remote_image_url_private, remote_image_dns, remote_image_fetch_failed, remote_image_timeout, remote_image_too_large, remote_image_unsupported] Error: type: object required: [success, code, message] properties: success: { type: boolean, const: false } code: { $ref: "#/components/schemas/ErrorCode" } message: { type: string, description: Текст для человека } retryable: { type: boolean, description: "true у retry_later" } additionalProperties: true InsufficientBalanceError: allOf: - $ref: "#/components/schemas/Error" - type: object properties: balanceRub: { type: number } priceRub: { type: number } topupUrl: { type: string, format: uri } Tier: type: object required: [id, title, priceRub, longEdgePx] properties: id: { type: string, enum: [standard, high] } title: { type: string } priceRub: { type: number, description: Цена одного кадра в рублях } model: { type: string } quality: { type: string } longEdgePx: { type: integer } summary: { type: string } Pricing: type: object description: Содержимое https://banan.wtf/integraciya/pricing.json required: [version, currency, tiers, defaultTier, topup, test, limits, rules] properties: version: { type: string } currency: { type: string, const: RUB } unit: { type: string, const: frame } tiers: type: array items: { $ref: "#/components/schemas/Tier" } defaultTier: { type: string } topup: type: object properties: minRub: { type: integer } maxRub: { type: integer } presetsRub: { type: array, items: { type: integer } } bonuses: type: array items: type: object properties: fromRub: { type: integer } percent: { type: number } providers: { type: array, items: { type: string } } invoiceMinRub: { type: integer } test: type: object description: Тестовый ключ модель не вызывает и отвечает тестовым кадром properties: static: { type: boolean, const: true } jobsPerDay: { type: integer, description: Тестовых задач в сутки на аккаунт } watermark: { type: boolean } limits: type: object properties: jobsPerMinutePerKey: { type: integer } readsPerMinutePerKey: { type: integer } maxSourceImageMb: { type: integer } activeKeysPerAccount: { type: integer } rules: { type: array, items: { type: string } } Me: type: object description: Аккаунт и аудитория, режим ключа, остаток баланса API, тестовый ключ и цены. required: [success, account, key, balance, framesLeft, test, pricing] properties: success: { type: boolean, const: true } account: type: object properties: userId: { type: [integer, string] } audience: type: object properties: id: { type: string, examples: [resale, marketplace, generic] } label: { type: string } key: type: object properties: id: { type: string } prefix: { type: string, examples: [bnk_live_a1b2c3] } label: { type: [string, "null"] } mode: { type: string, enum: [live, test] } balance: { $ref: "#/components/schemas/Balance" } framesLeft: { $ref: "#/components/schemas/FramesLeft" } test: { $ref: "#/components/schemas/TestKey" } pricing: type: object description: Краткая сетка цен; полная — GET /pricing properties: version: { type: string } currency: { type: string, const: RUB } unit: { type: string, const: frame } defaultTier: { type: string } tiers: type: array items: { $ref: "#/components/schemas/Tier" } topupUrl: { type: string, format: uri, description: Где пополнить баланс API } additionalProperties: true Balance: type: object description: Остаток рублёвого баланса API (на учётке плательщика — владельца пула) required: [rub, kopecks] properties: rub: { type: number } kopecks: { type: integer } FramesLeft: type: object description: Сколько кадров выйдет на остаток в каждом качестве required: [standard, high] properties: standard: { type: integer } high: { type: integer } TestKey: type: object description: Тестовый ключ модель не вызывает; лимит — задач в сутки на аккаунт required: [static, jobsPerDay, usedToday] properties: static: { type: boolean, const: true } jobsPerDay: { type: integer } usedToday: { type: integer } SceneInput: type: object required: [id, label, maxLength, required] properties: id: { type: string } label: { type: string } hint: { type: [string, "null"] } maxLength: { type: integer } required: { type: boolean } SceneVariant: type: object required: [id, title] properties: id: { type: string } title: { type: string } Scene: type: object required: [id, groupId, title, aspectRatio, inputs, variants] properties: id: { type: string, examples: [white_main, resale_clean_asis] } groupId: { type: string } title: { type: string } subtitle: { type: [string, "null"] } aspectRatio: { type: string, examples: ["3:4", "4:3", "1:1"] } fidelity: type: [string, "null"] enum: [preserve_condition, enhance_allowed, null] inputs: type: array items: { $ref: "#/components/schemas/SceneInput" } variants: type: array items: { $ref: "#/components/schemas/SceneVariant" } SceneGroup: type: object required: [id, title, scenes] properties: id: { type: string } title: { type: string } audience: { type: boolean, description: Группа аудитории аккаунта } scenes: type: array items: { $ref: "#/components/schemas/Scene" } JobCreate: type: object description: Нужен imageUrl или imageBase64 и sceneId или prompt. properties: imageUrl: type: string format: uri description: Публичная ссылка на фото до 25 МБ (JPEG, PNG, WebP, HEIC), общий срок скачивания 20 с imageBase64: type: string description: Data URL или чистый base64; тело запроса до 20 МБ sceneId: { type: string, description: "id сцены из GET /scenes" } variantId: { type: [string, "null"] } inputs: type: object additionalProperties: { type: string } prompt: { type: string, maxLength: 2000 } quality: { type: string, enum: [standard, high], default: standard } fidelity: { type: string, enum: [preserve_condition, enhance_allowed] } aspectRatio: { type: string, examples: ["3:4", "4:3", "1:1"], description: Без поля — формат сцены } branch: { type: string, maxLength: 40, description: Метка точки для отчёта по филиалам } externalId: type: string maxLength: 120 description: Ваш номер позиции. Метка, а не ключ идемпотентности; задачи с одним externalId — одно объявление в фиде webhookUrl: { type: string, format: uri } listing: type: object description: Поля объявления Avito XML; ключ — имя тега (кроме Id, Images, Image), значение до 4000 символов, до 40 полей maxProperties: 40 propertyNames: { pattern: "^[A-Z][A-Za-z0-9]{1,40}$" } additionalProperties: { type: [string, number, boolean] } anyOf: - required: [imageUrl] - required: [imageBase64] JobStatus: type: string enum: [queued, processing, completed, failed] Frame: type: object required: [index, url] properties: index: { type: integer } url: type: string format: uri description: Постоянная публичная ссылка https://banan.wtf/f/.jpg Job: type: object required: [id, status, frames] properties: id: { type: string } status: { $ref: "#/components/schemas/JobStatus" } mode: { type: string, enum: [live, test] } quality: { type: string, enum: [standard, high] } priceRub: { type: number, examples: [19] } refunded: { type: boolean } scene: type: [object, "null"] properties: id: { type: string } variantId: { type: [string, "null"] } fidelity: { type: [string, "null"] } branch: { type: [string, "null"] } externalId: { type: [string, "null"] } createdAt: { type: [string, "null"], format: date-time } completedAt: { type: [string, "null"], format: date-time } error: { type: [string, "null"] } frames: type: array items: { $ref: "#/components/schemas/Frame" } JobEnvelope: type: object required: [success, job] properties: success: { type: boolean, const: true } job: { $ref: "#/components/schemas/Job" } JobCreated: type: object required: [success, job, balance] properties: success: { type: boolean, const: true } job: { $ref: "#/components/schemas/Job" } balance: allOf: - $ref: "#/components/schemas/Balance" description: Остаток после резерва цены кадра; у тестового ключа не меняется WebhookEvent: type: object required: [event, occurredAt, attempt, job] properties: event: { type: string, enum: [job.completed, job.failed] } occurredAt: { type: string, format: date-time } attempt: { type: integer, minimum: 1 } job: { $ref: "#/components/schemas/Job" }