{"openapi":"3.1.0","info":{"title":"НейроХудожник API","version":"0.0.1","description":"Unified public gateway for AI image/video generation: credit-based billing, scoped API keys, session/agent auth, and asset re-hosting on Russian CDN."},"servers":[{"url":"http://localhost:8081","description":"Local dev"},{"url":"https://api.neuroartist.ru","description":"Production"}],"tags":[{"name":"Health","description":"Liveness probes"},{"name":"Models","description":"Public model catalog"},{"name":"Generation","description":"AI image/video generation (auth required)"},{"name":"Me","description":"Current user dashboard (auth required)"},{"name":"Billing","description":"Credit packs and payments"},{"name":"Admin","description":"Admin-only operations (session + role=admin)"},{"name":"Webhooks","description":"Incoming webhooks (signature-verified)"},{"name":"Changelog","description":"Public changelog feed"},{"name":"Integrations","description":"Partner-specific integration adapters (custom request/response contracts). Auth required via platform API keys."}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"Authorization","description":"Bearer API key. Send as `Authorization: Bearer na_live_...` header, or as `x-api-key: na_live_...`."},"SessionCookie":{"type":"apiKey","in":"cookie","name":"better-auth.session_token","description":"Better Auth session cookie, for web dashboard usage."}},"schemas":{"HealthResponse":{"type":"object","properties":{"status":{"type":"string","enum":["ok","degraded"],"example":"ok"},"db":{"type":"string","enum":["up","down"],"example":"up"},"uptime":{"type":"number","example":1234.56,"description":"Process uptime in seconds"},"timestamp":{"type":"string","format":"date-time","example":"2026-04-12T09:30:00.000Z"}},"required":["status","db","uptime","timestamp"]},"ChangelogResponse":{"type":"object","properties":{"entries":{"type":"array","items":{"$ref":"#/components/schemas/ChangelogEntry"}}},"required":["entries"]},"ChangelogEntry":{"type":"object","properties":{"version":{"type":"string","minLength":1,"example":"1.0.0"},"date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","example":"2026-04-14","description":"ISO-8601 calendar date"},"notes":{"type":"array","items":{"type":"string","minLength":1},"minItems":1}},"required":["version","date","notes"]},"ErrorResponse":{"type":"object","properties":{"error":{"type":"string","example":"unauthorized","description":"Machine-readable error code"},"message":{"type":"string","example":"session authentication required","description":"Human-readable explanation (optional)"}},"required":["error"]},"MeResponse":{"type":"object","properties":{"user":{"$ref":"#/components/schemas/AuthedUser"},"auth":{"type":"object","properties":{"source":{"type":"string","enum":["session","apiKey"]},"apiKeyId":{"type":"string","nullable":true}},"required":["source","apiKeyId"]}},"required":["user","auth"]},"AuthedUser":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string","format":"email"},"name":{"type":"string"},"role":{"type":"string","nullable":true}},"required":["id","email","name","role"]},"PatchMeResponse":{"type":"object","properties":{"user":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string","format":"email"},"name":{"type":"string"},"image":{"type":"string","nullable":true},"role":{"type":"string","nullable":true},"createdAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"}]}},"required":["id","email","name","image","role","createdAt"]}},"required":["user"]},"PatchMeBody":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100},"image":{"type":"string","nullable":true,"format":"uri"},"lowBalanceEmailOptOut":{"type":"boolean"}}},"SessionsList":{"type":"object","properties":{"sessions":{"type":"array","items":{"$ref":"#/components/schemas/SessionSummary"}}},"required":["sessions"]},"SessionSummary":{"type":"object","properties":{"id":{"type":"string"},"expiresAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"}]},"ipAddress":{"type":"string","nullable":true},"userAgent":{"type":"string","nullable":true},"createdAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"}]},"updatedAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"}]}},"required":["id","expiresAt","createdAt","updatedAt"]},"Balance":{"type":"object","properties":{"userId":{"type":"string"},"balance":{"type":"number","example":100.0001,"description":"Текущий баланс в ₽ (decimal). Минимальная единица — 0.0001 ₽."}},"required":["userId","balance"]},"TransactionList":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/CreditTransaction"}},"total":{"type":"integer","example":1},"limit":{"type":"integer","example":50},"offset":{"type":"integer","example":0}},"required":["items","total","limit","offset"]},"CreditTransaction":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"userId":{"type":"string"},"type":{"type":"string","example":"topup","description":"Known values: topup, deduct, refund, grant. Stored as free-form text."},"amount":{"type":"number","description":"В ₽ (decimal). Positive for topup/refund/grant, negative for deduct."},"balanceAfter":{"type":"number","description":"Баланс после транзакции в ₽ (decimal)."},"description":{"type":"string","nullable":true},"createdAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"}]}},"required":["id","userId","type","amount","description","createdAt"]},"UsageSnapshot":{"type":"object","properties":{"balance":{"type":"number","description":"Баланс в ₽ (decimal)."},"windows":{"type":"array","items":{"type":"object","properties":{"window":{"type":"string"},"credits":{"type":"number","description":"Сумма кредитов за окно в ₽ (decimal)."},"count":{"type":"integer"},"byModel":{"type":"array","items":{"type":"object","properties":{"modelId":{"type":"string"},"credits":{"type":"number","description":"В ₽ (decimal)."},"count":{"type":"integer"}},"required":["modelId","credits","count"]}}},"required":["window","credits","count","byModel"]}}},"required":["balance","windows"]},"UsageHistory":{"type":"object","properties":{"granularity":{"type":"string","enum":["hour","day"]},"from":{"type":"string","format":"date-time"},"to":{"type":"string","format":"date-time"},"series":{"type":"array","items":{"type":"object","properties":{"bucket":{"type":"string"},"credits":{"type":"number","description":"В ₽ (decimal)."},"count":{"type":"integer"}},"required":["bucket","credits","count"]}}},"required":["granularity","from","to","series"]},"UsageByModelResponse":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/UsageByModelItem"}}},"required":["items"]},"UsageByModelItem":{"type":"object","properties":{"modelId":{"type":"string","example":"fal-ai/nano-banana-pro"},"displayName":{"type":"string","nullable":true,"example":"Nano Banana Pro"},"priceUnit":{"type":"string","nullable":true,"example":"images"},"credits":{"type":"number","example":42.5,"description":"Сумма списанных кредитов в ₽ (decimal)."},"count":{"type":"integer","example":8}},"required":["modelId","displayName","priceUnit","credits","count"]},"UsageByKeyResponse":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/UsageByKeyItem"}}},"required":["items"]},"UsageByKeyItem":{"type":"object","properties":{"apiKeyId":{"type":"string","example":"cuid_abc123"},"keyName":{"type":"string","nullable":true,"example":"ci-runner"},"partialKey":{"type":"string","nullable":true,"example":"na_live_abc123"},"credits":{"type":"number","example":42.5,"description":"Сумма списанных кредитов в ₽ (decimal)."},"count":{"type":"integer","example":8},"lastUsedAt":{"type":"string","format":"date-time"}},"required":["apiKeyId","keyName","partialKey","credits","count","lastUsedAt"]},"ActivityList":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ActivityItem"}},"total":{"type":"integer","example":1},"limit":{"type":"integer","example":50},"offset":{"type":"integer","example":0}},"required":["items","total","limit","offset"]},"ActivityItem":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"modelId":{"type":"string","example":"nano-banana-pro"},"operation":{"type":"string","enum":["submit","status","result","cancel","stream"]},"credits":{"type":"number","example":0.5,"description":"Списано кредитов в ₽ (decimal)."},"status":{"type":"string","enum":["success","error"]},"errorCode":{"type":"string","nullable":true},"requestId":{"type":"string","nullable":true},"apiKeyId":{"type":"string","nullable":true},"durationMs":{"type":"integer","nullable":true},"createdAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"}]}},"required":["id","modelId","operation","credits","status","errorCode","requestId","apiKeyId","durationMs","createdAt"]},"SingleActivityResponse":{"type":"object","properties":{"record":{"$ref":"#/components/schemas/ActivityItem"},"assets":{"type":"array","items":{"$ref":"#/components/schemas/Asset"}}},"required":["record","assets"]},"Asset":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"falRequestId":{"type":"string"},"sourceUrl":{"type":"string"},"s3Key":{"type":"string","nullable":true},"s3Bucket":{"type":"string","nullable":true},"contentType":{"type":"string","nullable":true},"sizeBytes":{"type":"integer","nullable":true},"status":{"type":"string","enum":["pending","uploading","ready","failed"]},"error":{"type":"string","nullable":true},"publicUrl":{"type":"string","nullable":true,"description":"Public URL for ready assets (computed from S3 config). null for non-ready rows or when asset storage is disabled."},"createdAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"}]}},"required":["id","falRequestId","sourceUrl","s3Key","s3Bucket","contentType","sizeBytes","status","error","publicUrl","createdAt"]},"ApiKeysList":{"type":"object","properties":{"keys":{"type":"array","items":{"$ref":"#/components/schemas/ApiKeySummary"}}},"required":["keys"]},"ApiKeySummary":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string","nullable":true},"prefix":{"type":"string","nullable":true,"example":"na_live_"},"start":{"type":"string","nullable":true,"example":"na_live_abc123"},"enabled":{"type":"boolean","nullable":true},"expiresAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"},{"nullable":true}]},"createdAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"}]},"lastRequest":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"},{"nullable":true}]},"requestCount":{"type":"integer","nullable":true},"requests":{"type":"integer","example":42,"description":"Lifetime request count (all operations) for this key"},"lastUsedAt":{"type":"string","nullable":true,"format":"date-time","example":"2026-04-14T10:00:00Z","description":"ISO 8601 of most recent usage_record row for this key, or null if never used"},"lifetimeCostRub":{"type":"number","example":125.0001,"description":"Sum of credits spent (status=success rows only) for this key, in ₽ (decimal)."},"monthlyLimitRub":{"type":"integer","nullable":true,"minimum":0,"exclusiveMinimum":true,"example":5000,"description":"Per-key monthly spend cap in ₽ (from apikey.metadata.monthlyLimitRub). null when unset."}},"required":["id","name","prefix","start","enabled","expiresAt","createdAt","lastRequest","requestCount"]},"CreatedKey":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string","nullable":true},"key":{"type":"string","description":"Plaintext API key — shown ONCE. Store it immediately; can't be retrieved later.","example":"na_live_abc123..."},"prefix":{"type":"string","nullable":true},"start":{"type":"string","nullable":true},"expiresAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"},{"nullable":true}]},"createdAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"}]}},"required":["id","name","key","prefix","start","expiresAt","createdAt"]},"CreateKeyBody":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100},"expiresIn":{"type":"integer","minimum":0,"exclusiveMinimum":true,"description":"Seconds until key expires. Omit for never-expiring key."}}},"PatchKeyBody":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":64},"metadata":{"type":"object","properties":{"monthlyLimitRub":{"type":"integer","nullable":true,"minimum":0,"exclusiveMinimum":true}}}}},"SavedPaymentMethodsList":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/SavedPaymentMethod"}}},"required":["items"]},"SavedPaymentMethod":{"type":"object","properties":{"id":{"type":"string"},"cardLast4":{"type":"string","nullable":true,"example":"4444"},"cardBrand":{"type":"string","nullable":true,"example":"MasterCard"},"isDefault":{"type":"boolean"},"createdAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"}]}},"required":["id","cardLast4","cardBrand","isDefault","createdAt"]},"SetDefaultPaymentMethodResponse":{"type":"object","properties":{"method":{"$ref":"#/components/schemas/SavedPaymentMethod"}},"required":["method"]},"AutoTopupConfig":{"type":"object","properties":{"thresholdRub":{"type":"integer","nullable":true,"example":100,"description":"When balance crosses ≤ this value, auto-topup fires. null = disabled."},"amountRub":{"type":"integer","nullable":true,"example":500,"description":"Amount (₽) charged on each auto-topup. null = disabled."}},"required":["thresholdRub","amountRub"]},"PatchAutoTopupBody":{"type":"object","properties":{"enabled":{"type":"boolean"},"thresholdRub":{"type":"integer","minimum":10},"amountRub":{"type":"integer","minimum":50,"maximum":10000}}},"UploadResponse":{"type":"object","properties":{"assetId":{"type":"string","format":"uuid"},"url":{"type":"string","format":"uri","description":"Public S3 URL of the uploaded file. Usable as a fal.ai input URL."},"contentType":{"type":"string","example":"image/png"},"sizeBytes":{"type":"integer","minimum":0}},"required":["assetId","url","contentType","sizeBytes"]},"AdminStats":{"type":"object","properties":{"users":{"type":"integer"},"activeApiKeys":{"type":"integer"},"models":{"type":"integer"},"credits":{"type":"object","properties":{"issued":{"type":"number","description":"Всего выдано кредитов в ₽ (decimal)."},"used":{"type":"number","description":"Всего потрачено кредитов в ₽ (decimal)."}},"required":["issued","used"]}},"required":["users","activeApiKeys","models","credits"]},"AdminUserList":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/AdminUserListItem"}},"total":{"type":"integer","example":1},"limit":{"type":"integer","example":50},"offset":{"type":"integer","example":0}},"required":["items","total","limit","offset"]},"AdminUserListItem":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string","format":"email"},"name":{"type":"string"},"role":{"type":"string","nullable":true},"banned":{"type":"boolean","nullable":true},"createdAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"}]},"balance":{"type":"number","example":100.0001,"description":"Текущий баланс пользователя в ₽ (decimal)."}},"required":["id","email","name","role","banned","createdAt","balance"]},"AdminUserDetail":{"type":"object","properties":{"user":{"$ref":"#/components/schemas/AdminUserDetailed"},"balance":{"type":"number","description":"Баланс в ₽ (decimal)."},"usage":{"type":"array","items":{"type":"object","properties":{"window":{"type":"string"},"credits":{"type":"number","description":"В ₽ (decimal)."},"count":{"type":"integer"},"byModel":{"type":"array","items":{"type":"object","properties":{"modelId":{"type":"string"},"credits":{"type":"number","description":"В ₽ (decimal)."},"count":{"type":"integer"}},"required":["modelId","credits","count"]}}},"required":["window","credits","count","byModel"]}}},"required":["user","balance","usage"]},"AdminUserDetailed":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string","format":"email"},"name":{"type":"string"},"image":{"type":"string","nullable":true},"role":{"type":"string","nullable":true},"banned":{"type":"boolean","nullable":true},"banReason":{"type":"string","nullable":true},"banExpires":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"},{"nullable":true}]},"createdAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"}]},"updatedAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"}]}},"required":["id","email","name","image","role","banned","banReason","banExpires","createdAt","updatedAt"]},"AdminUserPatchResponse":{"type":"object","properties":{"user":{"$ref":"#/components/schemas/AdminUserDetailed"}},"required":["user"]},"PatchUserBody":{"type":"object","properties":{"role":{"type":"string","enum":["user","admin"]}},"required":["role"]},"OkResponse":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]}},"required":["ok"]},"BanBody":{"type":"object","properties":{"reason":{"type":"string"},"expiresAt":{"type":"string","nullable":true,"format":"date-time"}}},"TransactionWrapper":{"type":"object","properties":{"transaction":{"$ref":"#/components/schemas/CreditTransaction"}},"required":["transaction"]},"GrantBody":{"type":"object","properties":{"amount":{"type":"integer","minimum":0,"exclusiveMinimum":true,"example":100,"description":"Сумма в ₽ (целое число). Конвертируется в units внутри."},"description":{"type":"string"}},"required":["amount"]},"InsufficientBalance":{"type":"object","properties":{"error":{"type":"string","enum":["insufficient_balance"]},"message":{"type":"string"},"required":{"type":"number","example":0.5,"description":"Сколько ₽ нужно для этой операции (decimal, минимум 0.0001)."},"available":{"type":"number","example":0,"description":"Текущий баланс пользователя в ₽ (decimal)."}},"required":["error","required","available"]},"AdminTransactionList":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/CreditTransaction"}},"total":{"type":"integer","example":1},"limit":{"type":"integer","example":50},"offset":{"type":"integer","example":0}},"required":["items","total","limit","offset"]},"AdminUserUsage":{"type":"object","properties":{"windows":{"type":"array","items":{"type":"object","properties":{"window":{"type":"string"},"credits":{"type":"number","description":"В ₽ (decimal)."},"count":{"type":"integer"},"byModel":{"type":"array","items":{"type":"object","properties":{"modelId":{"type":"string"},"credits":{"type":"number","description":"В ₽ (decimal)."},"count":{"type":"integer"}},"required":["modelId","credits","count"]}}},"required":["window","credits","count","byModel"]}}},"required":["windows"]},"AdminSessionsList":{"type":"object","properties":{"sessions":{"type":"array","items":{"$ref":"#/components/schemas/SessionSummary"}}},"required":["sessions"]},"AdminKeysList":{"type":"object","properties":{"keys":{"type":"array","items":{"$ref":"#/components/schemas/ApiKeySummary"}}},"required":["keys"]},"PatchKeyResponse":{"type":"object","properties":{"key":{"$ref":"#/components/schemas/AdminApiKeyDetails"}},"required":["key"]},"AdminApiKeyDetails":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string","nullable":true},"enabled":{"type":"boolean","nullable":true},"rateLimitEnabled":{"type":"boolean","nullable":true},"rateLimitTimeWindow":{"type":"integer","nullable":true},"rateLimitMax":{"type":"integer","nullable":true},"remaining":{"type":"integer","nullable":true},"expiresAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"},{"nullable":true}]},"updatedAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"}]}},"required":["id","name","enabled","rateLimitEnabled","rateLimitTimeWindow","rateLimitMax","remaining","expiresAt","updatedAt"]},"TopUsersResponse":{"type":"object","properties":{"window":{"type":"string","enum":["5h","24h","7d","30d"],"description":"Rolling time window used by usage aggregations"},"items":{"type":"array","items":{"$ref":"#/components/schemas/TopUserRow"}}},"required":["window","items"]},"TopUserRow":{"type":"object","properties":{"userId":{"type":"string"},"email":{"type":"string","nullable":true,"format":"email"},"name":{"type":"string","nullable":true},"credits":{"type":"number","description":"В ₽ (decimal)."},"count":{"type":"integer"}},"required":["userId","email","name","credits","count"]},"AdminModelList":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/AdminModel"}},"total":{"type":"integer","example":1},"limit":{"type":"integer","example":50},"offset":{"type":"integer","example":0}},"required":["items","total","limit","offset"]},"AdminModel":{"type":"object","properties":{"modelId":{"type":"string"},"providerId":{"type":"string"},"alias":{"type":"string","nullable":true},"displayName":{"type":"string","nullable":true},"category":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"creditCost":{"type":"integer"},"discountPercent":{"type":"integer","minimum":0,"maximum":99},"visible":{"type":"boolean"},"tags":{"type":"array","nullable":true,"items":{"type":"string"}},"priceMultiplier":{"type":"number","nullable":true,"minimum":0},"schemaFetchedAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"},{"nullable":true}]},"createdAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"}]},"updatedAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"}]}},"required":["modelId","providerId","alias","displayName","category","description","creditCost","discountPercent","visible","tags","priceMultiplier","schemaFetchedAt","createdAt","updatedAt"]},"AdminModelWrapper":{"type":"object","properties":{"model":{"$ref":"#/components/schemas/AdminModel"}},"required":["model"]},"PricingConfigErrorResponse":{"allOf":[{"$ref":"#/components/schemas/ErrorResponse"},{"type":"object","properties":{"issues":{"type":"array","items":{"nullable":true}}},"required":["issues"]}]},"UpsertModelBody":{"type":"object","properties":{"modelId":{"type":"string","minLength":1,"example":"provider/model-name"},"providerId":{"type":"string","minLength":1,"example":"azure"},"alias":{"type":"string","minLength":1,"example":"gpt-image-2"},"displayName":{"type":"string","nullable":true},"category":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"creditCost":{"type":"integer","minimum":0},"discountPercent":{"type":"integer","minimum":0,"maximum":99,"description":"Optional per-model discount percent. Applied after RUB conversion and markup.","example":20},"visible":{"type":"boolean"},"pricingConfig":{"nullable":true,"description":"Universal pricing contract (USD-only). See src/lib/pricing/types.ts#PricingConfigSchema for full spec. Example: { version: 1, currency: \"USD\", pay_as_you_go: { images: { price: 0.15 } } }"},"priceMultiplier":{"type":"number","nullable":true,"minimum":0,"description":"Optional per-model margin multiplier. Overrides env.DEFAULT_PRICE_MULTIPLIER.","example":1.5},"tags":{"type":"array","nullable":true,"items":{"type":"string"}}},"required":["modelId"]},"SyncModelBody":{"type":"object","properties":{"modelId":{"type":"string","minLength":1}},"required":["modelId"]},"AdminTestEmailResponse":{"type":"object","properties":{"to":{"type":"string","format":"email"},"sent":{"type":"array","items":{"$ref":"#/components/schemas/AdminTestEmailResult"}}},"required":["to","sent"]},"AdminTestEmailResult":{"type":"object","properties":{"id":{"type":"string"},"ok":{"type":"boolean"},"detail":{"type":"string","nullable":true}},"required":["id","ok","detail"]},"AdminTestEmailBody":{"type":"object","properties":{"template":{"anyOf":[{"type":"string","enum":["all"]},{"type":"string","enum":["verify-email","welcome","password-reset","topup-succeeded","auto-topup-charged","auto-topup-failed","low-balance"]}]},"to":{"type":"string","format":"email"}},"required":["template"]},"ProviderInstance":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"type":{"type":"string","description":"Provider type: fal | azure"},"name":{"type":"string"},"displayName":{"type":"string"},"config":{"type":"object","additionalProperties":{"type":"string"},"description":"Provider config (plaintext). Contains credentials."},"enabled":{"type":"boolean"},"priority":{"type":"integer"},"createdAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"}]},"updatedAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"}]}},"required":["id","type","name","displayName","config","enabled","priority","createdAt","updatedAt"]},"CreateProviderBody":{"type":"object","properties":{"type":{"type":"string","enum":["fal","azure"]},"name":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[a-z0-9-]+$","description":"Уникальный ID инстанции. Используется как model.provider_id."},"displayName":{"type":"string","minLength":1,"maxLength":128},"config":{"type":"object","additionalProperties":{"type":"string"},"description":"Конфиг с кредами. fal: {falKey?}. azure: {endpoint, apiKey, imageApiVersion?, videoApiVersion?}."},"enabled":{"type":"boolean","default":true},"priority":{"type":"integer","minimum":0,"maximum":9999,"default":0}},"required":["type","name","displayName","config"]},"PatchProviderBody":{"type":"object","properties":{"displayName":{"type":"string","minLength":1,"maxLength":128},"config":{"type":"object","additionalProperties":{"type":"string"}},"enabled":{"type":"boolean"},"priority":{"type":"integer","minimum":0,"maximum":9999}}},"ProviderTestResult":{"type":"object","properties":{"ok":{"type":"boolean"},"latencyMs":{"type":"integer"},"error":{"type":"string"}},"required":["ok"]},"AdminInvoiceRequest":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"userId":{"type":"string"},"userEmail":{"type":"string","nullable":true},"userName":{"type":"string","nullable":true},"companyName":{"type":"string"},"inn":{"type":"string"},"kpp":{"type":"string","nullable":true},"legalAddress":{"type":"string"},"amountKopecks":{"type":"integer"},"purpose":{"type":"string","nullable":true},"contactEmail":{"type":"string","nullable":true},"status":{"type":"string"},"linkedPaymentId":{"type":"string","nullable":true},"invoiceNumber":{"type":"string","nullable":true},"internalNote":{"type":"string","nullable":true},"createdAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"}]},"updatedAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"}]},"invoicedAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"},{"nullable":true}]},"paidAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"},{"nullable":true}]}},"required":["id","userId","userEmail","userName","companyName","inn","kpp","legalAddress","amountKopecks","purpose","contactEmail","status","linkedPaymentId","invoiceNumber","internalNote","createdAt","updatedAt","invoicedAt","paidAt"]},"PatchInvoiceRequestBody":{"type":"object","properties":{"status":{"type":"string","enum":["pending","invoiced","paid","rejected"]},"invoiceNumber":{"type":"string","nullable":true,"minLength":1,"maxLength":64},"linkedPaymentId":{"type":"string","nullable":true,"minLength":1,"maxLength":64},"internalNote":{"type":"string","nullable":true,"maxLength":2000}}},"PromoCodeWithStats":{"allOf":[{"$ref":"#/components/schemas/PromoCode"},{"type":"object","properties":{"stats":{"$ref":"#/components/schemas/PromoStats"}},"required":["stats"]}]},"PromoStats":{"type":"object","properties":{"appliedCount":{"type":"integer"},"uniqueUsersCount":{"type":"integer"},"appliedTotalRub":{"type":"integer"},"bonusTotalRub":{"type":"integer"},"topUsers":{"type":"array","items":{"$ref":"#/components/schemas/PromoTopUser"}}},"required":["appliedCount","uniqueUsersCount","appliedTotalRub","bonusTotalRub","topUsers"]},"PromoTopUser":{"type":"object","properties":{"userId":{"type":"string"},"email":{"type":"string","nullable":true},"name":{"type":"string","nullable":true},"paymentsCount":{"type":"integer"},"totalAmountRub":{"type":"integer"}},"required":["userId","email","name","paymentsCount","totalAmountRub"]},"PromoCode":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"code":{"type":"string"},"description":{"type":"string","nullable":true},"bonusPercent":{"type":"integer"},"maxRedemptions":{"type":"integer","nullable":true},"redemptionCount":{"type":"integer"},"perUserLimit":{"type":"integer","nullable":true},"minAmountRub":{"type":"integer","nullable":true},"maxAmountRub":{"type":"integer","nullable":true},"validFrom":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"},{"nullable":true}]},"validUntil":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"},{"nullable":true}]},"isActive":{"type":"boolean"},"createdBy":{"type":"string","nullable":true},"createdAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"}]},"updatedAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"}]}},"required":["id","code","description","bonusPercent","maxRedemptions","redemptionCount","perUserLimit","minAmountRub","maxAmountRub","validFrom","validUntil","isActive","createdBy","createdAt","updatedAt"]},"CreatePromoCodeBody":{"type":"object","properties":{"code":{"type":"string","minLength":2,"maxLength":64,"pattern":"^[A-Za-z0-9_-]+$/u"},"description":{"type":"string","maxLength":500},"bonusPercent":{"type":"integer","minimum":1,"maximum":500},"maxRedemptions":{"type":"integer","nullable":true,"minimum":1},"perUserLimit":{"type":"integer","nullable":true,"minimum":1},"minAmountRub":{"type":"integer","nullable":true,"minimum":0},"maxAmountRub":{"type":"integer","nullable":true,"minimum":0},"validFrom":{"type":"string","nullable":true,"format":"date-time"},"validUntil":{"type":"string","nullable":true,"format":"date-time"},"isActive":{"type":"boolean","default":true}},"required":["code","bonusPercent"]},"PromoCodeDetail":{"allOf":[{"$ref":"#/components/schemas/PromoCode"},{"type":"object","properties":{"recentRedemptions":{"type":"array","items":{"$ref":"#/components/schemas/PromoRedemption"}}},"required":["recentRedemptions"]}]},"PromoRedemption":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"promoCodeId":{"type":"string","format":"uuid"},"userId":{"type":"string"},"yookassaPaymentId":{"type":"string","nullable":true,"format":"uuid"},"bonusPercent":{"type":"integer"},"baseAmountRub":{"type":"integer"},"bonusAmountRub":{"type":"integer"},"status":{"type":"string"},"createdAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"}]},"updatedAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"}]},"user":{"type":"object","nullable":true,"properties":{"id":{"type":"string"},"email":{"type":"string","nullable":true},"name":{"type":"string","nullable":true}},"required":["id","email","name"]}},"required":["id","promoCodeId","userId","yookassaPaymentId","bonusPercent","baseAmountRub","bonusAmountRub","status","createdAt","updatedAt","user"]},"PatchPromoCodeBody":{"type":"object","properties":{"description":{"type":"string","nullable":true,"maxLength":500},"bonusPercent":{"type":"integer","minimum":1,"maximum":500},"maxRedemptions":{"type":"integer","nullable":true,"minimum":0},"perUserLimit":{"type":"integer","nullable":true,"minimum":0},"minAmountRub":{"type":"integer","nullable":true,"minimum":0},"maxAmountRub":{"type":"integer","nullable":true,"minimum":0},"validFrom":{"type":"string","nullable":true,"format":"date-time"},"validUntil":{"type":"string","nullable":true,"format":"date-time"},"isActive":{"type":"boolean"}}},"ModelList":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Model"}},"total":{"type":"integer","example":1},"limit":{"type":"integer","example":50},"offset":{"type":"integer","example":0}},"required":["items","total","limit","offset"]},"Model":{"type":"object","properties":{"modelId":{"type":"string","example":"nano-banana-pro"},"displayName":{"type":"string","nullable":true},"category":{"type":"string","nullable":true,"example":"image"},"description":{"type":"string","nullable":true},"creditCost":{"type":"integer","example":5},"discountPercent":{"type":"integer","minimum":0,"maximum":99,"example":20,"description":"Per-model discount percent applied after RUB conversion."},"originalPriceRub":{"type":"number","nullable":true,"example":22.5,"description":"Base display price before applying the per-model discount."},"priceRub":{"type":"number","nullable":true,"example":18,"description":"Final display price in ₽ per 1 unit (image / second / megapixel) after discount. null if pricing not yet synced from provider."},"minGenerationPriceRub":{"type":"number","nullable":true,"example":15,"description":"Minimum realistic cost of a single full generation (₽, discounted) for models with composite billing (gpt-image-2 tokens, Sora 2 per-second per-resolution). UI shows this value prefixed with 'от' ('from'). null for models without an explicit minimum mapping."},"priceUnit":{"type":"string","nullable":true,"example":"images","description":"Billing unit from provider."},"modelLab":{"type":"string","nullable":true,"example":"Google","description":"Lab/vendor behind the model."},"modelFamily":{"type":"string","nullable":true,"example":"Nano Banana Pro","description":"Model family name."},"groupLabel":{"type":"string","nullable":true,"example":"Image Editing","description":"Human-readable group for UI clustering."},"tags":{"type":"array","nullable":true,"items":{"type":"string"},"example":["realism","typography"],"description":"Discovery tags."},"thumbnailUrl":{"type":"string","nullable":true,"description":"Provider-hosted preview URL."},"publishedAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"},{"nullable":true}]},"licenseType":{"type":"string","nullable":true,"example":"commercial"},"createdAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"}]}},"required":["modelId","displayName","category","description","creditCost","discountPercent","originalPriceRub","priceRub","minGenerationPriceRub","priceUnit","modelLab","modelFamily","groupLabel","tags","thumbnailUrl","publishedAt","licenseType","createdAt"]},"YourTunesResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"message":{"type":"string","example":"YourTunes cover generated successfully"},"artist":{"type":"string"},"release_name":{"type":"string"},"genre":{"type":"string"},"text_length":{"type":"integer","description":"Length of the supplied lyrics/text"},"cover_image_url":{"type":"string","nullable":true,"example":"https://cdn.neuroartist.ru/assets/....png","description":"URL of the generated cover. Rehosted on our CDN (S3) when asset storage is enabled; falls back to the provider URL otherwise."},"cover_image_prompt":{"type":"string","nullable":true,"description":"Prompt used to generate/edit the cover"}},"required":["success","message","artist","release_name","genre","text_length","cover_image_url","cover_image_prompt"]},"YourTunesGenerateForm":{"type":"object","properties":{"artist":{"type":"string","minLength":1,"example":"Белый реппер, The Bender","description":"Artist / исполнитель"},"release_name":{"type":"string","minLength":1,"example":"Катя","description":"Release name / название релиза"},"genre":{"type":"string","minLength":1,"example":"Rap","description":"Genre / жанр"},"text":{"type":"string","minLength":1,"example":"Они идут куда-то, верят плакатам...","description":"Lyrics / text used for mood analysis"}},"required":["artist","release_name","genre","text"]},"YourTunesEditForm":{"type":"object","properties":{"artist":{"type":"string","minLength":1},"release_name":{"type":"string","minLength":1},"genre":{"type":"string","minLength":1},"render_track_text":{"anyOf":[{"type":"boolean"},{"type":"string"}],"default":false,"description":"Render track title overlay"},"render_artist_text":{"anyOf":[{"type":"boolean"},{"type":"string"}],"default":false,"description":"Render artist name overlay"},"reference_image":{"type":"string","format":"binary","description":"Reference cover image (required)"}},"required":["artist","release_name","genre"]},"TopupResponse":{"type":"object","properties":{"payment":{"$ref":"#/components/schemas/Payment"},"confirmationUrl":{"type":"string","nullable":true,"format":"uri","description":"YooKassa hosted checkout — redirect the user here."},"promo":{"$ref":"#/components/schemas/PromoApplied"}},"required":["payment","confirmationUrl"]},"Payment":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"yookassaId":{"type":"string","nullable":true},"status":{"type":"string","example":"pending"},"creditsToGrantRub":{"type":"number","example":1000,"description":"Сумма пополнения в ₽ (decimal). Минимальная единица — 0.0001 ₽."},"amountKopecks":{"type":"integer"},"currency":{"type":"string","example":"RUB"},"description":{"type":"string","nullable":true},"createdAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"}]},"promoBonus":{"$ref":"#/components/schemas/PaymentPromoBonus"}},"required":["id","yookassaId","status","creditsToGrantRub","amountKopecks","currency","description","createdAt"]},"PaymentPromoBonus":{"type":"object","nullable":true,"properties":{"code":{"type":"string"},"bonusPercent":{"type":"integer"},"bonusAmountRub":{"type":"integer"}},"required":["code","bonusPercent","bonusAmountRub"]},"PromoApplied":{"type":"object","properties":{"code":{"type":"string","example":"WINTER20"},"bonusPercent":{"type":"integer","example":20},"baseAmountRub":{"type":"integer","example":1000},"bonusAmountRub":{"type":"integer","example":200},"finalCreditsRub":{"type":"integer","example":1200,"description":"Сколько фактически будет зачислено на баланс после оплаты."}},"required":["code","bonusPercent","baseAmountRub","bonusAmountRub","finalCreditsRub"]},"TopupRequest":{"type":"object","properties":{"amountRub":{"type":"integer","minimum":500,"example":1000,"description":"Top-up amount in rubles. Minimum 500 ₽."},"returnUrl":{"type":"string","format":"uri","example":"https://app.neuroartist.ru/billing/done","description":"Where YooKassa redirects the user after checkout"},"promoCode":{"type":"string","minLength":2,"maxLength":64,"pattern":"^[A-Za-z0-9_-]+$/u","example":"WINTER20","description":"Optional promo code that increases the credited amount by N% over the paid sum."}},"required":["amountRub","returnUrl"]},"PromoPreviewResponse":{"type":"object","properties":{"valid":{"type":"boolean","enum":[true]},"promo":{"$ref":"#/components/schemas/PromoApplied"}},"required":["valid","promo"]},"PromoPreviewRequest":{"type":"object","properties":{"code":{"type":"string","minLength":2,"maxLength":64,"pattern":"^[A-Za-z0-9_-]+$/u","example":"WINTER20","description":"Optional promo code that increases the credited amount by N% over the paid sum."},"amountRub":{"type":"integer","minimum":500,"example":1000}},"required":["code","amountRub"]},"PaymentList":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Payment"}},"total":{"type":"integer","example":1},"limit":{"type":"integer","example":50},"offset":{"type":"integer","example":0}},"required":["items","total","limit","offset"]},"PaymentDetail":{"type":"object","properties":{"payment":{"$ref":"#/components/schemas/Payment"}},"required":["payment"]},"ResendReceiptResponse":{"type":"object","properties":{"sent":{"type":"boolean"},"email":{"type":"string","nullable":true}},"required":["sent","email"]},"ModelDetailed":{"allOf":[{"$ref":"#/components/schemas/Model"},{"type":"object","properties":{"schema":{"nullable":true,"description":"OpenAPI input schema for the model"},"schemaRu":{"nullable":true,"description":"Russian overlay over the JSON Schema: { properties: { <field>: { title, description, enum[] } } }. Null if not translated yet."},"schemaFetchedAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"},{"nullable":true}]},"updatedAt":{"anyOf":[{"type":"string","format":"date-time"},{"type":"string","format":"date-time"}]}},"required":["schemaFetchedAt","updatedAt"]}]},"EstimateResponse":{"type":"object","properties":{"modelId":{"type":"string","example":"fal-ai/nano-banana-pro","description":"Internal provider model id"},"modelAlias":{"type":"string","example":"nano-banana-pro","description":"Public alias"},"estimatedCostRub":{"type":"number","example":12.0001,"description":"Estimated cost in ₽ (decimal, минимум 0.0001 ₽)."},"estimatedPriceRub":{"type":"number","example":11.25,"description":"Точная цена за единицу в ₽ (decimal). Совпадает с estimatedCostRub при quantity=1."},"priceUnit":{"type":"string","example":"images","description":"Unit the price is measured in (images, seconds, megapixels, flat)"},"quantity":{"type":"number","example":1,"description":"Quantity extracted from input"},"currency":{"type":"string","enum":["RUB"],"example":"RUB"}},"required":["modelId","modelAlias","estimatedCostRub","estimatedPriceRub","priceUnit","quantity","currency"]},"YookassaWebhookResponse":{"type":"object","properties":{"ok":{"type":"boolean"},"processed":{"type":"boolean"},"reason":{"type":"string"},"deduped":{"type":"boolean"}},"required":["ok"]},"YookassaWebhookBody":{"type":"object","properties":{"type":{"type":"string"},"event":{"type":"string","example":"payment.succeeded"},"object":{"type":"object","properties":{"id":{"type":"string"}},"required":["id"],"additionalProperties":{"nullable":true},"description":"YooKassa payment object"}},"required":["event","object"],"additionalProperties":{"nullable":true}},"SubmitResponse":{"type":"object","properties":{"requestId":{"type":"string","example":"e4a5f2b1-0000-0000-0000-000000000000","description":"Job id; use it with GET /queue/{modelId}/requests/{requestId}/..."},"status":{"type":"string","example":"IN_QUEUE"},"queuePosition":{"type":"integer","nullable":true}},"required":["requestId","status"]},"UnknownModel":{"type":"object","properties":{"error":{"type":"string","enum":["unknown_model"]},"message":{"type":"string"}},"required":["error","message"]},"ResultResponse":{"type":"object","properties":{"requestId":{"type":"string","example":"img-1777209916335-abcd1234"},"modelId":{"type":"string","example":"gpt-image-2-promo"},"status":{"type":"string","enum":["completed"]},"completedAt":{"type":"string","format":"date-time"},"output":{"type":"object","properties":{"images":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"contentType":{"type":"string","nullable":true},"width":{"type":"integer"},"height":{"type":"integer"}},"required":["url"],"additionalProperties":{"nullable":true}},"description":"Generated images. URLs point to gateway-managed CDN storage."},"video":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"contentType":{"type":"string","nullable":true}},"required":["url"],"additionalProperties":{"nullable":true}},"audio":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"contentType":{"type":"string","nullable":true}},"required":["url"],"additionalProperties":{"nullable":true}}},"additionalProperties":{"nullable":true},"description":"Gateway-normalized media output. Provider-specific payloads are adapted into this shape."},"usage":{"type":"object","additionalProperties":{"nullable":true}},"raw":{"nullable":true}},"required":["requestId","modelId","status","completedAt","output"],"additionalProperties":{"nullable":true}},"StatusResponse":{"type":"object","properties":{"status":{"type":"string","example":"COMPLETED","description":"IN_QUEUE | IN_PROGRESS | COMPLETED"},"request_id":{"type":"string"},"logs":{"type":"array","items":{"type":"object","properties":{"message":{"type":"string"},"timestamp":{"type":"string"}},"required":["message","timestamp"]}}},"required":["status","request_id"],"additionalProperties":{"nullable":true}},"CancelResponse":{"type":"object","properties":{"ok":{"type":"boolean"},"status":{"type":"string"}},"additionalProperties":{"nullable":true}},"RunTimeoutResponse":{"type":"object","properties":{"error":{"type":"string","enum":["timeout"]},"message":{"type":"string"},"requestId":{"type":"string"}},"required":["error","message","requestId"]}},"parameters":{}},"paths":{"/health":{"get":{"tags":["Health"],"summary":"Gateway liveness probe","description":"Returns 200 with `status: ok` when the gateway can reach its database. Returns 200 with `status: degraded` + `db: down` when the DB query fails — process is alive but unhealthy. Use this for Docker/K8s liveness + readiness probes.","responses":{"200":{"description":"Current liveness state","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}}}}},"/changelog.json":{"get":{"tags":["Changelog"],"summary":"Public changelog feed","description":"Returns `{ entries: [{ version, date, notes }] }` sorted by date descending, parsed from `changelog.yml` at repo root. Cached in-process for 60s. Public — no auth required.","responses":{"200":{"description":"Ordered changelog entries","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangelogResponse"}}}},"500":{"description":"Parse failure — file missing, malformed YAML, or shape invalid. Ops problem, not a client issue.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/me":{"get":{"tags":["Me"],"summary":"Current user profile","description":"Returns the currently authenticated user along with the auth source (session cookie vs API key) and, if applicable, the API key id.","security":[{"ApiKeyAuth":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"Current user profile","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"patch":{"tags":["Me"],"summary":"Update profile","description":"Updates mutable profile fields (name, avatar image). Session-only — API keys can't edit profile.","security":[{"SessionCookie":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchMeBody"}}}},"responses":{"200":{"description":"Updated profile","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchMeResponse"}}}},"400":{"description":"Invalid body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Session-only","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"User row not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/me/sessions":{"get":{"tags":["Me"],"summary":"List active sessions","description":"Returns all active Better Auth sessions for the current user. Session-only.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Sessions list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionsList"}}}},"403":{"description":"Session-only","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/me/sessions/{id}":{"delete":{"tags":["Me"],"summary":"Revoke session","description":"Revokes a specific session by id. Session-only.","security":[{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"Revoked"},"403":{"description":"Session-only","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/me/balance":{"get":{"tags":["Me"],"summary":"Current credit balance","security":[{"ApiKeyAuth":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"Current credit balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Balance"}}}}}}},"/me/transactions":{"get":{"tags":["Me"],"summary":"List credit transactions","description":"Paginated list of the user's credit transactions. Filter by type: topup/deduct/refund/grant.","security":[{"ApiKeyAuth":[]},{"SessionCookie":[]}],"parameters":[{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50,"example":50},"required":false,"name":"limit","in":"query"},{"schema":{"type":"integer","nullable":true,"minimum":0,"default":0,"example":0},"required":false,"name":"offset","in":"query"},{"schema":{"type":"string","enum":["topup","deduct","refund","grant"]},"required":false,"name":"type","in":"query"}],"responses":{"200":{"description":"Paginated transactions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransactionList"}}}}}}},"/me/usage":{"get":{"tags":["Me"],"summary":"Usage snapshot","description":"Returns aggregated credit usage across one or more rolling time windows (5h/24h/7d/30d). Includes current balance.","security":[{"ApiKeyAuth":[]},{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string","default":"5h,24h,7d,30d","example":"5h,24h,7d,30d","description":"Comma-separated windows"},"required":false,"description":"Comma-separated windows","name":"windows","in":"query"}],"responses":{"200":{"description":"Usage snapshot","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageSnapshot"}}}}}}},"/me/usage/history":{"get":{"tags":["Me"],"summary":"Usage time series","description":"Time-bucketed usage history for charting. Granularity — hour or day. Range — [from, to].","security":[{"ApiKeyAuth":[]},{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string","nullable":true,"format":"date-time","example":"2026-04-01T00:00:00Z"},"required":false,"name":"from","in":"query"},{"schema":{"type":"string","nullable":true,"format":"date-time","example":"2026-04-12T00:00:00Z"},"required":false,"name":"to","in":"query"},{"schema":{"type":"string","enum":["hour","day"],"default":"day"},"required":false,"name":"granularity","in":"query"}],"responses":{"200":{"description":"Usage time series","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageHistory"}}}}}}},"/me/usage/by-model":{"get":{"tags":["Me"],"summary":"Usage grouped by model","description":"Credit spend per model over a rolling window (5h/24h/7d/30d). Ordered by credits descending. Credits sum includes only `operation='submit' AND status='success'` rows (consistent with /me/keys lifetimeCostRub). Session or API-key auth.","security":[{"ApiKeyAuth":[]},{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string","enum":["5h","24h","7d","30d"],"default":"24h","description":"Rolling time window used by usage aggregations"},"required":false,"description":"Rolling time window used by usage aggregations","name":"window","in":"query"}],"responses":{"200":{"description":"Usage aggregated by model","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageByModelResponse"}}}},"400":{"description":"Invalid window","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/me/usage/by-key":{"get":{"tags":["Me"],"summary":"Usage grouped by API key","description":"Credit spend per API key over a rolling window (5h/24h/7d/30d). Excludes session-auth usage. Deleted keys still appear (keyName/partialKey null) — history is preserved. Ordered by credits descending. Session or API-key auth.","security":[{"ApiKeyAuth":[]},{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string","enum":["5h","24h","7d","30d"],"default":"24h","description":"Rolling time window used by usage aggregations"},"required":false,"description":"Rolling time window used by usage aggregations","name":"window","in":"query"}],"responses":{"200":{"description":"Usage aggregated by API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageByKeyResponse"}}}},"400":{"description":"Invalid window","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/me/activity":{"get":{"tags":["Me"],"summary":"Per-request activity feed","description":"Paginated feed of every usage-record row: each proxy call (submit/result/status/cancel/stream). Filter by `modelId` to scope to a single model.","security":[{"ApiKeyAuth":[]},{"SessionCookie":[]}],"parameters":[{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50,"example":50},"required":false,"name":"limit","in":"query"},{"schema":{"type":"integer","nullable":true,"minimum":0,"default":0,"example":0},"required":false,"name":"offset","in":"query"},{"schema":{"type":"string"},"required":false,"name":"modelId","in":"query"},{"schema":{"type":"string","example":"cuid_abc123","description":"Filter by Better Auth apikey.id"},"required":false,"description":"Filter by Better Auth apikey.id","name":"apiKeyId","in":"query"},{"schema":{"type":"string","enum":["submit","status","result","cancel","stream"],"description":"Filter by proxy operation (submit/status/result/cancel/stream)"},"required":false,"description":"Filter by proxy operation (submit/status/result/cancel/stream)","name":"operation","in":"query"},{"schema":{"type":"string","enum":["success","error"],"description":"Filter by outcome status (success/error)"},"required":false,"description":"Filter by outcome status (success/error)","name":"status","in":"query"}],"responses":{"200":{"description":"Paginated activity","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActivityList"}}}}}}},"/me/activity/{falRequestId}":{"get":{"tags":["Me"],"summary":"Single request detail","description":"Returns the `usage_record` row (operation='submit') for one fal request id plus every asset row produced by it. 404 when missing or not owned by caller — no distinction.","security":[{"ApiKeyAuth":[]},{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"description":"Provider request id returned by /submit"},"required":true,"description":"Provider request id returned by /submit","name":"falRequestId","in":"path"}],"responses":{"200":{"description":"Request detail with assets","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SingleActivityResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found or not owned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/me/keys":{"get":{"tags":["Me"],"summary":"List API keys","description":"Lists the user's API keys (metadata only — secret never returned). Session-only.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Keys list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeysList"}}}},"403":{"description":"Session-only","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"tags":["Me"],"summary":"Create API key","description":"Issues a new `na_live_*` API key. The plaintext key is returned ONCE in the response — store it immediately.","security":[{"SessionCookie":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateKeyBody"}}}},"responses":{"201":{"description":"Key created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatedKey"}}}},"403":{"description":"Session-only","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/me/keys/{id}":{"delete":{"tags":["Me"],"summary":"Delete API key","description":"Revokes a specific API key. Session-only.","security":[{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"Key deleted"},"403":{"description":"Session-only","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"patch":{"tags":["Me"],"summary":"Update API key","description":"Updates an API key's name or metadata (e.g. `metadata.monthlyLimitRub` — per-key monthly spend cap in ₽). Setting `metadata.monthlyLimitRub:null` removes the cap. Session-only.","security":[{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchKeyBody"}}}},"responses":{"200":{"description":"Updated key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeySummary"}}}},"400":{"description":"Invalid body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Session-only","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found / not owned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/me/payment-methods":{"get":{"tags":["Me"],"summary":"List saved payment methods","description":"Returns YooKassa saved payment methods captured from prior successful top-ups. Never exposes YooKassa's `payment_method_id` (sensitive).","security":[{"ApiKeyAuth":[]},{"SessionCookie":[]}],"responses":{"200":{"description":"Saved payment methods list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedPaymentMethodsList"}}}}}}},"/me/payment-methods/{id}":{"delete":{"tags":["Me"],"summary":"Delete saved payment method","description":"Deletes a saved payment method. If it was the default, the newest remaining method is promoted. Session-only.","security":[{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"Deleted"},"403":{"description":"Session-only","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found / not owned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/me/payment-methods/{id}/default":{"post":{"tags":["Me"],"summary":"Set default payment method","description":"Marks the given method as default — used for future auto-topup charges. Session-only.","security":[{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Default set","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetDefaultPaymentMethodResponse"}}}},"403":{"description":"Session-only","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found / not owned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/me/auto-topup":{"patch":{"tags":["Me"],"summary":"Configure auto-topup","description":"Sets or clears auto-topup (threshold + recurring amount). `enabled: false` clears both values. Session-only.","security":[{"SessionCookie":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchAutoTopupBody"}}}},"responses":{"200":{"description":"Current auto-topup config","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AutoTopupConfig"}}}},"400":{"description":"Invalid body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Session-only","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/me/uploads":{"post":{"tags":["Me"],"summary":"Upload an input file (image, video, or audio)","description":"Streams a multipart/form-data file (field name: `file`) to Cloud.ru S3 and returns a public URL that providers (fal.ai, Azure, etc.) can fetch as img2img / v2v / voice-clone input. Session-only (API keys not permitted). Max size configured via MAX_UPLOAD_BYTES; allowed MIME types: image/jpeg, image/png, image/webp, image/heic, image/heif, video/mp4, video/quicktime, video/webm, audio/mpeg, audio/mp3, audio/wav, audio/wave, audio/x-wav, audio/webm, audio/mp4, audio/ogg, audio/flac.","security":[{"SessionCookie":[]}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"string","format":"binary"}}}}}},"responses":{"201":{"description":"File uploaded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadResponse"}}}},"400":{"description":"Missing or malformed file field","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Session-only (API keys cannot upload)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"413":{"description":"File exceeds MAX_UPLOAD_BYTES","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"415":{"description":"Unsupported MIME type","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"Asset storage not configured on this deployment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/admin/stats":{"get":{"tags":["Admin"],"summary":"Global counters","description":"Returns global platform counters: users, active api keys, models, total credits issued/used.","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Global stats","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminStats"}}}},"403":{"description":"Admin role required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/admin/users":{"get":{"tags":["Admin"],"summary":"List users","description":"Paginated list with optional search by email/name and filter by role/ban status.","security":[{"SessionCookie":[]}],"parameters":[{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50,"example":50},"required":false,"name":"limit","in":"query"},{"schema":{"type":"integer","nullable":true,"minimum":0,"default":0,"example":0},"required":false,"name":"offset","in":"query"},{"schema":{"type":"string","description":"ILIKE search on email and name"},"required":false,"description":"ILIKE search on email and name","name":"q","in":"query"},{"schema":{"type":"string","enum":["user","admin"]},"required":false,"name":"role","in":"query"},{"schema":{"type":"boolean","nullable":true},"required":false,"name":"banned","in":"query"}],"responses":{"200":{"description":"Paginated users","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserList"}}}}}}},"/admin/users/{id}":{"get":{"tags":["Admin"],"summary":"Get user detail","description":"Full user card including ban state, current balance and usage across 24h/7d/30d windows.","security":[{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"User detail","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserDetail"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"patch":{"tags":["Admin"],"summary":"Update user role","description":"Sets `user.role` (Better Auth admin plugin). You cannot demote your own account from admin.","security":[{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchUserBody"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserPatchResponse"}}}},"403":{"description":"Cannot demote self","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/admin/users/{id}/role":{"post":{"tags":["Admin"],"summary":"Set user role (POST)","description":"Same as `PATCH /users/{id}` with `{ role }`. Preferred from browsers behind strict proxies.","security":[{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchUserBody"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserPatchResponse"}}}},"403":{"description":"Cannot demote self","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/admin/users/{id}/ban":{"post":{"tags":["Admin"],"summary":"Ban user","description":"Bans the user and atomically revokes all their active sessions. API keys remain enabled (admin can disable them separately).","security":[{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BanBody"}}}},"responses":{"200":{"description":"Banned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OkResponse"}}}}}}},"/admin/users/{id}/unban":{"post":{"tags":["Admin"],"summary":"Unban user","security":[{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Unbanned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OkResponse"}}}}}}},"/admin/users/{id}/credits/grant":{"post":{"tags":["Admin"],"summary":"Grant credits","description":"Atomically adds credits to user balance as a `grant` transaction.","security":[{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GrantBody"}}}},"responses":{"201":{"description":"Transaction created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransactionWrapper"}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/admin/users/{id}/credits/clawback":{"post":{"tags":["Admin"],"summary":"Clawback credits","description":"Deducts credits from user balance. Returns 402 if insufficient balance (same envelope as consumer-side insufficient balance).","security":[{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GrantBody"}}}},"responses":{"201":{"description":"Transaction created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransactionWrapper"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalance"}}}}}}},"/admin/users/{id}/transactions":{"get":{"tags":["Admin"],"summary":"User transactions","security":[{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50,"example":50},"required":false,"name":"limit","in":"query"},{"schema":{"type":"integer","nullable":true,"minimum":0,"default":0,"example":0},"required":false,"name":"offset","in":"query"},{"schema":{"type":"string","enum":["topup","deduct","refund","grant"]},"required":false,"name":"type","in":"query"}],"responses":{"200":{"description":"Paginated transactions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminTransactionList"}}}}}}},"/admin/users/{id}/usage":{"get":{"tags":["Admin"],"summary":"User usage snapshot","security":[{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","default":"5h,24h,7d,30d","example":"24h,7d,30d"},"required":false,"name":"windows","in":"query"}],"responses":{"200":{"description":"Usage snapshot","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserUsage"}}}}}}},"/admin/users/{id}/sessions":{"get":{"tags":["Admin"],"summary":"List user sessions","security":[{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Sessions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminSessionsList"}}}}}},"delete":{"tags":["Admin"],"summary":"Revoke all user sessions","description":"Forces the user to re-login on all devices.","security":[{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"Revoked"}}}},"/admin/users/{id}/keys":{"get":{"tags":["Admin"],"summary":"List user API keys","security":[{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Keys","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminKeysList"}}}}}}},"/admin/users/{id}/keys/{keyId}":{"delete":{"tags":["Admin"],"summary":"Delete a user API key","security":[{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"keyId","in":"path"}],"responses":{"204":{"description":"Deleted"}}},"patch":{"tags":["Admin"],"summary":"Tune API key","description":"Enable/disable, change rate limit window/max, reset remaining counter, set/clear expiry.","security":[{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string"},"required":true,"name":"keyId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchKeyBody"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchKeyResponse"}}}},"404":{"description":"Key not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/admin/transactions":{"get":{"tags":["Admin"],"summary":"Global transactions log","description":"All credit transactions across all users. Filter by userId and/or type.","security":[{"SessionCookie":[]}],"parameters":[{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50,"example":50},"required":false,"name":"limit","in":"query"},{"schema":{"type":"integer","nullable":true,"minimum":0,"default":0,"example":0},"required":false,"name":"offset","in":"query"},{"schema":{"type":"string"},"required":false,"name":"userId","in":"query"},{"schema":{"type":"string","enum":["topup","deduct","refund","grant"]},"required":false,"name":"type","in":"query"}],"responses":{"200":{"description":"Paginated transactions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminTransactionList"}}}}}}},"/admin/usage/top-users":{"get":{"tags":["Admin"],"summary":"Top credit consumers","description":"Ranked list of users by credit consumption within a rolling window.","security":[{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string","enum":["5h","24h","7d","30d"],"default":"30d","description":"Rolling time window used by usage aggregations"},"required":false,"description":"Rolling time window used by usage aggregations","name":"window","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Top users","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TopUsersResponse"}}}}}}},"/admin/models":{"get":{"tags":["Admin"],"summary":"List all models (incl. hidden)","security":[{"SessionCookie":[]}],"parameters":[{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50,"example":50},"required":false,"name":"limit","in":"query"},{"schema":{"type":"integer","nullable":true,"minimum":0,"default":0,"example":0},"required":false,"name":"offset","in":"query"},{"schema":{"type":"string"},"required":false,"name":"category","in":"query"},{"schema":{"type":"boolean","nullable":true},"required":false,"name":"visibleOnly","in":"query"},{"schema":{"type":"string"},"required":false,"name":"q","in":"query"}],"responses":{"200":{"description":"Paginated models","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminModelList"}}}}}},"post":{"tags":["Admin"],"summary":"Create or update a model","description":"Upserts a model row in the registry. If the model already exists, updates its mutable fields.","security":[{"SessionCookie":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertModelBody"}}}},"responses":{"201":{"description":"Model created/updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminModelWrapper"}}}},"400":{"description":"Invalid pricing config","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PricingConfigErrorResponse"}}}}}},"delete":{"tags":["Admin"],"summary":"Remove a model","description":"Removes a model from the registry. `modelId` is passed via body to avoid URL-encoding of slashes.","security":[{"SessionCookie":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SyncModelBody"}}}},"responses":{"204":{"description":"Deleted"}}}},"/admin/models/sync":{"post":{"tags":["Admin"],"summary":"Sync model schema from provider","description":"Fetches OpenAPI input schema for the given model from the provider and caches it in the registry.","security":[{"SessionCookie":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SyncModelBody"}}}},"responses":{"200":{"description":"Schema synced","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminModelWrapper"}}}},"404":{"description":"Model not found on provider","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/admin/models/sync-catalog":{"post":{"tags":["Admin"],"summary":"Force full catalog re-sync","description":"Triggers `syncAllCatalogs({ force: true })` for every registered provider. Fetches the full model list + pricing and upserts into the DB, bypassing the 6h TTL. Long-running (tens of seconds for ~1000 models).","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"Sync finished","content":{"application/json":{"schema":{"type":"object","properties":{"before":{"type":"integer"},"after":{"type":"integer"},"added":{"type":"integer"},"elapsedMs":{"type":"integer"}},"required":["before","after","added","elapsedMs"]}}}}}}},"/admin/email/test":{"post":{"tags":["Admin"],"summary":"Send transactional email probe","description":"Renders one of (or all) transactional email templates with sample data and dispatches via Rusender. Recipient defaults to the calling admin's own email. Use to verify deliverability, DKIM/SPF, and template rendering across mail clients.","security":[{"SessionCookie":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminTestEmailBody"}}}},"responses":{"200":{"description":"Probe(s) sent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminTestEmailResponse"}}}},"503":{"description":"RUSENDER_API_KEY is not configured","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/admin/providers":{"get":{"tags":["Admin"],"summary":"Список инстанций провайдеров","security":[{"SessionCookie":[]}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"providers":{"type":"array","items":{"$ref":"#/components/schemas/ProviderInstance"}}},"required":["providers"]}}}}}},"post":{"tags":["Admin"],"summary":"Создать инстанцию провайдера","security":[{"SessionCookie":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProviderBody"}}}},"responses":{"201":{"description":"Создан","content":{"application/json":{"schema":{"type":"object","properties":{"provider":{"$ref":"#/components/schemas/ProviderInstance"}},"required":["provider"]}}}},"409":{"description":"Имя уже занято","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/admin/providers/:id":{"patch":{"tags":["Admin"],"summary":"Обновить инстанцию провайдера","security":[{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchProviderBody"}}}},"responses":{"200":{"description":"Обновлено","content":{"application/json":{"schema":{"type":"object","properties":{"provider":{"$ref":"#/components/schemas/ProviderInstance"}},"required":["provider"]}}}},"404":{"description":"Не найдено","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"tags":["Admin"],"summary":"Удалить инстанцию провайдера","security":[{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"Удалено"},"404":{"description":"Не найдено","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/admin/providers/:id/test":{"post":{"tags":["Admin"],"summary":"Проверить подключение провайдера","description":"Инициализирует провайдер из конфига и выполняет тестовый запрос.","security":[{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Результат проверки","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProviderTestResult"}}}},"404":{"description":"Не найдено","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/admin/providers/:id/sync-catalog":{"post":{"tags":["Admin"],"summary":"Синхронизировать каталог моделей провайдера","security":[{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Результат синхронизации","content":{"application/json":{"schema":{"type":"object","properties":{"added":{"type":"integer"},"total":{"type":"integer"},"elapsedMs":{"type":"integer"}},"required":["added","total","elapsedMs"]}}}},"404":{"description":"Не найдено","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/admin/invoice-requests":{"get":{"tags":["Admin"],"summary":"Список B2B-заявок на счёт","security":[{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string","maxLength":64},"required":false,"name":"search","in":"query"},{"schema":{"type":"string","enum":["all","pending","invoiced","paid","rejected"],"default":"all"},"required":false,"name":"status","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"required":false,"name":"limit","in":"query"},{"schema":{"type":"integer","nullable":true,"minimum":0,"default":0},"required":false,"name":"offset","in":"query"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/AdminInvoiceRequest"}},"total":{"type":"integer"},"limit":{"type":"integer"},"offset":{"type":"integer"}},"required":["items","total","limit","offset"]}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/admin/invoice-requests/{id}":{"patch":{"tags":["Admin"],"summary":"Обновить статус/реквизиты заявки","description":"Используется оператором: меняет статус (pending → invoiced → paid / rejected), записывает номер счёта и заметку. Все поля опциональны, передавайте только то, что меняется.","security":[{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchInvoiceRequestBody"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminInvoiceRequest"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/admin/promo-codes":{"get":{"tags":["Admin"],"summary":"Список промокодов","security":[{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string","maxLength":64},"required":false,"name":"search","in":"query"},{"schema":{"type":"string","enum":["all","active","inactive"],"default":"all"},"required":false,"name":"status","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"required":false,"name":"limit","in":"query"},{"schema":{"type":"integer","nullable":true,"minimum":0,"default":0},"required":false,"name":"offset","in":"query"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/PromoCodeWithStats"}},"total":{"type":"integer"},"limit":{"type":"integer"},"offset":{"type":"integer"}},"required":["items","total","limit","offset"]}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"tags":["Admin"],"summary":"Создать промокод","security":[{"SessionCookie":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePromoCodeBody"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromoCode"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Promo code with this code already exists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/admin/promo-codes/{id}":{"get":{"tags":["Admin"],"summary":"Детали промокода + последние применения","security":[{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromoCodeDetail"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"patch":{"tags":["Admin"],"summary":"Обновить промокод","security":[{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchPromoCodeBody"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromoCode"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"tags":["Admin"],"summary":"Soft-удалить промокод (isActive=false)","security":[{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"Deactivated"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/admin/promo-codes/{id}/redemptions":{"get":{"tags":["Admin"],"summary":"История применений промокода","security":[{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string","format":"uuid"},"required":true,"name":"id","in":"path"},{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"required":false,"name":"limit","in":"query"},{"schema":{"type":"integer","nullable":true,"minimum":0,"default":0},"required":false,"name":"offset","in":"query"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/PromoRedemption"}},"total":{"type":"integer"},"limit":{"type":"integer"},"offset":{"type":"integer"}},"required":["items","total","limit","offset"]}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/models":{"get":{"tags":["Models"],"summary":"List public models","description":"Returns paginated catalog of visible models. Public endpoint — no authentication required. Default ordering (`sort=popular`): provider popularity rank; use `sort=by_category` for sectioned catalogs (grouped by model category, then rank).","parameters":[{"schema":{"type":"string","example":"image-to-image","description":"Filter by category (CSV). Example: `text-to-image` or `text-to-image,image-to-image`."},"required":false,"description":"Filter by category (CSV). Example: `text-to-image` or `text-to-image,image-to-image`.","name":"category","in":"query"},{"schema":{"type":"string","example":"Google","description":"Filter by lab/vendor (CSV). Example: `Google` or `Google,Black Forest Labs,Kling`."},"required":false,"description":"Filter by lab/vendor (CSV). Example: `Google` or `Google,Black Forest Labs,Kling`.","name":"lab","in":"query"},{"schema":{"type":"string","example":"realism","description":"Filter by tag(s) (CSV). AND semantics — model must carry all listed tags."},"required":false,"description":"Filter by tag(s) (CSV). AND semantics — model must carry all listed tags.","name":"tag","in":"query"},{"schema":{"type":"string","maxLength":64,"example":"banana","description":"Case-insensitive substring search across alias, displayName, description."},"required":false,"description":"Case-insensitive substring search across alias, displayName, description.","name":"search","in":"query"},{"schema":{"type":"string","enum":["popular","newest","price_asc","by_category"],"default":"popular","example":"popular","description":"Ordering strategy. `popular`: provider's ranked listing first (default). `newest`: latest publishedAt first. `price_asc`: cheapest first. `by_category`: catalog order (category → provider rank → date) for sectioned UIs."},"required":false,"description":"Ordering strategy. `popular`: provider's ranked listing first (default). `newest`: latest publishedAt first. `price_asc`: cheapest first. `by_category`: catalog order (category → provider rank → date) for sectioned UIs.","name":"sort","in":"query"},{"schema":{"type":"boolean","nullable":true,"default":false,"example":false,"description":"Include unbranded/community endpoints (modelLab=null). Defaults to false to keep the catalog focused on vendored models; pass `true` to surface the long tail."},"required":false,"description":"Include unbranded/community endpoints (modelLab=null). Defaults to false to keep the catalog focused on vendored models; pass `true` to surface the long tail.","name":"includeCommunity","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50,"example":50},"required":false,"name":"limit","in":"query"},{"schema":{"type":"integer","nullable":true,"minimum":0,"default":0,"example":0},"required":false,"name":"offset","in":"query"}],"responses":{"200":{"description":"Paginated list of visible models","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ModelList"}}}}}}},"/integrations/yourtunes":{"post":{"tags":["Integrations"],"summary":"YourTunes: generate album cover","description":"Generate a new album cover from artist/release/genre metadata plus lyrics. Uses `fal-ai/nano-banana` internally. Accepts `multipart/form-data`; returns JSON.","security":[{"ApiKeyAuth":[]}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/YourTunesGenerateForm"}}}},"responses":{"200":{"description":"Cover generated / edited successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/YourTunesResponse"}}}},"400":{"description":"Malformed multipart body or missing required field","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalance"}}}},"413":{"description":"Reference image exceeds MAX_UPLOAD_BYTES (yourtunes-edit only)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"415":{"description":"Reference image MIME not in allowed set: image/jpeg, image/png, image/webp (yourtunes-edit only)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"User-level generation rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Upstream generation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"502":{"description":"Upstream provider returned an error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"Upstream provider unavailable (circuit breaker tripped)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"504":{"description":"Generation exceeded the synchronous wait budget","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/integrations/yourtunes-edit":{"post":{"tags":["Integrations"],"summary":"YourTunes: add text overlay to cover","description":"Add artist name and/or track title as a text overlay on an existing cover image. Uses `fal-ai/nano-banana/edit`. Requires a reference image in the multipart body. Text rendered without commas by construction.","security":[{"ApiKeyAuth":[]}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/YourTunesEditForm"}}}},"responses":{"200":{"description":"Cover generated / edited successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/YourTunesResponse"}}}},"400":{"description":"Malformed multipart body or missing required field","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalance"}}}},"413":{"description":"Reference image exceeds MAX_UPLOAD_BYTES (yourtunes-edit only)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"415":{"description":"Reference image MIME not in allowed set: image/jpeg, image/png, image/webp (yourtunes-edit only)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"User-level generation rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Upstream generation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"502":{"description":"Upstream provider returned an error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"Upstream provider unavailable (circuit breaker tripped)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"504":{"description":"Generation exceeded the synchronous wait budget","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/billing/topup":{"post":{"tags":["Billing"],"summary":"Create a top-up payment","description":"Top up balance with any amount (minimum 500 ₽). Creates a YooKassa payment and returns a checkout URL. Session-only — API keys cannot initiate payments.","security":[{"SessionCookie":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TopupRequest"}}}},"responses":{"201":{"description":"Payment created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TopupResponse"}}}},"400":{"description":"Amount below minimum or validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Session-only endpoint — API key auth forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"502":{"description":"YooKassa provider error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/billing/promo-codes/preview":{"post":{"tags":["Billing"],"summary":"Preview promo-code discount","description":"Validates a promo code against the given amount and returns the bonus that would be applied if the user proceeded to top up. Does not reserve any slot.","security":[{"SessionCookie":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromoPreviewRequest"}}}},"responses":{"200":{"description":"Promo is applicable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromoPreviewResponse"}}}},"400":{"description":"Promo invalid (bad code, expired, limit reached, …)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Session-only endpoint","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/billing/payments":{"get":{"tags":["Billing"],"summary":"List user payments","description":"Paginated history of the current user's top-up payments.","security":[{"ApiKeyAuth":[]},{"SessionCookie":[]}],"parameters":[{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50,"example":50},"required":false,"name":"limit","in":"query"},{"schema":{"type":"integer","nullable":true,"minimum":0,"default":0,"example":0},"required":false,"name":"offset","in":"query"}],"responses":{"200":{"description":"Paginated payments","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentList"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/billing/payments/{id}":{"get":{"tags":["Billing"],"summary":"Get one payment","description":"Fetch a specific payment by its YooKassa ID. Scoped to the current user.","security":[{"ApiKeyAuth":[]},{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string","description":"YooKassa payment id"},"required":true,"description":"YooKassa payment id","name":"id","in":"path"}],"responses":{"200":{"description":"Payment detail","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentDetail"}}}},"404":{"description":"Payment not found or does not belong to user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/billing/payments/{id}/receipt/resend":{"post":{"tags":["Billing"],"summary":"Переотправить подтверждение оплаты на email","description":"Повторно отправляет на email пользователя письмо-подтверждение оплаты с реквизитами платежа (сумма, баланс, ID). Это не фискальный чек — настоящий чек YooKassa отправляет автоматически при успешной оплате один раз; API на повторную выдачу у YK нет.","security":[{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string","description":"YooKassa payment id"},"required":true,"description":"YooKassa payment id","name":"id","in":"path"}],"responses":{"200":{"description":"Письмо поставлено в очередь","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResendReceiptResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Session-only — API key auth forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Платёж не найден, чужой, или не успешный","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Не удалось отправить письмо","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/models/{modelId}":{"get":{"tags":["Models"],"summary":"Get model details","description":"Returns full model metadata including cached OpenAPI input schema. `modelId` may contain slashes (e.g. `kling-video/v2.6/pro/text-to-video`) — pass as-is in the path.","parameters":[{"schema":{"type":"string","minLength":1,"example":"nano-banana-pro","description":"Model identifier. May contain slashes — not URL-encoded."},"required":true,"description":"Model identifier. May contain slashes — not URL-encoded.","name":"modelId","in":"path"}],"responses":{"200":{"description":"Model detail with schema","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ModelDetailed"}}}},"404":{"description":"Unknown or hidden model","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/models/{modelId}/estimate":{"post":{"tags":["Models"],"summary":"Cost estimate for a generation request","description":"Previews the ₽ cost of `POST /queue/{modelId}` for the given input body without running generation. No side effects: no balance deduction, no usage record, no provider call.\n\nThe pricing logic is shared with `/queue/{modelId}` — the response reflects exactly what a submit would charge.","security":[{"ApiKeyAuth":[]},{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"example":"nano-banana-pro","description":"Model identifier. May contain slashes — not URL-encoded."},"required":true,"description":"Model identifier. May contain slashes — not URL-encoded.","name":"modelId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":{"nullable":true},"description":"Same shape as the submit body for the model"}}}},"responses":{"200":{"description":"Cost estimate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EstimateResponse"}}}},"400":{"description":"Invalid body (must be JSON object)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Unknown model","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/webhooks/yookassa/notify":{"post":{"tags":["Webhooks"],"summary":"YooKassa payment webhook","description":"Receives payment status updates from YooKassa. Protected by IP whitelist (YooKassa official ranges). **Not called by API consumers** — YooKassa posts here automatically.\n\nOn `payment.succeeded`: grants credits to the user.\n\nAlways returns 200 to prevent YooKassa from retrying.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/YookassaWebhookBody"}}}},"responses":{"200":{"description":"Webhook processed (always 200)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/YookassaWebhookResponse"}}}},"400":{"description":"Invalid JSON","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/queue/{modelId}":{"post":{"tags":["Generation"],"summary":"Submit a generation job","description":"Submits a generation request to the specified model.\n\nBehaviour:\n\n1. Authorizes the caller, looks up the model in our registry.\n2. Atomically deducts the configured credit cost from the caller's balance.\n3. Forwards the body to the generation provider, capturing the completion event for asset re-hosting.\n4. On provider failure, credits are refunded automatically.\n\nRequest body is model-specific — refer to the model's schema in `GET /models/{modelId}` for valid fields.\n\nReturns `requestId` immediately. Poll `GET /queue/{modelId}/requests/{requestId}/status` for completion, then `GET /queue/{modelId}/requests/{requestId}` for the final payload.","security":[{"ApiKeyAuth":[]},{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"example":"nano-banana-pro","description":"Model identifier (alias). May contain slashes — pass as-is, not URL-encoded."},"required":true,"description":"Model identifier (alias). May contain slashes — pass as-is, not URL-encoded.","name":"modelId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":{"nullable":true},"description":"Model-specific input — see GET /models/{modelId}"}}}},"responses":{"200":{"description":"Submitted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmitResponse"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalance"}}}},"404":{"description":"Unknown model (not in registry)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnknownModel"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/queue/{modelId}/requests/{requestId}":{"get":{"tags":["Generation"],"summary":"Get generation result","description":"Fetches the final result for a completed job. By default this returns the gateway-normalized result contract: `requestId`, `modelId`, `status`, `output`, optional `usage`, and optional sanitized `raw` provider payload. Provider-specific shapes are adapted inside the gateway, so clients do not need to branch on fal vs Azure.\n\nPass `?passthrough=1` only for debugging to fetch the original provider payload when the provider supports a result lookup.","security":[{"ApiKeyAuth":[]},{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"example":"nano-banana-pro"},"required":true,"name":"modelId","in":"path"},{"schema":{"type":"string","minLength":1,"example":"e4a5f2b1-..."},"required":true,"name":"requestId","in":"path"},{"schema":{"type":"string","example":"1","description":"Debug only. Set to `1` to bypass the normalized gateway contract and return the original provider payload when available."},"required":false,"description":"Debug only. Set to `1` to bypass the normalized gateway contract and return the original provider payload when available.","name":"passthrough","in":"query"}],"responses":{"200":{"description":"Generation result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResultResponse"}}}},"404":{"description":"Request not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/queue/{modelId}/requests/{requestId}/status":{"get":{"tags":["Generation"],"summary":"Poll generation status","description":"Returns the current status of a submitted job. Poll every few seconds until the job reaches a terminal state.","security":[{"ApiKeyAuth":[]},{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"example":"nano-banana-pro"},"required":true,"name":"modelId","in":"path"},{"schema":{"type":"string","minLength":1,"example":"e4a5f2b1-..."},"required":true,"name":"requestId","in":"path"},{"schema":{"type":"string","example":"1","description":"Set to `1` to include execution logs."},"required":false,"description":"Set to `1` to include execution logs.","name":"logs","in":"query"}],"responses":{"200":{"description":"Current status","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}}}}},"/queue/{modelId}/requests/{requestId}/cancel":{"put":{"tags":["Generation"],"summary":"Cancel a pending job","description":"Cancels a job that hasn't started generation yet. The provider ignores the request if it's already running. Note the method is **PUT** (not DELETE).","security":[{"ApiKeyAuth":[]},{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"example":"nano-banana-pro"},"required":true,"name":"modelId","in":"path"},{"schema":{"type":"string","minLength":1,"example":"e4a5f2b1-..."},"required":true,"name":"requestId","in":"path"}],"responses":{"200":{"description":"Cancellation requested","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CancelResponse"}}}}}}},"/run/{modelId}":{"post":{"tags":["Generation"],"summary":"Generate (synchronous)","description":"Submits a generation request and waits for the result. Returns the same gateway-normalized result contract as `GET /queue/{modelId}/requests/{requestId}`.\n\nInternally: submit → poll until completed → adapt provider payload to the gateway result spec with CDN URLs.\n\nTimeout: 5 minutes. If generation takes longer, returns 202 with a `requestId` to continue polling via queue endpoints.\n\nPricing: same as queue submit — cost is calculated from input parameters (duration, resolution, count) and deducted upfront. Overcharges are auto-refunded.","security":[{"ApiKeyAuth":[]},{"SessionCookie":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"example":"nano-banana-pro","description":"Model identifier (alias). May contain slashes — pass as-is, not URL-encoded."},"required":true,"description":"Model identifier (alias). May contain slashes — pass as-is, not URL-encoded.","name":"modelId","in":"path"},{"schema":{"type":"string","example":"1","description":"Debug only. Set to `1` to bypass the normalized gateway contract and return the original provider payload when available."},"required":false,"description":"Debug only. Set to `1` to bypass the normalized gateway contract and return the original provider payload when available.","name":"passthrough","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":{"nullable":true},"description":"Model-specific input — see GET /models/{modelId}"}}}},"responses":{"200":{"description":"Generation result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResultResponse"}}}},"202":{"description":"Generation still in progress (timeout). Use queue endpoints to poll.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunTimeoutResponse"}}}},"402":{"description":"Insufficient balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsufficientBalance"}}}},"404":{"description":"Unknown model","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnknownModel"}}}},"500":{"description":"Generation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}}}}