{"openapi":"3.1.0","info":{"title":"AIVeed API Platform","version":"1.0.0","description":"API-first platform for AI-powered video, image, and audio generation. Human docs at https://developer.aiveed.io/docs — LLM-sized summary at https://developer.aiveed.io/llms-full.txt.\n\n## Authentication\nSend your API key as `X-API-Key: aiv_sk_...` (or `Authorization: Bearer aiv_sk_...`). Keys are created on the developer dashboard. Key management routes require a session JWT (dashboard login) — an API key can never mint keys.\n\n## Rate limiting & tiers\nEvery request is rate-limited on a 60-second sliding window and counts against your key (X-RateLimit-* headers on every response). Tiers: free 10 rpm / 3 concurrent, pro 30 rpm / 10 concurrent, enterprise 100 rpm / 50 concurrent. A key is created pro if your account has a completed purchase, else free; a later purchase upgrades it automatically on next use. Dashboard (JWT) sessions are limited per-user at 100 rpm. Over the limit → 429 RATE_LIMIT_EXCEEDED with Retry-After.\n\n## Concurrency\nJob-creating routes (/generate/*, /extend/*, /batch) reject a request when your key already has max_concurrent jobs in flight → 429 concurrency_limit_reached.\n\n## Free-tier limits\nNew accounts get up to 200 free credits; until the first purchase an account can make one video (any model and options that fit within those credits), counted across the web app and the API. /batch requires a paid plan (403 upgrade_required on free keys), since a free account has only one video to make. No image model is free through the API — every generation is charged at its catalogue price. When a free trial is exhausted, generation returns 403 upgrade_required with a pricing_url.\n\n## Request bodies\nJSON bodies are capped at 1 MB (413 PAYLOAD_TOO_LARGE); /v1/uploads takes multipart instead with per-kind caps.\n\n## TOON\nSend `Accept: application/toon` on any endpoint to receive TOON (Token Optimized Object Notation) instead of JSON — a compact serialization sized for LLM context windows.\n\nAll usage is metered (endpoint, status, credits, latency) for billing and analytics. The full error-code table is in `x-error-codes`.","contact":{"email":"support@aiveed.io"}},"servers":[{"url":"https://api.aiveed.io/v1","description":"Production"},{"url":"https://api-dev.aiveed.io/v1","description":"Development"},{"url":"http://localhost:3216/v1","description":"Local"}],"security":[{"apiKey":[]},{"bearerAuth":[]}],"tags":[{"name":"System","description":"Health and service metadata"},{"name":"Discovery","description":"Model catalog and pricing — public, no auth"},{"name":"Auth","description":"API key lifecycle (session JWT only) and identity"},{"name":"Generation","description":"Create video, image, and audio jobs"},{"name":"Jobs","description":"Job status, listing, and cancellation"},{"name":"Library","description":"The account's successfully generated videos and images (platform + API), with durable hosted URLs"},{"name":"Batch","description":"Submit multiple jobs in one call (paid tiers)"},{"name":"Credits","description":"Balance and pricing"},{"name":"Usage","description":"Aggregated usage and effective limits"},{"name":"Webhooks","description":"Signed HTTPS callbacks on job completion/failure"},{"name":"Real-time","description":"Server-Sent Events job streams"},{"name":"Uploads","description":"Upload local media for use as generation inputs"},{"name":"Docs","description":"This spec and its UI"}],"x-error-codes":[{"code":"VALIDATION_ERROR","status":400,"description":"The request body or query failed schema validation. `details` lists the individual problems."},{"code":"INVALID_PARAM","status":400,"description":"A path parameter has the wrong type (e.g. a non-integer key or webhook id)."},{"code":"INVALID_IDEMPOTENCY_KEY","status":400,"description":"The Idempotency-Key header is malformed. Allowed: 1-200 characters of A-Z a-z 0-9 _ . -"},{"code":"AUTH_REQUIRED","status":401,"description":"No credentials. Send `X-API-Key: aiv_sk_...` or `Authorization: Bearer <key or session JWT>`."},{"code":"INVALID_API_KEY","status":401,"description":"The API key is malformed, unknown, or revoked."},{"code":"KEY_EXPIRED","status":401,"description":"The API key is past its expiry date. Create or rotate a key on the dashboard."},{"code":"TOKEN_MALFORMED","status":401,"description":"The Bearer token is not a structurally valid JWT."},{"code":"TOKEN_EXPIRED","status":401,"description":"The session JWT has expired — log in again."},{"code":"TOKEN_INVALID","status":401,"description":"The session JWT failed verification."},{"code":"USER_NOT_FOUND","status":401,"description":"The session JWT is valid but its user no longer exists."},{"code":"PASSWORD_CHANGED","status":401,"description":"The session JWT was issued before the last password change. Log in again."},{"code":"INVALID_TICKET","status":401,"description":"The SSE ticket is invalid, expired, or bound to a different job."},{"code":"INSUFFICIENT_CREDITS","status":402,"description":"Your balance does not cover the job cost. Nothing was charged."},{"code":"insufficient_scope","status":403,"description":"The API key lacks the required scope (`required_scope` names it), or an API key attempted session-only key management."},{"code":"upgrade_required","status":403,"description":"A paid-plan feature on a free key (e.g. /batch), or the free trial is exhausted. `pricing_url` links to plans."},{"code":"ACCOUNT_BANNED","status":403,"description":"The account is suspended. Contact support@aiveed.io."},{"code":"NOT_ELIGIBLE","status":403,"description":"The account cannot perform this action (e.g. unverified email, locked credits). `reason`/`reasons` explain."},{"code":"CONTENT_POLICY_VIOLATION","status":403,"description":"The prompt or input media failed content moderation. Repeated violations can suspend the account."},{"code":"JOB_NOT_FOUND","status":404,"description":"No job with that id belongs to you."},{"code":"KEY_NOT_FOUND","status":404,"description":"No such API key (or it is already revoked)."},{"code":"WEBHOOK_NOT_FOUND","status":404,"description":"No webhook config with that id belongs to you."},{"code":"NOT_FOUND","status":404,"description":"The endpoint or referenced resource does not exist."},{"code":"KEY_LIMIT_REACHED","status":409,"description":"Maximum active API keys reached — revoke one first."},{"code":"KEY_REVOKED","status":409,"description":"The key is revoked and cannot be rotated."},{"code":"WEBHOOK_LIMIT_REACHED","status":409,"description":"Maximum active webhooks reached — delete one first."},{"code":"CANNOT_CANCEL","status":409,"description":"Only queued jobs can be cancelled; this one already left the queue."},{"code":"PAYLOAD_TOO_LARGE","status":413,"description":"JSON bodies are capped at 1 MB; uploads at their per-kind size cap (image 10 MB, audio 5 MB, video 50 MB)."},{"code":"UNSUPPORTED_MEDIA_TYPE","status":415,"description":"The uploaded file type is not allowed for that `kind`."},{"code":"UNSUPPORTED_OPTION","status":422,"description":"A request option is not supported by this model (or a webhook URL is not https/publicly routable). `param` names the offending field."},{"code":"RATE_LIMIT_EXCEEDED","status":429,"description":"Over your per-minute request limit. Honor `Retry-After` and the X-RateLimit-* headers."},{"code":"concurrency_limit_reached","status":429,"description":"Your key already has `max_concurrent` jobs in flight — wait for one to finish."},{"code":"TOO_MANY_STREAMS","status":429,"description":"Too many SSE streams open for this account. Close one before opening another, or poll GET /v1/jobs/{id} instead."},{"code":"INTERNAL_ERROR","status":500,"description":"Unexpected server error. Safe to retry with the same Idempotency-Key."},{"code":"PROVIDER_ERROR","status":502,"description":"The upstream generation provider is temporarily unavailable. Charged jobs that fail are refunded automatically."},{"code":"GATEWAY_TIMEOUT","status":504,"description":"The generation service did not respond in time. The job may still be running — poll GET /v1/jobs/{id} rather than resubmitting, or the work could be charged twice."},{"code":"GENERATION_FAILED","status":500,"description":"Fallback code when a submission fails without a more specific cause."},{"code":"EXTEND_FAILED","status":500,"description":"Fallback code when a video extension fails without a more specific cause."},{"code":"SKIPPED","status":0,"description":"Batch per-item only: not submitted because an earlier item failed with INSUFFICIENT_CREDITS."}],"components":{"securitySchemes":{"apiKey":{"type":"apiKey","in":"header","name":"X-API-Key","description":"API key (format: aiv_sk_{64-hex-chars})"},"bearerAuth":{"type":"http","scheme":"bearer","description":"Authorization: Bearer — either an MVP session JWT (from aiveed.io login) or an API key (aiv_sk_...). Key management routes require a session JWT; API keys are rejected there with 403 insufficient_scope."}},"schemas":{"ErrorResponse":{"type":"object","description":"Every error uses this envelope. `code` is machine-stable (see x-error-codes); `message` is human-readable.","properties":{"error":{"const":true},"code":{"type":"string","enum":["VALIDATION_ERROR","INVALID_PARAM","INVALID_IDEMPOTENCY_KEY","AUTH_REQUIRED","INVALID_API_KEY","KEY_EXPIRED","TOKEN_MALFORMED","TOKEN_EXPIRED","TOKEN_INVALID","USER_NOT_FOUND","PASSWORD_CHANGED","INVALID_TICKET","INSUFFICIENT_CREDITS","insufficient_scope","upgrade_required","ACCOUNT_BANNED","NOT_ELIGIBLE","CONTENT_POLICY_VIOLATION","JOB_NOT_FOUND","KEY_NOT_FOUND","WEBHOOK_NOT_FOUND","NOT_FOUND","KEY_LIMIT_REACHED","KEY_REVOKED","WEBHOOK_LIMIT_REACHED","CANNOT_CANCEL","PAYLOAD_TOO_LARGE","UNSUPPORTED_MEDIA_TYPE","UNSUPPORTED_OPTION","RATE_LIMIT_EXCEEDED","concurrency_limit_reached","TOO_MANY_STREAMS","INTERNAL_ERROR","PROVIDER_ERROR","GATEWAY_TIMEOUT","GENERATION_FAILED","EXTEND_FAILED"]},"message":{"type":"string"},"details":{"description":"Optional — validation issues or provider context"},"param":{"type":"string","description":"Optional — the offending field for UNSUPPORTED_OPTION"}},"required":["error","code","message"]},"Job":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["queued","submitted","processing","completed","failed","cancelled"]},"type":{"type":"string","enum":["video","image","audio","extend"]},"model":{"type":"string"},"prompt":{"type":"string"},"progress":{"type":"integer","minimum":0,"maximum":100},"credits_charged":{"type":"integer"},"result_url":{"type":["string","null"],"description":"Hosted result URL once completed"},"error_message":{"type":["string","null"]},"content_policy_violation":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"completed_at":{"type":["string","null"],"format":"date-time"}},"required":["id","status","type","model"]},"CreatedJob":{"type":"object","description":"The job created by this request. Poll GET /v1/jobs/{id}, stream via SSE, or register a webhook to get the result.","properties":{"id":{"type":"string","description":"Job id (job_...)"},"status":{"type":"string","enum":["queued","submitted","processing","completed","failed","cancelled"]},"model":{"type":"string"},"credits_charged":{"type":"integer","description":"Credits charged for this job (refunded automatically if the provider fails it)"},"remaining_credits":{"type":"integer","description":"Your balance after the charge"},"estimated_seconds":{"type":"integer","description":"Rough time-to-result estimate"},"replayed":{"type":"boolean","description":"Present (true) when an Idempotency-Key replay returned this original job"}},"required":["id","status","model","credits_charged"]},"Video":{"type":"object","description":"A successfully generated video from the account library. video_url is the durable hosted MP4 (Cloudflare Stream for current videos).","properties":{"id":{"type":"integer"},"prompt":{"type":["string","null"]},"model":{"type":["string","null"],"description":"Stored provider model string (e.g. bytedance/seedance-2)"},"model_id":{"type":["string","null"],"description":"API model id (e.g. seedance-2) — what ?model= filters on; null for models the API does not offer"},"aspect_ratio":{"type":["string","null"]},"video_url":{"type":"string","description":"Durable playable MP4 URL"},"embed_url":{"type":["string","null"],"description":"Iframe embed player URL"},"thumbnail_url":{"type":["string","null"]},"extended_from_video_id":{"type":["integer","null"],"description":"Set when this video was created by extending another"},"created_at":{"type":"string","format":"date-time"},"completed_at":{"type":["string","null"],"format":"date-time"}},"required":["id","video_url","created_at"]},"Image":{"type":"object","description":"A successfully generated image from the account library. image_url is the durable hosted URL.","properties":{"id":{"type":"integer"},"prompt":{"type":["string","null"]},"model":{"type":["string","null"],"description":"Stored provider model string (e.g. grok-imagine/text-to-image)"},"model_id":{"type":["string","null"],"description":"API model id (e.g. grok-image) — what ?model= filters on; null for models the API does not offer"},"image_url":{"type":"string","description":"Durable hosted image URL"},"is_favorite":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"completed_at":{"type":["string","null"],"format":"date-time"}},"required":["id","image_url","created_at"]},"ApiKeyView":{"type":"object","description":"Masked key view — the secret and its hash are never returned after creation.","properties":{"id":{"type":"integer"},"prefix":{"type":"string","description":"First 12 characters of the key (aiv_sk_ + 5)"},"name":{"type":["string","null"]},"tier":{"type":"string","enum":["free","pro","enterprise"]},"scopes":{"type":"array","items":{"type":"string","enum":["generate","read"]}},"rate_limit_rpm":{"type":"integer"},"last_used_at":{"type":["string","null"],"format":"date-time"},"expires_at":{"type":["string","null"],"format":"date-time"},"created_at":{"type":"string","format":"date-time"}}},"ApiKeyWithSecret":{"type":"object","description":"Contains the raw key — shown ONCE, store it now.","properties":{"id":{"type":"integer"},"key":{"type":"string","description":"The full secret (aiv_sk_{64-hex}). Never shown again."},"prefix":{"type":"string"},"name":{"type":["string","null"]},"tier":{"type":"string"},"scopes":{"type":"array","items":{"type":"string"}},"expires_at":{"type":["string","null"],"format":"date-time"},"message":{"type":"string"}},"required":["id","key","prefix"]}}},"paths":{"/health":{"get":{"operationId":"getHealth","summary":"Health check","tags":["System"],"security":[],"responses":{"200":{"description":"Service healthy","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["ok","degraded"]},"service":{"type":"string"},"version":{"type":"string"},"timestamp":{"type":"string","format":"date-time"},"checks":{"type":"object","properties":{"database":{"type":"string"}}}}}}}},"503":{"description":"Service degraded (same body shape, status \"degraded\")"}}}},"/models":{"get":{"operationId":"listModels","summary":"Available models with pricing and capabilities","description":"The launch catalog: per-model credit tables (from the live pricing source), capability descriptors (aspect ratios, durations, resolutions, frames, references, modes), and the exact model-id lists the request schemas accept.","tags":["Discovery"],"security":[],"responses":{"200":{"description":"Catalog of video, image, and audio models","content":{"application/json":{"schema":{"type":"object","properties":{"version":{"type":"string","description":"Pricing snapshot version"},"video":{"type":"array","items":{"type":"object"}},"image":{"type":"array","items":{"type":"object"}},"audio":{"type":"array","items":{"type":"object"}},"extend":{"type":"object"},"supported_model_ids":{"type":"object"}}}}}}}}},"/auth/keys":{"post":{"operationId":"createApiKey","summary":"Create API key","description":"Session-only (a key cannot create keys — 403 insufficient_scope). Requires a verified email, an unbanned account, and unlocked credits. Max 5 active keys per account.","tags":["Auth"],"security":[{"bearerAuth":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100},"expires_in_days":{"type":"integer","exclusiveMinimum":0,"maximum":3650}}}}}},"responses":{"201":{"description":"API key created (raw key shown once)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyWithSecret"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Not eligible (unverified email / banned / locked credits), or an API key attempted key management","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Active-key limit reached (5 per account)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"get":{"operationId":"listApiKeys","summary":"List API keys","description":"Session-only. Returns masked views — never the secret or its hash.","tags":["Auth"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"List of your API keys","content":{"application/json":{"schema":{"type":"object","properties":{"keys":{"type":"array","items":{"$ref":"#/components/schemas/ApiKeyView"}},"total":{"type":"integer"}}}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"An API key attempted key management","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/auth/keys/{keyId}/rotate":{"post":{"operationId":"rotateApiKey","summary":"Rotate an API key","description":"Session-only. Mints a new secret (shown once) and invalidates the previous one immediately.","tags":["Auth"],"security":[{"bearerAuth":[]}],"parameters":[{"name":"keyId","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Rotated (new raw key shown once)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyWithSecret"}}}},"400":{"description":"keyId is not an integer","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"An API key attempted key management","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Key not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Key is revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/auth/keys/{keyId}":{"delete":{"operationId":"revokeApiKey","summary":"Revoke an API key","description":"Session-only. Soft-revokes the key; it stops authenticating immediately. Irreversible.","tags":["Auth"],"security":[{"bearerAuth":[]}],"parameters":[{"name":"keyId","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Revoked","content":{"application/json":{"schema":{"type":"object","properties":{"revoked":{"const":true},"id":{"type":"integer"}}}}}},"400":{"description":"keyId is not an integer","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"An API key attempted key management","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Key not found or already revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/auth/me":{"get":{"operationId":"getMe","summary":"Current user info","tags":["Auth"],"responses":{"200":{"description":"User id, email, credit balance","content":{"application/json":{"schema":{"type":"object","properties":{"user_id":{"type":"integer"},"email":{"type":"string"},"credits":{"type":"integer"}}}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/generate/video":{"post":{"operationId":"generateVideo","summary":"Generate a video","description":"Requires the `generate` scope. Charges the model price up front (see /v1/models and /v1/credits/pricing); failed jobs are refunded automatically. Options a model does not support are rejected with 422 UNSUPPORTED_OPTION naming the `param` — per-model capabilities are listed by GET /v1/models.","tags":["Generation"],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","pattern":"^[A-Za-z0-9_.\\-]{1,200}$"},"description":"Replay guard: retrying a request with the same key returns the ORIGINAL job (HTTP 200, `replayed: true`) instead of creating and charging a second one."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"model":{"type":"string","enum":["veo3-fast","veo3-quality","grok-video","kling-3.0","seedance-2-fast","seedance-2","seedance-2-mini","seedance-2.5","seedance-1.5-pro","seedance-v1-pro","gemini-omni","lipsync"]},"prompt":{"type":"string","minLength":1,"maxLength":20000},"aspect_ratio":{"type":"string","enum":["16:9","9:16","2:3","3:2","1:1","4:3","3:4","21:9","adaptive"]},"duration":{"type":"string","enum":["4","5","6","8","10","12","15","20","30"]},"mode":{"type":"string","enum":["std","pro","fun","normal","spicy"]},"resolution":{"type":"string","enum":["480p","720p","1080p","4k","480P","720P","1080P","4K"]},"image_url":{"type":"string","maxLength":2048,"format":"uri"},"first_frame_url":{"type":"string","maxLength":2048,"format":"uri"},"last_frame_url":{"type":"string","maxLength":2048,"format":"uri"},"reference_image_urls":{"maxItems":9,"type":"array","items":{"type":"string","maxLength":2048,"format":"uri"}},"reference_video_urls":{"maxItems":3,"type":"array","items":{"type":"string","maxLength":2048,"format":"uri"}},"reference_audio_urls":{"maxItems":1,"type":"array","items":{"type":"string","maxLength":2048,"format":"uri"}},"generate_audio":{"type":"boolean"},"sound":{"type":"boolean"},"multi_shots":{"type":"boolean"},"multi_prompt":{"maxItems":5,"type":"array","items":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"kling_elements":{"maxItems":3,"type":"array","items":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"audio_url":{"type":"string","maxLength":2048,"format":"uri"},"audio_duration":{"type":"number","exclusiveMinimum":0,"maximum":120},"lip_sync_mode":{"type":"string","enum":["normal","trim","collate"]},"character_asset_ids":{"maxItems":3,"type":"array","items":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"voice_asset_ids":{"maxItems":3,"type":"array","items":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"webhook_url":{"type":"string","maxLength":2048,"format":"uri"},"webhook_secret":{"type":"string","minLength":16,"maxLength":64}},"required":["model"]}}}},"responses":{"200":{"description":"Idempotent replay — the original job (replayed: true)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatedJob"}}}},"201":{"description":"Job created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatedJob"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"402":{"description":"Insufficient credits","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Missing scope, content policy violation, or upgrade required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"An option this model does not support (param names it)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate or concurrency limit","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"502":{"description":"Provider temporarily unavailable (nothing charged)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/generate/image":{"post":{"operationId":"generateImage","summary":"Generate or edit an image","description":"Requires the `generate` scope. Supply `image_urls` to edit instead of generate. Every image model is charged at its catalogue price — see GET /v1/credits/pricing.","tags":["Generation"],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","pattern":"^[A-Za-z0-9_.\\-]{1,200}$"},"description":"Replay guard: retrying a request with the same key returns the ORIGINAL job (HTTP 200, `replayed: true`) instead of creating and charging a second one."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"model":{"type":"string","enum":["nano-banana","nano-banana-pro","grok-image","gpt-image-2"]},"prompt":{"type":"string","minLength":1,"maxLength":10000},"aspect_ratio":{"type":"string","enum":["16:9","9:16","2:3","3:2","1:1","auto"]},"resolution":{"type":"string","enum":["1K","2K","4K","1k","2k","4k"]},"output_format":{"type":"string","enum":["png","jpeg"]},"image_urls":{"maxItems":5,"type":"array","items":{"type":"string","maxLength":2048,"format":"uri"}},"webhook_url":{"type":"string","maxLength":2048,"format":"uri"},"webhook_secret":{"type":"string","minLength":16,"maxLength":64}},"required":["model"]}}}},"responses":{"200":{"description":"Idempotent replay — the original job (replayed: true)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatedJob"}}}},"201":{"description":"Job created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatedJob"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"402":{"description":"Insufficient credits","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Missing scope, content policy violation, or upgrade required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"An option this model does not support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit or concurrency limit","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"502":{"description":"Provider temporarily unavailable (nothing charged)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/generate/audio":{"post":{"operationId":"generateAudio","summary":"Generate audio (voiceover or music)","description":"Requires the `generate` scope. `voiceover` (text-to-speech) needs `text` + `voice_id` and usually completes synchronously with a result_url; `music` needs `title`/`style` (+ `lyrics` unless `instrumental`) and resolves asynchronously like video jobs.","tags":["Generation"],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","pattern":"^[A-Za-z0-9_.\\-]{1,200}$"},"description":"Replay guard: retrying a request with the same key returns the ORIGINAL job (HTTP 200, `replayed: true`) instead of creating and charging a second one."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"model":{"type":"string","enum":["voiceover","music"]},"text":{"type":"string","minLength":1,"maxLength":5000},"voice_id":{"type":"string","minLength":1,"maxLength":64},"voice_name":{"type":"string","maxLength":100},"title":{"type":"string","minLength":1,"maxLength":80},"style":{"type":"string","minLength":1,"maxLength":1000},"instrumental":{"type":"boolean"},"lyrics":{"type":"string","maxLength":3000},"webhook_url":{"type":"string","maxLength":2048,"format":"uri"},"webhook_secret":{"type":"string","minLength":16,"maxLength":64}},"required":["model"]}}}},"responses":{"200":{"description":"Idempotent replay — the original job (replayed: true)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatedJob"}}}},"201":{"description":"Job created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatedJob"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"402":{"description":"Insufficient credits","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Missing scope, content policy violation, or upgrade required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"An option this model does not support","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate or concurrency limit","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"502":{"description":"Provider temporarily unavailable (nothing charged)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/extend/video":{"post":{"operationId":"extendVideo","summary":"Extend a completed Veo3 video (~7s)","description":"Requires the `generate` scope. `job_id` must be one of YOUR completed Veo3-family video jobs. Charges the flat extend price (see /v1/credits/pricing).","tags":["Generation"],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","pattern":"^[A-Za-z0-9_.\\-]{1,200}$"},"description":"Replay guard: retrying a request with the same key returns the ORIGINAL job (HTTP 200, `replayed: true`) instead of creating and charging a second one."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"job_id":{"type":"string","minLength":1},"prompt":{"type":"string","minLength":1,"maxLength":5000},"webhook_url":{"type":"string","maxLength":2048,"format":"uri"},"webhook_secret":{"type":"string","minLength":16,"maxLength":64}},"required":["job_id","prompt"]}}}},"responses":{"200":{"description":"Idempotent replay — the original job (replayed: true)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatedJob"}}}},"201":{"description":"Extension job created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatedJob"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"402":{"description":"Insufficient credits","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Original job not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"The source job is not an extendable video","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate or concurrency limit","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"502":{"description":"Provider temporarily unavailable (nothing charged)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/batch":{"post":{"operationId":"submitBatch","summary":"Submit multiple jobs (up to 10, submitted sequentially)","description":"Paid tiers only (403 upgrade_required on free keys). Each item is priced and charged individually. Submission stops at the first item that fails with insufficient credits: that item reports the 402 in its per-item result and later items are skipped (not charged). Other per-item errors do not stop the batch. Items accept the same fields as /generate/video and /generate/image plus a `type` discriminator.","tags":["Batch"],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","pattern":"^[A-Za-z0-9_.\\-]{1,200}$"},"description":"Replay guard: retrying a request with the same key returns the ORIGINAL job (HTTP 200, `replayed: true`) instead of creating and charging a second one."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"jobs":{"minItems":1,"maxItems":10,"type":"array","items":{"oneOf":[{"type":"object","properties":{"model":{"type":"string","enum":["veo3-fast","veo3-quality","grok-video","kling-3.0","seedance-2-fast","seedance-2","seedance-2-mini","seedance-2.5","seedance-1.5-pro","seedance-v1-pro","gemini-omni","lipsync"]},"prompt":{"type":"string","minLength":1,"maxLength":20000},"aspect_ratio":{"type":"string","enum":["16:9","9:16","2:3","3:2","1:1","4:3","3:4","21:9","adaptive"]},"duration":{"type":"string","enum":["4","5","6","8","10","12","15","20","30"]},"mode":{"type":"string","enum":["std","pro","fun","normal","spicy"]},"resolution":{"type":"string","enum":["480p","720p","1080p","4k","480P","720P","1080P","4K"]},"image_url":{"type":"string","maxLength":2048,"format":"uri"},"first_frame_url":{"type":"string","maxLength":2048,"format":"uri"},"last_frame_url":{"type":"string","maxLength":2048,"format":"uri"},"reference_image_urls":{"maxItems":9,"type":"array","items":{"type":"string","maxLength":2048,"format":"uri"}},"reference_video_urls":{"maxItems":3,"type":"array","items":{"type":"string","maxLength":2048,"format":"uri"}},"reference_audio_urls":{"maxItems":1,"type":"array","items":{"type":"string","maxLength":2048,"format":"uri"}},"generate_audio":{"type":"boolean"},"sound":{"type":"boolean"},"multi_shots":{"type":"boolean"},"multi_prompt":{"maxItems":5,"type":"array","items":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"kling_elements":{"maxItems":3,"type":"array","items":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"audio_url":{"type":"string","maxLength":2048,"format":"uri"},"audio_duration":{"type":"number","exclusiveMinimum":0,"maximum":120},"lip_sync_mode":{"type":"string","enum":["normal","trim","collate"]},"character_asset_ids":{"maxItems":3,"type":"array","items":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"voice_asset_ids":{"maxItems":3,"type":"array","items":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"webhook_url":{"type":"string","maxLength":2048,"format":"uri"},"webhook_secret":{"type":"string","minLength":16,"maxLength":64},"type":{"type":"string","const":"video"}},"required":["model","type"]},{"type":"object","properties":{"model":{"type":"string","enum":["nano-banana","nano-banana-pro","grok-image","gpt-image-2"]},"prompt":{"type":"string","minLength":1,"maxLength":10000},"aspect_ratio":{"type":"string","enum":["16:9","9:16","2:3","3:2","1:1","auto"]},"resolution":{"type":"string","enum":["1K","2K","4K","1k","2k","4k"]},"output_format":{"type":"string","enum":["png","jpeg"]},"image_urls":{"maxItems":5,"type":"array","items":{"type":"string","maxLength":2048,"format":"uri"}},"webhook_url":{"type":"string","maxLength":2048,"format":"uri"},"webhook_secret":{"type":"string","minLength":16,"maxLength":64},"type":{"type":"string","const":"image"}},"required":["model","type"]}]}}},"required":["jobs"]}}}},"responses":{"200":{"description":"Per-item results (submitted / error / skipped), estimated_credits, credits_charged","content":{"application/json":{"schema":{"type":"object","properties":{"total_items":{"type":"integer"},"submitted":{"type":"integer"},"estimated_credits":{"type":"integer"},"credits_charged":{"type":"integer"},"remaining_credits":{"type":"integer"},"warning":{"type":"string"},"replayed":{"type":"boolean"},"results":{"type":"array","items":{"type":"object","properties":{"index":{"type":"integer"},"status":{"type":"string","enum":["submitted","completed","failed","error","skipped"]},"job":{"type":"object"},"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}}}}}}}}}}},"400":{"description":"Validation error (per-item details)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Free tier (upgrade_required) or account not eligible to generate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate or concurrency limit","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/jobs":{"get":{"operationId":"listJobs","summary":"List jobs","description":"Requires the `read` scope.","tags":["Jobs"],"parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["queued","submitted","processing","completed","failed","cancelled"]}},{"name":"model","in":"query","schema":{"type":"string"}},{"name":"type","in":"query","schema":{"type":"string","enum":["video","image","extend","audio"]}},{"name":"limit","in":"query","schema":{"type":"integer","default":20,"maximum":100}},{"name":"offset","in":"query","schema":{"type":"integer","default":0}}],"responses":{"200":{"description":"Paginated job list (newest first)","content":{"application/json":{"schema":{"type":"object","properties":{"jobs":{"type":"array","items":{"$ref":"#/components/schemas/Job"}},"total":{"type":"integer"},"limit":{"type":"integer"},"offset":{"type":"integer"}}}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Missing read scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/jobs/{id}":{"get":{"operationId":"getJob","summary":"Get job status","description":"Requires the `read` scope.","tags":["Jobs"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Job details","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Job"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Job not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"operationId":"cancelJob","summary":"Cancel a queued job","description":"Requires the `read` scope. Only jobs still in `queued` can be cancelled.","tags":["Jobs"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Cancelled","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"const":"cancelled"}}}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Job not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Job already left the queue","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/videos":{"get":{"operationId":"listVideos","summary":"List your generated videos","description":"Requires the `read` scope. Returns the account's successfully completed videos (newest first) — both API-generated and platform-generated — with durable hosted URLs. Unlike a job's `result_url` (which can be a transient provider URL), `video_url` here is the permanently hosted MP4.","tags":["Library"],"parameters":[{"name":"model","in":"query","schema":{"type":"string"},"description":"An API model id (e.g. seedance-2, veo3-quality) — matches every way that model is stored; any other value matches the stored provider string exactly"},{"name":"limit","in":"query","schema":{"type":"integer","default":20,"maximum":100}},{"name":"offset","in":"query","schema":{"type":"integer","default":0}}],"responses":{"200":{"description":"Paginated video list (newest first)","content":{"application/json":{"schema":{"type":"object","properties":{"videos":{"type":"array","items":{"$ref":"#/components/schemas/Video"}},"total":{"type":"integer"},"limit":{"type":"integer"},"offset":{"type":"integer"}}}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Missing read scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/images":{"get":{"operationId":"listImages","summary":"List your generated images","description":"Requires the `read` scope. Returns the account's successfully completed images (newest first) — both API-generated and platform-generated — with durable hosted URLs. `favorite=true` narrows to favorited images.","tags":["Library"],"parameters":[{"name":"model","in":"query","schema":{"type":"string"},"description":"An API model id (e.g. seedance-2, veo3-quality) — matches every way that model is stored; any other value matches the stored provider string exactly"},{"name":"favorite","in":"query","schema":{"type":"boolean"}},{"name":"limit","in":"query","schema":{"type":"integer","default":20,"maximum":100}},{"name":"offset","in":"query","schema":{"type":"integer","default":0}}],"responses":{"200":{"description":"Paginated image list (newest first)","content":{"application/json":{"schema":{"type":"object","properties":{"images":{"type":"array","items":{"$ref":"#/components/schemas/Image"}},"total":{"type":"integer"},"limit":{"type":"integer"},"offset":{"type":"integer"}}}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Missing read scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/assets":{"get":{"operationId":"listAssets","summary":"List your Gemini Omni characters and voices","description":"Requires the `read` scope. The ids here are what `gemini-omni` takes as `character_asset_ids` and `voice_asset_ids`. Every account has the starter characters and voices (`starter: true`); ones you create on aiveed.io are listed too.","tags":["Library"],"responses":{"200":{"description":"Characters and voices","content":{"application/json":{"schema":{"type":"object","properties":{"characters":{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"},"starter":{"type":"boolean"},"description":{"type":["string","null"]},"thumbnail_url":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"}}}},"voices":{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"},"starter":{"type":"boolean"},"description":{"type":["string","null"]},"preview_url":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"}}}}}}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Missing read scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/credits":{"get":{"operationId":"getCredits","summary":"Credit balance","description":"Requires the `read` scope.","tags":["Credits"],"responses":{"200":{"description":"Current balance","content":{"application/json":{"schema":{"type":"object","properties":{"credits":{"type":"integer"},"user_id":{"type":"integer"}}}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Missing read scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/credits/pricing":{"get":{"operationId":"getPricing","summary":"Full pricing table (public)","description":"Machine-readable credit costs for every model, plus purchase plans. Agents should check this before generating — it is the live pricing source, never a stale copy.","tags":["Credits"],"security":[],"responses":{"200":{"description":"Pricing by model, plus purchase tiers","content":{"application/json":{"schema":{"type":"object","properties":{"version":{"type":"string"},"generation":{"type":"object"},"image":{"type":"object"},"audio":{"type":"object"},"extend":{"type":"object"},"purchase":{"type":"object"}}}}}}}}},"/usage/summary":{"get":{"operationId":"getUsageSummary","summary":"Usage summary (requests, net credit spend, jobs by status/model, daily series)","description":"Requires the `read` scope. Request metrics cover API-key traffic; job metrics cover every job. Credit spend is net of refunds.","tags":["Usage"],"parameters":[{"name":"window","in":"query","schema":{"type":"string","enum":["1d","7d","30d","90d"],"default":"30d"}}],"responses":{"200":{"description":"Aggregated usage for the window","content":{"application/json":{"schema":{"type":"object","properties":{"window":{"type":"string"},"requests":{"type":"object"},"credits_spent":{"type":"integer"},"jobs":{"type":"object"},"daily":{"type":"array","items":{"type":"object"}},"recent":{"type":"array","items":{"type":"object"}}}}}}},"400":{"description":"Invalid window","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/usage/limits":{"get":{"operationId":"getUsageLimits","summary":"Effective tier, rate limit, concurrency cap, and account caps","description":"Requires the `read` scope.","tags":["Usage"],"responses":{"200":{"description":"Current limits","content":{"application/json":{"schema":{"type":"object","properties":{"tier":{"type":"string","enum":["free","pro","enterprise"]},"rate_limit_rpm":{"type":"integer"},"rate_limit_window_seconds":{"type":"integer"},"max_concurrent":{"type":"integer"},"max_active_api_keys":{"type":"integer"},"max_webhook_configs":{"type":"integer"}}}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/webhooks":{"post":{"operationId":"createWebhook","summary":"Register webhook URL (max 5 active per account)","description":"Requires the `generate` scope. URLs must be https and publicly routable (private, loopback, and cloud-metadata addresses are rejected; redirects are never followed). All deliveries are signed — if you do not supply a secret, one is generated and returned ONCE in this response.\n\nDeliveries carry `X-AIVeed-Signature: t=<unix-seconds>,sha256=<hex>` where `<hex>` = HMAC-SHA256(secret, `t + \".\" + raw_body`). Verify like this (Node):\n```js\nconst [tPart, sigPart] = header.split(\",\");\nconst t = Number(tPart.slice(2)), sig = sigPart.slice(7);\nconst expected = crypto.createHmac(\"sha256\", secret)\n  .update(`${t}.${rawBody}`).digest(\"hex\");\nconst fresh = Math.abs(Date.now() / 1000 - t) <= 300; // tolerance\nconst valid = fresh && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));\n```\nReject the delivery unless BOTH checks pass — the timestamp bound is your replay protection. Deliveries are at-least-once and retried with backoff (an endpoint failing 10 consecutive times is deactivated); dedupe on (job.id, event).","tags":["Webhooks"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","maxLength":2048,"format":"uri"},"events":{"type":"array","items":{"type":"string","enum":["job.completed","job.failed"]}},"secret":{"type":"string","minLength":16,"maxLength":64}},"required":["url"]}}}},"responses":{"201":{"description":"Webhook registered (generated secret shown once)","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"integer"},"url":{"type":"string"},"events":{"type":"array","items":{"type":"string"}},"active":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"secret":{"type":"string","description":"Only present when auto-generated — shown once"},"secret_note":{"type":"string"},"signature":{"type":"object"}}}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Missing generate scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Webhook limit reached (5 active per account)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"URL rejected (not https, or resolves to a private/reserved address)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"get":{"operationId":"listWebhooks","summary":"List webhooks","description":"Requires the `generate` scope. Secrets are never returned.","tags":["Webhooks"],"responses":{"200":{"description":"Registered webhooks","content":{"application/json":{"schema":{"type":"object","properties":{"webhooks":{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer"},"url":{"type":"string"},"events":{"type":"array","items":{"type":"string"}},"active":{"type":"boolean","description":"false after 10 consecutive delivery failures"},"failure_count":{"type":"integer"},"last_delivered_at":{"type":["string","null"],"format":"date-time"},"created_at":{"type":"string","format":"date-time"}}}},"total":{"type":"integer"}}}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/webhooks/{id}":{"delete":{"operationId":"deleteWebhook","summary":"Delete a webhook","description":"Requires the `generate` scope. Deliveries stop immediately.","tags":["Webhooks"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"const":true},"id":{"type":"integer"}}}}}},"400":{"description":"id is not an integer","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Webhook not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/sse/tickets":{"post":{"operationId":"createSseTicket","summary":"Mint a short-lived SSE ticket","description":"EventSource cannot send headers, so exchange your session/API key here for a ticket bound to one job, then stream with GET /sse/jobs/{id}?ticket=... . Requires the read scope; the ticket expires in 5 minutes.","tags":["Real-time"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["jobId"],"properties":{"jobId":{"type":"string","description":"Also accepted as job_id"}}}}}},"responses":{"200":{"description":"Ticket","content":{"application/json":{"schema":{"type":"object","properties":{"ticket":{"type":"string"},"expires_in":{"type":"integer"}}}}}},"400":{"description":"jobId missing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Missing read scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Job not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/sse/jobs/{id}":{"get":{"operationId":"streamJob","summary":"SSE stream for job progress","description":"Auth by ?ticket= (browsers via EventSource) or a normal X-API-Key / Bearer header (non-browser clients). Query ?api_key= is not accepted. Events: `status` ({ id, status, progress, result_url?, error_message? }), `heartbeat`, and a final `done`.","tags":["Real-time"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"ticket","in":"query","required":false,"schema":{"type":"string"},"description":"Ticket from POST /sse/tickets (browser EventSource auth)"}],"responses":{"200":{"description":"SSE event stream (text/event-stream)"},"401":{"description":"Invalid or expired ticket / missing auth","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Job not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/uploads":{"post":{"operationId":"uploadFile","summary":"Upload media for use as a generation input","description":"Requires the `generate` scope. Multipart form: `file` + `kind` (image | audio | video, default image). Size caps: image 10 MB, audio 5 MB, video 50 MB. Allowed extensions — image: .jpg .jpeg .png .webp .avif, audio: .mp3 .wav .aac .m4a .mp4 .ogg, video: .mp4 .mov .avi .webm .mpg .mpeg. Returns a CDN URL usable in first_frame_url, image_urls, audio_url, reference_*_urls, etc. Image and audio URLs are durable (safe to store and reuse); video URLs are transient (~90 days).","tags":["Uploads"],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary"},"kind":{"type":"string","enum":["image","audio","video"],"default":"image"}}}}}},"responses":{"201":{"description":"Uploaded","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","description":"CDN URL to pass in generation requests"},"kind":{"type":"string"},"original_name":{"type":"string"},"size":{"type":"integer"},"expires":{"type":["string","null"]}}}}}},"400":{"description":"Not multipart, missing file, or bad kind","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"413":{"description":"File over the per-kind size cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"415":{"description":"File type not allowed for that kind","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"502":{"description":"Upload service temporarily unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/openapi.json":{"get":{"operationId":"getOpenApiSpec","summary":"This spec","tags":["Docs"],"security":[],"responses":{"200":{"description":"The OpenAPI 3.1 document"}}}},"/docs":{"get":{"operationId":"getDocsUi","summary":"Interactive API reference (Scalar UI)","tags":["Docs"],"security":[],"responses":{"200":{"description":"HTML"}}}}}}