diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index cfe68b5..5b4f3f5 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "beatapi-codex-plugin", "version": "0.2.0", - "description": "Create and manage BeatAPI async and realtime AI video APIs from Codex.", + "description": "Create and manage BeatAPI image, video, Effect, workflow, and realtime APIs from Codex.", "author": { "name": "BeatAPI", "email": "support@beatapi.io", @@ -15,14 +15,17 @@ "ai-video", "music-video", "ecommerce-video", - "realtime-video" + "realtime-video", + "image-generation", + "video-generation", + "ai-effects" ], "skills": "./skills/", "mcpServers": "./.mcp.json", "interface": { "displayName": "BeatAPI", - "shortDescription": "Manage async and realtime AI video APIs", - "longDescription": "Use one existing BeatAPI account and API key from Codex to upload inputs, check credits and concurrency, create asynchronous Music Video and Ecommerce Video tasks, manage short-lived Realtime Video sessions, monitor progress, retrieve hosted results, and configure webhooks.", + "shortDescription": "Manage generation, Effects, workflows, and realtime", + "longDescription": "Use one existing BeatAPI account and API key from Codex to discover public models and Effects, upload inputs, check USD balance and concurrency, create image, video, Effect, Music Video, and Ecommerce Video tasks, manage short-lived Realtime Video sessions, monitor progress, retrieve hosted results, and configure webhooks.", "developerName": "BeatAPI", "category": "Creativity", "capabilities": [ @@ -35,8 +38,11 @@ "defaultPrompt": [ "Use $beatapi-video to create a music video from my images and audio.", "Use $beatapi-video to turn my product images into a vertical ad.", + "Use $beatapi-video to generate an image with Nano Banana Pro.", + "Use $beatapi-video to create a Seedance 2.5 video.", + "Use $beatapi-video to list current Effects for this portrait.", "Use $beatapi-video to create a 60-second realtime session for my web app.", - "Use $beatapi-video to check my credits and task status." + "Use $beatapi-video to check my USD balance and task status." ], "brandColor": "#2563FF", "composerIcon": "./assets/icon.png", diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bcff7d..e9a8fd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ ## Unreleased +- Added MCP tools for text-model discovery, non-streaming text responses, + generation-model discovery, generic image/video generation, versioned + Effects, and Video Analysis. +- Synchronized the canonical Skill, typed client runtime, and OpenAPI snapshot + with the complete 29-operation contract and USD-denominated usage semantics. +- Replaced hardcoded image/video model unions with a stable `model` plus + `parameters` interface so newly published model IDs do not require a plugin + release. +- Added host Configure guidance for `BEATAPI_API_KEY` and kept credentials out + of tool arguments and model-visible results. + ## 0.2.0 - 2026-07-31 - Added Realtime Video session create, read, and close MCP tools. diff --git a/README.md b/README.md index 7f52744..7dd9380 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,19 @@ # BeatAPI Codex Plugin -Create and manage BeatAPI asynchronous workflows and Realtime Video sessions +Create and manage BeatAPI image, video, Effect, workflow, and Realtime APIs directly from Codex. The plugin combines the canonical `beatapi-video` Skill with a bundled local MCP server and uses the same API key as the BeatAPI CLI. ## What users can do -- inspect workflows, credits, usage, and concurrency; -- upload local images, audio, and SRT files; +- discover stable image/video model aliases and published Effects; +- list authenticated text models and create non-streaming text responses only + when the user explicitly asks to use BeatAPI for text; +- create image and video tasks through a model-agnostic request shape, plus + versioned Effect tasks; +- analyze uploaded MP4 or MOV files through the async Video Analysis workflow; +- inspect workflows, USD balance, usage, and concurrency; +- upload local images, audio, video, and SRT files; - create automatic or manual Music Video tasks; - inspect, edit, materialize, and compose storyboard shots; - create Ecommerce Video tasks; @@ -56,7 +62,10 @@ Restart the desktop app after installation. Useful starter requests include: - “Use `$beatapi-video` to create a music video from my images and audio.” - “Turn these product photos into a 15-second 9:16 ad.” -- “Check my BeatAPI credits and the status of task `task_...`.” +- “Generate an image with Nano Banana Pro.” +- “Make a 10-second Seedance 2.5 video with these references.” +- “List the current Effects and run one on this portrait.” +- “Check my BeatAPI balance and the status of task `task_...`.” - “Create a 60-second Realtime Video session for `https://app.example.com`.” ## Package layout diff --git a/contract/beatapi.openapi.yaml b/contract/beatapi.openapi.yaml index 65e1dc9..a988a34 100644 --- a/contract/beatapi.openapi.yaml +++ b/contract/beatapi.openapi.yaml @@ -6,11 +6,15 @@ info: name: BeatAPI Terms of Service url: https://beatapi.io/terms-of-service description: | - BeatAPI provides async video workflows and short-lived Realtime Video - Sessions behind one BeatAPI-native API. Async integrations create a task, - poll until it finishes, then read the hosted video URL from `output.media`. - Realtime browser integrations create a Session with the same Bearer API key, - then pass only the returned BeatAPI `client_secret` to `@beatapi/realtime`. + BeatAPI provides one API key for public text models, image generation, video generation, + video analysis, Effects, asynchronous video workflows, and short-lived Realtime Video Sessions. + + For asynchronous operations, create a task, poll the shared task endpoint or receive webhook events, + and read hosted output URLs from `output.media`. For Realtime, create a Session on a trusted server + with your Bearer API key and pass only the returned short-lived `client_secret` to the browser. + + Customer balances and usage are USD-denominated. Compatibility fields such + as `credit_balance` and `credits_reserved` remain in the API; 1 Credit = $1 USD. ## 5 minute Quick Start @@ -18,7 +22,7 @@ info: 2. Create an API key in [Dashboard → API Keys](https://beatapi.io/dashboard/apikeys) and send it as `Authorization: Bearer `. - Credit packs are available from + USD balance top-ups are available from [Dashboard → Billing](https://beatapi.io/dashboard/billing). 3. Use public HTTPS URLs for input media. If your files are local, upload them with `POST /v1/files` first. @@ -27,7 +31,8 @@ info: 6. Add webhooks later if you do not want to poll. ```bash - export BEATAPI_API_KEY="sk_your_key" + read -rsp "BeatAPI API key: " BEATAPI_API_KEY && echo + export BEATAPI_API_KEY curl https://api.beatapi.io/v1/workflows @@ -69,15 +74,15 @@ info: Public workflow inputs must use HTTPS URLs that are reachable from the public internet. Localhost, private network URLs, and data URLs are rejected. - Use `POST /v1/files` for local images, audio, or subtitles. + Use `POST /v1/files` for local images, audio, subtitles, or Motion Control reference videos. - Each verified new user account starts with 50 welcome credits valid for 14 days and - 1 active processing concurrency. Lifetime paid credit purchases unlock higher limits: + Each verified new user account starts with a $2 welcome balance that never expires and + 1 active processing concurrency. Lifetime paid purchases unlock higher limits: $10+ = 2, $100+ = 5, $1,000+ = 10, $5,000+ = 15, and $20,000+ = 30. `GET /v1/usage` returns current usage totals, concurrency limit, and active processing task count. Active concurrency measures tasks that are currently using BeatAPI processing resources. storyboard_ready and requires_action - tasks can have settled credits but do not count toward active processing + tasks can have settled USD usage but do not count toward active processing concurrency. ## Webhooks are optional @@ -95,8 +100,16 @@ tags: description: Discover the workflow IDs available for task creation. - name: Music Video description: Create music video tasks from images, audio, and optional creative controls. + - name: Effects + description: Discover versioned effects and create image or video effect tasks. + - name: Generation + description: Discover BeatAPI generation models and create image or video tasks. + - name: Text + description: Discover enabled text models and call them through OpenAI, Anthropic, or Gemini-compatible request formats. - name: Ecommerce Video description: Create product ad video tasks from product images and duration. + - name: Video Analysis + description: Analyze uploaded videos with standard or deep multimodal reasoning. - name: Tasks description: Poll task status and read output URLs. - name: Usage @@ -107,6 +120,70 @@ tags: description: Upload local assets and use the returned HTTPS URL as workflow input. - name: Webhooks description: Manage optional completion callbacks. +webhooks: + taskCompleted: + post: + operationId: receiveBeatApiTaskEvent + x-fern-ignore: true + tags: [Webhooks] + summary: Receive a BeatAPI task completion event + description: | + BeatAPI sends this request to each active endpoint subscribed to the event. + Verify `x-beatapi-signature` against the exact request body and use polling + as the source of truth if delivery is delayed or fails. + security: [] + parameters: + - in: header + name: x-beatapi-event + required: true + schema: { type: string, enum: [task.succeeded, task.failed] } + - in: header + name: x-beatapi-timestamp + required: true + schema: { type: string } + - in: header + name: x-beatapi-signature + required: true + schema: { type: string } + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/WebhookEvent' } + example: + id: evt_123 + event: task.succeeded + created_at: 1782210300 + data: + id: task_8K2qA + object: task + task_kind: video + capability_id: seedance-2.5 + capability_version: null + media_type: video + model: seedance-2.5 + status: succeeded + stage: succeeded + created_at: 1782210000 + updated_at: 1782210300 + completed_at: 1782210300 + output: + media: + - type: video + url: https://media.beatapi.io/outputs/task_8K2qA/0.mp4 + mime_type: video/mp4 + r2_url: https://media.beatapi.io/outputs/task_8K2qA/0.mp4 + usage: + credits_reserved: 1.55 + credits_charged: 1.55 + billable_duration_seconds: 5 + credits_settled: 1.55 + credits_refunded: 0 + request_id: req_abc123 + error_code: null + error_message: null + responses: + '200': { description: Event accepted } components: securitySchemes: BearerAuth: @@ -114,14 +191,61 @@ components: scheme: bearer bearerFormat: sk_xxx description: 'Send your API key as `Authorization: Bearer `.' + ApiKeyHeader: + type: apiKey + in: header + name: x-api-key + description: Anthropic-compatible API key header. Use a BeatAPI API key. + GoogleApiKeyHeader: + type: apiKey + in: header + name: x-goog-api-key + description: Gemini-compatible API key header. Use a BeatAPI API key. + GoogleApiKeyQuery: + type: apiKey + in: query + name: key + description: Gemini SDK compatibility only. Prefer the x-goog-api-key header when possible. schemas: + TextModelId: + type: string + description: Public text model id exposed by BeatAPI. Call GET /v1/models to discover the models enabled for your environment. + TextModel: + type: object + additionalProperties: false + required: [id, object, created, owned_by] + properties: + id: { $ref: '#/components/schemas/TextModelId' } + object: { type: string, const: model } + created: { type: integer, example: 1788220800 } + owned_by: { type: string, const: beatapi } + TextModelList: + type: object + additionalProperties: false + required: [object, data] + properties: + object: { type: string, const: list } + data: + type: array + items: { $ref: '#/components/schemas/TextModel' } + TextPassthroughRequest: + type: object + description: SDK-compatible text request. BeatAPI preserves supported provider-format fields and streams the matching response format back. + required: [model] + properties: + model: { $ref: '#/components/schemas/TextModelId' } + additionalProperties: true + TextPassthroughResponse: + type: object + description: Response body in the selected SDK-compatible wire format. + additionalProperties: true Workflow: type: object required: [id, object, name, description] properties: id: type: string - enum: [music-video, ecommerce-video] + enum: [music-video, ecommerce-video, video-analysis] example: music-video object: type: string @@ -144,17 +268,22 @@ components: example: shot_xxx index: type: integer + description: Zero-based shot order in the storyboard. example: 0 status: $ref: '#/components/schemas/TaskStatus' + description: Current lifecycle state for this storyboard shot. duration_seconds: type: integer + description: Planned or generated shot duration in seconds. example: 5 prompt: type: string + description: Creative instruction used to generate this shot. example: Opening lyric shot. lyric_text: type: string + description: Lyric segment aligned with this shot when available. example: Intro media: type: object @@ -162,19 +291,24 @@ components: properties: type: type: string + description: Hosted media type for the materialized shot. example: video url: type: string format: uri + description: BeatAPI-hosted HTTPS URL for the materialized shot. example: https://media.beatapi.io/outputs/task_8K2qA/shots/0.mp4 mime_type: type: string + description: MIME type of the hosted shot media. example: video/mp4 created_at: type: integer + description: Unix timestamp when the shot record was created. example: 1782210000 updated_at: type: integer + description: Unix timestamp when the shot record last changed. example: 1782210300 Storyboard: type: object @@ -182,6 +316,7 @@ components: properties: shots: type: array + description: Ordered Music Video storyboard shots. The array may be empty before storyboard generation completes. items: $ref: '#/components/schemas/StoryboardShot' ShotMedia: @@ -230,46 +365,88 @@ components: required: [credits_reserved, credits_settled, credits_refunded, credits_charged] properties: credits_reserved: - type: integer - description: BeatAPI customer credits reserved for this task. + type: number + format: double + multipleOf: 0.01 + description: USD amount reserved for this task. The compatibility field name is retained; 1 Credit equals $1 USD. credits_charged: - type: integer - description: BeatAPI customer credits charged when the task or operation is accepted. + type: number + format: double + multipleOf: 0.01 + description: USD amount charged when the task or operation is accepted. billable_duration_seconds: type: integer - description: Server-detected or request-declared billable duration used for credit calculation. + description: Server-detected or request-declared billable duration used for USD calculation. credits_settled: - type: integer - description: BeatAPI customer credits settled after successful work. + type: number + format: double + multipleOf: 0.01 + description: USD amount settled after successful work. credits_refunded: - type: integer - description: BeatAPI customer credits refunded after failed eligible work. + type: number + format: double + multipleOf: 0.01 + description: USD amount refunded after failed eligible work. Task: type: object - required: [id, object, workflow, status, stage, created_at, updated_at, completed_at, output, usage, request_id, error_code, error_message] + required: [id, object, task_kind, capability_id, capability_version, status, stage, created_at, updated_at, completed_at, output, usage, request_id, error_code, error_message] properties: id: type: string + description: Stable BeatAPI task ID used for polling and support. example: task_8K2qA object: type: string enum: [task] + description: Object discriminator; always `task`. + task_kind: + type: string + enum: [workflow, effect, image, video] + description: Public task family that determines which capability fields are present. + capability_id: + type: string + description: Stable BeatAPI workflow, Effect, or generation model ID selected when the task was accepted. + capability_version: + type: [integer, 'null'] + description: Immutable capability version used by this task. Legacy workflow rows are returned as version 1. workflow: type: string - enum: [music-video, ecommerce-video] + enum: [music-video, ecommerce-video, video-analysis] + description: Present for workflow tasks; identifies the selected BeatAPI workflow. example: music-video + effect_id: + type: string + description: Present for Effect tasks; stable selected Effect ID. + example: video-muscle-max + effect_version: + type: integer + description: Present for Effect tasks; immutable Effect version used for processing. + example: 1 + media_type: + type: string + enum: [image, video] + description: Present when task_kind is image or video. + model: + type: string + description: Stable BeatAPI model alias. It is independent from internal execution routing. status: $ref: '#/components/schemas/TaskStatus' + description: Current task lifecycle status. Stop polling at `succeeded` or `failed`; Music Video can also require manual action. stage: $ref: '#/components/schemas/TaskStatus' + description: Current processing stage, exposed separately so workflow progress can be tracked. storyboard: $ref: '#/components/schemas/Storyboard' + description: Music Video storyboard metadata when available. created_at: type: integer + description: Unix timestamp when BeatAPI accepted the task. updated_at: type: integer + description: Unix timestamp of the latest task update. completed_at: type: [integer, 'null'] + description: Terminal Unix timestamp, or null while work is in progress. output: description: Output is null until the task succeeds. oneOf: @@ -279,54 +456,155 @@ components: properties: media: type: array + description: BeatAPI-hosted result assets. items: type: object required: [type, url, mime_type] properties: type: type: string - enum: [video] + enum: [image, video] + description: Result asset type. url: type: string format: uri + description: BeatAPI-hosted HTTPS result URL. mime_type: type: string - example: video/mp4 + description: Result asset MIME type. + examples: [video/mp4, image/png, image/jpeg, image/webp] r2_url: type: string format: uri + description: Primary BeatAPI-hosted result URL for clients that need one canonical asset. + - type: object + required: [text, usage, finish_reason] + properties: + text: + type: string + description: Completed video analysis text. + usage: + type: object + description: Measured token usage used for final USD settlement. + required: [input_tokens, output_tokens, total_tokens] + properties: + input_tokens: + type: integer + minimum: 0 + description: Tokens consumed by the prompt and video input. + output_tokens: + type: integer + minimum: 0 + description: Tokens consumed by visible output and model reasoning. + total_tokens: + type: integer + minimum: 0 + description: Total measured input and output tokens. + finish_reason: + type: [string, 'null'] + description: Upstream-compatible completion reason. usage: $ref: '#/components/schemas/TaskUsage' + description: USD reservation, settlement, refund, and optional billable duration for this task. request_id: type: string + description: Correlation ID to retain for logs and BeatAPI support. example: req_abc123 error_code: type: [string, 'null'] + description: Machine-readable terminal failure code, or null when no task failure is recorded. example: processing_timeout error_message: type: [string, 'null'] + description: Human-readable terminal failure detail, or null when no task failure is recorded. + Effect: + type: object + required: [id, object, name, description, output_type, category, tags, input, options, preview, version, status] + properties: + id: { type: string, example: video-muscle-max } + object: { type: string, enum: [effect] } + name: { type: string, example: Muscle Transformation } + description: { type: string } + output_type: { type: string, enum: [image, video] } + category: { type: string, example: transformation } + tags: { type: array, items: { type: string } } + input: + type: object + required: [images_min, images_max, accepted_types] + properties: + images_min: { type: integer, minimum: 1 } + images_max: { type: integer, minimum: 1 } + accepted_types: + type: array + items: { type: string, enum: [image/jpeg, image/png, image/webp] } + max_size_mb: + type: integer + minimum: 1 + description: Maximum downloaded bytes per input image. When omitted, BeatAPI enforces 50 MB. + max_dimension_px: + type: integer + minimum: 1 + description: Maximum decoded width or height. BeatAPI inspects the actual image header before charging. + subject_requirements: { type: array, items: { type: string } } + options: + type: object + properties: + aspect_ratios: { type: array, items: { type: string } } + resolutions: { type: array, items: { type: string } } + duration_seconds: { type: array, items: { type: integer } } + bgm: { type: boolean } + seed: { type: boolean } + preview: + type: object + required: [cover_url, media_url] + properties: + cover_url: { type: [string, 'null'], format: uri } + media_url: { type: [string, 'null'], format: uri } + version: { type: integer, minimum: 1 } + status: { type: string, enum: [testing, active, paused] } + EffectResponse: + type: object + required: [data] + properties: + data: { $ref: '#/components/schemas/Effect' } + EffectListResponse: + type: object + required: [data] + properties: + data: + type: object + required: [object, data] + properties: + object: { type: string, enum: [list] } + data: { type: array, items: { $ref: '#/components/schemas/Effect' } } File: type: object required: [id, object, url, key, mime_type, size_bytes, purpose, created_at] properties: id: type: string + description: Stable uploaded file ID. example: file_3xYz9 object: type: string enum: [file] + description: Object discriminator; always `file`. url: type: string format: uri + description: Long-lived BeatAPI HTTPS URL to use in workflow or model requests. example: https://media.beatapi.io/inputs/file_3xYz9.mp3 key: type: string + description: BeatAPI storage key for support and diagnostics. example: inputs/file_3xYz9.mp3 mime_type: type: string + description: Accepted MIME type detected for the uploaded file. example: audio/mpeg size_bytes: type: integer + description: Uploaded file size in bytes. example: 1048576 audio_duration_seconds: type: number @@ -336,11 +614,29 @@ components: type: string description: Duration detection method used for uploaded audio. example: mp3_frame_scan + video_duration_seconds: + type: number + description: Present for MP4/MOV uploads after server-side container inspection. + example: 15.25 + video_duration_source: + type: string + description: Duration and dimension detection method used for the uploaded video. + example: mp4_boxes + width: + type: integer + description: Detected pixel width for uploaded images and videos. + example: 720 + height: + type: integer + description: Detected pixel height for uploaded images and videos. + example: 1280 purpose: type: string enum: [input] + description: File purpose; currently always `input`. created_at: type: integer + description: Unix timestamp when the file was stored. example: 1782210000 WebhookEndpoint: type: object @@ -348,33 +644,41 @@ components: properties: id: type: string + description: Stable webhook endpoint ID used for get, update, and delete operations. example: wh_9aBcD object: type: string enum: [webhook_endpoint] + description: Object discriminator; always `webhook_endpoint`. url: type: string format: uri + description: Public HTTPS callback URL receiving subscribed task events. example: https://example.com/beatapi-webhook description: type: string + description: Account-defined label for the endpoint. example: Production webhook events: type: array + description: Task event types delivered to this endpoint. items: type: string enum: [task.succeeded, task.failed] status: type: string enum: [active, disabled] + description: Delivery status. Disabled endpoints do not receive events. secret: type: string description: Returned in full only when the endpoint is created. Later responses return a masked value. example: whsec_example_masked created_at: type: integer + description: Unix timestamp when the endpoint was created. updated_at: type: integer + description: Unix timestamp when the endpoint last changed. WebhookEvent: type: object required: [id, event, created_at, data] @@ -406,28 +710,576 @@ components: properties: data: $ref: '#/components/schemas/WorkflowList' + GenerationModel: + type: object + additionalProperties: false + required: [id, object, name, media_type, input_modes] + properties: + id: + type: string + enum: [nano-banana, nano-banana-2, nano-banana-2-lite, nano-banana-pro, gpt-image-2, seedream-5-pro, grok-imagine-image-2.0, minimax-h3, grok-imagine-video-1.5, seedance-2, seedance-2-fast, seedance-2-mini, veo-3.1, seedance-2.5, kling-3, kling-2.6-motion-control, kling-3-motion-control, wan-3.0, wan-3.0-prime, happyhorse-1.0, happyhorse-1.1, minimax-h3-max, minimax-h3-max-turbo] + object: { type: string, enum: [generation_model] } + name: { type: string } + media_type: { type: string, enum: [image, video] } + input_modes: + type: array + items: { type: string, enum: [text, image, frames, reference] } + GenerationModelList: + type: object + required: [object, data] + properties: + object: { type: string, enum: [list] } + data: + type: array + items: { $ref: '#/components/schemas/GenerationModel' } + GenerationModelListResponse: + type: object + required: [data] + properties: + data: { $ref: '#/components/schemas/GenerationModelList' } + ImageGenerationTaskCreateRequest: + oneOf: + - $ref: '#/components/schemas/NanoBananaImageRequest' + - $ref: '#/components/schemas/NanoBanana2ImageRequest' + - $ref: '#/components/schemas/NanoBanana2LiteImageRequest' + - $ref: '#/components/schemas/NanoBananaProImageRequest' + - $ref: '#/components/schemas/GptImage2Request' + - $ref: '#/components/schemas/Seedream5ProImageRequest' + - $ref: '#/components/schemas/GrokImagineImage20Request' + discriminator: + propertyName: model + mapping: + nano-banana: '#/components/schemas/NanoBananaImageRequest' + nano-banana-2: '#/components/schemas/NanoBanana2ImageRequest' + nano-banana-2-lite: '#/components/schemas/NanoBanana2LiteImageRequest' + nano-banana-pro: '#/components/schemas/NanoBananaProImageRequest' + gpt-image-2: '#/components/schemas/GptImage2Request' + seedream-5-pro: '#/components/schemas/Seedream5ProImageRequest' + grok-imagine-image-2.0: '#/components/schemas/GrokImagineImage20Request' + NanoBananaImageRequest: + type: object + additionalProperties: false + required: [model, prompt] + properties: + model: { type: string, const: nano-banana, description: Must be `nano-banana`. } + prompt: { type: string, minLength: 1, maxLength: 5000, description: Generation or image-editing instructions. } + images: + type: array + minItems: 1 + maxItems: 10 + description: Public HTTPS reference-image URLs. Omit for text-to-image. + items: { type: string, format: uri, pattern: '^https://' } + aspect_ratio: + type: string + enum: ['1:1', '9:16', '16:9', '3:4', '4:3', '3:2', '2:3', '5:4', '4:5', '21:9', auto] + default: '1:1' + description: Output image aspect ratio. + output_format: { type: string, enum: [png, jpeg], default: png, description: Output image file format. } + NanoBanana2ImageRequest: + type: object + additionalProperties: false + required: [model, prompt] + properties: + model: { type: string, const: nano-banana-2, description: Must be `nano-banana-2`. } + prompt: { type: string, minLength: 1, maxLength: 5000, description: Generation or image-editing instructions. } + images: + type: array + minItems: 1 + maxItems: 10 + description: Public HTTPS reference-image URLs. Omit for text-to-image. + items: { type: string, format: uri, pattern: '^https://' } + aspect_ratio: + type: string + enum: ['1:1', '9:16', '16:9', '3:4', '4:3', '3:2', '2:3', '5:4', '4:5', '21:9', auto] + default: '1:1' + description: Output image aspect ratio. + resolution: { type: string, enum: [1K, 2K, 4K], default: 1K, description: Output resolution tier. } + output_format: { type: string, enum: [png, jpeg], default: png, description: Output image file format. } + NanoBanana2LiteImageRequest: + type: object + additionalProperties: false + required: [model, prompt] + properties: + model: { type: string, const: nano-banana-2-lite, description: Must be `nano-banana-2-lite`. } + prompt: { type: string, minLength: 1, maxLength: 5000, description: Generation or image-editing instructions. } + images: + type: array + minItems: 1 + maxItems: 10 + description: Public HTTPS reference-image URLs. Omit for text-to-image. + items: { type: string, format: uri, pattern: '^https://' } + aspect_ratio: + type: string + enum: ['1:1', '9:16', '16:9', '3:4', '4:3', '3:2', '2:3', '5:4', '4:5', '21:9', auto] + default: '1:1' + description: Output image aspect ratio. + output_format: { type: string, enum: [png, jpeg], default: png, description: Output image file format. } + NanoBananaProImageRequest: + type: object + additionalProperties: false + required: [model, prompt] + properties: + model: { type: string, const: nano-banana-pro, description: Must be `nano-banana-pro`. } + prompt: { type: string, minLength: 1, maxLength: 5000, description: Generation or image-editing instructions. } + images: + type: array + minItems: 1 + maxItems: 8 + description: Public HTTPS reference-image URLs. Omit for text-to-image. + items: { type: string, format: uri, pattern: '^https://' } + aspect_ratio: + type: string + enum: ['1:1', '2:3', '3:2', '3:4', '4:3', '4:5', '5:4', '9:16', '16:9', '21:9', auto] + default: '1:1' + description: Output image aspect ratio. + resolution: { type: string, enum: [1K, 2K, 4K], default: 1K, description: Output resolution tier. } + output_format: { type: string, enum: [png, jpg], default: png, description: Output image file format. } + GptImage2Request: + type: object + additionalProperties: false + required: [model, prompt] + properties: + model: { type: string, const: gpt-image-2, description: Must be `gpt-image-2`. } + prompt: { type: string, minLength: 1, maxLength: 5000, description: Generation or image-editing instructions. } + images: + type: array + minItems: 1 + maxItems: 16 + description: Public HTTPS reference-image URLs. Omit for text-to-image. + items: { type: string, format: uri, pattern: '^https://' } + aspect_ratio: + type: string + enum: [auto, '1:1', '3:2', '2:3', '4:3', '3:4', '5:4', '4:5', '16:9', '9:16', '2:1', '1:2', '3:1', '1:3', '21:9', '9:21'] + default: auto + description: Output image aspect ratio. + resolution: { type: string, enum: [1K, 2K, 4K], default: 1K, description: Output resolution tier. } + Seedream5ProImageRequest: + type: object + additionalProperties: false + required: [model, prompt] + properties: + model: { type: string, const: seedream-5-pro, description: Must be `seedream-5-pro`. } + prompt: { type: string, minLength: 1, maxLength: 5000, description: Generation or image-editing instructions. } + images: + type: array + minItems: 1 + maxItems: 10 + description: Public HTTPS reference-image URLs. Omit for text-to-image. + items: { type: string, format: uri, pattern: '^https://' } + aspect_ratio: + type: string + enum: [auto, '1:1', '4:3', '3:4', '16:9', '9:16', '3:2', '2:3', '21:9'] + default: '1:1' + description: Output image aspect ratio. + resolution: { type: string, enum: [1K, 2K, 4K], default: 1K, description: Output resolution tier. } + output_format: { type: string, enum: [png, jpeg], default: png, description: Output image file format. } + GrokImagineImage20Request: + type: object + additionalProperties: false + required: [model, prompt] + description: Omit `images` for text-to-image. Supply one to five images for editing; `auto` aspect ratio is available only when images are supplied. + properties: + model: { type: string, const: grok-imagine-image-2.0, description: Must be `grok-imagine-image-2.0`. } + prompt: { type: string, minLength: 1, maxLength: 5000, description: Generation or image-editing instructions. } + images: + type: array + minItems: 1 + maxItems: 5 + description: Public HTTPS reference-image URLs. Omit for text-to-image. + items: { type: string, format: uri, pattern: '^https://' } + aspect_ratio: + type: string + enum: ['1:1', '2:3', '3:2', '16:9', '9:16', auto] + default: '1:1' + description: Output image aspect ratio. `auto` requires at least one image. + VideoGenerationTaskCreateRequest: + oneOf: + - $ref: '#/components/schemas/MinimaxH3VideoRequest' + - $ref: '#/components/schemas/GrokImagineVideo15Request' + - $ref: '#/components/schemas/Seedance2VideoRequest' + - $ref: '#/components/schemas/Seedance2FastVideoRequest' + - $ref: '#/components/schemas/Seedance2MiniVideoRequest' + - $ref: '#/components/schemas/Veo31VideoRequest' + - $ref: '#/components/schemas/Seedance25VideoRequest' + - $ref: '#/components/schemas/Kling3VideoRequest' + - $ref: '#/components/schemas/Kling26MotionControlVideoRequest' + - $ref: '#/components/schemas/Kling3MotionControlVideoRequest' + - $ref: '#/components/schemas/Wan30VideoRequest' + - $ref: '#/components/schemas/Wan30PrimeVideoRequest' + - $ref: '#/components/schemas/HappyHorse10VideoRequest' + - $ref: '#/components/schemas/HappyHorse11VideoRequest' + - $ref: '#/components/schemas/MinimaxH3MaxVideoRequest' + - $ref: '#/components/schemas/MinimaxH3MaxTurboVideoRequest' + discriminator: + propertyName: model + mapping: + minimax-h3: '#/components/schemas/MinimaxH3VideoRequest' + grok-imagine-video-1.5: '#/components/schemas/GrokImagineVideo15Request' + seedance-2: '#/components/schemas/Seedance2VideoRequest' + seedance-2-fast: '#/components/schemas/Seedance2FastVideoRequest' + seedance-2-mini: '#/components/schemas/Seedance2MiniVideoRequest' + veo-3.1: '#/components/schemas/Veo31VideoRequest' + seedance-2.5: '#/components/schemas/Seedance25VideoRequest' + kling-3: '#/components/schemas/Kling3VideoRequest' + kling-2.6-motion-control: '#/components/schemas/Kling26MotionControlVideoRequest' + kling-3-motion-control: '#/components/schemas/Kling3MotionControlVideoRequest' + wan-3.0: '#/components/schemas/Wan30VideoRequest' + wan-3.0-prime: '#/components/schemas/Wan30PrimeVideoRequest' + happyhorse-1.0: '#/components/schemas/HappyHorse10VideoRequest' + happyhorse-1.1: '#/components/schemas/HappyHorse11VideoRequest' + minimax-h3-max: '#/components/schemas/MinimaxH3MaxVideoRequest' + minimax-h3-max-turbo: '#/components/schemas/MinimaxH3MaxTurboVideoRequest' + MinimaxH3VideoRequest: + type: object + additionalProperties: false + required: [model, prompt] + description: '`images` cannot be combined with any `reference_*` input. An audio reference also requires at least one reference image or video.' + properties: + model: { type: string, const: minimax-h3, description: Must be `minimax-h3`. } + prompt: { type: string, minLength: 1, maxLength: 5000, description: Video generation instructions. } + images: { type: array, minItems: 1, maxItems: 2, description: One first-frame image or first- and last-frame images as public HTTPS URLs., items: { type: string, format: uri, pattern: '^https://' } } + reference_images: { type: array, minItems: 1, maxItems: 9, description: Public HTTPS image references for multimodal reference generation., items: { type: string, format: uri, pattern: '^https://' } } + reference_videos: { type: array, minItems: 1, maxItems: 3, description: Public HTTPS video references for multimodal reference generation., items: { type: string, format: uri, pattern: '^https://' } } + reference_audios: { type: array, minItems: 1, maxItems: 3, description: Public HTTPS audio references for multimodal reference generation. Audio also requires at least one reference image or video., items: { type: string, format: uri, pattern: '^https://' } } + duration: { type: integer, minimum: 4, maximum: 15, default: 5, description: Requested output duration in seconds. } + aspect_ratio: + type: string + enum: [adaptive, '21:9', '16:9', '4:3', '1:1', '3:4', '9:16'] + description: Text mode defaults to 16:9 and does not accept adaptive. Frame mode always uses adaptive. Reference mode defaults to adaptive and also accepts a concrete ratio. + resolution: { type: string, enum: [768P, 2K], default: 768P, description: Output resolution tier. } + GrokImagineVideo15Request: + type: object + additionalProperties: false + required: [model, prompt] + description: '`images` accepts one first frame and cannot be combined with `reference_images`. Omit `aspect_ratio` when `images` is supplied. 1080p accepts at most one image.' + properties: + model: { type: string, const: grok-imagine-video-1.5, description: Must be `grok-imagine-video-1.5`. } + prompt: { type: string, minLength: 1, maxLength: 4096, description: Video generation instructions. } + images: { type: array, minItems: 1, maxItems: 1, description: One first-frame image as a public HTTPS URL., items: { type: string, format: uri, pattern: '^https://' } } + reference_images: { type: array, minItems: 1, maxItems: 7, description: One to seven public HTTPS reference images., items: { type: string, format: uri, pattern: '^https://' } } + duration: { type: integer, minimum: 1, maximum: 15, default: 8, description: Requested output duration in seconds. } + aspect_ratio: { type: string, enum: ['1:1', '16:9', '9:16', '3:2', '2:3', auto], default: '16:9', description: Output video aspect ratio. Omit when one first-frame image is supplied. } + resolution: { type: string, enum: [480p, 720p, 1080p], default: 480p, description: Output resolution tier. 1080p accepts at most one image. } + Seedance2VideoRequest: + type: object + additionalProperties: false + required: [model, prompt] + description: '`images` cannot be combined with any `reference_*` input. An audio reference also requires at least one reference image or video.' + properties: + model: { type: string, const: seedance-2, description: Must be `seedance-2`. } + prompt: { type: string, minLength: 1, maxLength: 5000, description: Video generation instructions. } + images: { type: array, minItems: 1, maxItems: 2, description: One first-frame image or first- and last-frame images as public HTTPS URLs., items: { type: string, format: uri, pattern: '^https://' } } + reference_images: { type: array, minItems: 1, maxItems: 9, description: Public HTTPS image references for multimodal reference generation., items: { type: string, format: uri, pattern: '^https://' } } + reference_videos: { type: array, minItems: 1, maxItems: 3, description: Public HTTPS video references for multimodal reference generation., items: { type: string, format: uri, pattern: '^https://' } } + reference_audios: { type: array, minItems: 1, maxItems: 3, description: Public HTTPS audio references. Audio also requires at least one reference image or video., items: { type: string, format: uri, pattern: '^https://' } } + duration: { type: integer, minimum: 4, maximum: 15, default: 5, description: Requested output duration in seconds. } + aspect_ratio: { type: string, enum: [adaptive, '21:9', '16:9', '4:3', '1:1', '3:4', '9:16'], default: adaptive, description: Output video aspect ratio. } + resolution: { type: string, enum: [480p, 720p, 1080p, 4k, 4K], default: 720p, description: Output resolution tier. 4k and 4K are equivalent. 1080p is not supported with reference images. } + generate_audio: { type: boolean, default: true, description: Generate synchronized audio with the video. } + Seedance2FastVideoRequest: + type: object + additionalProperties: false + required: [model, prompt] + description: '`images` cannot be combined with any `reference_*` input. An audio reference also requires at least one reference image or video.' + properties: + model: { type: string, const: seedance-2-fast, description: Must be `seedance-2-fast`. } + prompt: { type: string, minLength: 1, maxLength: 5000, description: Video generation instructions. } + images: { type: array, minItems: 1, maxItems: 2, description: One first-frame image or first- and last-frame images as public HTTPS URLs., items: { type: string, format: uri, pattern: '^https://' } } + reference_images: { type: array, minItems: 1, maxItems: 9, description: Public HTTPS image references for multimodal reference generation., items: { type: string, format: uri, pattern: '^https://' } } + reference_videos: { type: array, minItems: 1, maxItems: 3, description: Public HTTPS video references for multimodal reference generation., items: { type: string, format: uri, pattern: '^https://' } } + reference_audios: { type: array, minItems: 1, maxItems: 3, description: Public HTTPS audio references. Audio also requires at least one reference image or video., items: { type: string, format: uri, pattern: '^https://' } } + duration: { type: integer, minimum: 4, maximum: 15, default: 5, description: Requested output duration in seconds. } + aspect_ratio: { type: string, enum: [adaptive, '21:9', '16:9', '4:3', '1:1', '3:4', '9:16'], default: adaptive, description: Output video aspect ratio. } + resolution: { type: string, enum: [480p, 720p], default: 720p, description: Output resolution tier. } + generate_audio: { type: boolean, default: true, description: Generate synchronized audio with the video. } + Seedance2MiniVideoRequest: + type: object + additionalProperties: false + required: [model, prompt] + description: 'Low-cost Seedance 2.0 route. `images` cannot be combined with any `reference_*` input. Generated audio is not supported.' + properties: + model: { type: string, const: seedance-2-mini, description: Must be `seedance-2-mini`. } + prompt: { type: string, minLength: 1, maxLength: 5000, description: Video generation instructions. } + images: { type: array, minItems: 1, maxItems: 2, description: One first-frame image or first- and last-frame images as public HTTPS URLs., items: { type: string, format: uri, pattern: '^https://' } } + reference_images: { type: array, minItems: 1, maxItems: 9, description: Public HTTPS image references for multimodal reference generation., items: { type: string, format: uri, pattern: '^https://' } } + reference_videos: { type: array, minItems: 1, maxItems: 3, description: Public HTTPS video references for multimodal reference generation., items: { type: string, format: uri, pattern: '^https://' } } + reference_audios: { type: array, minItems: 1, maxItems: 3, description: Public HTTPS audio references. Audio also requires at least one reference image or video., items: { type: string, format: uri, pattern: '^https://' } } + duration: { type: integer, minimum: 4, maximum: 15, default: 5, description: Requested output duration in seconds. } + aspect_ratio: { type: string, enum: [adaptive, '21:9', '16:9', '4:3', '1:1', '3:4', '9:16'], default: adaptive, description: Output video aspect ratio. } + resolution: { type: string, enum: [480p, 720p], default: 720p, description: Output resolution tier. } + Veo31VideoRequest: + allOf: + - oneOf: + - $ref: '#/components/schemas/Veo31TextOrFrameVideoRequest' + - $ref: '#/components/schemas/Veo31ReferenceVideoRequest' + Veo31TextOrFrameVideoRequest: + type: object + additionalProperties: false + required: [model, prompt] + description: | + Veo 3.1 text or first/last-frame generation. Output is fixed at 8 seconds + and defaults to Quality at 720p. Price depends on quality and resolution. + properties: + model: { type: string, const: veo-3.1, description: Must be `veo-3.1`. } + prompt: { type: string, minLength: 1, maxLength: 5000, description: Video generation instructions. } + images: { type: array, minItems: 1, maxItems: 2, description: One first-frame image or first- and last-frame images as public HTTPS URLs., items: { type: string, format: uri, pattern: '^https://' } } + aspect_ratio: { type: string, enum: ['16:9', '9:16', auto], default: '16:9', description: Output video aspect ratio. } + resolution: { type: string, enum: [720p, 1080p, 4k, 4K], default: 720p, description: Output video resolution. 4k and 4K are equivalent. Price depends on quality and resolution. } + quality: { type: string, enum: [Quality, Fast, Lite], default: Quality, description: Text or frame generation tier. } + watermark: { type: string, description: Optional watermark text forwarded to the selected model. } + enable_translation: { type: boolean, description: Allow prompt translation before generation. } + Veo31ReferenceVideoRequest: + type: object + additionalProperties: false + required: [model, prompt, reference_images] + description: | + Veo 3.1 reference-image generation. Output is fixed at 8 seconds and + supports the Fast or Lite tier, defaulting to Fast at 720p. Price depends + on quality and resolution. + properties: + model: { type: string, const: veo-3.1, description: Must be `veo-3.1`. } + prompt: { type: string, minLength: 1, maxLength: 5000, description: Video generation instructions. } + reference_images: { type: array, minItems: 1, maxItems: 3, description: Public HTTPS reference images., items: { type: string, format: uri, pattern: '^https://' } } + aspect_ratio: { type: string, enum: ['16:9', '9:16', auto], default: '16:9', description: Output video aspect ratio. } + resolution: { type: string, enum: [720p, 1080p, 4k, 4K], default: 720p, description: Output video resolution. 4k and 4K are equivalent. Price depends on quality and resolution. } + quality: { type: string, enum: [Fast, Lite], default: Fast, description: Reference-image generation tier. } + watermark: { type: string, description: Optional watermark text forwarded to the selected model. } + enable_translation: { type: boolean, description: Allow prompt translation before generation. } + Seedance25VideoRequest: + type: object + additionalProperties: false + required: [model, prompt] + description: '`images` cannot be combined with any `reference_*` input. Audio-only reference generation is supported.' + properties: + model: { type: string, const: seedance-2.5, description: Must be `seedance-2.5`. } + prompt: { type: string, minLength: 1, maxLength: 5000, description: Video generation instructions. } + images: { type: array, minItems: 1, maxItems: 2, description: One first-frame image or first- and last-frame images as public HTTPS URLs., items: { type: string, format: uri, pattern: '^https://' } } + reference_images: { type: array, minItems: 1, maxItems: 30, description: Public HTTPS image references for multimodal reference generation., items: { type: string, format: uri, pattern: '^https://' } } + reference_videos: { type: array, minItems: 1, maxItems: 10, description: Public HTTPS video references for multimodal reference generation., items: { type: string, format: uri, pattern: '^https://' } } + reference_audios: { type: array, minItems: 1, maxItems: 10, description: Public HTTPS audio references. Audio-only reference generation is supported., items: { type: string, format: uri, pattern: '^https://' } } + duration: { type: integer, minimum: 4, maximum: 30, default: 5, description: Requested output duration in seconds. } + aspect_ratio: { type: string, enum: [adaptive, '21:9', '16:9', '4:3', '1:1', '3:4', '9:16'], default: adaptive, description: Output video aspect ratio. } + resolution: { type: string, enum: [480p, 720p, 1080p], default: 720p, description: Output resolution tier. 480p and 1080p are priced separately; see the pricing page. } + generate_audio: { type: boolean, default: true, description: Generate synchronized audio with the video. } + seed: { type: integer, minimum: -1, maximum: 4294967295, default: -1, description: Reproducibility seed. Use -1 for a random seed. } + KlingShot: + type: object + additionalProperties: false + required: [prompt, duration] + properties: + prompt: { type: string, minLength: 1, maxLength: 500, description: Instructions for this shot. } + duration: { type: integer, minimum: 1, maximum: 12, description: Shot duration in seconds. All shot durations must sum to the task duration. } + KlingElement: + type: object + additionalProperties: false + required: [name, element_input_urls] + description: Use 2-4 image URLs or one video URL. A video element may include one audio URL and a 3-8 second segment in milliseconds. + properties: + name: { type: string, minLength: 1, description: Stable name used to reference this element in the prompt. } + description: { type: string, description: Optional description of the subject or object. } + element_input_urls: + type: array + minItems: 1 + maxItems: 4 + description: Two to four image URLs, or one video URL. + items: { type: string, format: uri, pattern: '^https://' } + element_input_audio_urls: + type: array + minItems: 1 + maxItems: 1 + description: Optional audio URL used with a video element. + items: { type: string, format: uri, pattern: '^https://' } + start_time: { type: integer, minimum: 0, maximum: 30000, description: Video element segment start time in milliseconds. } + end_time: { type: integer, minimum: 0, maximum: 30000, description: Video element segment end time in milliseconds. The segment must be 3-8 seconds. } + Kling3VideoRequest: + type: object + additionalProperties: false + required: [model, prompt] + description: Multi-shot mode accepts one first-frame image, requires `multi_prompt`, and defaults sound to true. Shot durations must sum to `duration`. + properties: + model: { type: string, const: kling-3, description: Must be `kling-3`. } + prompt: { type: string, minLength: 1, maxLength: 5000, description: Video generation instructions. } + images: { type: array, minItems: 1, maxItems: 2, description: One first-frame image or first- and last-frame images as public HTTPS URLs. Multi-shot mode accepts exactly one., items: { type: string, format: uri, pattern: '^https://' } } + duration: { type: integer, minimum: 3, maximum: 15, default: 5, description: Requested output duration in seconds. } + aspect_ratio: + type: string + enum: ['16:9', '9:16', '1:1'] + description: Defaults to 16:9 for text generation. Omit it with frame images to adapt to the input aspect ratio. + resolution: { type: string, enum: [std, pro, 4K], default: pro, description: Output quality tier. } + sound: { type: boolean, description: Generate synchronized sound. Defaults to true in multi-shot mode. } + multi_shots: { type: boolean, default: false, description: Enable storyboard-style multi-shot generation. } + multi_prompt: + type: array + minItems: 1 + maxItems: 5 + description: Shot definitions required when `multi_shots=true`. + items: { $ref: '#/components/schemas/KlingShot' } + elements: + type: array + maxItems: 3 + description: Up to three reusable subject or object references. + items: { $ref: '#/components/schemas/KlingElement' } + Kling26MotionControlVideoRequest: + type: object + additionalProperties: false + required: [model, images, reference_videos] + description: | + Transfer motion from one uploaded 3–30 second MP4/MOV video to one + uploaded character image. Both URLs must come from `/v1/files` for the + current BeatAPI account. BeatAPI detects the reference-video duration + server-side and reserves USD at the selected per-second rate, rounding + fractional seconds up. The image must be 10 MB or smaller; the video + may be up to 100 MB. + properties: + model: { type: string, const: kling-2.6-motion-control, description: Must be `kling-2.6-motion-control`. } + prompt: { type: string, maxLength: 2500, description: Optional motion or scene guidance. } + images: + type: array + minItems: 1 + maxItems: 1 + description: Exactly one character-image URL returned by the current account's `/v1/files` upload. + items: { type: string, format: uri, pattern: '^https://' } + reference_videos: + type: array + minItems: 1 + maxItems: 1 + description: Exactly one 3–30 second MP4/MOV URL returned by the current account's `/v1/files` upload. Its detected duration determines billing. + items: { type: string, format: uri, pattern: '^https://' } + resolution: { type: string, enum: [720p, 1080p], default: 720p, description: Output resolution and per-second price tier. } + character_orientation: { type: string, enum: [image, video], default: image, description: Image orientation supports motion videos up to 10 seconds; video orientation supports up to 30 seconds. } + Kling3MotionControlVideoRequest: + type: object + additionalProperties: false + required: [model, images, reference_videos] + description: | + Kling 3.0 motion transfer using exactly one uploaded image and one + uploaded 3–30 second MP4/MOV. Both assets must exceed 340 px in width + and height and use an aspect ratio from 2:5 to 5:2. BeatAPI detects the + reference-video duration server-side and reserves USD at the selected + per-second rate, rounding fractional seconds up. + properties: + model: { type: string, const: kling-3-motion-control, description: Must be `kling-3-motion-control`. } + prompt: { type: string, maxLength: 2500, description: Optional motion or scene guidance. } + images: + type: array + minItems: 1 + maxItems: 1 + description: Exactly one character-image URL returned by the current account's `/v1/files` upload; maximum 10 MB. + items: { type: string, format: uri, pattern: '^https://' } + reference_videos: + type: array + minItems: 1 + maxItems: 1 + description: Exactly one 3–30 second MP4/MOV URL returned by the current account's `/v1/files` upload; maximum 100 MB. Its detected duration determines billing. + items: { type: string, format: uri, pattern: '^https://' } + resolution: { type: string, enum: [720p, 1080p], default: 720p, description: Output resolution and per-second price tier. } + character_orientation: { type: string, enum: [image, video], default: image, description: Image orientation supports motion videos up to 10 seconds; video orientation supports up to 30 seconds. } + background_source: { type: string, enum: [input_video, input_image], default: input_video, description: Preserve the background from the motion video or character image. } + Wan30VideoRequest: + type: object + additionalProperties: false + required: [model, prompt] + description: 'Renders 2–30 seconds in a single pass. `images` starts the render from a picture; reference videos and audio travel alongside it.' + properties: + model: { type: string, const: wan-3.0, description: Must be `wan-3.0`. } + prompt: { type: string, minLength: 1, maxLength: 5000, description: Video generation instructions. } + images: { type: array, minItems: 1, maxItems: 10, description: Public HTTPS images. One starts an image-to-video render; more are used as visual references., items: { type: string, format: uri, pattern: '^https://' } } + reference_videos: { type: array, minItems: 1, maxItems: 5, description: Public HTTPS video references. A request that carries one is billed at 1.5x., items: { type: string, format: uri, pattern: '^https://' } } + reference_audios: { type: array, minItems: 1, maxItems: 5, description: Public HTTPS audio references., items: { type: string, format: uri, pattern: '^https://' } } + duration: { type: integer, minimum: 2, maximum: 30, default: 5, description: Requested output duration in seconds. Any whole number in range; there is no long-clip surcharge. } + aspect_ratio: { type: string, enum: ['16:9', '9:16', '1:1', '4:3', '3:4'], default: '16:9', description: Output video aspect ratio. } + resolution: { type: string, enum: [480p, 720p, 1080p], default: 720p, description: Output resolution tier. Price scales with it. } + Wan30PrimeVideoRequest: + type: object + additionalProperties: false + required: [model, prompt] + description: 'Renders 2–30 seconds in a single pass. `images` starts the render from a picture; reference videos and audio travel alongside it.' + properties: + model: { type: string, const: wan-3.0-prime, description: Must be `wan-3.0-prime`. } + prompt: { type: string, minLength: 1, maxLength: 5000, description: Video generation instructions. } + images: { type: array, minItems: 1, maxItems: 10, description: Public HTTPS images. One starts an image-to-video render; more are used as visual references., items: { type: string, format: uri, pattern: '^https://' } } + reference_videos: { type: array, minItems: 1, maxItems: 5, description: Public HTTPS video references. A request that carries one is billed at 1.5x., items: { type: string, format: uri, pattern: '^https://' } } + reference_audios: { type: array, minItems: 1, maxItems: 5, description: Public HTTPS audio references., items: { type: string, format: uri, pattern: '^https://' } } + duration: { type: integer, minimum: 2, maximum: 30, default: 5, description: Requested output duration in seconds. Any whole number in range; there is no long-clip surcharge. } + aspect_ratio: { type: string, enum: ['16:9', '9:16', '1:1', '4:3', '3:4'], default: '16:9', description: Output video aspect ratio. } + resolution: { type: string, enum: [480p, 720p, 1080p], default: 720p, description: Output resolution tier. Price scales with it. } + HappyHorse10VideoRequest: + type: object + additionalProperties: false + required: [model, prompt, images] + description: 'Image to video only — this model publishes no text-to-video mode, so `images` is required.' + properties: + model: { type: string, const: happyhorse-1.0, description: Must be `happyhorse-1.0`. } + prompt: { type: string, minLength: 1, maxLength: 5000, description: 'What should happen in the shot — the motion, the expression, the camera.' } + images: { type: array, minItems: 1, maxItems: 9, description: Source images as public HTTPS URLs. At least one is required., items: { type: string, format: uri, pattern: '^https://' } } + duration: { type: integer, minimum: 3, maximum: 15, default: 5, description: Requested output duration in seconds. } + aspect_ratio: { type: string, enum: ['16:9', '9:16', '1:1', '4:3', '3:4'], default: '16:9', description: Output video aspect ratio. } + resolution: { type: string, enum: [720p, 1080p], default: 720p, description: Output resolution tier. Price scales with it. } + HappyHorse11VideoRequest: + type: object + additionalProperties: false + required: [model, prompt, images] + description: 'Image to video only — this model publishes no text-to-video mode, so `images` is required.' + properties: + model: { type: string, const: happyhorse-1.1, description: Must be `happyhorse-1.1`. } + prompt: { type: string, minLength: 1, maxLength: 5000, description: 'What should happen in the shot — the motion, the expression, the camera.' } + images: { type: array, minItems: 1, maxItems: 9, description: Source images as public HTTPS URLs. At least one is required., items: { type: string, format: uri, pattern: '^https://' } } + duration: { type: integer, minimum: 3, maximum: 15, default: 5, description: Requested output duration in seconds. } + aspect_ratio: { type: string, enum: ['16:9', '9:16', '1:1', '4:3', '3:4'], default: '16:9', description: Output video aspect ratio. } + resolution: { type: string, enum: [720p, 1080p], default: 720p, description: Output resolution tier. Price scales with it. } + MinimaxH3MaxVideoRequest: + type: object + additionalProperties: false + required: [model, prompt] + description: 'Text to video, or image to video when `images` carries a first frame. A second image becomes the last frame. Output tops out at 768P — MiniMax H3 renders 2K for less per second.' + properties: + model: { type: string, const: minimax-h3-max, description: Must be `minimax-h3-max`. } + prompt: { type: string, minLength: 1, maxLength: 5000, description: 'What should happen in the shot — the motion, the expression, the camera.' } + images: { type: array, minItems: 1, maxItems: 2, description: 'Public HTTPS images. One starts the render from a first frame; a second becomes the last frame, in first-to-last order.', items: { type: string, format: uri, pattern: '^https://' } } + duration: { type: integer, minimum: 1, default: 5, description: 'Requested output duration in seconds. Billed per second at the rate for the chosen resolution. If a duration is unsupported, the API returns `400`.' } + resolution: { type: string, enum: ['480P', '768P'], default: '768P', description: 'Output resolution. Upper-case P, and the only two values this model accepts. Price scales with it.' } + seed: { type: integer, minimum: 0, description: Reuse a seed to re-render the same motion. A random seed is chosen when omitted. } + MinimaxH3MaxTurboVideoRequest: + type: object + additionalProperties: false + required: [model, prompt] + description: 'H3 Max on a faster stack — the same request contract and the same modes, roughly 2.5x quicker, at half the per-second rate.' + properties: + model: { type: string, const: minimax-h3-max-turbo, description: Must be `minimax-h3-max-turbo`. } + prompt: { type: string, minLength: 1, maxLength: 5000, description: 'What should happen in the shot — the motion, the expression, the camera.' } + images: { type: array, minItems: 1, maxItems: 2, description: 'Public HTTPS images. One starts the render from a first frame; a second becomes the last frame, in first-to-last order.', items: { type: string, format: uri, pattern: '^https://' } } + duration: { type: integer, minimum: 1, default: 5, description: 'Requested output duration in seconds. Billed per second at the rate for the chosen resolution. If a duration is unsupported, the API returns `400`.' } + resolution: { type: string, enum: ['480P', '768P'], default: '768P', description: 'Output resolution. Upper-case P, and the only two values this model accepts. Price scales with it.' } + seed: { type: integer, minimum: 0, description: Reuse a seed to re-render the same motion. A random seed is chosen when omitted. } TaskResponse: type: object required: [data] properties: data: $ref: '#/components/schemas/Task' + description: Accepted or current BeatAPI task state. Usage: type: object - required: [object, credit_balance, total_tasks, credits_settled, credits_refunded, concurrency, by_workflow] + required: [object, credit_balance, total_tasks, credits_settled, credits_refunded, concurrency, by_workflow, by_capability, by_model, by_api_key] properties: object: type: string enum: [usage] credit_balance: - type: integer - description: Current credit balance. It may be negative. + type: number + format: double + multipleOf: 0.01 + description: Current USD balance. The compatibility field name is retained; 1 Credit equals $1 USD. The balance may be negative. total_tasks: type: integer credits_settled: - type: integer + type: number + format: double + multipleOf: 0.01 credits_refunded: - type: integer + type: number + format: double + multipleOf: 0.01 concurrency: type: object required: [limit, active] @@ -437,20 +1289,68 @@ components: example: 2 active: type: integer - description: Active processing tasks currently using BeatAPI processing resources. Music Video storyboard_ready and requires_action tasks can have settled credits without counting toward this value. + description: Active processing tasks currently using BeatAPI processing resources. Music Video storyboard_ready and requires_action tasks can have settled USD usage without counting toward this value. by_workflow: type: array + description: Compatibility view containing workflow tasks only. Image, video, and Effect tasks are reported under by_capability instead. items: type: object required: [workflow, tasks, credits_settled] properties: workflow: type: string - enum: [music-video, ecommerce-video] + enum: [music-video, ecommerce-video, video-analysis] tasks: type: integer credits_settled: + type: number + format: double + multipleOf: 0.01 + by_capability: + type: array + items: + type: object + required: [task_kind, capability_id, tasks, credits_settled] + properties: + task_kind: + type: string + enum: [workflow, effect, image, video] + capability_id: + type: string + tasks: type: integer + credits_settled: + type: number + format: double + multipleOf: 0.01 + by_model: + type: array + items: + type: object + required: [media_type, model, tasks, credits_settled] + properties: + media_type: + type: string + enum: [image, video] + model: + type: string + tasks: + type: integer + credits_settled: + type: number + format: double + multipleOf: 0.01 + by_api_key: + type: array + items: + type: object + required: [api_key_id, title, key_prefix, tasks, credits_settled] + properties: + api_key_id: { type: string } + title: { type: string } + key_prefix: { type: string } + tasks: { type: integer } + credits_settled: { type: number, format: double, multipleOf: 0.01 } realtime: type: object required: [sessions, credits, active] @@ -459,8 +1359,10 @@ components: type: integer description: Total BeatAPI realtime sessions for this account. credits: - type: integer - description: Credits settled by connected realtime sessions. + type: number + format: double + multipleOf: 0.01 + description: USD amount settled by connected realtime sessions. active: type: integer description: Realtime sessions in ready, connecting, or active state. @@ -470,6 +1372,238 @@ components: properties: data: $ref: '#/components/schemas/Usage' + VideoAnalysisTaskCreateRequest: + type: object + additionalProperties: false + required: [video_url, prompt] + properties: + video_url: + type: string + format: uri + description: BeatAPI-hosted MP4 or MOV input URL returned by POST /v1/files for the current account. Maximum verified duration is 600 seconds. + prompt: + type: string + minLength: 1 + maxLength: 12000 + description: Analysis instruction. Ask for timestamped output when temporal precision matters. + analysis_depth: + type: string + enum: [standard, deep] + default: standard + description: Standard is the default low-cost route; deep uses the higher-reasoning route. + max_output_tokens: + type: integer + minimum: 256 + maximum: 8192 + default: 2048 + description: Requested answer budget. Provider-reported output usage can include hidden reasoning tokens above this value; BeatAPI records the variance for audit and settles actual reported usage within the task reservation. + MusicVideoTaskCreateRequest: + oneOf: + - $ref: '#/components/schemas/StandardMusicVideoTaskCreateRequest' + - $ref: '#/components/schemas/PremiumMusicVideoTaskCreateRequest' + discriminator: + propertyName: mv_tier + mapping: + standard: '#/components/schemas/StandardMusicVideoTaskCreateRequest' + premium: '#/components/schemas/PremiumMusicVideoTaskCreateRequest' + StandardMusicVideoTaskCreateRequest: + type: object + required: [images, audio_url] + allOf: + - if: + required: [lip_sync] + properties: + lip_sync: { const: true } + then: + required: [lip_ref_url] + properties: + lip_ref_url: {} + not: + anyOf: + - required: [mv_mode] + properties: { mv_mode: {} } + - required: [lip_ref_urls] + properties: { lip_ref_urls: {} } + properties: + mv_tier: + type: string + enum: [standard] + default: standard + description: May be omitted to preserve the backwards-compatible Standard contract. + images: + type: array + minItems: 1 + maxItems: 7 + description: Standard scene images. Provide 1-7 public HTTPS PNG, JPEG, or WebP URLs; place the primary subject or opening scene first. Upload local files through `POST /v1/files` and use the returned `data.url`. + items: { type: string, format: uri } + audio_url: + type: string + format: uri + description: Public HTTPS audio URL; Standard audio must be 10-180 seconds. + prompt: { type: string, maxLength: 3000, description: "Optional creative direction for story, setting, performance, camera, lighting, and pacing. Maximum 3000 characters." } + language: { type: string, enum: [en, zh], description: Dialogue and lyric language used by the Standard workflow. } + quality: { type: string, enum: [standard, high], default: standard, description: Generation quality tier. High quality is unavailable at 540p. } + style: { type: string, maxLength: 200, description: "Optional concise visual style, such as cinematic, anime, documentary, or fashion editorial." } + aspect_ratio: { type: string, enum: ['1:1', '16:9', '9:16', '4:3', '3:4'], description: Target output placement. Set explicitly for the destination player or social feed. } + resolution: { type: string, enum: [540p, 720p, 1080p], default: 720p, description: Output resolution. 540p cannot be combined with high quality or lip sync. } + lip_sync: + type: boolean + default: false + description: Generate lip-synchronized performance. When true, `lip_ref_url` is required. + lip_ref_url: + type: string + format: uri + description: Public HTTPS close-up, front-facing face image used for Standard lip sync. + add_subtitle: { type: boolean, default: false, description: Burn generated or supplied subtitles into the final video. } + subtitle_color: { type: string, pattern: '^#[0-9A-Fa-f]{6}$', example: '#FFFFFF', description: Subtitle text color as a six-digit hexadecimal value. Used when subtitles are enabled. } + srt_url: { type: string, format: uri, description: Optional public HTTPS `.srt` subtitle file. Upload a local subtitle through `POST /v1/files`. } + duration: + type: integer + minimum: 10 + maximum: 180 + description: Billing fallback only; detected audio duration wins. + compose_mode: + type: string + enum: [auto, manual] + default: auto + description: Auto composes the final Music Video; manual pauses at `requires_action` so shots can be reviewed or edited before compose. + PremiumMusicVideoTaskCreateRequest: + allOf: + - oneOf: + - $ref: '#/components/schemas/PremiumMusicVideoSingTaskCreateRequest' + - $ref: '#/components/schemas/PremiumMusicVideoSingPerformTaskCreateRequest' + - $ref: '#/components/schemas/PremiumMusicVideoDanceTaskCreateRequest' + - $ref: '#/components/schemas/PremiumMusicVideoPerformTaskCreateRequest' + discriminator: + propertyName: mv_mode + mapping: + sing: '#/components/schemas/PremiumMusicVideoSingTaskCreateRequest' + sing_perform: '#/components/schemas/PremiumMusicVideoSingPerformTaskCreateRequest' + dance: '#/components/schemas/PremiumMusicVideoDanceTaskCreateRequest' + perform: '#/components/schemas/PremiumMusicVideoPerformTaskCreateRequest' + PremiumMusicVideoTaskRequestBase: + type: object + required: [mv_tier, mv_mode, audio_url] + not: + anyOf: + - required: [quality] + properties: { quality: {} } + - required: [language] + properties: { language: {} } + - required: [lip_sync] + properties: { lip_sync: {} } + - required: [lip_ref_url] + properties: { lip_ref_url: {} } + - required: [srt_url] + properties: { srt_url: {} } + - required: [compose_mode] + properties: { compose_mode: {} } + properties: + mv_tier: { type: string, enum: [premium], description: Selects the Premium Music Video workflow and its mode-specific inputs. } + mv_mode: { type: string, enum: [sing, sing_perform, dance, perform], description: Premium performance mode. Sing modes require `lip_ref_urls`; dance and perform require exactly six `images`. } + audio_url: + type: string + format: uri + description: Public HTTPS audio URL; Premium audio must be 10-300 seconds. + prompt: { type: string, maxLength: 3000, description: "Optional creative direction for story, setting, performance, camera, lighting, and pacing. Maximum 3000 characters." } + style: { type: string, maxLength: 200 } + aspect_ratio: { type: string, enum: ['1:1', '16:9', '9:16', '4:3', '3:4'], description: Target output placement. Set explicitly for the destination player or social feed. } + resolution: + type: string + enum: [720p] + default: 720p + description: Premium output is fixed to 720p. + add_subtitle: { type: boolean, default: false, description: Burn generated subtitles into the final video. } + subtitle_color: { type: string, pattern: '^#[0-9A-Fa-f]{6}$', example: '#FFFFFF', description: Subtitle text color as a six-digit hexadecimal value. Used when subtitles are enabled. } + duration: + type: integer + minimum: 10 + maximum: 300 + description: Premium billing fallback only; detected audio duration wins. + PremiumMusicVideoSingTaskCreateRequest: + allOf: + - $ref: '#/components/schemas/PremiumMusicVideoTaskRequestBase' + - type: object + required: [lip_ref_urls] + properties: + mv_mode: { type: string, enum: [sing] } + images: + type: array + minItems: 0 + maxItems: 6 + description: Optional Premium scene images for sing mode. Provide up to six public HTTPS PNG, JPEG, or WebP URLs. + items: { type: string, format: uri } + lip_ref_urls: + type: array + minItems: 1 + maxItems: 2 + description: Required for sing mode. One or two public HTTPS close-up, front-facing face images for lip synchronization. + items: { type: string, format: uri } + PremiumMusicVideoSingPerformTaskCreateRequest: + allOf: + - $ref: '#/components/schemas/PremiumMusicVideoTaskRequestBase' + - type: object + required: [lip_ref_urls] + properties: + mv_mode: { type: string, enum: [sing_perform] } + images: + type: array + minItems: 0 + maxItems: 6 + description: Optional Premium scene images for sing and perform mode. Provide up to six public HTTPS PNG, JPEG, or WebP URLs. + items: { type: string, format: uri } + lip_ref_urls: + type: array + minItems: 1 + maxItems: 2 + description: Required for sing and perform mode. One or two public HTTPS close-up, front-facing face images for lip synchronization. + items: { type: string, format: uri } + PremiumMusicVideoDanceTaskCreateRequest: + allOf: + - $ref: '#/components/schemas/PremiumMusicVideoTaskRequestBase' + - type: object + required: [images] + not: + required: [lip_ref_urls] + properties: { lip_ref_urls: {} } + properties: + mv_mode: { type: string, enum: [dance] } + images: + type: array + minItems: 6 + maxItems: 6 + description: Required for dance mode. Provide exactly six public HTTPS PNG, JPEG, or WebP scene images. + items: { type: string, format: uri } + PremiumMusicVideoPerformTaskCreateRequest: + allOf: + - $ref: '#/components/schemas/PremiumMusicVideoTaskRequestBase' + - type: object + required: [images] + not: + required: [lip_ref_urls] + properties: { lip_ref_urls: {} } + properties: + mv_mode: { type: string, enum: [perform] } + images: + type: array + minItems: 6 + maxItems: 6 + description: Required for perform mode. Provide exactly six public HTTPS PNG, JPEG, or WebP scene images. + items: { type: string, format: uri } + EditMusicVideoShotRequest: + type: object + additionalProperties: false + required: [prompt] + properties: + prompt: + type: string + maxLength: 3000 + images: + type: array + minItems: 0 + maxItems: 6 + description: Premium tasks only. Optional replacement scene images; an empty array is treated as omitted. Standard tasks reject this field. + items: { type: string, format: uri } RealtimeSession: type: object required: [id, object, status, expires_at, max_duration_seconds, allowed_origins, credits, request_id, created_at, connected_at, closed_at] @@ -477,41 +1611,66 @@ components: id: type: string pattern: '^rts_' + description: Stable Realtime Session ID used to inspect or close the session. object: type: string enum: [realtime.session] + description: Object discriminator; always `realtime.session`. status: type: string enum: [ready, connecting, active, closed, failed, expired] description: Active means BeatAPI accepted the first billing heartbeat after remote output began. - client_secret: - type: string - description: Returned only by POST. Give this short-lived BeatAPI secret to the browser SDK; never give the browser an sk_ API key. - pattern: '^brt_live_' expires_at: type: string format: date-time + description: Time when the unconnected short-lived session credential expires. max_duration_seconds: type: integer enum: [15, 60, 300] + description: Maximum selected live duration and billing tier in seconds. allowed_origins: type: array - items: { type: string, format: uri } + description: Exact browser origins authorized to use this Session. + items: + type: string + format: uri + pattern: '^(https://[A-Za-z0-9.-]+(?::[0-9]+)?|http://(?:localhost|127\\.0\\.0\\.1|\\[::1\\])(?::[0-9]+)?)$' + description: Exact browser origin. Use HTTPS in production; HTTP is accepted only for localhost development. + example: https://app.example.com credits: type: object required: [reserved, settled, refunded] + description: USD reservation, settlement, and refund lifecycle for this Realtime Session. Compatibility field names are retained. properties: - reserved: { type: integer } - settled: { type: integer } - refunded: { type: integer } + reserved: { type: number, format: double, multipleOf: 0.01, description: USD amount reserved when the Session is created. } + settled: { type: number, format: double, multipleOf: 0.01, description: USD amount settled after the first accepted billing heartbeat. } + refunded: { type: number, format: double, multipleOf: 0.01, description: USD amount refunded if the Session ends without billing activation. } request_id: type: string - created_at: { type: string, format: date-time } + description: Correlation ID to retain for logs and BeatAPI support. + created_at: { type: string, format: date-time, description: Time when the Session was created. } connected_at: type: [string, 'null'] format: date-time description: Time of the first accepted BeatAPI billing heartbeat; null before billing activation. - closed_at: { type: [string, 'null'], format: date-time } + closed_at: { type: [string, 'null'], format: date-time, description: "Time when the Session closed, or null while it remains open." } + RealtimeSessionCreated: + allOf: + - $ref: '#/components/schemas/RealtimeSession' + - type: object + required: [client_secret] + properties: + client_secret: + type: string + description: Short-lived BeatAPI browser credential returned only by POST. Never expose an sk_ API key to the browser. + pattern: '^brt_live_' + RealtimeSessionCreateResponse: + type: object + required: [data] + properties: + data: + $ref: '#/components/schemas/RealtimeSessionCreated' + description: Created Realtime Session including the one-time short-lived browser credential. RealtimeSessionResponse: type: object required: [data] @@ -524,6 +1683,7 @@ components: properties: data: $ref: '#/components/schemas/File' + description: Uploaded file metadata and the public HTTPS URL to use in later requests. WebhookEndpointList: type: object required: [object, data] @@ -547,6 +1707,7 @@ components: properties: data: $ref: '#/components/schemas/WebhookEndpoint' + description: Created or retrieved webhook endpoint. Public API responses return the full signing secret at creation and mask it afterward; authenticated dashboard owners can explicitly reveal it again. DeleteResponse: type: object required: [data] @@ -565,10 +1726,12 @@ components: properties: error: type: object + description: Structured BeatAPI error. Use `code` for program logic and retain `request_id` for support. required: [code, message, request_id] properties: code: type: string + description: Stable machine-readable error code. enum: - bad_request - unauthorized @@ -578,6 +1741,7 @@ components: - idempotency_conflict - user_concurrency_exceeded - rate_limit_exceeded + - content_policy_violation - processing_unavailable - processing_failed - processing_timeout @@ -592,8 +1756,10 @@ components: - internal_error message: type: string + description: Human-readable detail intended for logs and debugging. request_id: type: string + description: Correlation ID to retain for BeatAPI support. retry_after_seconds: type: integer description: Present on retryable rate-limit or capacity responses when the client should wait before retrying. @@ -637,7 +1803,214 @@ components: message: Too many polling requests. Poll every 5-10 seconds. request_id: req_xxx retry_after_seconds: 12 + InternalError: + description: BeatAPI could not complete the request because of an internal or storage failure. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: + code: internal_error + message: Internal error. Contact support with the request_id if the problem continues. + request_id: req_xxx + ProcessingUnavailable: + description: BeatAPI processing is temporarily unavailable or did not complete within the processing window. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: + code: processing_unavailable + message: Task processing is temporarily unavailable. + request_id: req_xxx paths: + /v1/models: + get: + operationId: listTextModels + tags: [Text] + summary: List available text models + description: Returns the text models currently enabled for this BeatAPI environment in OpenAI list format. + security: + - BearerAuth: [] + - ApiKeyHeader: [] + - GoogleApiKeyHeader: [] + responses: + '200': + description: OpenAI-compatible model list + content: + application/json: + schema: { $ref: '#/components/schemas/TextModelList' } + example: + object: list + data: + - id: gpt-5.6-sol + object: model + created: 1788220800 + owned_by: beatapi + - id: gpt-5.6-terra + object: model + created: 1788220800 + owned_by: beatapi + - id: gpt-5.6-luna + object: model + created: 1788220800 + owned_by: beatapi + - id: claude-fable-5-1 + object: model + created: 1788220800 + owned_by: beatapi + '401': { description: Invalid or missing BeatAPI API key } + '404': { description: Text API is not enabled for this environment } + '429': { description: Request rate limit exceeded } + + /v1/responses: + post: + operationId: createTextResponse + tags: [Text] + summary: Create a text response + description: Recommended OpenAI-compatible surface for reasoning, tools, structured outputs, and streaming. + security: + - BearerAuth: [] + - ApiKeyHeader: [] + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/TextPassthroughRequest' } + example: + model: gpt-5.6-sol + input: Design a resilient webhook retry strategy for a payments API. + reasoning: { effort: medium } + stream: true + responses: + '200': + description: OpenAI-compatible JSON response or server-sent event stream + content: + application/json: + schema: { $ref: '#/components/schemas/TextPassthroughResponse' } + text/event-stream: + schema: { type: string } + '401': { description: Invalid or missing BeatAPI API key } + '402': { description: Insufficient BeatAPI USD balance } + '429': { description: Rate limit or settlement backlog } + '502': { description: Text gateway could not complete the request } + '503': { description: Text service is temporarily unavailable } + + /v1/chat/completions: + post: + operationId: createChatCompletion + tags: [Text] + summary: Create a text chat completion + description: OpenAI Chat Completions-compatible endpoint for existing SDK integrations. + security: + - BearerAuth: [] + - ApiKeyHeader: [] + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/TextPassthroughRequest' } + example: + model: gpt-5.6-terra + messages: + - role: user + content: Summarize the attached product requirements into an implementation plan. + stream: true + responses: + '200': + description: OpenAI-compatible JSON response or server-sent event stream + content: + application/json: + schema: { $ref: '#/components/schemas/TextPassthroughResponse' } + text/event-stream: + schema: { type: string } + '401': { description: Invalid or missing BeatAPI API key } + '402': { description: Insufficient BeatAPI USD balance } + '429': { description: Rate limit or settlement backlog } + '502': { description: Text gateway could not complete the request } + '503': { description: Text service is temporarily unavailable } + + /v1/messages: + post: + operationId: createMessage + tags: [Text] + summary: Create an Anthropic-compatible message + description: Anthropic Messages-compatible endpoint. Send the BeatAPI key with x-api-key or Bearer authentication. + security: + - ApiKeyHeader: [] + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/TextPassthroughRequest' } + example: + model: gpt-5.6-luna + max_tokens: 1024 + messages: + - role: user + content: Classify this support request and return JSON. + responses: + '200': + description: Anthropic-compatible JSON response or server-sent event stream + content: + application/json: + schema: { $ref: '#/components/schemas/TextPassthroughResponse' } + text/event-stream: + schema: { type: string } + '401': { description: Invalid or missing BeatAPI API key } + '402': { description: Insufficient BeatAPI USD balance } + '429': { description: Rate limit or settlement backlog } + '502': { description: Text gateway could not complete the request } + '503': { description: Text service is temporarily unavailable } + + /v1beta/models/{model}:{action}: + post: + operationId: generateGeminiCompatibleContent + tags: [Text] + summary: Generate text content with a Gemini-compatible request + description: Gemini-compatible endpoint for generateContent and streamGenerateContent. The BeatAPI key is removed before forwarding. + security: + - GoogleApiKeyHeader: [] + - GoogleApiKeyQuery: [] + - BearerAuth: [] + parameters: + - in: path + name: model + required: true + schema: { $ref: '#/components/schemas/TextModelId' } + - in: path + name: action + required: true + schema: { type: string, enum: [generateContent, streamGenerateContent] } + requestBody: + required: true + content: + application/json: + schema: + type: object + additionalProperties: true + example: + contents: + - role: user + parts: + - text: Explain this architecture decision in three concise bullets. + responses: + '200': + description: Gemini-compatible JSON response or server-sent event stream + content: + application/json: + schema: { $ref: '#/components/schemas/TextPassthroughResponse' } + text/event-stream: + schema: { type: string } + '401': { description: Invalid or missing BeatAPI API key } + '402': { description: Insufficient BeatAPI USD balance } + '429': { description: Rate limit or settlement backlog } + '502': { description: Text gateway could not complete the request } + '503': { description: Text service is temporarily unavailable } + /v1/workflows: get: operationId: listWorkflows @@ -646,53 +2019,596 @@ paths: summary: List launch workflows security: [] responses: - '200': - description: Workflow list + '200': + description: Workflow list + content: + application/json: + schema: + $ref: '#/components/schemas/WorkflowListResponse' + example: + data: + object: list + data: + - id: music-video + object: workflow + name: Music Video API + description: Generate short music video clips from audio, lyrics, and visual direction. + - id: ecommerce-video + object: workflow + name: Ecommerce Video API + description: Generate product ad videos from product images and a short creative brief. + - id: video-analysis + object: workflow + name: Video Analysis API + description: Analyze an uploaded video with timestamp-aware multimodal reasoning. + '429': + $ref: '#/components/responses/RateLimited' + + /v1/media/models: + get: + operationId: listGenerationModels + tags: [Generation] + summary: List BeatAPI image and video generation models + description: Returns stable BeatAPI model aliases and public input modes. Internal execution routing is not part of this contract. + security: [] + parameters: + - in: query + name: media_type + schema: { type: string, enum: [image, video] } + responses: + '200': + description: Generation model list + content: + application/json: + schema: { $ref: '#/components/schemas/GenerationModelListResponse' } + '400': { $ref: '#/components/responses/BadRequest' } + '429': { $ref: '#/components/responses/RateLimited' } + + /v1/images/tasks: + post: + operationId: createImageGenerationTask + tags: [Generation] + summary: Create an image generation task + description: | + Creates one asynchronous image task. Select the model-specific request + contract with `model`, save the returned `data.id`, and poll + `GET /v1/tasks/{task_id}` until the task succeeds or fails. + security: [{ BearerAuth: [] }] + parameters: + - in: header + name: Idempotency-Key + required: false + schema: { type: string, maxLength: 255 } + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/ImageGenerationTaskCreateRequest' } + examples: + Nano Banana: + summary: Nano Banana + value: + model: nano-banana + prompt: Editorial product photograph on a warm stone pedestal. + aspect_ratio: '1:1' + output_format: png + Nano Banana 2: + summary: Nano Banana 2 at 2K + value: + model: nano-banana-2 + prompt: Editorial campaign image with crisp product typography. + aspect_ratio: '4:5' + resolution: 2K + output_format: png + Nano Banana 2 Lite: + summary: Nano Banana 2 Lite at 1K + value: + model: nano-banana-2-lite + prompt: Fast social product visual on a clean studio background. + aspect_ratio: '1:1' + output_format: jpeg + Nano Banana Pro: + summary: Nano Banana Pro + value: + model: nano-banana-pro + prompt: Place the supplied product in a premium editorial studio scene. + images: ['https://media.beatapi.io/samples/smart-bottle.png'] + aspect_ratio: '4:5' + resolution: 2K + output_format: png + GPT Image 2: + summary: GPT Image 2 + value: + model: gpt-image-2 + prompt: Create a clean campaign image from the supplied product reference. + images: ['https://media.beatapi.io/samples/smart-bottle.png'] + aspect_ratio: '1:1' + resolution: 2K + Seedream 5 Pro: + summary: Seedream 5 Pro + value: + model: seedream-5-pro + prompt: Recompose the product as a cinematic storefront campaign image. + images: ['https://media.beatapi.io/samples/smart-bottle.png'] + Grok Imagine Image 2.0: + summary: Grok Imagine Image 2.0 + value: + model: grok-imagine-image-2.0 + prompt: Turn the supplied product into a bold launch campaign visual. + images: ['https://media.beatapi.io/samples/smart-bottle.png'] + aspect_ratio: '16:9' + responses: + '201': + description: Image generation task accepted + content: + application/json: + schema: { $ref: '#/components/schemas/TaskResponse' } + examples: + Nano Banana Pro: + summary: Nano Banana Pro + value: + data: + id: task_8K2qA + object: task + task_kind: image + capability_id: nano-banana-pro + capability_version: null + media_type: image + model: nano-banana-pro + status: queued + stage: queued + created_at: 1782210000 + updated_at: 1782210000 + completed_at: null + output: null + usage: + credits_reserved: 0.09 + credits_charged: 0.09 + credits_settled: 0 + credits_refunded: 0 + request_id: req_abc123 + error_code: null + error_message: null + '400': { $ref: '#/components/responses/BadRequest' } + '401': { $ref: '#/components/responses/Unauthorized' } + '402': { description: Insufficient USD balance, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } + '409': { description: Idempotency key conflicts with another request body, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } + '429': { $ref: '#/components/responses/RateLimited' } + + /v1/videos/tasks: + post: + operationId: createVideoGenerationTask + tags: [Generation] + summary: Create a video generation task + description: | + Creates one asynchronous video task. Select the model-specific request + contract with `model`, save the returned `data.id`, and poll + `GET /v1/tasks/{task_id}` until the task succeeds or fails. + security: [{ BearerAuth: [] }] + parameters: + - in: header + name: Idempotency-Key + required: false + schema: { type: string, maxLength: 255 } + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/VideoGenerationTaskCreateRequest' } + examples: + MiniMax H3: + summary: MiniMax H3 + value: + model: minimax-h3 + prompt: A slow cinematic push through a misty mountain village at dawn. + duration: 5 + aspect_ratio: '16:9' + resolution: 768P + Seedance 2: + summary: Seedance 2 + value: + model: seedance-2 + prompt: A handheld tracking shot through a crowded neon night market. + duration: 8 + aspect_ratio: '16:9' + resolution: 1080p + generate_audio: true + Seedance 2 Fast: + summary: Seedance 2 Fast + value: + model: seedance-2-fast + prompt: A fast cinematic production draft through a neon night market. + duration: 5 + aspect_ratio: '16:9' + resolution: 720p + generate_audio: true + Seedance 2 Mini: + summary: Seedance 2 Mini + value: + model: seedance-2-mini + prompt: A low-cost storyboard draft for a product reveal. + duration: 5 + aspect_ratio: '16:9' + resolution: 720p + Veo 3.1 Quality: + summary: Veo 3.1 Quality + value: + model: veo-3.1 + prompt: A cinematic aerial reveal of a quiet coastal village at sunrise. + aspect_ratio: '16:9' + resolution: 720p + quality: Quality + Veo 3.1 Fast: + summary: Veo 3.1 Fast + value: + model: veo-3.1 + prompt: A fast cinematic product reveal with natural camera motion. + aspect_ratio: '16:9' + resolution: 1080p + quality: Fast + Veo 3.1 Lite: + summary: Veo 3.1 Lite + value: + model: veo-3.1 + prompt: A concise storyboard-ready product reveal. + aspect_ratio: '16:9' + resolution: 4k + quality: Lite + Veo 3.1 Reference Fast: + summary: Veo 3.1 Reference Fast + value: + model: veo-3.1 + prompt: Create a cohesive cinematic scene using the supplied visual references. + reference_images: + - https://media.beatapi.io/samples/neon-singer.png + - https://media.beatapi.io/samples/smart-bottle.png + aspect_ratio: '16:9' + resolution: 720p + quality: Fast + Seedance 2.5: + summary: Seedance 2.5 + value: + model: seedance-2.5 + prompt: A cinematic tracking shot through a rain-lit night market. + duration: 5 + aspect_ratio: '16:9' + resolution: 720p + generate_audio: true + seed: -1 + Kling 3: + summary: Kling 3 + value: + model: kling-3 + prompt: A dramatic product reveal with a slow orbiting camera move. + duration: 5 + aspect_ratio: '16:9' + resolution: pro + sound: true + Kling 2.6 Motion Control: + summary: Kling 2.6 Motion Control + value: + model: kling-2.6-motion-control + prompt: Keep the character identity stable while following the reference motion. + images: + - https://media.beatapi.io/inputs/character.png + reference_videos: + - https://media.beatapi.io/inputs/motion.mp4 + resolution: 720p + character_orientation: video + Kling 3.0 Motion Control: + summary: Kling 3.0 Motion Control + value: + model: kling-3-motion-control + prompt: Preserve the character and transfer the full-body motion precisely. + images: + - https://media.beatapi.io/inputs/character.png + reference_videos: + - https://media.beatapi.io/inputs/motion.mp4 + resolution: 1080p + character_orientation: image + background_source: input_video + responses: + '201': + description: Video generation task accepted + content: + application/json: + schema: { $ref: '#/components/schemas/TaskResponse' } + examples: + MiniMax H3: + summary: MiniMax H3 + value: + data: + id: task_8K2qA + object: task + task_kind: video + capability_id: minimax-h3 + capability_version: null + media_type: video + model: minimax-h3 + status: queued + stage: queued + created_at: 1782210000 + updated_at: 1782210000 + completed_at: null + output: null + usage: + credits_reserved: 0.2 + credits_charged: 0.2 + billable_duration_seconds: 5 + credits_settled: 0 + credits_refunded: 0 + request_id: req_abc123 + error_code: null + error_message: null + '400': { $ref: '#/components/responses/BadRequest' } + '401': { $ref: '#/components/responses/Unauthorized' } + '402': { description: Insufficient USD balance, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } + '409': { description: Idempotency key conflicts with another request body, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } + '429': { $ref: '#/components/responses/RateLimited' } + + /v1/effects: + get: + operationId: listEffects + tags: [Effects] + summary: List active Effects + security: [] + description: Returns only versioned Effects that have passed BeatAPI publication gates. Internal integration names, template ids, costs, and execution context are never exposed. + parameters: + - in: query + name: output_type + schema: { type: string, enum: [image, video] } + - in: query + name: category + schema: { type: string } + responses: + '200': + description: Active Effect catalog + content: + application/json: + schema: { $ref: '#/components/schemas/EffectListResponse' } + '400': { $ref: '#/components/responses/BadRequest' } + '429': { $ref: '#/components/responses/RateLimited' } + + /v1/effects/{effect_id}: + get: + operationId: getEffect + tags: [Effects] + summary: Get an active Effect + security: [] + parameters: + - in: path + name: effect_id + required: true + schema: { type: string } + responses: + '200': + description: Effect definition and immutable current version contract + content: + application/json: + schema: { $ref: '#/components/schemas/EffectResponse' } + '404': + description: Effect is unknown or not currently published. + content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } + + /v1/effects/tasks: + post: + operationId: createEffectTask + tags: [Effects] + summary: Create an Effect task + security: [{ BearerAuth: [] }] + description: | + Creates an asynchronous image or video Effect task. Read the catalog + first: image count, accepted input types, output resolution/duration, + and execution contract are fixed by the selected Effect version. Send + an `Idempotency-Key`; an exact replay returns the + accepted task before remote input URLs are revalidated, while a changed + body returns `idempotency_conflict`. + + The USD amount is reserved atomically when accepted, settled on success, and + fully refunded after a definite processing failure. An uncertain create + result is not blindly retried and never switches integrations automatically. + parameters: + - in: header + name: Idempotency-Key + required: false + schema: { type: string, maxLength: 255 } + requestBody: + required: true + content: + application/json: + schema: + type: object + additionalProperties: false + required: [effect_id, images] + properties: + effect_id: { type: string, example: video-muscle-max, description: Stable published Effect ID from `GET /v1/effects`. } + effect_version: + type: integer + minimum: 1 + description: Optional immutable version. Omit to use the current published version. + images: + type: array + minItems: 1 + maxItems: 7 + description: Public HTTPS input images in the order required by the selected Effect version. Read `GET /v1/effects/{effect_id}` for the exact count and accepted media rules; upload local files with `POST /v1/files`. + items: { type: string, format: uri } + options: + type: object + additionalProperties: false + description: Optional controls supported by the selected Effect version. Omit unsupported controls; the catalog is the source of truth. + properties: + aspect_ratio: { type: string, description: Requested output aspect ratio when the selected Effect exposes this option. } + resolution: { type: string, description: Requested output resolution when the selected Effect exposes this option. } + duration: { type: integer, description: Requested video duration in seconds when the selected Effect exposes this option. } + bgm: { type: boolean, description: Include background music when supported by the selected Effect. } + seed: { type: integer, description: Optional deterministic seed when supported by the selected Effect. } + example: + effect_id: video-muscle-max + images: ['https://media.beatapi.io/samples/portrait.png'] + options: { resolution: 720p, duration: 12 } + responses: + '201': + description: Effect task accepted + content: + application/json: + schema: { $ref: '#/components/schemas/TaskResponse' } + example: + data: + id: task_effect123 + object: task + task_kind: effect + capability_id: video-muscle-max + capability_version: 1 + effect_id: video-muscle-max + effect_version: 1 + status: queued + stage: queued + created_at: 1782210000 + updated_at: 1782210000 + completed_at: null + output: null + usage: { credits_reserved: 1.2, credits_charged: 1.2, credits_settled: 0, credits_refunded: 0 } + request_id: req_effect123 + error_code: null + error_message: null + '400': { $ref: '#/components/responses/BadRequest' } + '401': { $ref: '#/components/responses/Unauthorized' } + '402': + description: Insufficient USD balance. + content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } + '404': + description: Effect or requested version is unavailable. + content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } + '409': + description: Idempotency key conflicts with another request body. + content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } + '429': { $ref: '#/components/responses/RateLimited' } + + /v1/video-analysis/tasks: + post: + operationId: createVideoAnalysisTask + tags: [Video Analysis] + x-apidog-folder: Video Analysis API + summary: Analyze an uploaded video + security: + - BearerAuth: [] + parameters: + - in: header + name: Idempotency-Key + required: false + schema: { type: string } + description: Optional retry key. Reusing the same key with the same normalized request returns the accepted task. + example: video-analysis-cus_123-01 + description: | + Analyze one MP4 or MOV previously uploaded with `POST /v1/files` by the + current BeatAPI account. `standard` is the default low-cost route; + `deep` uses the higher-reasoning route. BeatAPI reserves an estimate from + verified video duration and the output budget, then settles from actual + input and output token usage. Standard costs $0.36 per 1M input tokens and + $1.60 per 1M output tokens; Deep costs $0.72 per 1M input tokens and $5.00 + per 1M output tokens. Each completed task is rounded up to the nearest + $0.01 because the shared USD balance settles in cents. Save `data.id` and poll the shared Task + endpoint if the task remains queued for processing capacity. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/VideoAnalysisTaskCreateRequest' + examples: + standard: + summary: Timestamped standard analysis + value: + video_url: https://media.beatapi.io/uploads/input.mp4 + prompt: Return a timestamped action summary and identify every scene transition. + analysis_depth: standard + max_output_tokens: 2048 + deep: + summary: Deep motion review + value: + video_url: https://media.beatapi.io/uploads/input.mp4 + prompt: Inspect body motion, contact, continuity, and fast transitions with timestamps. + analysis_depth: deep + max_output_tokens: 4096 + responses: + '201': + description: Analysis accepted for asynchronous processing. content: application/json: schema: - $ref: '#/components/schemas/WorkflowListResponse' + $ref: '#/components/schemas/TaskResponse' example: data: - object: list - data: - - id: music-video - object: workflow - name: Music Video API - description: Generate short music video clips from audio, lyrics, and visual direction. - - id: ecommerce-video - object: workflow - name: Ecommerce Video API - description: Generate product ad videos from product images and a short creative brief. - '429': - $ref: '#/components/responses/RateLimited' + id: task_va8K2qA + object: task + task_kind: workflow + capability_id: video-analysis + capability_version: 1 + workflow: video-analysis + status: queued + stage: queued + created_at: 1787385600 + updated_at: 1787385600 + completed_at: null + output: null + usage: + credits_reserved: 0.01 + credits_charged: 0.01 + billable_duration_seconds: 60 + credits_settled: 0 + credits_refunded: 0 + request_id: req_va123 + error_code: null + error_message: null + '400': { $ref: '#/components/responses/BadRequest' } + '401': { $ref: '#/components/responses/Unauthorized' } + '402': + description: Account balance is not sufficient for the reserved analysis envelope. + content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } + '409': + description: Idempotency key conflicts with another request body. + content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } + '429': { $ref: '#/components/responses/RateLimited' } /v1/music-video/tasks: post: operationId: createMusicVideoTask tags: [Music Video] x-apidog-folder: Music Video API/Create Video - summary: Create Music Video + summary: Create a Music Video workflow task security: - BearerAuth: [] + parameters: + - in: header + name: Idempotency-Key + required: false + schema: { type: string } + description: Optional retry key. Reusing the same key with the same request body returns the accepted task; reusing it with a different body returns `409 idempotency_conflict`. + example: mv-create-cus_123-01 description: | - Music Video requires public HTTPS image URLs and a public HTTPS audio URL. - Prompt, language, quality, style, lip reference, subtitle, and format - controls are optional. BeatAPI detects the audio duration before task - creation and charges the detected billable duration at the selected - per-second customer-credit rate. If audio duration cannot be detected, - `duration` is used as the billing fallback. + Music Video requires public HTTPS media URLs. Requests that omit + `mv_tier` use `standard` and preserve the existing API behavior. + Premium retains the configured integration host but uses a distinct + execution path selected internally by BeatAPI. + Shared controls include prompt, aspect ratio, subtitles, and the tier's + billing fallback. Language, quality, `lip_sync`, `lip_ref_url`, + `srt_url`, and `compose_mode` are Standard-only. Premium uses `mv_mode` + plus `style` and mode-specific images or `lip_ref_urls`. BeatAPI detects audio duration before task + creation and records the billable duration in Task usage. If audio + duration cannot be detected, `duration` is used as the billing fallback. Input limits: - - Images must contain 1-7 public HTTPS URLs. + - Standard images must contain 1-7 public HTTPS URLs. + - Premium `sing` and `sing_perform` accept 0-6 scene images and require 1-2 `lip_ref_urls`. + - Premium `dance` and `perform` require exactly 6 scene images. - Use png, jpg, jpeg, or webp images; each image should be 50 MB or smaller. - Image aspect ratio should be between 1:4 and 4:1. - - Audio must be a public HTTPS mp3, wav, aac, or m4a URL between 10 and 180 seconds. + - Standard audio must be 10-180 seconds; Premium audio must be 10-300 seconds and contain vocals or lyrics rather than instrumental-only audio. - The audio file should be 50 MB or smaller. - `prompt` is optional and must be at most 3000 characters. - - `lip_ref_url`, when provided, must be a public HTTPS image URL. Use a clear, front-facing close-up face reference for best lip-sync results. - - `srt_url`, when provided, must point to an `.srt` subtitle file. - - `duration` is only a billing fallback when BeatAPI cannot detect the audio length; it must be 10-180 seconds and cannot override a detected audio duration. + - Standard `lip_sync=true` requires `lip_ref_url`. It must be a public HTTPS image URL showing a clear, front-facing close-up face. + - Standard `srt_url`, when provided, must point to an `.srt` subtitle file. + - `duration` is only a billing fallback when BeatAPI cannot detect the audio length; Standard accepts 10-180 seconds and Premium accepts 10-300 seconds. It cannot override a detected audio duration. BeatAPI validates URL shape, text limits, enum values, combination limits, and audio duration at task creation. Files uploaded through @@ -700,15 +2616,6 @@ paths: Third-party media URLs must follow the same media requirements and may be rejected during processing if invalid. - Customer pricing: - - MV 540p standard: 4 credits/s - - MV 720p standard: 5 credits/s - - MV 1080p standard: 6 credits/s - - lip_sync add-on: +2 credits/s - - MV 720p high: 16 credits/s - - MV 1080p high: 18 credits/s - - Ecommerce Video 1080p: 15 credits/s - Combination limits: - `quality=high` is not supported with `resolution=540p`. - `lip_sync=true` is not supported with `resolution=540p`. @@ -724,76 +2631,23 @@ paths: content: application/json: schema: - type: object - required: [images, audio_url] - properties: - images: - type: array - minItems: 1 - maxItems: 7 - description: 1-7 public HTTPS image URLs. Use png, jpg, jpeg, or webp images; each image should be 50 MB or smaller, with aspect ratio from 1:4 to 4:1. /v1/files uploads are checked before use; third-party URLs may be rejected during processing if invalid. - items: - type: string - format: uri - audio_url: - type: string - format: uri - description: Public HTTPS audio URL. Use mp3, wav, aac, or m4a; file size should be 50 MB or smaller and duration must be 10-180 seconds. - prompt: - type: string - maxLength: 3000 - description: Optional creative prompt, at most 3000 characters. - language: - type: string - enum: [en, zh] - lip_sync: - type: boolean - lip_ref_url: - type: string - format: uri - description: Public HTTPS image URL for lip-sync face reference. Use a clear, front-facing close-up face reference. - style: - type: string - maxLength: 200 - description: Optional style phrase, at most 200 characters. - quality: - type: string - enum: [standard, high] - default: standard - aspect_ratio: - type: string - enum: ['1:1', '16:9', '9:16', '4:3', '3:4'] - resolution: - type: string - enum: [540p, 720p, 1080p] - default: 720p - add_subtitle: - type: boolean - subtitle_color: - type: string - pattern: '^#[0-9A-Fa-f]{6}$' - example: '#FFFFFF' - srt_url: - type: string - format: uri - duration: - type: integer - minimum: 10 - maximum: 180 - description: Billing fallback when audio duration cannot be detected. It must be 10-180 seconds and cannot override a detected audio duration. - compose_mode: - type: string - enum: [auto, manual] - default: auto - example: - images: - - https://media.beatapi.io/samples/neon-singer.png - audio_url: https://media.beatapi.io/samples/neon-singer-preview.mp3 - prompt: Neon rooftop performance with metro cutaways and cinematic light trails. - language: en - quality: standard - resolution: 720p - compose_mode: auto + $ref: '#/components/schemas/MusicVideoTaskCreateRequest' + examples: + standard_backwards_compatible: + summary: Standard music video + value: + mv_tier: standard + images: ['https://media.beatapi.io/samples/neon-singer.png'] + audio_url: https://media.beatapi.io/samples/neon-singer-preview.mp3 + resolution: 720p + premium_sing: + value: { mv_tier: premium, mv_mode: sing, images: [], lip_ref_urls: ['https://media.beatapi.io/samples/singer.png'], audio_url: 'https://media.beatapi.io/samples/song.mp3', resolution: 720p } + premium_sing_perform: + value: { mv_tier: premium, mv_mode: sing_perform, images: ['https://media.beatapi.io/samples/stage.png'], lip_ref_urls: ['https://media.beatapi.io/samples/singer.png'], audio_url: 'https://media.beatapi.io/samples/song.mp3', resolution: 720p } + premium_dance: + value: { mv_tier: premium, mv_mode: dance, images: ['https://media.beatapi.io/1.png', 'https://media.beatapi.io/2.png', 'https://media.beatapi.io/3.png', 'https://media.beatapi.io/4.png', 'https://media.beatapi.io/5.png', 'https://media.beatapi.io/6.png'], audio_url: 'https://media.beatapi.io/samples/song.mp3', resolution: 720p } + premium_perform: + value: { mv_tier: premium, mv_mode: perform, images: ['https://media.beatapi.io/1.png', 'https://media.beatapi.io/2.png', 'https://media.beatapi.io/3.png', 'https://media.beatapi.io/4.png', 'https://media.beatapi.io/5.png', 'https://media.beatapi.io/6.png'], audio_url: 'https://media.beatapi.io/samples/song.mp3', resolution: 720p } responses: '201': description: Task accepted @@ -801,28 +2655,34 @@ paths: application/json: schema: $ref: '#/components/schemas/TaskResponse' - example: - data: - id: task_8K2qA - object: task - workflow: music-video - status: queued - stage: queued - storyboard: - shots: [] - created_at: 1782210000 - updated_at: 1782210000 - completed_at: null - output: null - usage: - credits_reserved: 75 - credits_charged: 75 - billable_duration_seconds: 15 - credits_settled: 0 - credits_refunded: 0 - request_id: req_abc123 - error_code: null - error_message: null + examples: + standard_backwards_compatible: + summary: Standard music video + value: + data: + id: task_8K2qA + object: task + task_kind: workflow + capability_id: music-video + capability_version: 1 + workflow: music-video + status: queued + stage: queued + storyboard: + shots: [] + created_at: 1782210000 + updated_at: 1782210000 + completed_at: null + output: null + usage: + credits_reserved: 1.5 + credits_charged: 1.5 + billable_duration_seconds: 15 + credits_settled: 0 + credits_refunded: 0 + request_id: req_abc123 + error_code: null + error_message: null '400': $ref: '#/components/responses/BadRequest' '401': @@ -838,6 +2698,17 @@ paths: code: insufficient_credits message: Account balance is not sufficient for this task. request_id: req_xxx + '409': + description: The Idempotency-Key was reused with a different body or while another request with that key is still being processed. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: + code: idempotency_conflict + message: This Idempotency-Key was already used with a different request body. + request_id: req_xxx '429': description: User concurrency exceeded. content: @@ -855,13 +2726,16 @@ paths: operationId: editMusicVideoShot tags: [Music Video] x-apidog-folder: Music Video API/Advanced Editing - summary: Edit Shot + summary: Edit a Music Video storyboard shot security: - BearerAuth: [] description: | Edit one storyboard shot using its BeatAPI `shot_id`. This operation - charges BeatAPI customer credits using the selected quality/resolution - rate and the shot duration. Default shot duration is 5 seconds. + charges the customer USD balance using the applicable task tier and shot + duration. Standard edits accept only `prompt`. Premium edits accept + `prompt` plus up to 6 optional replacement `images`. Generation quality, + resolution, and shot duration are inherited from the original task and + are not editable request fields. When the edit finishes, BeatAPI stores the edited shot media and exposes it on that shot. The existing final Music Video is not replaced until you call compose with the selected shot ids. @@ -878,35 +2752,21 @@ paths: schema: type: string example: shot_xxx + - in: header + name: Idempotency-Key + required: false + schema: { type: string } + description: Optional retry key. Reusing the same key for this task, shot, and request body returns the accepted task without charging the USD amount again; changing any of them returns `409 idempotency_conflict`. + example: music-edit-task_8K2qA-shot_xxx-01 requestBody: required: true content: application/json: schema: - type: object - required: [prompt] - properties: - prompt: - type: string - maxLength: 3000 - duration: - type: integer - minimum: 1 - maximum: 180 - default: 5 - quality: - type: string - enum: [standard, high] - default: standard - resolution: - type: string - enum: [540p, 720p, 1080p] - default: 720p + $ref: '#/components/schemas/EditMusicVideoShotRequest' example: prompt: Night city chorus with brighter face lighting. - duration: 5 - quality: standard - resolution: 720p + images: ['https://media.beatapi.io/samples/stage.png'] responses: '202': description: Shot edit accepted @@ -918,19 +2778,37 @@ paths: $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' + '402': + description: Account balance is not sufficient for this shot edit. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '409': + description: The Idempotency-Key was reused for a different task, shot, or request body, or the same request is still being processed. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' '404': description: Task or shot not found. content: application/json: schema: $ref: '#/components/schemas/Error' + '429': + $ref: '#/components/responses/RateLimited' + '500': + $ref: '#/components/responses/InternalError' + '502': + $ref: '#/components/responses/ProcessingUnavailable' /v1/music-video/tasks/{task_id}/shots/{shot_id}/media: post: operationId: getMusicVideoShotMedia tags: [Music Video] x-apidog-folder: Music Video API/Advanced Editing - summary: Get Shot Media + summary: Retrieve a Music Video storyboard shot media URL security: - BearerAuth: [] description: | @@ -989,18 +2867,24 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + '429': + $ref: '#/components/responses/RateLimited' + '500': + $ref: '#/components/responses/InternalError' + '502': + $ref: '#/components/responses/ProcessingUnavailable' /v1/music-video/tasks/{task_id}/compose: post: operationId: composeMusicVideoTask tags: [Music Video] x-apidog-folder: Music Video API/Advanced Editing - summary: Compose Video + summary: Compose a Music Video task from selected shots security: - BearerAuth: [] description: | Compose selected BeatAPI storyboard shots into the final Music Video. - This operation charges a fixed 1 BeatAPI customer credit. + This operation charges a fixed $1 USD. parameters: - in: path name: task_id @@ -1008,6 +2892,12 @@ paths: schema: type: string example: task_8K2qA + - in: header + name: Idempotency-Key + required: false + schema: { type: string } + description: Optional retry key. Reusing the same key for this task and request body returns the accepted task without charging the $1 compose amount again; changing either returns `409 idempotency_conflict`. + example: music-compose-task_8K2qA-01 requestBody: required: true content: @@ -1034,22 +2924,51 @@ paths: $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' + '402': + description: Account balance is not sufficient for this compose operation. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '409': + description: The Idempotency-Key was reused for a different task or request body, or the same request is still being processed. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' '404': description: Task or shot not found. content: application/json: schema: $ref: '#/components/schemas/Error' + '429': + $ref: '#/components/responses/RateLimited' + '500': + $ref: '#/components/responses/InternalError' + '502': + $ref: '#/components/responses/ProcessingUnavailable' /v1/ecommerce-video/tasks: post: operationId: createEcommerceVideoTask tags: [Ecommerce Video] x-apidog-folder: Ecommerce Video API - summary: Create Ecommerce Video + summary: Create an Ecommerce Video workflow task security: - BearerAuth: [] - description: Ecommerce Video requires product images and an explicit output duration. + parameters: + - in: header + name: Idempotency-Key + required: false + schema: { type: string } + description: Optional retry key. Reusing the same key with the same request body returns the accepted task; reusing it with a different body returns `409 idempotency_conflict`. + example: ecommerce-create-cus_123-01 + description: | + Ecommerce Video creates a complete product video from public HTTPS product or + lifestyle images, an explicit duration, and optional creative direction. Upload + local images with `POST /v1/files`, save the returned Task ID, and poll + `GET /v1/tasks/{task_id}` until the task succeeds or fails. requestBody: required: true content: @@ -1062,6 +2981,7 @@ paths: type: array minItems: 1 maxItems: 7 + description: Primary product or scene image first, followed by up to six additional public HTTPS PNG, JPEG, or WebP product or lifestyle images. Upload local files with `POST /v1/files` and use the returned `data.url`. items: type: string format: uri @@ -1069,21 +2989,25 @@ paths: type: integer minimum: 10 maximum: 60 + description: Required target output duration in seconds and the basis for USD calculation. Allowed range is 10-60 seconds. prompt: type: string maxLength: 2000 + description: Optional creative direction, audience, product benefit, offer, tone, scenes, or call to action. Maximum 2000 characters. aspect_ratio: type: string enum: ['16:9', '9:16', '1:1'] + description: Target output placement. Use 16:9 for landscape, 9:16 for vertical social, or 1:1 for square placements; set explicitly for stable layout. language: type: string enum: [en, zh] + description: Dialogue and narration language. Use `en` for English or `zh` for Chinese; set explicitly when the prompt contains mixed languages. example: images: - https://media.beatapi.io/samples/smart-bottle.png duration: 15 prompt: Fast product launch ad for paid social. - aspect_ratio: 9:16 + aspect_ratio: '9:16' responses: '201': description: Task accepted @@ -1095,6 +3019,9 @@ paths: data: id: task_p9Lm2 object: task + task_kind: workflow + capability_id: ecommerce-video + capability_version: 1 workflow: ecommerce-video status: queued stage: queued @@ -1103,8 +3030,8 @@ paths: completed_at: null output: null usage: - credits_reserved: 225 - credits_charged: 225 + credits_reserved: 4.5 + credits_charged: 4.5 billable_duration_seconds: 15 credits_settled: 0 credits_refunded: 0 @@ -1126,6 +3053,17 @@ paths: code: insufficient_credits message: Account balance is not sufficient for this task. request_id: req_xxx + '409': + description: The Idempotency-Key was reused with a different body or while another request with that key is still being processed. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: + code: idempotency_conflict + message: This Idempotency-Key was already used with a different request body. + request_id: req_xxx '429': description: User concurrency exceeded. content: @@ -1168,6 +3106,9 @@ paths: data: id: task_8K2qA object: task + task_kind: workflow + capability_id: music-video + capability_version: 1 workflow: music-video status: queued stage: queued @@ -1178,8 +3119,8 @@ paths: completed_at: null output: null usage: - credits_reserved: 75 - credits_charged: 75 + credits_reserved: 1.5 + credits_charged: 1.5 billable_duration_seconds: 15 credits_settled: 0 credits_refunded: 0 @@ -1192,6 +3133,9 @@ paths: data: id: task_8K2qA object: task + task_kind: workflow + capability_id: music-video + capability_version: 1 workflow: music-video status: storyboard_ready stage: storyboard_ready @@ -1210,10 +3154,10 @@ paths: updated_at: 1782210300 output: null usage: - credits_reserved: 75 - credits_charged: 75 + credits_reserved: 1.5 + credits_charged: 1.5 billable_duration_seconds: 15 - credits_settled: 75 + credits_settled: 1.5 credits_refunded: 0 request_id: req_abc123 error_code: null @@ -1224,6 +3168,9 @@ paths: data: id: task_8K2qA object: task + task_kind: workflow + capability_id: music-video + capability_version: 1 workflow: music-video status: failed stage: failed @@ -1234,11 +3181,11 @@ paths: completed_at: 1782210600 output: null usage: - credits_reserved: 75 - credits_charged: 75 + credits_reserved: 1.5 + credits_charged: 1.5 billable_duration_seconds: 15 credits_settled: 0 - credits_refunded: 75 + credits_refunded: 1.5 request_id: req_abc123 error_code: processing_timeout error_message: Task waited too long for platform capacity. @@ -1262,9 +3209,9 @@ paths: security: - BearerAuth: [] description: | - Reserve credits and allocate a short-lived BeatAPI realtime session. Send a unique + Reserve the selected USD amount and allocate a short-lived BeatAPI realtime session. Send a unique `Idempotency-Key`; retries with the same user, key, and body return the same session - and deterministic short-lived `client_secret` without reserving credits or capacity + and deterministic short-lived `client_secret` without reserving funds or capacity twice. The browser receives only that BeatAPI secret and connects with `@beatapi/realtime`. @@ -1280,7 +3227,8 @@ paths: - in: header name: Idempotency-Key required: true - schema: { type: string, maxLength: 128 } + schema: { type: string, minLength: 1, maxLength: 128 } + example: rts-create-cus_123-01 requestBody: required: true content: @@ -1292,14 +3240,22 @@ paths: max_duration_seconds: type: integer enum: [15, 60, 300] + description: Required maximum live session duration in seconds. The USD amount is reserved for the selected 15, 60, or 300 second tier. allowed_origins: type: array minItems: 1 maxItems: 10 - items: { type: string, format: uri } + description: Exact browser origins allowed to use the short-lived session secret. + items: + type: string + format: uri + pattern: '^(https://[A-Za-z0-9.-]+(?::[0-9]+)?|http://(?:localhost|127\\.0\\.0\\.1|\\[::1\\])(?::[0-9]+)?)$' + description: Exact browser origin. Use HTTPS in production; HTTP is accepted only for localhost development. + example: https://app.example.com metadata: type: object maxProperties: 20 + description: Optional server-defined string metadata for your own correlation. Up to 20 keys; keys are at most 64 characters and values at most 256 characters. propertyNames: { maxLength: 64 } additionalProperties: { type: string, maxLength: 256 } example: @@ -1311,11 +3267,25 @@ paths: description: Realtime session created content: application/json: - schema: { $ref: '#/components/schemas/RealtimeSessionResponse' } + schema: { $ref: '#/components/schemas/RealtimeSessionCreateResponse' } + example: + data: + id: rts_8K2qA + object: realtime.session + status: ready + client_secret: brt_live_example_short_lived_secret + expires_at: '2026-08-12T10:01:00.000Z' + max_duration_seconds: 60 + allowed_origins: ['https://app.example.com'] + credits: { reserved: 1.2, settled: 0, refunded: 0 } + request_id: req_abc123 + created_at: '2026-08-12T10:00:00.000Z' + connected_at: null + closed_at: null '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '402': - description: Insufficient credits + description: Insufficient USD balance content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } '409': description: Idempotency conflict @@ -1379,24 +3349,44 @@ paths: example: data: object: usage - credit_balance: 1080 + credit_balance: 21.6 total_tasks: 12 - credits_settled: 720 - credits_refunded: 450 + credits_settled: 14.4 + credits_refunded: 9 concurrency: limit: 2 active: 1 realtime: sessions: 3 - credits: 90 + credits: 1.8 active: 1 by_workflow: - workflow: music-video tasks: 8 - credits_settled: 480 + credits_settled: 9.6 - workflow: ecommerce-video tasks: 4 - credits_settled: 240 + credits_settled: 4.8 + by_capability: + - task_kind: image + capability_id: seedream-5-pro + tasks: 3 + credits_settled: 0.42 + - task_kind: video + capability_id: veo-3.1 + tasks: 2 + credits_settled: 14 + by_model: + - media_type: image + model: seedream-5-pro + tasks: 3 + credits_settled: 0.42 + by_api_key: + - api_key_id: key_abc123 + title: Production + key_prefix: sk_live_abcd + tasks: 12 + credits_settled: 14.4 '401': $ref: '#/components/responses/Unauthorized' @@ -1409,28 +3399,43 @@ paths: security: - BearerAuth: [] description: | - Use file upload when your images, audio, or subtitle files are not + Use file upload when your images, videos, audio, or subtitle files are not already hosted at public HTTPS URLs. The returned HTTPS URL can be used - directly in `images`, `audio_url`, or `srt_url`. + directly in `images`, `reference_videos`, `audio_url`, or `srt_url`. Limits: - - Maximum file size is 50 MB. + - Images, audio, and subtitles: maximum 50 MB. + - Motion videos: maximum 100 MB. - Images: `png`, `jpg`, `jpeg`, `webp` (`image/png`, `image/jpeg`, `image/webp`). - Audio: `mp3`, `wav`, `aac`, `m4a` (`audio/mpeg`, `audio/wav`, `audio/aac`, `audio/mp4`). - - Audio uploads must be 10-180 seconds. + - Audio uploads must be 10-300 seconds. The selected Music Video tier applies its own task limit: Standard 10-180 seconds; Premium 10-300 seconds. + - Motion videos: `mp4`, `mov` (`video/mp4`, `video/quicktime`), 3-30 seconds. Duration and dimensions are detected during upload. - Subtitles: `srt` (`application/x-subrip`; multipart uploads may use `text/plain` only when the filename ends in `.srt`). - - PDF, generic text files, octet-stream uploads, videos, and zip files + - PDF, generic text files, octet-stream uploads, Matroska videos, and zip files are not supported for launch. - Send either multipart form-data with a `file` field, or send the raw file body with the asset `Content-Type`. + - `Content-Length` is required and is validated before BeatAPI buffers + the request body; chunked uploads without a declared length are rejected. - Returned URLs are HTTPS and long-lived for launch. - Uploaded audio files are duration-checked during upload. The response includes `audio_duration_seconds` when the uploaded asset is audio. + - Uploaded videos are signature-, duration-, and dimension-checked. The + response includes `video_duration_seconds`, `width`, and `height`. - Workflow task inputs still require public HTTPS URLs. Localhost, private network URLs, and data URLs are not accepted. + parameters: + - in: header + name: Content-Length + required: true + description: Exact request-body length in bytes. For multipart uploads this includes multipart framing overhead. + schema: + type: integer + minimum: 1 + maximum: 105906176 requestBody: required: true content: @@ -1445,6 +3450,26 @@ paths: purpose: type: string enum: [input] + image/png: + schema: { type: string, format: binary } + image/jpeg: + schema: { type: string, format: binary } + image/webp: + schema: { type: string, format: binary } + audio/mpeg: + schema: { type: string, format: binary } + audio/wav: + schema: { type: string, format: binary } + audio/aac: + schema: { type: string, format: binary } + audio/mp4: + schema: { type: string, format: binary } + video/mp4: + schema: { type: string, format: binary } + video/quicktime: + schema: { type: string, format: binary } + application/x-subrip: + schema: { type: string, format: binary } responses: '201': description: File uploaded @@ -1468,6 +3493,10 @@ paths: $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' + '429': + $ref: '#/components/responses/RateLimited' + '500': + $ref: '#/components/responses/InternalError' /v1/webhooks: get: @@ -1499,6 +3528,10 @@ paths: updated_at: 1782210000 '401': $ref: '#/components/responses/Unauthorized' + '429': + $ref: '#/components/responses/RateLimited' + '500': + $ref: '#/components/responses/InternalError' post: operationId: createWebhookEndpoint tags: [Webhooks] @@ -1507,8 +3540,9 @@ paths: security: - BearerAuth: [] description: | - The signing secret is returned only once at creation time. Store it - securely. Later responses return a masked secret. + The public API returns the signing secret in full at creation time. Store + it securely; later public API responses return a masked secret. An + authenticated dashboard owner can explicitly reveal the secret again. BeatAPI sends these headers with each delivery: - `x-beatapi-event`: `task.succeeded` or `task.failed` @@ -1572,8 +3606,9 @@ paths: ``` Reject old timestamps to prevent replay attacks. A 5 minute window is - recommended. Failed deliveries are retried at most 3 times with fixed - backoff windows of 1 minute, 5 minutes, and 15 minutes. Polling + recommended. A delivery is attempted at most 3 times total: the initial + request plus up to 2 retries, with fixed backoff windows of 1 minute and + 5 minutes. Polling `GET /v1/tasks/{task_id}` remains the source of truth. requestBody: required: true @@ -1586,10 +3621,14 @@ paths: url: type: string format: uri + pattern: '^https://' + description: Public HTTPS callback URL that accepts BeatAPI task events. Do not use localhost or a private-network URL. description: type: string + description: Optional internal label for identifying the endpoint in your account. events: type: array + description: Task events to deliver. Omit to subscribe to both `task.succeeded` and `task.failed`. items: type: string enum: [task.succeeded, task.failed] @@ -1618,6 +3657,10 @@ paths: $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' + '429': + $ref: '#/components/responses/RateLimited' + '500': + $ref: '#/components/responses/InternalError' /v1/webhooks/{id}: get: @@ -1660,6 +3703,10 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + '429': + $ref: '#/components/responses/RateLimited' + '500': + $ref: '#/components/responses/InternalError' patch: operationId: updateWebhookEndpoint tags: [Webhooks] @@ -1684,6 +3731,8 @@ paths: url: type: string format: uri + pattern: '^https://' + description: Public HTTPS callback URL that accepts BeatAPI task events. Do not use localhost or a private-network URL. description: type: string status: @@ -1707,6 +3756,16 @@ paths: $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' + '404': + description: Webhook endpoint not found. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '429': + $ref: '#/components/responses/RateLimited' + '500': + $ref: '#/components/responses/InternalError' delete: operationId: deleteWebhookEndpoint tags: [Webhooks] @@ -1734,3 +3793,13 @@ paths: deleted: true '401': $ref: '#/components/responses/Unauthorized' + '404': + description: Webhook endpoint not found. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '429': + $ref: '#/components/responses/RateLimited' + '500': + $ref: '#/components/responses/InternalError' diff --git a/contract/contract.lock.json b/contract/contract.lock.json index 47af603..2ccc3fe 100644 --- a/contract/contract.lock.json +++ b/contract/contract.lock.json @@ -1,6 +1,6 @@ { "source": "https://github.com/BeatAPI/beatapi-examples", - "ref": "8f7d3cff33445ded4d3c94f0fb8ac5060d790148", + "ref": "83a139a123a3139cf53a362132b7b1d8a0066e1f", "openapiVersion": "1.0.0-launch", - "sha256": "290100dba10bb14b040f5a826657ad7d4a01f179fc28ef69ea0bdcaa66f7dad3" + "sha256": "bcd8dfb2124e7815ea52e513c99a2522e749316fc41f02a6f70957d3e3ebe293" } diff --git a/generated/runtime.lock.json b/generated/runtime.lock.json index 5bb650c..d0a5640 100644 --- a/generated/runtime.lock.json +++ b/generated/runtime.lock.json @@ -1,5 +1,5 @@ { "source": "https://github.com/BeatAPI/beatapi-cli/tree/main/packages/client", - "ref": "ec973b34894551ee2fafb27b7c8f3881ecef0b8b", - "sha256": "bbcc96e34af27bdf2a719e60c60ee075f0526b416e9a82b8ee349fadc5a9eb73" + "ref": "db95c2b44e415ae963faaa7c0ca491653cdf3f96", + "sha256": "67d390cb6a5d49f40fc3a2f1d9da5fa2af9d852d36d470d2eeb0ec4d926591a7" } diff --git a/generated/skill.lock.json b/generated/skill.lock.json index e5dd8e4..d9754e5 100644 --- a/generated/skill.lock.json +++ b/generated/skill.lock.json @@ -1,5 +1,5 @@ { "source": "https://github.com/BeatAPI/beatapi-skill/tree/main/skills/beatapi-video", - "ref": "62ceeaeb2ec461f70159899385920f21aa804e19", - "sha256": "f50bf504fcb43224dec22ab1f94a00c407a4d459c33babb21153d466263b41e6" + "ref": "72a734cb921b63a37e17c2600dfe9796bb038721", + "sha256": "e6fcf273f4165af34861c1463b2abc4947049cda256f857e6f142754b1f6ed7c" } diff --git a/mcp/server.mjs b/mcp/server.mjs index 313ad10..14d9e4b 100644 --- a/mcp/server.mjs +++ b/mcp/server.mjs @@ -3109,9 +3109,28 @@ var require_utils = __commonJS({ "use strict"; var isUUID = RegExp.prototype.test.bind(/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iu); var isIPv4 = RegExp.prototype.test.bind(/^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/u); + var isPort = RegExp.prototype.test.bind(/^\d*$/u); var isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu); var isUnreserved = RegExp.prototype.test.bind(/^[\da-z\-._~]$/iu); - var isPathCharacter = RegExp.prototype.test.bind(/^[\da-z\-._~!$&'()*+,;=:@/]$/iu); + var isPathCharacter = RegExp.prototype.test.bind(/^[A-Za-z0-9\-._~!$&'()*+,;=:@/]$/u); + var isQueryFragmentCharacter = RegExp.prototype.test.bind(/^[A-Za-z0-9\-._~!$&'()*+,;=:@/?]$/u); + var isUserinfoCharacter = RegExp.prototype.test.bind(/^[A-Za-z0-9\-._~!$&'()*+,;=:]$/u); + var BYTE_HEX = new Array(256); + { + const HEX_DIGITS = "0123456789ABCDEF"; + for (let i = 0; i < 256; i++) { + BYTE_HEX[i] = "%" + HEX_DIGITS[i >> 4] + HEX_DIGITS[i & 15]; + } + } + function percentEncodeNonAscii(cp) { + if (cp < 2048) { + return BYTE_HEX[192 | cp >> 6] + BYTE_HEX[128 | cp & 63]; + } + if (cp < 65536) { + return BYTE_HEX[224 | cp >> 12] + BYTE_HEX[128 | cp >> 6 & 63] + BYTE_HEX[128 | cp & 63]; + } + return BYTE_HEX[240 | cp >> 18] + BYTE_HEX[128 | cp >> 12 & 63] + BYTE_HEX[128 | cp >> 6 & 63] + BYTE_HEX[128 | cp & 63]; + } function stringArrayToHexStripped(input) { let acc = ""; let code = 0; @@ -3136,91 +3155,105 @@ var require_utils = __commonJS({ } return acc; } + var isHextet = RegExp.prototype.test.bind(/^[\dA-Fa-f]{1,4}$/); + var isIPvFuture = RegExp.prototype.test.bind(/^[vV][\dA-Fa-f]+\.[A-Za-z\d\-._~!$&'()*+,;=:]+$/); + var isZoneCharacter = RegExp.prototype.test.bind(/^[A-Za-z\d\-._~]$/); var nonSimpleDomain = RegExp.prototype.test.bind(/[^!"$&'()*+,\-.;=_`a-z{}~]/u); - function consumeIsZone(buffer) { - buffer.length = 0; - return true; - } - function consumeHextets(buffer, address, output) { - if (buffer.length) { - const hex3 = stringArrayToHexStripped(buffer); - if (hex3 !== "") { - address.push(hex3); - } else { - output.error = true; - return false; + function isZoneIdentifier(zone) { + if (zone.length === 0) return false; + for (let i = 0; i < zone.length; i++) { + if (isZoneCharacter(zone[i])) continue; + if (zone[i] === "%" && i + 2 < zone.length && isHexPair(zone.slice(i + 1, i + 3))) { + i += 2; + continue; } - buffer.length = 0; + return false; } return true; } - function getIPV6(input) { - let tokenCount = 0; - const output = { error: false, address: "", zone: "" }; - const address = []; - const buffer = []; - let endipv6Encountered = false; - let endIpv6 = false; - let consume = consumeHextets; - for (let i = 0; i < input.length; i++) { - const cursor = input[i]; - if (cursor === "[" || cursor === "]") { - continue; - } - if (cursor === ":") { - if (endipv6Encountered === true) { - endIpv6 = true; - } - if (!consume(buffer, address, output)) { - break; + function compressIPv6ZeroRun(hextets) { + let bestStart = -1; + let bestLength = 0; + let runStart = -1; + let runLength = 0; + for (let i = 0; i < hextets.length; i++) { + if (hextets[i] === "0") { + if (runStart === -1) runStart = i; + runLength++; + if (runLength > bestLength) { + bestLength = runLength; + bestStart = runStart; } - if (++tokenCount > 7) { - output.error = true; - break; - } - if (i > 0 && input[i - 1] === ":") { - endipv6Encountered = true; - } - address.push(":"); - continue; - } else if (cursor === "%") { - if (!consume(buffer, address, output)) { - break; - } - consume = consumeIsZone; } else { - buffer.push(cursor); + runStart = -1; + runLength = 0; + } + } + if (bestLength < 2) return hextets.join(":"); + const head = hextets.slice(0, bestStart).join(":"); + const tail = hextets.slice(bestStart + bestLength).join(":"); + return head + "::" + tail; + } + function normalizeIPv6Address(input) { + const compression = input.indexOf("::"); + if (compression !== -1 && input.indexOf("::", compression + 1) !== -1) return void 0; + const left = compression === -1 ? input.split(":") : input.slice(0, compression).split(":"); + const right = compression === -1 ? [] : input.slice(compression + 2).split(":"); + if (compression !== -1) { + if (left.length === 1 && left[0] === "") left.length = 0; + if (right.length === 1 && right[0] === "") right.length = 0; + } + const parts = left.concat(right); + let hextetCount = 0; + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + if (part === "") return void 0; + if (part.indexOf(".") !== -1) { + if (i !== parts.length - 1 || compression !== -1 && right.length === 0 || !isIPv4(part)) return void 0; + hextetCount += 2; continue; } + if (!isHextet(part)) return void 0; + parts[i] = parseInt(part, 16).toString(16); + hextetCount++; } - if (buffer.length) { - if (consume === consumeIsZone) { - output.zone = buffer.join(""); - } else if (endIpv6) { - address.push(buffer.join("")); - } else { - address.push(stringArrayToHexStripped(buffer)); - } + if (compression === -1) { + if (hextetCount !== 8) return void 0; + return compressIPv6ZeroRun(parts); } - output.address = address.join(""); - return output; + if (hextetCount >= 8) return void 0; + const expanded = parts.slice(0, left.length); + for (let i = hextetCount; i < 8; i++) expanded.push("0"); + for (let i = left.length; i < parts.length; i++) expanded.push(parts[i]); + return compressIPv6ZeroRun(expanded); } function normalizeIPv6(host) { - if (findToken(host, ":") < 2) { - return { host, isIPV6: false }; - } - const ipv63 = getIPV6(host); - if (!ipv63.error) { - let newHost = ipv63.address; - let escapedHost = ipv63.address; - if (ipv63.zone) { - newHost += "%" + ipv63.zone; - escapedHost += "%25" + ipv63.zone; - } - return { host: newHost, isIPV6: true, escapedHost }; - } else { - return { host, isIPV6: false }; - } + const bracketed = host[0] === "[" && host[host.length - 1] === "]"; + const hasBracket = host[0] === "[" || host[host.length - 1] === "]"; + if (hasBracket && !bracketed) return { host, isIPV6: false, error: true }; + let input = bracketed ? host.slice(1, -1) : host; + if (bracketed && isIPvFuture(input)) { + input = input.toLowerCase(); + return { host: `[${input}]`, escapedHost: input, isIPV6: false, isIPVFuture: true }; + } + if (findToken(input, ":") < 2) { + return { host, isIPV6: false, error: bracketed }; + } + let zoneIdentifier = ""; + const zoneSeparator = input.indexOf("%"); + if (zoneSeparator !== -1) { + const separatorLength = input.slice(zoneSeparator, zoneSeparator + 3).toLowerCase() === "%25" ? 3 : 1; + zoneIdentifier = input.slice(zoneSeparator + separatorLength); + if (!isZoneIdentifier(zoneIdentifier)) return { host, isIPV6: false, error: true }; + input = input.slice(0, zoneSeparator); + } + const address = normalizeIPv6Address(input); + if (address === void 0) return { host, isIPV6: false, error: true }; + return { + host: address + (zoneIdentifier ? "%" + zoneIdentifier : ""), + escapedHost: address + (zoneIdentifier ? "%25" + zoneIdentifier : ""), + isIPV6: true + }; } function findToken(str, token) { let ind = 0; @@ -3339,7 +3372,8 @@ var require_utils = __commonJS({ function normalizePathEncoding(input) { let output = ""; for (let i = 0; i < input.length; i++) { - if (input[i] === "%" && i + 2 < input.length) { + const ch = input[i]; + if (ch === "%" && i + 2 < input.length) { const hex3 = input.slice(i + 1, i + 3); if (isHexPair(hex3)) { const normalizedHex = hex3.toUpperCase(); @@ -3353,10 +3387,152 @@ var require_utils = __commonJS({ continue; } } - if (isPathCharacter(input[i])) { - output += input[i]; + if (isPathCharacter(ch)) { + output += ch; } else { - output += escape(input[i]); + const code = input.charCodeAt(i); + if (code < 128) { + output += isEscapeSafe(code) ? ch : BYTE_HEX[code]; + } else if (code < 55296 || code > 57343) { + output += percentEncodeNonAscii(code); + } else if (code <= 56319 && i + 1 < input.length) { + const low = input.charCodeAt(i + 1); + if (low >= 56320 && low <= 57343) { + output += percentEncodeNonAscii(65536 + (code - 55296 << 10) + (low - 56320)); + i++; + } else { + output += percentEncodeNonAscii(65533); + } + } else { + output += percentEncodeNonAscii(65533); + } + } + } + return output; + } + function serializePathEncoding(input, pathNoScheme = false) { + let output = ""; + let firstSegment = pathNoScheme && input[0] !== "/"; + for (let i = 0; i < input.length; i++) { + const ch = input[i]; + if (ch === "%" && i + 2 < input.length) { + const hex3 = input.slice(i + 1, i + 3); + if (isHexPair(hex3)) { + output += "%" + hex3.toUpperCase(); + i += 2; + continue; + } + } + if (ch === "/") { + firstSegment = false; + } + if (isPathCharacter(ch) && (ch !== ":" || !firstSegment)) { + output += ch; + } else { + const code = input.charCodeAt(i); + if (code < 128) { + output += BYTE_HEX[code]; + } else if (code < 55296 || code > 57343) { + output += percentEncodeNonAscii(code); + } else if (code <= 56319 && i + 1 < input.length) { + const low = input.charCodeAt(i + 1); + if (low >= 56320 && low <= 57343) { + output += percentEncodeNonAscii(65536 + (code - 55296 << 10) + (low - 56320)); + i++; + } else { + output += percentEncodeNonAscii(65533); + } + } else { + output += percentEncodeNonAscii(65533); + } + } + } + return output; + } + function encodeComponent(input, isAllowed) { + let output = ""; + for (let i = 0; i < input.length; i++) { + const ch = input[i]; + if (ch === "%" && i + 2 < input.length) { + const hex3 = input.slice(i + 1, i + 3); + if (isHexPair(hex3)) { + output += "%" + hex3.toUpperCase(); + i += 2; + continue; + } + } + if (isAllowed(ch)) { + output += ch; + } else { + const code = input.charCodeAt(i); + if (code < 128) { + output += BYTE_HEX[code]; + } else if (code < 55296 || code > 57343) { + output += percentEncodeNonAscii(code); + } else if (code <= 56319 && i + 1 < input.length) { + const low = input.charCodeAt(i + 1); + if (low >= 56320 && low <= 57343) { + output += percentEncodeNonAscii(65536 + (code - 55296 << 10) + (low - 56320)); + i++; + } else { + output += percentEncodeNonAscii(65533); + } + } else { + output += percentEncodeNonAscii(65533); + } + } + } + return output; + } + function encodeUserinfo(input) { + return encodeComponent(input, isUserinfoCharacter); + } + function encodeQuery(input) { + return encodeComponent(input, isQueryFragmentCharacter); + } + function encodeFragment(input) { + return encodeComponent(input, isQueryFragmentCharacter); + } + function isEscapeSafe(cp) { + return cp >= 48 && cp <= 57 || cp >= 65 && cp <= 90 || cp >= 97 && cp <= 122 || cp === 42 || cp === 43 || cp === 45 || cp === 46 || cp === 47 || cp === 64 || cp === 95; + } + function normalizeQueryFragmentEncoding(input) { + let output = ""; + for (let i = 0; i < input.length; i++) { + const ch = input[i]; + if (ch === "%" && i + 2 < input.length) { + const hex3 = input.slice(i + 1, i + 3); + if (isHexPair(hex3)) { + const normalizedHex = hex3.toUpperCase(); + const decoded = String.fromCharCode(parseInt(normalizedHex, 16)); + if (isUnreserved(decoded)) { + output += decoded; + } else { + output += "%" + normalizedHex; + } + i += 2; + continue; + } + } + if (isQueryFragmentCharacter(ch)) { + output += ch; + } else { + const code = input.charCodeAt(i); + if (code < 128) { + output += isEscapeSafe(code) ? ch : BYTE_HEX[code]; + } else if (code < 55296 || code > 57343) { + output += percentEncodeNonAscii(code); + } else if (code <= 56319 && i + 1 < input.length) { + const low = input.charCodeAt(i + 1); + if (low >= 56320 && low <= 57343) { + output += percentEncodeNonAscii(65536 + (code - 55296 << 10) + (low - 56320)); + i++; + } else { + output += percentEncodeNonAscii(65533); + } + } else { + output += percentEncodeNonAscii(65533); + } } } return output; @@ -3379,14 +3555,18 @@ var require_utils = __commonJS({ function recomposeAuthority(component) { const uriTokens = []; if (component.userinfo !== void 0) { - uriTokens.push(component.userinfo); + uriTokens.push(encodeUserinfo(component.userinfo)); uriTokens.push("@"); } if (component.host !== void 0) { - let host = unescape(component.host); + let host = component.host; if (!isIPv4(host)) { - const ipV6res = normalizeIPv6(host); - if (ipV6res.isIPV6 === true) { + let ipV6res = normalizeIPv6(host); + if (ipV6res.isIPV6 !== true && ipV6res.isIPVFuture !== true) { + host = normalizePercentEncoding(host, true); + ipV6res = normalizeIPv6(host); + } + if (ipV6res.isIPV6 === true || ipV6res.isIPVFuture === true) { host = `[${ipV6res.escapedHost}]`; } else { host = reescapeHostDelimiters(host, false); @@ -3395,8 +3575,12 @@ var require_utils = __commonJS({ uriTokens.push(host); } if (typeof component.port === "number" || typeof component.port === "string") { + const port = String(component.port); + if (!isPort(port)) { + throw new TypeError("URI port is malformed."); + } uriTokens.push(":"); - uriTokens.push(String(component.port)); + uriTokens.push(port); } return uriTokens.length ? uriTokens.join("") : void 0; } @@ -3406,6 +3590,11 @@ var require_utils = __commonJS({ reescapeHostDelimiters, normalizePercentEncoding, normalizePathEncoding, + serializePathEncoding, + normalizeQueryFragmentEncoding, + encodeUserinfo, + encodeQuery, + encodeFragment, escapePreservingEscapes, removeDotSegments, isIPv4, @@ -3421,7 +3610,7 @@ var require_schemes = __commonJS({ "node_modules/fast-uri/lib/schemes.js"(exports, module) { "use strict"; var { isUUID } = require_utils(); - var URN_REG = /([\da-z][\d\-a-z]{0,31}):((?:[\w!$'()*+,\-.:;=@]|%[\da-f]{2})+)/iu; + var URN_REG = /^([\da-z][\d\-a-z]{0,31}):((?:[\w!$'()*+,\-./:;=@]|%[\da-f]{2})+)$/iu; var supportedSchemeNames = ( /** @type {const} */ [ @@ -3482,9 +3671,10 @@ var require_schemes = __commonJS({ wsComponent.secure = void 0; } if (wsComponent.resourceName) { - const [path, query] = wsComponent.resourceName.split("?"); + const queryIndex = wsComponent.resourceName.indexOf("?"); + const path = queryIndex === -1 ? wsComponent.resourceName : wsComponent.resourceName.slice(0, queryIndex); wsComponent.path = path && path !== "/" ? path : void 0; - wsComponent.query = query; + wsComponent.query = queryIndex === -1 ? void 0 : wsComponent.resourceName.slice(queryIndex + 1); wsComponent.resourceName = void 0; } wsComponent.fragment = void 0; @@ -3496,7 +3686,7 @@ var require_schemes = __commonJS({ return urnComponent; } const matches = urnComponent.path.match(URN_REG); - if (matches) { + if (matches && matches[0] === urnComponent.path) { const scheme = options.scheme || urnComponent.scheme || "urn"; urnComponent.nid = matches[1].toLowerCase(); urnComponent.nss = matches[2]; @@ -3630,8 +3820,17 @@ var require_schemes = __commonJS({ var require_fast_uri = __commonJS({ "node_modules/fast-uri/index.js"(exports, module) { "use strict"; - var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, escapePreservingEscapes, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils(); + var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, serializePathEncoding, normalizeQueryFragmentEncoding, encodeQuery, encodeFragment, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils(); var { SCHEMES, getSchemeHandler } = require_schemes(); + var VALID_SCHEME = /^[A-Za-z][A-Za-z0-9+.-]*$/u; + var MALFORMED_SCHEME_ERROR = "URI scheme is malformed."; + function decodeValidScheme(scheme) { + const decodedScheme = unescape(String(scheme)); + if (!VALID_SCHEME.test(decodedScheme)) { + throw new TypeError(MALFORMED_SCHEME_ERROR); + } + return decodedScheme; + } function normalize(uri2, options) { if (typeof uri2 === "string") { uri2 = /** @type {T} */ @@ -3644,12 +3843,34 @@ var require_fast_uri = __commonJS({ } function resolve2(baseURI, relativeURI, options) { const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" }; - const { parsed: baseParsed, malformedAuthorityOrPort: baseMalformed } = parseWithStatus(baseURI, schemelessOptions); - const { parsed: relativeParsed, malformedAuthorityOrPort: relativeMalformed } = parseWithStatus(relativeURI, schemelessOptions); - if (baseMalformed || relativeMalformed) { + const { + parsed: baseParsed, + malformedAuthorityOrPort: baseMalformed, + malformedPercentEncoding: baseMalformedPercentEncoding, + malformedSchemeSpecific: baseMalformedSchemeSpecific, + malformedHost: baseMalformedHost, + malformedScheme: baseMalformedScheme + } = parseWithStatus(baseURI, schemelessOptions); + const { + parsed: relativeParsed, + malformedAuthorityOrPort: relativeMalformed, + malformedPercentEncoding: relativeMalformedPercentEncoding, + malformedSchemeSpecific: relativeMalformedSchemeSpecific, + malformedHost: relativeMalformedHost, + malformedScheme: relativeMalformedScheme + } = parseWithStatus(relativeURI, schemelessOptions); + if (baseMalformed || relativeMalformed || baseMalformedPercentEncoding || relativeMalformedPercentEncoding || baseMalformedSchemeSpecific || relativeMalformedSchemeSpecific || baseMalformedHost || relativeMalformedHost || baseMalformedScheme || relativeMalformedScheme) { throw new Error(baseParsed.error || relativeParsed.error || "URI is malformed."); } const resolved = resolveComponent(baseParsed, relativeParsed, schemelessOptions, true); + const resolvedSchemeHandler = getSchemeHandler(options && options.scheme || resolved.scheme); + const resolvedHost = resolved.host; + const resolvedHostIsIP = resolvedHost !== void 0 && resolvedHost !== "" && (isIPv4(resolvedHost) || normalizeIPv6(resolvedHost).isIPV6); + canonicalizeHost(resolved, options || {}, resolvedSchemeHandler, resolvedHostIsIP); + const encodedASCIIHost = resolvedHost && resolvedHost.indexOf("%") !== -1 && !new RegExp("\\P{ASCII}", "u").test(resolvedHost); + if (resolved.error && !encodedASCIIHost) { + throw new Error(resolved.error); + } schemelessOptions.skipEscape = true; return serialize(resolved, schemelessOptions); } @@ -3709,7 +3930,7 @@ var require_fast_uri = __commonJS({ function equal(uriA, uriB, options) { const normalizedA = normalizeComparableURI(uriA, options); const normalizedB = normalizeComparableURI(uriB, options); - return normalizedA !== void 0 && normalizedB !== void 0 && normalizedA.toLowerCase() === normalizedB.toLowerCase(); + return normalizedA !== void 0 && normalizedB !== void 0 && normalizedA === normalizedB; } function serialize(cmpts, opts) { const component = { @@ -3730,19 +3951,22 @@ var require_fast_uri = __commonJS({ }; const options = Object.assign({}, opts); const uriTokens = []; + if (component.scheme) { + component.scheme = decodeValidScheme(component.scheme); + } const schemeHandler = getSchemeHandler(options.scheme || component.scheme); if (schemeHandler && schemeHandler.serialize) schemeHandler.serialize(component, options); + const hasAuthority = component.userinfo !== void 0 || component.host !== void 0 || component.port !== void 0; + const pathNoScheme = !options.skipEscape && component.scheme === void 0 && !hasAuthority; if (component.path !== void 0) { if (!options.skipEscape) { - component.path = escapePreservingEscapes(component.path); - if (component.scheme !== void 0) { - component.path = component.path.split("%3A").join(":"); - } + component.path = serializePathEncoding(component.path, pathNoScheme); } else { component.path = normalizePercentEncoding(component.path); } } if (options.reference !== "suffix" && component.scheme) { + component.scheme = decodeValidScheme(component.scheme); uriTokens.push(component.scheme, ":"); } const authority = recomposeAuthority(component); @@ -3760,16 +3984,19 @@ var require_fast_uri = __commonJS({ if (!options.absolutePath && (!schemeHandler || !schemeHandler.absolutePath)) { s = removeDotSegments(s); } + if (pathNoScheme) { + s = serializePathEncoding(s, true); + } if (authority === void 0 && s[0] === "/" && s[1] === "/") { s = "/%2F" + s.slice(2); } uriTokens.push(s); } if (component.query !== void 0) { - uriTokens.push("?", component.query); + uriTokens.push("?", encodeQuery(component.query)); } if (component.fragment !== void 0) { - uriTokens.push("#", component.fragment); + uriTokens.push("#", encodeFragment(component.fragment)); } return uriTokens.join(""); } @@ -3785,6 +4012,35 @@ var require_fast_uri = __commonJS({ } return void 0; } + function hasMalformedPercentEncoding(component) { + if (component === void 0) return false; + let percent = component.indexOf("%"); + while (percent !== -1) { + if (percent + 2 >= component.length || !/^[\da-f]{2}$/iu.test(component.slice(percent + 1, percent + 3))) { + return true; + } + percent = component.indexOf("%", percent + 3); + } + return false; + } + function isIPLiteral(host) { + return host[0] === "[" && host[host.length - 1] === "]"; + } + function hasMalformedComponentPercentEncoding(matches) { + const host = matches[4]; + return hasMalformedPercentEncoding(matches[3]) || host !== void 0 && !isIPLiteral(host) && hasMalformedPercentEncoding(host) || hasMalformedPercentEncoding(matches[6]) || hasMalformedPercentEncoding(matches[7]) || hasMalformedPercentEncoding(matches[8]); + } + function canonicalizeHost(parsed, options, schemeHandler, isIP) { + if (!options.unicodeSupport && (!schemeHandler || !schemeHandler.unicodeSupport) && parsed.host && !isIPLiteral(parsed.host) && (options.domainHost || schemeHandler && schemeHandler.domainHost) && isIP === false && nonSimpleDomain(parsed.host)) { + try { + parsed.host = new URL("http://" + parsed.host).hostname; + } catch (e) { + parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e; + return true; + } + } + return false; + } function parseWithStatus(uri2, opts) { const options = Object.assign({}, opts); const parsed = { @@ -3797,6 +4053,11 @@ var require_fast_uri = __commonJS({ fragment: void 0 }; let malformedAuthorityOrPort = false; + let malformedPercentEncoding = false; + let malformedSchemeSpecific = false; + let malformedHost = false; + let malformedIPLiteral = false; + let malformedScheme = false; let isIP = false; if (options.reference === "suffix") { if (options.scheme) { @@ -3833,6 +4094,19 @@ var require_fast_uri = __commonJS({ parsed.path = matches[6] || ""; parsed.query = matches[7]; parsed.fragment = matches[8]; + if (parsed.scheme !== void 0) { + const decodedScheme = unescape(parsed.scheme); + if (VALID_SCHEME.test(decodedScheme)) { + parsed.scheme = decodedScheme.toLowerCase(); + } else { + parsed.error = parsed.error || MALFORMED_SCHEME_ERROR; + malformedScheme = true; + } + } + malformedPercentEncoding = hasMalformedComponentPercentEncoding(matches); + if (malformedPercentEncoding) { + parsed.error = parsed.error || "URI contains malformed percent-encoding."; + } if (isNaN(parsed.port)) { parsed.port = matches[5]; } @@ -3844,9 +4118,16 @@ var require_fast_uri = __commonJS({ if (parsed.host) { const ipv4result = isIPv4(parsed.host); if (ipv4result === false) { + const bracketedIPLiteral = isIPLiteral(parsed.host); + const hasIPLiteralBracket = parsed.host.indexOf("[") !== -1 || parsed.host.indexOf("]") !== -1; const ipv6result = normalizeIPv6(parsed.host); - parsed.host = ipv6result.host.toLowerCase(); - isIP = ipv6result.isIPV6; + isIP = ipv6result.isIPV6 || ipv6result.isIPVFuture === true; + malformedIPLiteral = hasIPLiteralBracket && (!bracketedIPLiteral || ipv6result.error === true); + parsed.host = isIP ? ipv6result.host : ipv6result.host.toLowerCase(); + if (malformedIPLiteral) { + parsed.error = parsed.error || "URI host is malformed."; + malformedAuthorityOrPort = true; + } } else { isIP = true; } @@ -3864,42 +4145,36 @@ var require_fast_uri = __commonJS({ parsed.error = parsed.error || "URI is not a " + options.reference + " reference."; } const schemeHandler = getSchemeHandler(options.scheme || parsed.scheme); - if (!options.unicodeSupport && (!schemeHandler || !schemeHandler.unicodeSupport)) { - if (parsed.host && (options.domainHost || schemeHandler && schemeHandler.domainHost) && isIP === false && nonSimpleDomain(parsed.host)) { - try { - parsed.host = new URL("http://" + parsed.host).hostname; - } catch (e) { - parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e; - } - } + if (!malformedIPLiteral) { + malformedHost = canonicalizeHost(parsed, options, schemeHandler, isIP); } if (!schemeHandler || schemeHandler && !schemeHandler.skipNormalize) { if (uri2.indexOf("%") !== -1) { - if (parsed.scheme !== void 0) { - parsed.scheme = unescape(parsed.scheme); - } - if (parsed.host !== void 0) { - parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP); + if (parsed.host !== void 0 && !malformedIPLiteral) { + const host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true); + parsed.host = reescapeHostDelimiters(host, isIP); } } if (parsed.path) { parsed.path = normalizePathEncoding(parsed.path); } + if (parsed.query) { + parsed.query = normalizeQueryFragmentEncoding(parsed.query); + } if (parsed.fragment) { - try { - parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment)); - } catch { - parsed.error = parsed.error || "URI malformed"; - } + parsed.fragment = normalizeQueryFragmentEncoding(parsed.fragment); } } if (schemeHandler && schemeHandler.parse) { schemeHandler.parse(parsed, options); + if (schemeHandler === SCHEMES.urn && parsed.nid === void 0) { + malformedSchemeSpecific = true; + } } } else { parsed.error = parsed.error || "URI can not be parsed."; } - return { parsed, malformedAuthorityOrPort }; + return { parsed, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme }; } function parse3(uri2, opts) { return parseWithStatus(uri2, opts).parsed; @@ -3908,20 +4183,28 @@ var require_fast_uri = __commonJS({ return normalizeStringWithStatus(uri2, opts).normalized; } function normalizeStringWithStatus(uri2, opts) { - const { parsed, malformedAuthorityOrPort } = parseWithStatus(uri2, opts); + const { parsed, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme } = parseWithStatus(uri2, opts); return { - normalized: malformedAuthorityOrPort ? uri2 : serialize(parsed, opts), - malformedAuthorityOrPort + normalized: malformedAuthorityOrPort || malformedPercentEncoding || malformedSchemeSpecific || malformedHost || malformedScheme ? uri2 : serialize(parsed, opts), + malformedAuthorityOrPort, + malformedPercentEncoding, + malformedSchemeSpecific, + malformedHost, + malformedScheme }; } function normalizeComparableURI(uri2, opts) { - if (typeof uri2 === "string") { - const { normalized, malformedAuthorityOrPort } = normalizeStringWithStatus(uri2, opts); - return malformedAuthorityOrPort ? void 0 : normalized; + if (typeof uri2 !== "string" && typeof uri2 !== "object") { + return void 0; } - if (typeof uri2 === "object") { - return serialize(uri2, opts); + let value; + try { + value = typeof uri2 === "string" ? uri2 : serialize(uri2, opts); + } catch { + return void 0; } + const { normalized, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme } = normalizeStringWithStatus(value, opts); + return malformedAuthorityOrPort || malformedPercentEncoding || malformedSchemeSpecific || malformedHost || malformedScheme ? void 0 : normalized; } var fastUri = { SCHEMES, @@ -31148,7 +31431,9 @@ var BeatAPIClient = class { ...body === void 0 ? {} : { body } }); const payload = await readPayload(response); - if (response.ok) return unwrapData(payload); + if (response.ok) { + return options.responseShape === "raw" ? payload : unwrapData(payload); + } const error51 = errorFromResponse(response, payload); if (attempt >= maxAttempts || !RETRYABLE_STATUS_CODES.has(response.status) || error51.code === "user_concurrency_exceeded") { throw error51; @@ -31187,6 +31472,65 @@ var BeatAPIClient = class { { authenticated: false } ).then((result) => result.data); } + listTextModels() { + return this.request( + "/v1/models", + { responseShape: "raw" } + ).then((result) => result.data); + } + listGenerationModels() { + return this.request( + "/v1/media/models", + { authenticated: false } + ).then((result) => result.data); + } + createImageTask(input) { + return this.request("/v1/images/tasks", { method: "POST", body: input }); + } + createVideoTask(input) { + return this.request("/v1/videos/tasks", { method: "POST", body: input }); + } + listEffects(filters = {}) { + const query = new URLSearchParams(); + if (filters.outputType) query.set("output_type", filters.outputType); + if (filters.category) query.set("category", filters.category); + const suffix = query.size > 0 ? `?${query.toString()}` : ""; + return this.request( + `/v1/effects${suffix}`, + { authenticated: false } + ).then((result) => result.data); + } + getEffect(effectId) { + return this.request(`/v1/effects/${encodePathSegment(effectId)}`, { + authenticated: false + }); + } + createEffectTask(input, options) { + const idempotencyKey = options.idempotencyKey.trim(); + if (!idempotencyKey) { + throw new TypeError("idempotencyKey must not be empty."); + } + return this.request("/v1/effects/tasks", { + method: "POST", + body: input, + headers: { "idempotency-key": idempotencyKey } + }); + } + createTextResponse(input) { + return this.request("/v1/responses", { + method: "POST", + body: input, + responseShape: "raw" + }); + } + createVideoAnalysisTask(input, options = {}) { + const idempotencyKey = options.idempotencyKey?.trim(); + return this.request("/v1/video-analysis/tasks", { + method: "POST", + body: input, + ...idempotencyKey ? { headers: { "idempotency-key": idempotencyKey } } : {} + }); + } getUsage() { return this.request("/v1/usage"); } @@ -31309,6 +31653,8 @@ var MIME_TYPES = { ".m4a": "audio/mp4", ".mp3": "audio/mpeg", ".png": "image/png", + ".mov": "video/quicktime", + ".mp4": "video/mp4", ".srt": "application/x-subrip", ".wav": "audio/wav", ".webp": "image/webp" @@ -31323,6 +31669,24 @@ function without(input, keys) { Object.entries(input).filter(([key]) => !keys.includes(key)) ); } +function nestedObject(input, key) { + const value = input[key]; + if (!value || typeof value !== "object" || Array.isArray(value)) { + throw new TypeError(`${key} must be an object.`); + } + return value; +} +function generationBody(input) { + return { + ...nestedObject(input, "parameters"), + model: stringValue(input, "model") + }; +} +function directApiKeyRequired(capability) { + throw new Error( + `${capability} requires BEATAPI_API_KEY in the plugin Configure screen or host environment. Do not paste the key into chat.` + ); +} function redactText(value) { return value.replace(/\bsk_[A-Za-z0-9_-]{6,}\b/g, "[REDACTED_API_KEY]").replace(/\bwhsec_[A-Za-z0-9_-]{6,}\b/g, "[REDACTED_WEBHOOK_SECRET]").replace(/Bearer\s+[A-Za-z0-9._~-]+/gi, "Bearer [REDACTED]"); } @@ -31468,6 +31832,25 @@ var BeatAPIExecutor = class { if (name === "beatapi_list_workflows") { return sanitize(await this.direct.listWorkflows()); } + if (name === "beatapi_list_text_models") { + if (!this.usesDirectClient) directApiKeyRequired("Text model discovery"); + return sanitize(await this.direct.listTextModels()); + } + if (name === "beatapi_list_generation_models") { + return sanitize(await this.direct.listGenerationModels()); + } + if (name === "beatapi_list_effects") { + return sanitize(await this.direct.listEffects({ + ...typeof input.output_type === "string" ? { outputType: input.output_type } : {}, + ...typeof input.category === "string" ? { category: input.category } : {} + })); + } + if (name === "beatapi_get_effect") { + return sanitize(await this.direct.getEffect(stringValue(input, "effect_id"))); + } + if (!this.usesDirectClient && (name === "beatapi_create_text_response" || name === "beatapi_analyze_video")) { + directApiKeyRequired("This BeatAPI capability"); + } if (!this.usesDirectClient) return this.executeViaCli(name, input); return this.executeDirect(name, input); } @@ -31491,7 +31874,7 @@ var BeatAPIExecutor = class { configured: false, auth_source: null, setup_reason: "cli_not_installed", - next_step: "Install the BeatAPI CLI with `npm install --global beatapi`, then run `beatapi auth login` in a terminal. Do not paste the API key into chat." + next_step: "Use the plugin Configure action to store BEATAPI_API_KEY, or install the BeatAPI CLI with `npm install --global beatapi` and run `beatapi auth login` in a terminal. Do not paste the API key into chat." }; } if (isMissingCliAuthentication(error51)) { @@ -31499,7 +31882,7 @@ var BeatAPIExecutor = class { configured: false, auth_source: null, setup_reason: "authentication_required", - next_step: "Run `beatapi auth login` in a terminal, then check setup again. Do not paste the API key into chat." + next_step: "Use the plugin Configure action to store BEATAPI_API_KEY, or run `beatapi auth login` in a terminal, then check setup again. Do not paste the API key into chat." }; } const detail = cliErrorText(error51); @@ -31513,6 +31896,42 @@ var BeatAPIExecutor = class { switch (name) { case "beatapi_get_usage": return sanitize(await this.direct.getUsage()); + case "beatapi_create_text_response": + return sanitize( + await this.direct.createTextResponse({ + ...nestedObject(input, "request"), + model: stringValue(input, "model"), + stream: false + }) + ); + case "beatapi_create_image": + return sanitize( + await this.direct.createImageTask( + generationBody(input) + ) + ); + case "beatapi_create_video": + return sanitize( + await this.direct.createVideoTask( + generationBody(input) + ) + ); + case "beatapi_create_effect": + return sanitize( + await this.direct.createEffectTask( + without(input, ["idempotency_key"]), + { idempotencyKey: stringValue(input, "idempotency_key") } + ) + ); + case "beatapi_analyze_video": + return sanitize( + await this.direct.createVideoAnalysisTask( + without(input, ["idempotency_key"]), + { + ...typeof input.idempotency_key === "string" ? { idempotencyKey: input.idempotency_key } : {} + } + ) + ); case "beatapi_upload_file": { const path = resolve(stringValue(input, "path")); const info = await stat(path); @@ -31643,6 +32062,31 @@ var BeatAPIExecutor = class { case "beatapi_get_usage": result = await runCli(["usage"]); break; + case "beatapi_create_image": + result = await withJsonFile( + generationBody(input), + (path) => runCli(["images", "create", "--file", path]) + ); + break; + case "beatapi_create_video": + result = await withJsonFile( + generationBody(input), + (path) => runCli(["videos", "create", "--file", path]) + ); + break; + case "beatapi_create_effect": + result = await withJsonFile( + without(input, ["idempotency_key"]), + (path) => runCli([ + "effects", + "create", + "--file", + path, + "--idempotency-key", + stringValue(input, "idempotency_key") + ]) + ); + break; case "beatapi_upload_file": result = await runCli(["files", "upload", resolve(stringValue(input, "path"))]); break; @@ -31838,6 +32282,42 @@ var httpsOrigin = external_exports.string().url().superRefine((value, context) = }); } }); +var generationImages = (max) => external_exports.array(httpsUrl).min(1).max(max); +var generationParameters = external_exports.record(external_exports.string(), external_exports.unknown()).refine((value) => !("model" in value), { + message: "model is a top-level field and must not appear in parameters." +}); +var generationTaskInput = external_exports.object({ + model: id.describe( + "A current public model ID returned by beatapi_list_generation_models." + ), + parameters: generationParameters.describe( + "Model-specific request fields from the bundled OpenAPI contract, excluding model." + ) +}).strict(); +var textRequest = external_exports.record(external_exports.string(), external_exports.unknown()).superRefine((value, context) => { + for (const reserved of ["model", "stream"]) { + if (reserved in value) { + context.addIssue({ + code: "custom", + path: [reserved], + message: `${reserved} is managed by the plugin and must not appear in request.` + }); + } + } +}); +var effectTaskInput = external_exports.object({ + effect_id: id, + effect_version: external_exports.number().int().min(1).optional(), + images: generationImages(7), + options: external_exports.object({ + aspect_ratio: external_exports.string().optional(), + resolution: external_exports.string().optional(), + duration: external_exports.number().int().optional(), + bgm: external_exports.boolean().optional(), + seed: external_exports.number().int().optional() + }).strict().optional(), + idempotency_key: external_exports.string().trim().min(1).max(255) +}).strict(); var musicVideoInput = external_exports.object({ images: imageUrls, audio_url: httpsUrl, @@ -31901,6 +32381,85 @@ var toolDefinitions = [ inputSchema: external_exports.object({}).strict(), annotations: readOnly }, + { + name: "beatapi_list_text_models", + title: "List BeatAPI text models", + description: "List the text model IDs currently enabled for this authenticated BeatAPI account.", + inputSchema: external_exports.object({}).strict(), + annotations: readOnly + }, + { + name: "beatapi_create_text_response", + title: "Create BeatAPI text response", + description: "Paid mutation: create one non-streaming text response. Call only when the user explicitly asks for BeatAPI text generation or names a BeatAPI text model.", + inputSchema: external_exports.object({ + model: id.describe( + "A current text model ID returned by beatapi_list_text_models." + ), + request: textRequest.describe( + "OpenAI Responses-compatible fields excluding model and stream." + ) + }).strict(), + annotations: write + }, + { + name: "beatapi_list_generation_models", + title: "List BeatAPI generation models", + description: "List stable public BeatAPI image and video model aliases and input modes. Authentication is not required.", + inputSchema: external_exports.object({}).strict(), + annotations: readOnly + }, + { + name: "beatapi_create_image", + title: "Create BeatAPI image", + description: "Paid mutation: create one asynchronous image task with a stable BeatAPI model alias.", + inputSchema: generationTaskInput, + annotations: write + }, + { + name: "beatapi_create_video", + title: "Create BeatAPI video", + description: "Paid mutation: create one asynchronous model-specific video task.", + inputSchema: generationTaskInput, + annotations: write + }, + { + name: "beatapi_list_effects", + title: "List BeatAPI Effects", + description: "List active published Effects. Authentication is not required.", + inputSchema: external_exports.object({ + output_type: external_exports.enum(["image", "video"]).optional(), + category: external_exports.string().trim().min(1).optional() + }).strict(), + annotations: readOnly + }, + { + name: "beatapi_get_effect", + title: "Get BeatAPI Effect", + description: "Read one published Effect and its current immutable input contract. Authentication is not required.", + inputSchema: external_exports.object({ effect_id: id }).strict(), + annotations: readOnly + }, + { + name: "beatapi_create_effect", + title: "Create BeatAPI Effect task", + description: "Paid mutation: create one versioned Effect task after validating inputs against the published Effect contract.", + inputSchema: effectTaskInput, + annotations: write + }, + { + name: "beatapi_analyze_video", + title: "Analyze video with BeatAPI", + description: "Paid mutation: create one asynchronous Video Analysis task for a BeatAPI-hosted MP4 or MOV.", + inputSchema: external_exports.object({ + video_url: httpsUrl, + prompt: external_exports.string().trim().min(1).max(12e3), + analysis_depth: external_exports.enum(["standard", "deep"]).optional(), + max_output_tokens: external_exports.number().int().min(256).max(8192).optional(), + idempotency_key: external_exports.string().trim().min(1).max(255).optional() + }).strict(), + annotations: write + }, { name: "beatapi_get_usage", title: "Get BeatAPI usage", diff --git a/mcp/src/executor.ts b/mcp/src/executor.ts index 99e55ec..a302e85 100644 --- a/mcp/src/executor.ts +++ b/mcp/src/executor.ts @@ -16,11 +16,16 @@ import { promisify } from "node:util"; import { BeatAPIClient, BeatAPIError, + type CreateEffectTaskInput, type CreateRealtimeSessionInput, type CreateWebhookInput, type EcommerceVideoTaskInput, + type ImageGenerationTaskInput, type MusicVideoShotEditInput, type MusicVideoTaskInput, + type TextResponseInput, + type VideoAnalysisTaskInput, + type VideoGenerationTaskInput, type UpdateWebhookInput, } from "../vendor/client/index.js"; @@ -33,6 +38,8 @@ const MIME_TYPES: Readonly> = { ".m4a": "audio/mp4", ".mp3": "audio/mpeg", ".png": "image/png", + ".mov": "video/quicktime", + ".mp4": "video/mp4", ".srt": "application/x-subrip", ".wav": "audio/wav", ".webp": "image/webp", @@ -52,6 +59,27 @@ function without(input: T, keys: string[]): Record { + const value = input[key]; + if (!value || typeof value !== "object" || Array.isArray(value)) { + throw new TypeError(`${key} must be an object.`); + } + return value as Record; +} + +function generationBody(input: Input): Record { + return { + ...nestedObject(input, "parameters"), + model: stringValue(input, "model"), + }; +} + +function directApiKeyRequired(capability: string): never { + throw new Error( + `${capability} requires BEATAPI_API_KEY in the plugin Configure screen or host environment. Do not paste the key into chat.`, + ); +} + function redactText(value: string): string { return value .replace(/\bsk_[A-Za-z0-9_-]{6,}\b/g, "[REDACTED_API_KEY]") @@ -247,6 +275,31 @@ export class BeatAPIExecutor { if (name === "beatapi_list_workflows") { return sanitize(await this.direct.listWorkflows()); } + if (name === "beatapi_list_text_models") { + if (!this.usesDirectClient) directApiKeyRequired("Text model discovery"); + return sanitize(await this.direct.listTextModels()); + } + if (name === "beatapi_list_generation_models") { + return sanitize(await this.direct.listGenerationModels()); + } + if (name === "beatapi_list_effects") { + return sanitize(await this.direct.listEffects({ + ...(typeof input.output_type === "string" + ? { outputType: input.output_type as "image" | "video" } + : {}), + ...(typeof input.category === "string" ? { category: input.category } : {}), + })); + } + if (name === "beatapi_get_effect") { + return sanitize(await this.direct.getEffect(stringValue(input, "effect_id"))); + } + if ( + !this.usesDirectClient && + (name === "beatapi_create_text_response" || + name === "beatapi_analyze_video") + ) { + directApiKeyRequired("This BeatAPI capability"); + } if (!this.usesDirectClient) return this.executeViaCli(name, input); return this.executeDirect(name, input); } @@ -272,7 +325,7 @@ export class BeatAPIExecutor { auth_source: null, setup_reason: "cli_not_installed", next_step: - "Install the BeatAPI CLI with `npm install --global beatapi`, then run `beatapi auth login` in a terminal. Do not paste the API key into chat.", + "Use the plugin Configure action to store BEATAPI_API_KEY, or install the BeatAPI CLI with `npm install --global beatapi` and run `beatapi auth login` in a terminal. Do not paste the API key into chat.", }; } if (isMissingCliAuthentication(error)) { @@ -281,7 +334,7 @@ export class BeatAPIExecutor { auth_source: null, setup_reason: "authentication_required", next_step: - "Run `beatapi auth login` in a terminal, then check setup again. Do not paste the API key into chat.", + "Use the plugin Configure action to store BEATAPI_API_KEY, or run `beatapi auth login` in a terminal, then check setup again. Do not paste the API key into chat.", }; } const detail = cliErrorText(error); @@ -296,6 +349,44 @@ export class BeatAPIExecutor { switch (name) { case "beatapi_get_usage": return sanitize(await this.direct.getUsage()); + case "beatapi_create_text_response": + return sanitize( + await this.direct.createTextResponse({ + ...nestedObject(input, "request"), + model: stringValue(input, "model"), + stream: false, + } as TextResponseInput), + ); + case "beatapi_create_image": + return sanitize( + await this.direct.createImageTask( + generationBody(input) as ImageGenerationTaskInput, + ), + ); + case "beatapi_create_video": + return sanitize( + await this.direct.createVideoTask( + generationBody(input) as VideoGenerationTaskInput, + ), + ); + case "beatapi_create_effect": + return sanitize( + await this.direct.createEffectTask( + without(input, ["idempotency_key"]) as CreateEffectTaskInput, + { idempotencyKey: stringValue(input, "idempotency_key") }, + ), + ); + case "beatapi_analyze_video": + return sanitize( + await this.direct.createVideoAnalysisTask( + without(input, ["idempotency_key"]) as VideoAnalysisTaskInput, + { + ...(typeof input.idempotency_key === "string" + ? { idempotencyKey: input.idempotency_key } + : {}), + }, + ), + ); case "beatapi_upload_file": { const path = resolve(stringValue(input, "path")); const info = await stat(path); @@ -423,6 +514,28 @@ export class BeatAPIExecutor { case "beatapi_get_usage": result = await runCli(["usage"]); break; + case "beatapi_create_image": + result = await withJsonFile(generationBody(input), (path) => + runCli(["images", "create", "--file", path]), + ); + break; + case "beatapi_create_video": + result = await withJsonFile(generationBody(input), (path) => + runCli(["videos", "create", "--file", path]), + ); + break; + case "beatapi_create_effect": + result = await withJsonFile(without(input, ["idempotency_key"]), (path) => + runCli([ + "effects", + "create", + "--file", + path, + "--idempotency-key", + stringValue(input, "idempotency_key"), + ]), + ); + break; case "beatapi_upload_file": result = await runCli(["files", "upload", resolve(stringValue(input, "path"))]); break; diff --git a/mcp/src/tools.ts b/mcp/src/tools.ts index b139d59..c0158b6 100644 --- a/mcp/src/tools.ts +++ b/mcp/src/tools.ts @@ -62,6 +62,50 @@ const httpsOrigin = z.string().url().superRefine((value, context) => { }); } }); +const generationImages = (max: number) => z.array(httpsUrl).min(1).max(max); +const generationParameters = z + .record(z.string(), z.unknown()) + .refine((value) => !("model" in value), { + message: "model is a top-level field and must not appear in parameters.", + }); +const generationTaskInput = z + .object({ + model: id.describe( + "A current public model ID returned by beatapi_list_generation_models.", + ), + parameters: generationParameters.describe( + "Model-specific request fields from the bundled OpenAPI contract, excluding model.", + ), + }) + .strict(); + +const textRequest = z + .record(z.string(), z.unknown()) + .superRefine((value, context) => { + for (const reserved of ["model", "stream"]) { + if (reserved in value) { + context.addIssue({ + code: "custom", + path: [reserved], + message: `${reserved} is managed by the plugin and must not appear in request.`, + }); + } + } + }); + +const effectTaskInput = z.object({ + effect_id: id, + effect_version: z.number().int().min(1).optional(), + images: generationImages(7), + options: z.object({ + aspect_ratio: z.string().optional(), + resolution: z.string().optional(), + duration: z.number().int().optional(), + bgm: z.boolean().optional(), + seed: z.number().int().optional(), + }).strict().optional(), + idempotency_key: z.string().trim().min(1).max(255), +}).strict(); const musicVideoInput = z .object({ @@ -135,6 +179,92 @@ export const toolDefinitions: readonly ToolDefinition[] = [ inputSchema: z.object({}).strict(), annotations: readOnly, }, + { + name: "beatapi_list_text_models", + title: "List BeatAPI text models", + description: + "List the text model IDs currently enabled for this authenticated BeatAPI account.", + inputSchema: z.object({}).strict(), + annotations: readOnly, + }, + { + name: "beatapi_create_text_response", + title: "Create BeatAPI text response", + description: + "Paid mutation: create one non-streaming text response. Call only when the user explicitly asks for BeatAPI text generation or names a BeatAPI text model.", + inputSchema: z + .object({ + model: id.describe( + "A current text model ID returned by beatapi_list_text_models.", + ), + request: textRequest.describe( + "OpenAI Responses-compatible fields excluding model and stream.", + ), + }) + .strict(), + annotations: write, + }, + { + name: "beatapi_list_generation_models", + title: "List BeatAPI generation models", + description: "List stable public BeatAPI image and video model aliases and input modes. Authentication is not required.", + inputSchema: z.object({}).strict(), + annotations: readOnly, + }, + { + name: "beatapi_create_image", + title: "Create BeatAPI image", + description: "Paid mutation: create one asynchronous image task with a stable BeatAPI model alias.", + inputSchema: generationTaskInput, + annotations: write, + }, + { + name: "beatapi_create_video", + title: "Create BeatAPI video", + description: "Paid mutation: create one asynchronous model-specific video task.", + inputSchema: generationTaskInput, + annotations: write, + }, + { + name: "beatapi_list_effects", + title: "List BeatAPI Effects", + description: "List active published Effects. Authentication is not required.", + inputSchema: z.object({ + output_type: z.enum(["image", "video"]).optional(), + category: z.string().trim().min(1).optional(), + }).strict(), + annotations: readOnly, + }, + { + name: "beatapi_get_effect", + title: "Get BeatAPI Effect", + description: "Read one published Effect and its current immutable input contract. Authentication is not required.", + inputSchema: z.object({ effect_id: id }).strict(), + annotations: readOnly, + }, + { + name: "beatapi_create_effect", + title: "Create BeatAPI Effect task", + description: "Paid mutation: create one versioned Effect task after validating inputs against the published Effect contract.", + inputSchema: effectTaskInput, + annotations: write, + }, + { + name: "beatapi_analyze_video", + title: "Analyze video with BeatAPI", + description: + "Paid mutation: create one asynchronous Video Analysis task for a BeatAPI-hosted MP4 or MOV.", + inputSchema: z + .object({ + video_url: httpsUrl, + prompt: z.string().trim().min(1).max(12000), + analysis_depth: z.enum(["standard", "deep"]).optional(), + max_output_tokens: z.number().int().min(256).max(8192).optional(), + idempotency_key: z.string().trim().min(1).max(255).optional(), + }) + .strict(), + annotations: write, + }, { name: "beatapi_get_usage", title: "Get BeatAPI usage", diff --git a/mcp/vendor/client/client.ts b/mcp/vendor/client/client.ts index 30d3f2f..c0edccf 100644 --- a/mcp/vendor/client/client.ts +++ b/mcp/vendor/client/client.ts @@ -2,6 +2,7 @@ import { BeatAPIError } from "./errors.js"; import type { components, operations } from "./types.generated.js"; export type BeatAPIWorkflow = components["schemas"]["Workflow"]; +export type BeatAPITextModel = components["schemas"]["TextModel"]; export type BeatAPITaskStatus = components["schemas"]["TaskStatus"]; export type BeatAPITask = components["schemas"]["Task"]; export type BeatAPIUsage = components["schemas"]["Usage"]; @@ -9,6 +10,8 @@ export type BeatAPIFile = components["schemas"]["File"]; export type BeatAPIShotMedia = components["schemas"]["ShotMedia"]; export type BeatAPIWebhook = components["schemas"]["WebhookEndpoint"]; export type BeatAPIRealtimeSession = components["schemas"]["RealtimeSession"]; +export type BeatAPIGenerationModel = components["schemas"]["GenerationModel"]; +export type BeatAPIEffect = components["schemas"]["Effect"]; export type BeatAPIDeleteResult = components["schemas"]["DeleteResponse"]["data"]; export type MusicVideoTaskInput = @@ -25,6 +28,17 @@ export type UpdateWebhookInput = operations["updateWebhookEndpoint"]["requestBody"]["content"]["application/json"]; export type CreateRealtimeSessionInput = operations["createRealtimeSession"]["requestBody"]["content"]["application/json"]; +export type TextResponseInput = + operations["createTextResponse"]["requestBody"]["content"]["application/json"]; +export type TextResponseOutput = components["schemas"]["TextPassthroughResponse"]; +export type VideoAnalysisTaskInput = + operations["createVideoAnalysisTask"]["requestBody"]["content"]["application/json"]; +export type ImageGenerationTaskInput = + operations["createImageGenerationTask"]["requestBody"]["content"]["application/json"]; +export type VideoGenerationTaskInput = + operations["createVideoGenerationTask"]["requestBody"]["content"]["application/json"]; +export type CreateEffectTaskInput = + operations["createEffectTask"]["requestBody"]["content"]["application/json"]; type FetchLike = ( input: string | URL | Request, @@ -50,6 +64,7 @@ interface RequestOptions { body?: unknown | undefined; headers?: HeadersInit | undefined; authenticated?: boolean | undefined; + responseShape?: "beatapi" | "raw" | undefined; retry?: RetryOptions | undefined; } @@ -223,7 +238,11 @@ export class BeatAPIClient { }); const payload = await readPayload(response); - if (response.ok) return unwrapData(payload); + if (response.ok) { + return options.responseShape === "raw" + ? (payload as T) + : unwrapData(payload); + } const error = errorFromResponse(response, payload); if ( @@ -276,6 +295,84 @@ export class BeatAPIClient { ).then((result) => result.data); } + listTextModels(): Promise { + return this.request<{ object: "list"; data: BeatAPITextModel[] }>( + "/v1/models", + { responseShape: "raw" }, + ).then((result) => result.data); + } + + listGenerationModels(): Promise { + return this.request<{ object: "list"; data: BeatAPIGenerationModel[] }>( + "/v1/media/models", + { authenticated: false }, + ).then((result) => result.data); + } + + createImageTask(input: ImageGenerationTaskInput): Promise { + return this.request("/v1/images/tasks", { method: "POST", body: input }); + } + + createVideoTask(input: VideoGenerationTaskInput): Promise { + return this.request("/v1/videos/tasks", { method: "POST", body: input }); + } + + listEffects( + filters: { outputType?: "image" | "video"; category?: string } = {}, + ): Promise { + const query = new URLSearchParams(); + if (filters.outputType) query.set("output_type", filters.outputType); + if (filters.category) query.set("category", filters.category); + const suffix = query.size > 0 ? `?${query.toString()}` : ""; + return this.request<{ object: "list"; data: BeatAPIEffect[] }>( + `/v1/effects${suffix}`, + { authenticated: false }, + ).then((result) => result.data); + } + + getEffect(effectId: string): Promise { + return this.request(`/v1/effects/${encodePathSegment(effectId)}`, { + authenticated: false, + }); + } + + createEffectTask( + input: CreateEffectTaskInput, + options: { idempotencyKey: string }, + ): Promise { + const idempotencyKey = options.idempotencyKey.trim(); + if (!idempotencyKey) { + throw new TypeError("idempotencyKey must not be empty."); + } + return this.request("/v1/effects/tasks", { + method: "POST", + body: input, + headers: { "idempotency-key": idempotencyKey }, + }); + } + + createTextResponse(input: TextResponseInput): Promise { + return this.request("/v1/responses", { + method: "POST", + body: input, + responseShape: "raw", + }); + } + + createVideoAnalysisTask( + input: VideoAnalysisTaskInput, + options: { idempotencyKey?: string } = {}, + ): Promise { + const idempotencyKey = options.idempotencyKey?.trim(); + return this.request("/v1/video-analysis/tasks", { + method: "POST", + body: input, + ...(idempotencyKey + ? { headers: { "idempotency-key": idempotencyKey } } + : {}), + }); + } + getUsage(): Promise { return this.request("/v1/usage"); } diff --git a/mcp/vendor/client/index.ts b/mcp/vendor/client/index.ts index 9aaa491..cae98da 100644 --- a/mcp/vendor/client/index.ts +++ b/mcp/vendor/client/index.ts @@ -2,9 +2,12 @@ export { BeatAPIClient, type BeatAPIClientOptions, type BeatAPIDeleteResult, + type BeatAPIEffect, type BeatAPIFile, + type BeatAPIGenerationModel, type BeatAPIRealtimeSession, type BeatAPIShotMedia, + type BeatAPITextModel, type BeatAPITask, type BeatAPITaskStatus, type BeatAPIUsage, @@ -12,13 +15,19 @@ export { type BeatAPIWorkflow, type CreateWebhookInput, type CreateRealtimeSessionInput, + type CreateEffectTaskInput, type EcommerceVideoTaskInput, + type ImageGenerationTaskInput, type MusicVideoComposeInput, type MusicVideoShotEditInput, type MusicVideoTaskInput, type RetryOptions, + type TextResponseInput, + type TextResponseOutput, type UpdateWebhookInput, type UploadFileOptions, + type VideoAnalysisTaskInput, + type VideoGenerationTaskInput, type WaitForTaskOptions, } from "./client.js"; export { BeatAPIError, type BeatAPIErrorOptions } from "./errors.js"; diff --git a/mcp/vendor/client/types.generated.ts b/mcp/vendor/client/types.generated.ts index d030b36..1b93586 100644 --- a/mcp/vendor/client/types.generated.ts +++ b/mcp/vendor/client/types.generated.ts @@ -4,6 +4,106 @@ */ export interface paths { + "/v1/models": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List available text models + * @description Returns the text models currently enabled for this BeatAPI environment in OpenAI list format. + */ + get: operations["listTextModels"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/responses": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Create a text response + * @description Recommended OpenAI-compatible surface for reasoning, tools, structured outputs, and streaming. + */ + post: operations["createTextResponse"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/chat/completions": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Create a text chat completion + * @description OpenAI Chat Completions-compatible endpoint for existing SDK integrations. + */ + post: operations["createChatCompletion"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/messages": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Create an Anthropic-compatible message + * @description Anthropic Messages-compatible endpoint. Send the BeatAPI key with x-api-key or Bearer authentication. + */ + post: operations["createMessage"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1beta/models/{model}:{action}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Generate text content with a Gemini-compatible request + * @description Gemini-compatible endpoint for generateContent and streamGenerateContent. The BeatAPI key is removed before forwarding. + */ + post: operations["generateGeminiCompatibleContent"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/v1/workflows": { parameters: { query?: never; @@ -21,6 +121,164 @@ export interface paths { patch?: never; trace?: never; }; + "/v1/media/models": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List BeatAPI image and video generation models + * @description Returns stable BeatAPI model aliases and public input modes. Internal execution routing is not part of this contract. + */ + get: operations["listGenerationModels"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/images/tasks": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Create an image generation task + * @description Creates one asynchronous image task. Select the model-specific request + * contract with `model`, save the returned `data.id`, and poll + * `GET /v1/tasks/{task_id}` until the task succeeds or fails. + */ + post: operations["createImageGenerationTask"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/videos/tasks": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Create a video generation task + * @description Creates one asynchronous video task. Select the model-specific request + * contract with `model`, save the returned `data.id`, and poll + * `GET /v1/tasks/{task_id}` until the task succeeds or fails. + */ + post: operations["createVideoGenerationTask"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/effects": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List active Effects + * @description Returns only versioned Effects that have passed BeatAPI publication gates. Internal integration names, template ids, costs, and execution context are never exposed. + */ + get: operations["listEffects"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/effects/{effect_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get an active Effect */ + get: operations["getEffect"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/effects/tasks": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Create an Effect task + * @description Creates an asynchronous image or video Effect task. Read the catalog + * first: image count, accepted input types, output resolution/duration, + * and execution contract are fixed by the selected Effect version. Send + * an `Idempotency-Key`; an exact replay returns the + * accepted task before remote input URLs are revalidated, while a changed + * body returns `idempotency_conflict`. + * + * The USD amount is reserved atomically when accepted, settled on success, and + * fully refunded after a definite processing failure. An uncertain create + * result is not blindly retried and never switches integrations automatically. + */ + post: operations["createEffectTask"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/video-analysis/tasks": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Analyze an uploaded video + * @description Analyze one MP4 or MOV previously uploaded with `POST /v1/files` by the + * current BeatAPI account. `standard` is the default low-cost route; + * `deep` uses the higher-reasoning route. BeatAPI reserves an estimate from + * verified video duration and the output budget, then settles from actual + * input and output token usage. Standard costs $0.36 per 1M input tokens and + * $1.60 per 1M output tokens; Deep costs $0.72 per 1M input tokens and $5.00 + * per 1M output tokens. Each completed task is rounded up to the nearest + * $0.01 because the shared USD balance settles in cents. Save `data.id` and poll the shared Task + * endpoint if the task remains queued for processing capacity. + */ + post: operations["createVideoAnalysisTask"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/v1/music-video/tasks": { parameters: { query?: never; @@ -31,24 +289,30 @@ export interface paths { get?: never; put?: never; /** - * Create Music Video - * @description Music Video requires public HTTPS image URLs and a public HTTPS audio URL. - * Prompt, language, quality, style, lip reference, subtitle, and format - * controls are optional. BeatAPI detects the audio duration before task - * creation and charges the detected billable duration at the selected - * per-second customer-credit rate. If audio duration cannot be detected, - * `duration` is used as the billing fallback. + * Create a Music Video workflow task + * @description Music Video requires public HTTPS media URLs. Requests that omit + * `mv_tier` use `standard` and preserve the existing API behavior. + * Premium retains the configured integration host but uses a distinct + * execution path selected internally by BeatAPI. + * Shared controls include prompt, aspect ratio, subtitles, and the tier's + * billing fallback. Language, quality, `lip_sync`, `lip_ref_url`, + * `srt_url`, and `compose_mode` are Standard-only. Premium uses `mv_mode` + * plus `style` and mode-specific images or `lip_ref_urls`. BeatAPI detects audio duration before task + * creation and records the billable duration in Task usage. If audio + * duration cannot be detected, `duration` is used as the billing fallback. * * Input limits: - * - Images must contain 1-7 public HTTPS URLs. + * - Standard images must contain 1-7 public HTTPS URLs. + * - Premium `sing` and `sing_perform` accept 0-6 scene images and require 1-2 `lip_ref_urls`. + * - Premium `dance` and `perform` require exactly 6 scene images. * - Use png, jpg, jpeg, or webp images; each image should be 50 MB or smaller. * - Image aspect ratio should be between 1:4 and 4:1. - * - Audio must be a public HTTPS mp3, wav, aac, or m4a URL between 10 and 180 seconds. + * - Standard audio must be 10-180 seconds; Premium audio must be 10-300 seconds and contain vocals or lyrics rather than instrumental-only audio. * - The audio file should be 50 MB or smaller. * - `prompt` is optional and must be at most 3000 characters. - * - `lip_ref_url`, when provided, must be a public HTTPS image URL. Use a clear, front-facing close-up face reference for best lip-sync results. - * - `srt_url`, when provided, must point to an `.srt` subtitle file. - * - `duration` is only a billing fallback when BeatAPI cannot detect the audio length; it must be 10-180 seconds and cannot override a detected audio duration. + * - Standard `lip_sync=true` requires `lip_ref_url`. It must be a public HTTPS image URL showing a clear, front-facing close-up face. + * - Standard `srt_url`, when provided, must point to an `.srt` subtitle file. + * - `duration` is only a billing fallback when BeatAPI cannot detect the audio length; Standard accepts 10-180 seconds and Premium accepts 10-300 seconds. It cannot override a detected audio duration. * * BeatAPI validates URL shape, text limits, enum values, combination * limits, and audio duration at task creation. Files uploaded through @@ -56,15 +320,6 @@ export interface paths { * Third-party media URLs must follow the same media requirements and may * be rejected during processing if invalid. * - * Customer pricing: - * - MV 540p standard: 4 credits/s - * - MV 720p standard: 5 credits/s - * - MV 1080p standard: 6 credits/s - * - lip_sync add-on: +2 credits/s - * - MV 720p high: 16 credits/s - * - MV 1080p high: 18 credits/s - * - Ecommerce Video 1080p: 15 credits/s - * * Combination limits: * - `quality=high` is not supported with `resolution=540p`. * - `lip_sync=true` is not supported with `resolution=540p`. @@ -93,10 +348,13 @@ export interface paths { get?: never; put?: never; /** - * Edit Shot + * Edit a Music Video storyboard shot * @description Edit one storyboard shot using its BeatAPI `shot_id`. This operation - * charges BeatAPI customer credits using the selected quality/resolution - * rate and the shot duration. Default shot duration is 5 seconds. + * charges the customer USD balance using the applicable task tier and shot + * duration. Standard edits accept only `prompt`. Premium edits accept + * `prompt` plus up to 6 optional replacement `images`. Generation quality, + * resolution, and shot duration are inherited from the original task and + * are not editable request fields. * When the edit finishes, BeatAPI stores the edited shot media and exposes * it on that shot. The existing final Music Video is not replaced until * you call compose with the selected shot ids. @@ -118,7 +376,7 @@ export interface paths { get?: never; put?: never; /** - * Get Shot Media + * Retrieve a Music Video storyboard shot media URL * @description Materialize one storyboard shot video using its BeatAPI `shot_id`. * If the shot has not been stored yet, BeatAPI retrieves the current shot * video, stores it under BeatAPI media storage, and returns a BeatAPI media @@ -144,9 +402,9 @@ export interface paths { get?: never; put?: never; /** - * Compose Video + * Compose a Music Video task from selected shots * @description Compose selected BeatAPI storyboard shots into the final Music Video. - * This operation charges a fixed 1 BeatAPI customer credit. + * This operation charges a fixed $1 USD. */ post: operations["composeMusicVideoTask"]; delete?: never; @@ -165,8 +423,11 @@ export interface paths { get?: never; put?: never; /** - * Create Ecommerce Video - * @description Ecommerce Video requires product images and an explicit output duration. + * Create an Ecommerce Video workflow task + * @description Ecommerce Video creates a complete product video from public HTTPS product or + * lifestyle images, an explicit duration, and optional creative direction. Upload + * local images with `POST /v1/files`, save the returned Task ID, and poll + * `GET /v1/tasks/{task_id}` until the task succeeds or fails. */ post: operations["createEcommerceVideoTask"]; delete?: never; @@ -206,9 +467,9 @@ export interface paths { put?: never; /** * Create a realtime browser session - * @description Reserve credits and allocate a short-lived BeatAPI realtime session. Send a unique + * @description Reserve the selected USD amount and allocate a short-lived BeatAPI realtime session. Send a unique * `Idempotency-Key`; retries with the same user, key, and body return the same session - * and deterministic short-lived `client_secret` without reserving credits or capacity + * and deterministic short-lived `client_secret` without reserving funds or capacity * twice. The browser receives only that BeatAPI secret and connects with * `@beatapi/realtime`. * @@ -279,26 +540,32 @@ export interface paths { put?: never; /** * Upload a file for workflow inputs - * @description Use file upload when your images, audio, or subtitle files are not + * @description Use file upload when your images, videos, audio, or subtitle files are not * already hosted at public HTTPS URLs. The returned HTTPS URL can be used - * directly in `images`, `audio_url`, or `srt_url`. + * directly in `images`, `reference_videos`, `audio_url`, or `srt_url`. * * Limits: - * - Maximum file size is 50 MB. + * - Images, audio, and subtitles: maximum 50 MB. + * - Motion videos: maximum 100 MB. * - Images: `png`, `jpg`, `jpeg`, `webp` * (`image/png`, `image/jpeg`, `image/webp`). * - Audio: `mp3`, `wav`, `aac`, `m4a` * (`audio/mpeg`, `audio/wav`, `audio/aac`, `audio/mp4`). - * - Audio uploads must be 10-180 seconds. + * - Audio uploads must be 10-300 seconds. The selected Music Video tier applies its own task limit: Standard 10-180 seconds; Premium 10-300 seconds. + * - Motion videos: `mp4`, `mov` (`video/mp4`, `video/quicktime`), 3-30 seconds. Duration and dimensions are detected during upload. * - Subtitles: `srt` (`application/x-subrip`; multipart uploads may use * `text/plain` only when the filename ends in `.srt`). - * - PDF, generic text files, octet-stream uploads, videos, and zip files + * - PDF, generic text files, octet-stream uploads, Matroska videos, and zip files * are not supported for launch. * - Send either multipart form-data with a `file` field, or send the raw * file body with the asset `Content-Type`. + * - `Content-Length` is required and is validated before BeatAPI buffers + * the request body; chunked uploads without a declared length are rejected. * - Returned URLs are HTTPS and long-lived for launch. * - Uploaded audio files are duration-checked during upload. The response * includes `audio_duration_seconds` when the uploaded asset is audio. + * - Uploaded videos are signature-, duration-, and dimension-checked. The + * response includes `video_duration_seconds`, `width`, and `height`. * - Workflow task inputs still require public HTTPS URLs. Localhost, * private network URLs, and data URLs are not accepted. */ @@ -321,8 +588,9 @@ export interface paths { put?: never; /** * Create a webhook endpoint - * @description The signing secret is returned only once at creation time. Store it - * securely. Later responses return a masked secret. + * @description The public API returns the signing secret in full at creation time. Store + * it securely; later public API responses return a masked secret. An + * authenticated dashboard owner can explicitly reveal the secret again. * * BeatAPI sends these headers with each delivery: * - `x-beatapi-event`: `task.succeeded` or `task.failed` @@ -386,8 +654,9 @@ export interface paths { * ``` * * Reject old timestamps to prevent replay attacks. A 5 minute window is - * recommended. Failed deliveries are retried at most 3 times with fixed - * backoff windows of 1 minute, 5 minutes, and 15 minutes. Polling + * recommended. A delivery is attempted at most 3 times total: the initial + * request plus up to 2 retries, with fixed backoff windows of 1 minute and + * 5 minutes. Polling * `GET /v1/tasks/{task_id}` remains the source of truth. */ post: operations["createWebhookEndpoint"]; @@ -417,15 +686,64 @@ export interface paths { trace?: never; }; } -export type webhooks = Record; +export interface webhooks { + taskCompleted: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Receive a BeatAPI task completion event + * @description BeatAPI sends this request to each active endpoint subscribed to the event. + * Verify `x-beatapi-signature` against the exact request body and use polling + * as the source of truth if delivery is delayed or fails. + */ + post: operations["receiveBeatApiTaskEvent"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} export interface components { schemas: { + /** @description Public text model id exposed by BeatAPI. Call GET /v1/models to discover the models enabled for your environment. */ + TextModelId: string; + TextModel: { + id: components["schemas"]["TextModelId"]; + /** @constant */ + object: "model"; + /** @example 1788220800 */ + created: number; + /** @constant */ + owned_by: "beatapi"; + }; + TextModelList: { + /** @constant */ + object: "list"; + data: components["schemas"]["TextModel"][]; + }; + /** @description SDK-compatible text request. BeatAPI preserves supported provider-format fields and streams the matching response format back. */ + TextPassthroughRequest: { + model: components["schemas"]["TextModelId"]; + } & { + [key: string]: unknown; + }; + /** @description Response body in the selected SDK-compatible wire format. */ + TextPassthroughResponse: { + [key: string]: unknown; + }; Workflow: { /** * @example music-video * @enum {string} */ - id: "music-video" | "ecommerce-video"; + id: "music-video" | "ecommerce-video" | "video-analysis"; /** @enum {string} */ object: "workflow"; /** @example Music Video API */ @@ -440,33 +758,60 @@ export interface components { * @example shot_xxx */ id: string; - /** @example 0 */ + /** + * @description Zero-based shot order in the storyboard. + * @example 0 + */ index: number; + /** @description Current lifecycle state for this storyboard shot. */ status: components["schemas"]["TaskStatus"]; - /** @example 5 */ + /** + * @description Planned or generated shot duration in seconds. + * @example 5 + */ duration_seconds?: number; - /** @example Opening lyric shot. */ + /** + * @description Creative instruction used to generate this shot. + * @example Opening lyric shot. + */ prompt?: string; - /** @example Intro */ + /** + * @description Lyric segment aligned with this shot when available. + * @example Intro + */ lyric_text?: string; /** @description Present only after the shot media has been materialized or after a shot edit finishes. */ media?: { - /** @example video */ + /** + * @description Hosted media type for the materialized shot. + * @example video + */ type?: string; /** * Format: uri + * @description BeatAPI-hosted HTTPS URL for the materialized shot. * @example https://media.beatapi.io/outputs/task_8K2qA/shots/0.mp4 */ url?: string; - /** @example video/mp4 */ + /** + * @description MIME type of the hosted shot media. + * @example video/mp4 + */ mime_type?: string; }; - /** @example 1782210000 */ + /** + * @description Unix timestamp when the shot record was created. + * @example 1782210000 + */ created_at: number; - /** @example 1782210300 */ + /** + * @description Unix timestamp when the shot record last changed. + * @example 1782210300 + */ updated_at: number; }; Storyboard: { + /** @description Ordered Music Video storyboard shots. The array may be empty before storyboard generation completes. */ shots: components["schemas"]["StoryboardShot"][]; }; ShotMedia: { @@ -498,68 +843,223 @@ export interface components { request_id: string; }; TaskUsage: { - /** @description BeatAPI customer credits reserved for this task. */ + /** + * Format: double + * @description USD amount reserved for this task. The compatibility field name is retained; 1 Credit equals $1 USD. + */ credits_reserved: number; - /** @description BeatAPI customer credits charged when the task or operation is accepted. */ + /** + * Format: double + * @description USD amount charged when the task or operation is accepted. + */ credits_charged: number; - /** @description Server-detected or request-declared billable duration used for credit calculation. */ + /** @description Server-detected or request-declared billable duration used for USD calculation. */ billable_duration_seconds?: number; - /** @description BeatAPI customer credits settled after successful work. */ + /** + * Format: double + * @description USD amount settled after successful work. + */ credits_settled: number; - /** @description BeatAPI customer credits refunded after failed eligible work. */ + /** + * Format: double + * @description USD amount refunded after failed eligible work. + */ credits_refunded: number; }; Task: { - /** @example task_8K2qA */ + /** + * @description Stable BeatAPI task ID used for polling and support. + * @example task_8K2qA + */ id: string; - /** @enum {string} */ + /** + * @description Object discriminator; always `task`. + * @enum {string} + */ object: "task"; /** + * @description Public task family that determines which capability fields are present. + * @enum {string} + */ + task_kind: "workflow" | "effect" | "image" | "video"; + /** @description Stable BeatAPI workflow, Effect, or generation model ID selected when the task was accepted. */ + capability_id: string; + /** @description Immutable capability version used by this task. Legacy workflow rows are returned as version 1. */ + capability_version: number | null; + /** + * @description Present for workflow tasks; identifies the selected BeatAPI workflow. * @example music-video * @enum {string} */ - workflow: "music-video" | "ecommerce-video"; - status: components["schemas"]["TaskStatus"]; - stage: components["schemas"]["TaskStatus"]; - storyboard?: components["schemas"]["Storyboard"]; - created_at: number; + workflow?: "music-video" | "ecommerce-video" | "video-analysis"; + /** + * @description Present for Effect tasks; stable selected Effect ID. + * @example video-muscle-max + */ + effect_id?: string; + /** + * @description Present for Effect tasks; immutable Effect version used for processing. + * @example 1 + */ + effect_version?: number; + /** + * @description Present when task_kind is image or video. + * @enum {string} + */ + media_type?: "image" | "video"; + /** @description Stable BeatAPI model alias. It is independent from internal execution routing. */ + model?: string; + /** @description Current task lifecycle status. Stop polling at `succeeded` or `failed`; Music Video can also require manual action. */ + status: components["schemas"]["TaskStatus"]; + /** @description Current processing stage, exposed separately so workflow progress can be tracked. */ + stage: components["schemas"]["TaskStatus"]; + /** @description Music Video storyboard metadata when available. */ + storyboard?: components["schemas"]["Storyboard"]; + /** @description Unix timestamp when BeatAPI accepted the task. */ + created_at: number; + /** @description Unix timestamp of the latest task update. */ updated_at: number; + /** @description Terminal Unix timestamp, or null while work is in progress. */ completed_at: number | null; /** @description Output is null until the task succeeds. */ output: null | { + /** @description BeatAPI-hosted result assets. */ media: { - /** @enum {string} */ - type: "video"; - /** Format: uri */ + /** + * @description Result asset type. + * @enum {string} + */ + type: "image" | "video"; + /** + * Format: uri + * @description BeatAPI-hosted HTTPS result URL. + */ url: string; - /** @example video/mp4 */ + /** + * @description Result asset MIME type. + * @example video/mp4 + * @example image/png + * @example image/jpeg + * @example image/webp + */ mime_type: string; }[]; - /** Format: uri */ + /** + * Format: uri + * @description Primary BeatAPI-hosted result URL for clients that need one canonical asset. + */ r2_url: string; + } | { + /** @description Completed video analysis text. */ + text: string; + /** @description Measured token usage used for final USD settlement. */ + usage: { + /** @description Tokens consumed by the prompt and video input. */ + input_tokens: number; + /** @description Tokens consumed by visible output and model reasoning. */ + output_tokens: number; + /** @description Total measured input and output tokens. */ + total_tokens: number; + }; + /** @description Upstream-compatible completion reason. */ + finish_reason: string | null; }; + /** @description USD reservation, settlement, refund, and optional billable duration for this task. */ usage: components["schemas"]["TaskUsage"]; - /** @example req_abc123 */ + /** + * @description Correlation ID to retain for logs and BeatAPI support. + * @example req_abc123 + */ request_id: string; - /** @example processing_timeout */ + /** + * @description Machine-readable terminal failure code, or null when no task failure is recorded. + * @example processing_timeout + */ error_code: string | null; + /** @description Human-readable terminal failure detail, or null when no task failure is recorded. */ error_message: string | null; }; - File: { - /** @example file_3xYz9 */ + Effect: { + /** @example video-muscle-max */ id: string; /** @enum {string} */ + object: "effect"; + /** @example Muscle Transformation */ + name: string; + description: string; + /** @enum {string} */ + output_type: "image" | "video"; + /** @example transformation */ + category: string; + tags: string[]; + input: { + images_min: number; + images_max: number; + accepted_types: ("image/jpeg" | "image/png" | "image/webp")[]; + /** @description Maximum downloaded bytes per input image. When omitted, BeatAPI enforces 50 MB. */ + max_size_mb?: number; + /** @description Maximum decoded width or height. BeatAPI inspects the actual image header before charging. */ + max_dimension_px?: number; + subject_requirements?: string[]; + }; + options: { + aspect_ratios?: string[]; + resolutions?: string[]; + duration_seconds?: number[]; + bgm?: boolean; + seed?: boolean; + }; + preview: { + /** Format: uri */ + cover_url: string | null; + /** Format: uri */ + media_url: string | null; + }; + version: number; + /** @enum {string} */ + status: "testing" | "active" | "paused"; + }; + EffectResponse: { + data: components["schemas"]["Effect"]; + }; + EffectListResponse: { + data: { + /** @enum {string} */ + object: "list"; + data: components["schemas"]["Effect"][]; + }; + }; + File: { + /** + * @description Stable uploaded file ID. + * @example file_3xYz9 + */ + id: string; + /** + * @description Object discriminator; always `file`. + * @enum {string} + */ object: "file"; /** * Format: uri + * @description Long-lived BeatAPI HTTPS URL to use in workflow or model requests. * @example https://media.beatapi.io/inputs/file_3xYz9.mp3 */ url: string; - /** @example inputs/file_3xYz9.mp3 */ + /** + * @description BeatAPI storage key for support and diagnostics. + * @example inputs/file_3xYz9.mp3 + */ key: string; - /** @example audio/mpeg */ + /** + * @description Accepted MIME type detected for the uploaded file. + * @example audio/mpeg + */ mime_type: string; - /** @example 1048576 */ + /** + * @description Uploaded file size in bytes. + * @example 1048576 + */ size_bytes: number; /** * @description Present for uploaded audio files after server-side duration detection. @@ -571,32 +1071,74 @@ export interface components { * @example mp3_frame_scan */ audio_duration_source?: string; - /** @enum {string} */ + /** + * @description Present for MP4/MOV uploads after server-side container inspection. + * @example 15.25 + */ + video_duration_seconds?: number; + /** + * @description Duration and dimension detection method used for the uploaded video. + * @example mp4_boxes + */ + video_duration_source?: string; + /** + * @description Detected pixel width for uploaded images and videos. + * @example 720 + */ + width?: number; + /** + * @description Detected pixel height for uploaded images and videos. + * @example 1280 + */ + height?: number; + /** + * @description File purpose; currently always `input`. + * @enum {string} + */ purpose: "input"; - /** @example 1782210000 */ + /** + * @description Unix timestamp when the file was stored. + * @example 1782210000 + */ created_at: number; }; WebhookEndpoint: { - /** @example wh_9aBcD */ + /** + * @description Stable webhook endpoint ID used for get, update, and delete operations. + * @example wh_9aBcD + */ id: string; - /** @enum {string} */ + /** + * @description Object discriminator; always `webhook_endpoint`. + * @enum {string} + */ object: "webhook_endpoint"; /** * Format: uri + * @description Public HTTPS callback URL receiving subscribed task events. * @example https://example.com/beatapi-webhook */ url: string; - /** @example Production webhook */ + /** + * @description Account-defined label for the endpoint. + * @example Production webhook + */ description: string; + /** @description Task event types delivered to this endpoint. */ events: ("task.succeeded" | "task.failed")[]; - /** @enum {string} */ + /** + * @description Delivery status. Disabled endpoints do not receive events. + * @enum {string} + */ status: "active" | "disabled"; /** * @description Returned in full only when the endpoint is created. Later responses return a masked value. * @example whsec_example_masked */ secret: string; + /** @description Unix timestamp when the endpoint was created. */ created_at: number; + /** @description Unix timestamp when the endpoint last changed. */ updated_at: number; }; WebhookEvent: { @@ -615,128 +1157,1237 @@ export interface components { WorkflowListResponse: { data: components["schemas"]["WorkflowList"]; }; - TaskResponse: { - data: components["schemas"]["Task"]; + GenerationModel: { + /** @enum {string} */ + id: "nano-banana" | "nano-banana-2" | "nano-banana-2-lite" | "nano-banana-pro" | "gpt-image-2" | "seedream-5-pro" | "grok-imagine-image-2.0" | "minimax-h3" | "grok-imagine-video-1.5" | "seedance-2" | "seedance-2-fast" | "seedance-2-mini" | "veo-3.1" | "seedance-2.5" | "kling-3" | "kling-2.6-motion-control" | "kling-3-motion-control" | "wan-3.0" | "wan-3.0-prime" | "happyhorse-1.0" | "happyhorse-1.1" | "minimax-h3-max" | "minimax-h3-max-turbo"; + /** @enum {string} */ + object: "generation_model"; + name: string; + /** @enum {string} */ + media_type: "image" | "video"; + input_modes: ("text" | "image" | "frames" | "reference")[]; }; - Usage: { + GenerationModelList: { /** @enum {string} */ - object: "usage"; - /** @description Current credit balance. It may be negative. */ - credit_balance: number; - total_tasks: number; - credits_settled: number; - credits_refunded: number; - concurrency: { - /** @example 2 */ - limit: number; - /** @description Active processing tasks currently using BeatAPI processing resources. Music Video storyboard_ready and requires_action tasks can have settled credits without counting toward this value. */ - active: number; - }; - by_workflow: { - /** @enum {string} */ - workflow: "music-video" | "ecommerce-video"; - tasks: number; - credits_settled: number; - }[]; - realtime?: { - /** @description Total BeatAPI realtime sessions for this account. */ - sessions: number; - /** @description Credits settled by connected realtime sessions. */ - credits: number; - /** @description Realtime sessions in ready, connecting, or active state. */ - active: number; - }; + object: "list"; + data: components["schemas"]["GenerationModel"][]; }; - UsageResponse: { - data: components["schemas"]["Usage"]; + GenerationModelListResponse: { + data: components["schemas"]["GenerationModelList"]; }; - RealtimeSession: { - id: string; - /** @enum {string} */ - object: "realtime.session"; + ImageGenerationTaskCreateRequest: components["schemas"]["NanoBananaImageRequest"] | components["schemas"]["NanoBanana2ImageRequest"] | components["schemas"]["NanoBanana2LiteImageRequest"] | components["schemas"]["NanoBananaProImageRequest"] | components["schemas"]["GptImage2Request"] | components["schemas"]["Seedream5ProImageRequest"] | components["schemas"]["GrokImagineImage20Request"]; + NanoBananaImageRequest: { /** - * @description Active means BeatAPI accepted the first billing heartbeat after remote output began. + * @description Must be `nano-banana`. (enum property replaced by openapi-typescript) * @enum {string} */ - status: "ready" | "connecting" | "active" | "closed" | "failed" | "expired"; - /** @description Returned only by POST. Give this short-lived BeatAPI secret to the browser SDK; never give the browser an sk_ API key. */ - client_secret?: string; - /** Format: date-time */ - expires_at: string; - /** @enum {integer} */ - max_duration_seconds: 15 | 60 | 300; - allowed_origins: string[]; - credits: { - reserved: number; - settled: number; - refunded: number; - }; - request_id: string; - /** Format: date-time */ - created_at: string; + model: "nano-banana"; + /** @description Generation or image-editing instructions. */ + prompt: string; + /** @description Public HTTPS reference-image URLs. Omit for text-to-image. */ + images?: string[]; /** - * Format: date-time - * @description Time of the first accepted BeatAPI billing heartbeat; null before billing activation. + * @description Output image aspect ratio. + * @default 1:1 + * @enum {string} */ - connected_at: string | null; - /** Format: date-time */ - closed_at: string | null; + aspect_ratio: "1:1" | "9:16" | "16:9" | "3:4" | "4:3" | "3:2" | "2:3" | "5:4" | "4:5" | "21:9" | "auto"; + /** + * @description Output image file format. + * @default png + * @enum {string} + */ + output_format: "png" | "jpeg"; }; - RealtimeSessionResponse: { - data: components["schemas"]["RealtimeSession"]; + NanoBanana2ImageRequest: { + /** + * @description Must be `nano-banana-2`. (enum property replaced by openapi-typescript) + * @enum {string} + */ + model: "nano-banana-2"; + /** @description Generation or image-editing instructions. */ + prompt: string; + /** @description Public HTTPS reference-image URLs. Omit for text-to-image. */ + images?: string[]; + /** + * @description Output image aspect ratio. + * @default 1:1 + * @enum {string} + */ + aspect_ratio: "1:1" | "9:16" | "16:9" | "3:4" | "4:3" | "3:2" | "2:3" | "5:4" | "4:5" | "21:9" | "auto"; + /** + * @description Output resolution tier. + * @default 1K + * @enum {string} + */ + resolution: "1K" | "2K" | "4K"; + /** + * @description Output image file format. + * @default png + * @enum {string} + */ + output_format: "png" | "jpeg"; }; - FileResponse: { - data: components["schemas"]["File"]; + NanoBanana2LiteImageRequest: { + /** + * @description Must be `nano-banana-2-lite`. (enum property replaced by openapi-typescript) + * @enum {string} + */ + model: "nano-banana-2-lite"; + /** @description Generation or image-editing instructions. */ + prompt: string; + /** @description Public HTTPS reference-image URLs. Omit for text-to-image. */ + images?: string[]; + /** + * @description Output image aspect ratio. + * @default 1:1 + * @enum {string} + */ + aspect_ratio: "1:1" | "9:16" | "16:9" | "3:4" | "4:3" | "3:2" | "2:3" | "5:4" | "4:5" | "21:9" | "auto"; + /** + * @description Output image file format. + * @default png + * @enum {string} + */ + output_format: "png" | "jpeg"; }; - WebhookEndpointList: { - /** @enum {string} */ - object: "list"; - data: components["schemas"]["WebhookEndpoint"][]; + NanoBananaProImageRequest: { + /** + * @description Must be `nano-banana-pro`. (enum property replaced by openapi-typescript) + * @enum {string} + */ + model: "nano-banana-pro"; + /** @description Generation or image-editing instructions. */ + prompt: string; + /** @description Public HTTPS reference-image URLs. Omit for text-to-image. */ + images?: string[]; + /** + * @description Output image aspect ratio. + * @default 1:1 + * @enum {string} + */ + aspect_ratio: "1:1" | "2:3" | "3:2" | "3:4" | "4:3" | "4:5" | "5:4" | "9:16" | "16:9" | "21:9" | "auto"; + /** + * @description Output resolution tier. + * @default 1K + * @enum {string} + */ + resolution: "1K" | "2K" | "4K"; + /** + * @description Output image file format. + * @default png + * @enum {string} + */ + output_format: "png" | "jpg"; }; - WebhookEndpointListResponse: { - data: components["schemas"]["WebhookEndpointList"]; + GptImage2Request: { + /** + * @description Must be `gpt-image-2`. (enum property replaced by openapi-typescript) + * @enum {string} + */ + model: "gpt-image-2"; + /** @description Generation or image-editing instructions. */ + prompt: string; + /** @description Public HTTPS reference-image URLs. Omit for text-to-image. */ + images?: string[]; + /** + * @description Output image aspect ratio. + * @default auto + * @enum {string} + */ + aspect_ratio: "auto" | "1:1" | "3:2" | "2:3" | "4:3" | "3:4" | "5:4" | "4:5" | "16:9" | "9:16" | "2:1" | "1:2" | "3:1" | "1:3" | "21:9" | "9:21"; + /** + * @description Output resolution tier. + * @default 1K + * @enum {string} + */ + resolution: "1K" | "2K" | "4K"; }; - WebhookEndpointResponse: { - data: components["schemas"]["WebhookEndpoint"]; + Seedream5ProImageRequest: { + /** + * @description Must be `seedream-5-pro`. (enum property replaced by openapi-typescript) + * @enum {string} + */ + model: "seedream-5-pro"; + /** @description Generation or image-editing instructions. */ + prompt: string; + /** @description Public HTTPS reference-image URLs. Omit for text-to-image. */ + images?: string[]; + /** + * @description Output image aspect ratio. + * @default 1:1 + * @enum {string} + */ + aspect_ratio: "auto" | "1:1" | "4:3" | "3:4" | "16:9" | "9:16" | "3:2" | "2:3" | "21:9"; + /** + * @description Output resolution tier. + * @default 1K + * @enum {string} + */ + resolution: "1K" | "2K" | "4K"; + /** + * @description Output image file format. + * @default png + * @enum {string} + */ + output_format: "png" | "jpeg"; }; - DeleteResponse: { - data: { - id: string; - deleted: boolean; - }; + /** @description Omit `images` for text-to-image. Supply one to five images for editing; `auto` aspect ratio is available only when images are supplied. */ + GrokImagineImage20Request: { + /** + * @description Must be `grok-imagine-image-2.0`. (enum property replaced by openapi-typescript) + * @enum {string} + */ + model: "grok-imagine-image-2.0"; + /** @description Generation or image-editing instructions. */ + prompt: string; + /** @description Public HTTPS reference-image URLs. Omit for text-to-image. */ + images?: string[]; + /** + * @description Output image aspect ratio. `auto` requires at least one image. + * @default 1:1 + * @enum {string} + */ + aspect_ratio: "1:1" | "2:3" | "3:2" | "16:9" | "9:16" | "auto"; }; - Error: { - error: { - /** @enum {string} */ - code: "bad_request" | "unauthorized" | "forbidden" | "not_found" | "insufficient_credits" | "idempotency_conflict" | "user_concurrency_exceeded" | "rate_limit_exceeded" | "processing_unavailable" | "processing_failed" | "processing_timeout" | "result_transfer_failed" | "invalid_signature" | "realtime_disabled" | "realtime_capacity_unavailable" | "realtime_session_expired" | "origin_not_allowed" | "invalid_client_secret" | "transport_not_allowed" | "internal_error"; - message: string; - request_id: string; - /** @description Present on retryable rate-limit or capacity responses when the client should wait before retrying. */ - retry_after_seconds?: number; - }; + VideoGenerationTaskCreateRequest: components["schemas"]["MinimaxH3VideoRequest"] | components["schemas"]["GrokImagineVideo15Request"] | components["schemas"]["Seedance2VideoRequest"] | components["schemas"]["Seedance2FastVideoRequest"] | components["schemas"]["Seedance2MiniVideoRequest"] | components["schemas"]["Veo31VideoRequest"] | components["schemas"]["Seedance25VideoRequest"] | components["schemas"]["Kling3VideoRequest"] | components["schemas"]["Kling26MotionControlVideoRequest"] | components["schemas"]["Kling3MotionControlVideoRequest"] | components["schemas"]["Wan30VideoRequest"] | components["schemas"]["Wan30PrimeVideoRequest"] | components["schemas"]["HappyHorse10VideoRequest"] | components["schemas"]["HappyHorse11VideoRequest"] | components["schemas"]["MinimaxH3MaxVideoRequest"] | components["schemas"]["MinimaxH3MaxTurboVideoRequest"]; + /** @description `images` cannot be combined with any `reference_*` input. An audio reference also requires at least one reference image or video. */ + MinimaxH3VideoRequest: { + /** + * @description Must be `minimax-h3`. (enum property replaced by openapi-typescript) + * @enum {string} + */ + model: "minimax-h3"; + /** @description Video generation instructions. */ + prompt: string; + /** @description One first-frame image or first- and last-frame images as public HTTPS URLs. */ + images?: string[]; + /** @description Public HTTPS image references for multimodal reference generation. */ + reference_images?: string[]; + /** @description Public HTTPS video references for multimodal reference generation. */ + reference_videos?: string[]; + /** @description Public HTTPS audio references for multimodal reference generation. Audio also requires at least one reference image or video. */ + reference_audios?: string[]; + /** + * @description Requested output duration in seconds. + * @default 5 + */ + duration: number; + /** + * @description Text mode defaults to 16:9 and does not accept adaptive. Frame mode always uses adaptive. Reference mode defaults to adaptive and also accepts a concrete ratio. + * @enum {string} + */ + aspect_ratio?: "adaptive" | "21:9" | "16:9" | "4:3" | "1:1" | "3:4" | "9:16"; + /** + * @description Output resolution tier. + * @default 768P + * @enum {string} + */ + resolution: "768P" | "2K"; }; - }; - responses: { - /** @description Missing, invalid, or inactive API key. */ - Unauthorized: { - headers: { - [name: string]: unknown; - }; - content: { - /** - * @example { - * "error": { - * "code": "unauthorized", - * "message": "Missing or invalid API key.", - * "request_id": "req_xxx" - * } - * } - */ - "application/json": components["schemas"]["Error"]; + /** @description `images` accepts one first frame and cannot be combined with `reference_images`. Omit `aspect_ratio` when `images` is supplied. 1080p accepts at most one image. */ + GrokImagineVideo15Request: { + /** + * @description Must be `grok-imagine-video-1.5`. (enum property replaced by openapi-typescript) + * @enum {string} + */ + model: "grok-imagine-video-1.5"; + /** @description Video generation instructions. */ + prompt: string; + /** @description One first-frame image as a public HTTPS URL. */ + images?: string[]; + /** @description One to seven public HTTPS reference images. */ + reference_images?: string[]; + /** + * @description Requested output duration in seconds. + * @default 8 + */ + duration: number; + /** + * @description Output video aspect ratio. Omit when one first-frame image is supplied. + * @default 16:9 + * @enum {string} + */ + aspect_ratio: "1:1" | "16:9" | "9:16" | "3:2" | "2:3" | "auto"; + /** + * @description Output resolution tier. 1080p accepts at most one image. + * @default 480p + * @enum {string} + */ + resolution: "480p" | "720p" | "1080p"; + }; + /** @description `images` cannot be combined with any `reference_*` input. An audio reference also requires at least one reference image or video. */ + Seedance2VideoRequest: { + /** + * @description Must be `seedance-2`. (enum property replaced by openapi-typescript) + * @enum {string} + */ + model: "seedance-2"; + /** @description Video generation instructions. */ + prompt: string; + /** @description One first-frame image or first- and last-frame images as public HTTPS URLs. */ + images?: string[]; + /** @description Public HTTPS image references for multimodal reference generation. */ + reference_images?: string[]; + /** @description Public HTTPS video references for multimodal reference generation. */ + reference_videos?: string[]; + /** @description Public HTTPS audio references. Audio also requires at least one reference image or video. */ + reference_audios?: string[]; + /** + * @description Requested output duration in seconds. + * @default 5 + */ + duration: number; + /** + * @description Output video aspect ratio. + * @default adaptive + * @enum {string} + */ + aspect_ratio: "adaptive" | "21:9" | "16:9" | "4:3" | "1:1" | "3:4" | "9:16"; + /** + * @description Output resolution tier. 4k and 4K are equivalent. 1080p is not supported with reference images. + * @default 720p + * @enum {string} + */ + resolution: "480p" | "720p" | "1080p" | "4k" | "4K"; + /** + * @description Generate synchronized audio with the video. + * @default true + */ + generate_audio: boolean; + }; + /** @description `images` cannot be combined with any `reference_*` input. An audio reference also requires at least one reference image or video. */ + Seedance2FastVideoRequest: { + /** + * @description Must be `seedance-2-fast`. (enum property replaced by openapi-typescript) + * @enum {string} + */ + model: "seedance-2-fast"; + /** @description Video generation instructions. */ + prompt: string; + /** @description One first-frame image or first- and last-frame images as public HTTPS URLs. */ + images?: string[]; + /** @description Public HTTPS image references for multimodal reference generation. */ + reference_images?: string[]; + /** @description Public HTTPS video references for multimodal reference generation. */ + reference_videos?: string[]; + /** @description Public HTTPS audio references. Audio also requires at least one reference image or video. */ + reference_audios?: string[]; + /** + * @description Requested output duration in seconds. + * @default 5 + */ + duration: number; + /** + * @description Output video aspect ratio. + * @default adaptive + * @enum {string} + */ + aspect_ratio: "adaptive" | "21:9" | "16:9" | "4:3" | "1:1" | "3:4" | "9:16"; + /** + * @description Output resolution tier. + * @default 720p + * @enum {string} + */ + resolution: "480p" | "720p"; + /** + * @description Generate synchronized audio with the video. + * @default true + */ + generate_audio: boolean; + }; + /** @description Low-cost Seedance 2.0 route. `images` cannot be combined with any `reference_*` input. Generated audio is not supported. */ + Seedance2MiniVideoRequest: { + /** + * @description Must be `seedance-2-mini`. (enum property replaced by openapi-typescript) + * @enum {string} + */ + model: "seedance-2-mini"; + /** @description Video generation instructions. */ + prompt: string; + /** @description One first-frame image or first- and last-frame images as public HTTPS URLs. */ + images?: string[]; + /** @description Public HTTPS image references for multimodal reference generation. */ + reference_images?: string[]; + /** @description Public HTTPS video references for multimodal reference generation. */ + reference_videos?: string[]; + /** @description Public HTTPS audio references. Audio also requires at least one reference image or video. */ + reference_audios?: string[]; + /** + * @description Requested output duration in seconds. + * @default 5 + */ + duration: number; + /** + * @description Output video aspect ratio. + * @default adaptive + * @enum {string} + */ + aspect_ratio: "adaptive" | "21:9" | "16:9" | "4:3" | "1:1" | "3:4" | "9:16"; + /** + * @description Output resolution tier. + * @default 720p + * @enum {string} + */ + resolution: "480p" | "720p"; + }; + Veo31VideoRequest: (components["schemas"]["Veo31TextOrFrameVideoRequest"] | components["schemas"]["Veo31ReferenceVideoRequest"]) & { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + model: "veo-3.1"; + }; + /** + * @description Veo 3.1 text or first/last-frame generation. Output is fixed at 8 seconds + * and defaults to Quality at 720p. Price depends on quality and resolution. + */ + Veo31TextOrFrameVideoRequest: { + /** + * @description Must be `veo-3.1`. + * @constant + */ + model: "veo-3.1"; + /** @description Video generation instructions. */ + prompt: string; + /** @description One first-frame image or first- and last-frame images as public HTTPS URLs. */ + images?: string[]; + /** + * @description Output video aspect ratio. + * @default 16:9 + * @enum {string} + */ + aspect_ratio: "16:9" | "9:16" | "auto"; + /** + * @description Output video resolution. 4k and 4K are equivalent. Price depends on quality and resolution. + * @default 720p + * @enum {string} + */ + resolution: "720p" | "1080p" | "4k" | "4K"; + /** + * @description Text or frame generation tier. + * @default Quality + * @enum {string} + */ + quality: "Quality" | "Fast" | "Lite"; + /** @description Optional watermark text forwarded to the selected model. */ + watermark?: string; + /** @description Allow prompt translation before generation. */ + enable_translation?: boolean; + }; + /** + * @description Veo 3.1 reference-image generation. Output is fixed at 8 seconds and + * supports the Fast or Lite tier, defaulting to Fast at 720p. Price depends + * on quality and resolution. + */ + Veo31ReferenceVideoRequest: { + /** + * @description Must be `veo-3.1`. + * @constant + */ + model: "veo-3.1"; + /** @description Video generation instructions. */ + prompt: string; + /** @description Public HTTPS reference images. */ + reference_images: string[]; + /** + * @description Output video aspect ratio. + * @default 16:9 + * @enum {string} + */ + aspect_ratio: "16:9" | "9:16" | "auto"; + /** + * @description Output video resolution. 4k and 4K are equivalent. Price depends on quality and resolution. + * @default 720p + * @enum {string} + */ + resolution: "720p" | "1080p" | "4k" | "4K"; + /** + * @description Reference-image generation tier. + * @default Fast + * @enum {string} + */ + quality: "Fast" | "Lite"; + /** @description Optional watermark text forwarded to the selected model. */ + watermark?: string; + /** @description Allow prompt translation before generation. */ + enable_translation?: boolean; + }; + /** @description `images` cannot be combined with any `reference_*` input. Audio-only reference generation is supported. */ + Seedance25VideoRequest: { + /** + * @description Must be `seedance-2.5`. (enum property replaced by openapi-typescript) + * @enum {string} + */ + model: "seedance-2.5"; + /** @description Video generation instructions. */ + prompt: string; + /** @description One first-frame image or first- and last-frame images as public HTTPS URLs. */ + images?: string[]; + /** @description Public HTTPS image references for multimodal reference generation. */ + reference_images?: string[]; + /** @description Public HTTPS video references for multimodal reference generation. */ + reference_videos?: string[]; + /** @description Public HTTPS audio references. Audio-only reference generation is supported. */ + reference_audios?: string[]; + /** + * @description Requested output duration in seconds. + * @default 5 + */ + duration: number; + /** + * @description Output video aspect ratio. + * @default adaptive + * @enum {string} + */ + aspect_ratio: "adaptive" | "21:9" | "16:9" | "4:3" | "1:1" | "3:4" | "9:16"; + /** + * @description Output resolution tier. 480p and 1080p are priced separately; see the pricing page. + * @default 720p + * @enum {string} + */ + resolution: "480p" | "720p" | "1080p"; + /** + * @description Generate synchronized audio with the video. + * @default true + */ + generate_audio: boolean; + /** + * @description Reproducibility seed. Use -1 for a random seed. + * @default -1 + */ + seed: number; + }; + KlingShot: { + /** @description Instructions for this shot. */ + prompt: string; + /** @description Shot duration in seconds. All shot durations must sum to the task duration. */ + duration: number; + }; + /** @description Use 2-4 image URLs or one video URL. A video element may include one audio URL and a 3-8 second segment in milliseconds. */ + KlingElement: { + /** @description Stable name used to reference this element in the prompt. */ + name: string; + /** @description Optional description of the subject or object. */ + description?: string; + /** @description Two to four image URLs, or one video URL. */ + element_input_urls: string[]; + /** @description Optional audio URL used with a video element. */ + element_input_audio_urls?: string[]; + /** @description Video element segment start time in milliseconds. */ + start_time?: number; + /** @description Video element segment end time in milliseconds. The segment must be 3-8 seconds. */ + end_time?: number; + }; + /** @description Multi-shot mode accepts one first-frame image, requires `multi_prompt`, and defaults sound to true. Shot durations must sum to `duration`. */ + Kling3VideoRequest: { + /** + * @description Must be `kling-3`. (enum property replaced by openapi-typescript) + * @enum {string} + */ + model: "kling-3"; + /** @description Video generation instructions. */ + prompt: string; + /** @description One first-frame image or first- and last-frame images as public HTTPS URLs. Multi-shot mode accepts exactly one. */ + images?: string[]; + /** + * @description Requested output duration in seconds. + * @default 5 + */ + duration: number; + /** + * @description Defaults to 16:9 for text generation. Omit it with frame images to adapt to the input aspect ratio. + * @enum {string} + */ + aspect_ratio?: "16:9" | "9:16" | "1:1"; + /** + * @description Output quality tier. + * @default pro + * @enum {string} + */ + resolution: "std" | "pro" | "4K"; + /** @description Generate synchronized sound. Defaults to true in multi-shot mode. */ + sound?: boolean; + /** + * @description Enable storyboard-style multi-shot generation. + * @default false + */ + multi_shots: boolean; + /** @description Shot definitions required when `multi_shots=true`. */ + multi_prompt?: components["schemas"]["KlingShot"][]; + /** @description Up to three reusable subject or object references. */ + elements?: components["schemas"]["KlingElement"][]; + }; + /** + * @description Transfer motion from one uploaded 3–30 second MP4/MOV video to one + * uploaded character image. Both URLs must come from `/v1/files` for the + * current BeatAPI account. BeatAPI detects the reference-video duration + * server-side and reserves USD at the selected per-second rate, rounding + * fractional seconds up. The image must be 10 MB or smaller; the video + * may be up to 100 MB. + */ + Kling26MotionControlVideoRequest: { + /** + * @description Must be `kling-2.6-motion-control`. (enum property replaced by openapi-typescript) + * @enum {string} + */ + model: "kling-2.6-motion-control"; + /** @description Optional motion or scene guidance. */ + prompt?: string; + /** @description Exactly one character-image URL returned by the current account's `/v1/files` upload. */ + images: string[]; + /** @description Exactly one 3–30 second MP4/MOV URL returned by the current account's `/v1/files` upload. Its detected duration determines billing. */ + reference_videos: string[]; + /** + * @description Output resolution and per-second price tier. + * @default 720p + * @enum {string} + */ + resolution: "720p" | "1080p"; + /** + * @description Image orientation supports motion videos up to 10 seconds; video orientation supports up to 30 seconds. + * @default image + * @enum {string} + */ + character_orientation: "image" | "video"; + }; + /** + * @description Kling 3.0 motion transfer using exactly one uploaded image and one + * uploaded 3–30 second MP4/MOV. Both assets must exceed 340 px in width + * and height and use an aspect ratio from 2:5 to 5:2. BeatAPI detects the + * reference-video duration server-side and reserves USD at the selected + * per-second rate, rounding fractional seconds up. + */ + Kling3MotionControlVideoRequest: { + /** + * @description Must be `kling-3-motion-control`. (enum property replaced by openapi-typescript) + * @enum {string} + */ + model: "kling-3-motion-control"; + /** @description Optional motion or scene guidance. */ + prompt?: string; + /** @description Exactly one character-image URL returned by the current account's `/v1/files` upload; maximum 10 MB. */ + images: string[]; + /** @description Exactly one 3–30 second MP4/MOV URL returned by the current account's `/v1/files` upload; maximum 100 MB. Its detected duration determines billing. */ + reference_videos: string[]; + /** + * @description Output resolution and per-second price tier. + * @default 720p + * @enum {string} + */ + resolution: "720p" | "1080p"; + /** + * @description Image orientation supports motion videos up to 10 seconds; video orientation supports up to 30 seconds. + * @default image + * @enum {string} + */ + character_orientation: "image" | "video"; + /** + * @description Preserve the background from the motion video or character image. + * @default input_video + * @enum {string} + */ + background_source: "input_video" | "input_image"; + }; + /** @description Renders 2–30 seconds in a single pass. `images` starts the render from a picture; reference videos and audio travel alongside it. */ + Wan30VideoRequest: { + /** + * @description Must be `wan-3.0`. (enum property replaced by openapi-typescript) + * @enum {string} + */ + model: "wan-3.0"; + /** @description Video generation instructions. */ + prompt: string; + /** @description Public HTTPS images. One starts an image-to-video render; more are used as visual references. */ + images?: string[]; + /** @description Public HTTPS video references. A request that carries one is billed at 1.5x. */ + reference_videos?: string[]; + /** @description Public HTTPS audio references. */ + reference_audios?: string[]; + /** + * @description Requested output duration in seconds. Any whole number in range; there is no long-clip surcharge. + * @default 5 + */ + duration: number; + /** + * @description Output video aspect ratio. + * @default 16:9 + * @enum {string} + */ + aspect_ratio: "16:9" | "9:16" | "1:1" | "4:3" | "3:4"; + /** + * @description Output resolution tier. Price scales with it. + * @default 720p + * @enum {string} + */ + resolution: "480p" | "720p" | "1080p"; + }; + /** @description Renders 2–30 seconds in a single pass. `images` starts the render from a picture; reference videos and audio travel alongside it. */ + Wan30PrimeVideoRequest: { + /** + * @description Must be `wan-3.0-prime`. (enum property replaced by openapi-typescript) + * @enum {string} + */ + model: "wan-3.0-prime"; + /** @description Video generation instructions. */ + prompt: string; + /** @description Public HTTPS images. One starts an image-to-video render; more are used as visual references. */ + images?: string[]; + /** @description Public HTTPS video references. A request that carries one is billed at 1.5x. */ + reference_videos?: string[]; + /** @description Public HTTPS audio references. */ + reference_audios?: string[]; + /** + * @description Requested output duration in seconds. Any whole number in range; there is no long-clip surcharge. + * @default 5 + */ + duration: number; + /** + * @description Output video aspect ratio. + * @default 16:9 + * @enum {string} + */ + aspect_ratio: "16:9" | "9:16" | "1:1" | "4:3" | "3:4"; + /** + * @description Output resolution tier. Price scales with it. + * @default 720p + * @enum {string} + */ + resolution: "480p" | "720p" | "1080p"; + }; + /** @description Image to video only — this model publishes no text-to-video mode, so `images` is required. */ + HappyHorse10VideoRequest: { + /** + * @description Must be `happyhorse-1.0`. (enum property replaced by openapi-typescript) + * @enum {string} + */ + model: "happyhorse-1.0"; + /** @description What should happen in the shot — the motion, the expression, the camera. */ + prompt: string; + /** @description Source images as public HTTPS URLs. At least one is required. */ + images: string[]; + /** + * @description Requested output duration in seconds. + * @default 5 + */ + duration: number; + /** + * @description Output video aspect ratio. + * @default 16:9 + * @enum {string} + */ + aspect_ratio: "16:9" | "9:16" | "1:1" | "4:3" | "3:4"; + /** + * @description Output resolution tier. Price scales with it. + * @default 720p + * @enum {string} + */ + resolution: "720p" | "1080p"; + }; + /** @description Image to video only — this model publishes no text-to-video mode, so `images` is required. */ + HappyHorse11VideoRequest: { + /** + * @description Must be `happyhorse-1.1`. (enum property replaced by openapi-typescript) + * @enum {string} + */ + model: "happyhorse-1.1"; + /** @description What should happen in the shot — the motion, the expression, the camera. */ + prompt: string; + /** @description Source images as public HTTPS URLs. At least one is required. */ + images: string[]; + /** + * @description Requested output duration in seconds. + * @default 5 + */ + duration: number; + /** + * @description Output video aspect ratio. + * @default 16:9 + * @enum {string} + */ + aspect_ratio: "16:9" | "9:16" | "1:1" | "4:3" | "3:4"; + /** + * @description Output resolution tier. Price scales with it. + * @default 720p + * @enum {string} + */ + resolution: "720p" | "1080p"; + }; + /** @description Text to video, or image to video when `images` carries a first frame. A second image becomes the last frame. Output tops out at 768P — MiniMax H3 renders 2K for less per second. */ + MinimaxH3MaxVideoRequest: { + /** + * @description Must be `minimax-h3-max`. (enum property replaced by openapi-typescript) + * @enum {string} + */ + model: "minimax-h3-max"; + /** @description What should happen in the shot — the motion, the expression, the camera. */ + prompt: string; + /** @description Public HTTPS images. One starts the render from a first frame; a second becomes the last frame, in first-to-last order. */ + images?: string[]; + /** + * @description Requested output duration in seconds. Billed per second at the rate for the chosen resolution. If a duration is unsupported, the API returns `400`. + * @default 5 + */ + duration: number; + /** + * @description Output resolution. Upper-case P, and the only two values this model accepts. Price scales with it. + * @default 768P + * @enum {string} + */ + resolution: "480P" | "768P"; + /** @description Reuse a seed to re-render the same motion. A random seed is chosen when omitted. */ + seed?: number; + }; + /** @description H3 Max on a faster stack — the same request contract and the same modes, roughly 2.5x quicker, at half the per-second rate. */ + MinimaxH3MaxTurboVideoRequest: { + /** + * @description Must be `minimax-h3-max-turbo`. (enum property replaced by openapi-typescript) + * @enum {string} + */ + model: "minimax-h3-max-turbo"; + /** @description What should happen in the shot — the motion, the expression, the camera. */ + prompt: string; + /** @description Public HTTPS images. One starts the render from a first frame; a second becomes the last frame, in first-to-last order. */ + images?: string[]; + /** + * @description Requested output duration in seconds. Billed per second at the rate for the chosen resolution. If a duration is unsupported, the API returns `400`. + * @default 5 + */ + duration: number; + /** + * @description Output resolution. Upper-case P, and the only two values this model accepts. Price scales with it. + * @default 768P + * @enum {string} + */ + resolution: "480P" | "768P"; + /** @description Reuse a seed to re-render the same motion. A random seed is chosen when omitted. */ + seed?: number; + }; + TaskResponse: { + /** @description Accepted or current BeatAPI task state. */ + data: components["schemas"]["Task"]; + }; + Usage: { + /** @enum {string} */ + object: "usage"; + /** + * Format: double + * @description Current USD balance. The compatibility field name is retained; 1 Credit equals $1 USD. The balance may be negative. + */ + credit_balance: number; + total_tasks: number; + /** Format: double */ + credits_settled: number; + /** Format: double */ + credits_refunded: number; + concurrency: { + /** @example 2 */ + limit: number; + /** @description Active processing tasks currently using BeatAPI processing resources. Music Video storyboard_ready and requires_action tasks can have settled USD usage without counting toward this value. */ + active: number; + }; + /** @description Compatibility view containing workflow tasks only. Image, video, and Effect tasks are reported under by_capability instead. */ + by_workflow: { + /** @enum {string} */ + workflow: "music-video" | "ecommerce-video" | "video-analysis"; + tasks: number; + /** Format: double */ + credits_settled: number; + }[]; + by_capability: { + /** @enum {string} */ + task_kind: "workflow" | "effect" | "image" | "video"; + capability_id: string; + tasks: number; + /** Format: double */ + credits_settled: number; + }[]; + by_model: { + /** @enum {string} */ + media_type: "image" | "video"; + model: string; + tasks: number; + /** Format: double */ + credits_settled: number; + }[]; + by_api_key: { + api_key_id: string; + title: string; + key_prefix: string; + tasks: number; + /** Format: double */ + credits_settled: number; + }[]; + realtime?: { + /** @description Total BeatAPI realtime sessions for this account. */ + sessions: number; + /** + * Format: double + * @description USD amount settled by connected realtime sessions. + */ + credits: number; + /** @description Realtime sessions in ready, connecting, or active state. */ + active: number; + }; + }; + UsageResponse: { + data: components["schemas"]["Usage"]; + }; + VideoAnalysisTaskCreateRequest: { + /** + * Format: uri + * @description BeatAPI-hosted MP4 or MOV input URL returned by POST /v1/files for the current account. Maximum verified duration is 600 seconds. + */ + video_url: string; + /** @description Analysis instruction. Ask for timestamped output when temporal precision matters. */ + prompt: string; + /** + * @description Standard is the default low-cost route; deep uses the higher-reasoning route. + * @default standard + * @enum {string} + */ + analysis_depth: "standard" | "deep"; + /** + * @description Requested answer budget. Provider-reported output usage can include hidden reasoning tokens above this value; BeatAPI records the variance for audit and settles actual reported usage within the task reservation. + * @default 2048 + */ + max_output_tokens: number; + }; + MusicVideoTaskCreateRequest: components["schemas"]["StandardMusicVideoTaskCreateRequest"] | components["schemas"]["PremiumMusicVideoTaskCreateRequest"]; + StandardMusicVideoTaskCreateRequest: { + /** + * @description May be omitted to preserve the backwards-compatible Standard contract. + * @default standard + * @enum {string} + */ + mv_tier: "standard"; + /** @description Standard scene images. Provide 1-7 public HTTPS PNG, JPEG, or WebP URLs; place the primary subject or opening scene first. Upload local files through `POST /v1/files` and use the returned `data.url`. */ + images: string[]; + /** + * Format: uri + * @description Public HTTPS audio URL; Standard audio must be 10-180 seconds. + */ + audio_url: string; + /** @description Optional creative direction for story, setting, performance, camera, lighting, and pacing. Maximum 3000 characters. */ + prompt?: string; + /** + * @description Dialogue and lyric language used by the Standard workflow. + * @enum {string} + */ + language?: "en" | "zh"; + /** + * @description Generation quality tier. High quality is unavailable at 540p. + * @default standard + * @enum {string} + */ + quality: "standard" | "high"; + /** @description Optional concise visual style, such as cinematic, anime, documentary, or fashion editorial. */ + style?: string; + /** + * @description Target output placement. Set explicitly for the destination player or social feed. + * @enum {string} + */ + aspect_ratio?: "1:1" | "16:9" | "9:16" | "4:3" | "3:4"; + /** + * @description Output resolution. 540p cannot be combined with high quality or lip sync. + * @default 720p + * @enum {string} + */ + resolution: "540p" | "720p" | "1080p"; + /** + * @description Generate lip-synchronized performance. When true, `lip_ref_url` is required. + * @default false + */ + lip_sync: boolean; + /** + * Format: uri + * @description Public HTTPS close-up, front-facing face image used for Standard lip sync. + */ + lip_ref_url?: string; + /** + * @description Burn generated or supplied subtitles into the final video. + * @default false + */ + add_subtitle: boolean; + /** + * @description Subtitle text color as a six-digit hexadecimal value. Used when subtitles are enabled. + * @example #FFFFFF + */ + subtitle_color?: string; + /** + * Format: uri + * @description Optional public HTTPS `.srt` subtitle file. Upload a local subtitle through `POST /v1/files`. + */ + srt_url?: string; + /** @description Billing fallback only; detected audio duration wins. */ + duration?: number; + /** + * @description Auto composes the final Music Video; manual pauses at `requires_action` so shots can be reviewed or edited before compose. + * @default auto + * @enum {string} + */ + compose_mode: "auto" | "manual"; + } & (unknown & { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + mv_tier: "standard"; + }); + PremiumMusicVideoTaskCreateRequest: (components["schemas"]["PremiumMusicVideoSingTaskCreateRequest"] | components["schemas"]["PremiumMusicVideoSingPerformTaskCreateRequest"] | components["schemas"]["PremiumMusicVideoDanceTaskCreateRequest"] | components["schemas"]["PremiumMusicVideoPerformTaskCreateRequest"]) & { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + mv_tier: "premium"; + }; + PremiumMusicVideoTaskRequestBase: { + /** + * @description Selects the Premium Music Video workflow and its mode-specific inputs. + * @enum {string} + */ + mv_tier: "premium"; + /** + * @description Premium performance mode. Sing modes require `lip_ref_urls`; dance and perform require exactly six `images`. + * @enum {string} + */ + mv_mode: "sing" | "sing_perform" | "dance" | "perform"; + /** + * Format: uri + * @description Public HTTPS audio URL; Premium audio must be 10-300 seconds. + */ + audio_url: string; + /** @description Optional creative direction for story, setting, performance, camera, lighting, and pacing. Maximum 3000 characters. */ + prompt?: string; + style?: string; + /** + * @description Target output placement. Set explicitly for the destination player or social feed. + * @enum {string} + */ + aspect_ratio?: "1:1" | "16:9" | "9:16" | "4:3" | "3:4"; + /** + * @description Premium output is fixed to 720p. + * @default 720p + * @enum {string} + */ + resolution: "720p"; + /** + * @description Burn generated subtitles into the final video. + * @default false + */ + add_subtitle: boolean; + /** + * @description Subtitle text color as a six-digit hexadecimal value. Used when subtitles are enabled. + * @example #FFFFFF + */ + subtitle_color?: string; + /** @description Premium billing fallback only; detected audio duration wins. */ + duration?: number; + }; + PremiumMusicVideoSingTaskCreateRequest: components["schemas"]["PremiumMusicVideoTaskRequestBase"] & { + /** @enum {string} */ + mv_mode?: "sing"; + /** @description Optional Premium scene images for sing mode. Provide up to six public HTTPS PNG, JPEG, or WebP URLs. */ + images?: string[]; + /** @description Required for sing mode. One or two public HTTPS close-up, front-facing face images for lip synchronization. */ + lip_ref_urls: string[]; + }; + PremiumMusicVideoSingPerformTaskCreateRequest: components["schemas"]["PremiumMusicVideoTaskRequestBase"] & { + /** @enum {string} */ + mv_mode?: "sing_perform"; + /** @description Optional Premium scene images for sing and perform mode. Provide up to six public HTTPS PNG, JPEG, or WebP URLs. */ + images?: string[]; + /** @description Required for sing and perform mode. One or two public HTTPS close-up, front-facing face images for lip synchronization. */ + lip_ref_urls: string[]; + }; + PremiumMusicVideoDanceTaskCreateRequest: components["schemas"]["PremiumMusicVideoTaskRequestBase"] & { + /** @enum {string} */ + mv_mode?: "dance"; + /** @description Required for dance mode. Provide exactly six public HTTPS PNG, JPEG, or WebP scene images. */ + images: string[]; + }; + PremiumMusicVideoPerformTaskCreateRequest: components["schemas"]["PremiumMusicVideoTaskRequestBase"] & { + /** @enum {string} */ + mv_mode?: "perform"; + /** @description Required for perform mode. Provide exactly six public HTTPS PNG, JPEG, or WebP scene images. */ + images: string[]; + }; + EditMusicVideoShotRequest: { + prompt: string; + /** @description Premium tasks only. Optional replacement scene images; an empty array is treated as omitted. Standard tasks reject this field. */ + images?: string[]; + }; + RealtimeSession: { + /** @description Stable Realtime Session ID used to inspect or close the session. */ + id: string; + /** + * @description Object discriminator; always `realtime.session`. + * @enum {string} + */ + object: "realtime.session"; + /** + * @description Active means BeatAPI accepted the first billing heartbeat after remote output began. + * @enum {string} + */ + status: "ready" | "connecting" | "active" | "closed" | "failed" | "expired"; + /** + * Format: date-time + * @description Time when the unconnected short-lived session credential expires. + */ + expires_at: string; + /** + * @description Maximum selected live duration and billing tier in seconds. + * @enum {integer} + */ + max_duration_seconds: 15 | 60 | 300; + /** @description Exact browser origins authorized to use this Session. */ + allowed_origins: string[]; + /** @description USD reservation, settlement, and refund lifecycle for this Realtime Session. Compatibility field names are retained. */ + credits: { + /** + * Format: double + * @description USD amount reserved when the Session is created. + */ + reserved: number; + /** + * Format: double + * @description USD amount settled after the first accepted billing heartbeat. + */ + settled: number; + /** + * Format: double + * @description USD amount refunded if the Session ends without billing activation. + */ + refunded: number; + }; + /** @description Correlation ID to retain for logs and BeatAPI support. */ + request_id: string; + /** + * Format: date-time + * @description Time when the Session was created. + */ + created_at: string; + /** + * Format: date-time + * @description Time of the first accepted BeatAPI billing heartbeat; null before billing activation. + */ + connected_at: string | null; + /** + * Format: date-time + * @description Time when the Session closed, or null while it remains open. + */ + closed_at: string | null; + }; + RealtimeSessionCreated: components["schemas"]["RealtimeSession"] & { + /** @description Short-lived BeatAPI browser credential returned only by POST. Never expose an sk_ API key to the browser. */ + client_secret: string; + }; + RealtimeSessionCreateResponse: { + /** @description Created Realtime Session including the one-time short-lived browser credential. */ + data: components["schemas"]["RealtimeSessionCreated"]; + }; + RealtimeSessionResponse: { + data: components["schemas"]["RealtimeSession"]; + }; + FileResponse: { + /** @description Uploaded file metadata and the public HTTPS URL to use in later requests. */ + data: components["schemas"]["File"]; + }; + WebhookEndpointList: { + /** @enum {string} */ + object: "list"; + data: components["schemas"]["WebhookEndpoint"][]; + }; + WebhookEndpointListResponse: { + data: components["schemas"]["WebhookEndpointList"]; + }; + WebhookEndpointResponse: { + /** @description Created or retrieved webhook endpoint. Public API responses return the full signing secret at creation and mask it afterward; authenticated dashboard owners can explicitly reveal it again. */ + data: components["schemas"]["WebhookEndpoint"]; + }; + DeleteResponse: { + data: { + id: string; + deleted: boolean; + }; + }; + Error: { + /** @description Structured BeatAPI error. Use `code` for program logic and retain `request_id` for support. */ + error: { + /** + * @description Stable machine-readable error code. + * @enum {string} + */ + code: "bad_request" | "unauthorized" | "forbidden" | "not_found" | "insufficient_credits" | "idempotency_conflict" | "user_concurrency_exceeded" | "rate_limit_exceeded" | "content_policy_violation" | "processing_unavailable" | "processing_failed" | "processing_timeout" | "result_transfer_failed" | "invalid_signature" | "realtime_disabled" | "realtime_capacity_unavailable" | "realtime_session_expired" | "origin_not_allowed" | "invalid_client_secret" | "transport_not_allowed" | "internal_error"; + /** @description Human-readable detail intended for logs and debugging. */ + message: string; + /** @description Correlation ID to retain for BeatAPI support. */ + request_id: string; + /** @description Present on retryable rate-limit or capacity responses when the client should wait before retrying. */ + retry_after_seconds?: number; + }; + }; + }; + responses: { + /** @description Missing, invalid, or inactive API key. */ + Unauthorized: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Missing or invalid API key.", + * "request_id": "req_xxx" + * } + * } + */ + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Invalid request. */ + BadRequest: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad_request", + * "message": "images must contain 1-7 public HTTPS URLs.", + * "request_id": "req_xxx" + * } + * } + */ + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Request rate limit exceeded. */ + RateLimited: { + headers: { + /** @description Seconds to wait before retrying the request. */ + "Retry-After"?: number; + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "rate_limit_exceeded", + * "message": "Too many polling requests. Poll every 5-10 seconds.", + * "request_id": "req_xxx", + * "retry_after_seconds": 12 + * } + * } + */ + "application/json": components["schemas"]["Error"]; }; }; - /** @description Invalid request. */ - BadRequest: { + /** @description BeatAPI could not complete the request because of an internal or storage failure. */ + InternalError: { headers: { [name: string]: unknown; }; @@ -744,8 +2395,8 @@ export interface components { /** * @example { * "error": { - * "code": "bad_request", - * "message": "images must contain 1-7 public HTTPS URLs.", + * "code": "internal_error", + * "message": "Internal error. Contact support with the request_id if the problem continues.", * "request_id": "req_xxx" * } * } @@ -753,35 +2404,397 @@ export interface components { "application/json": components["schemas"]["Error"]; }; }; - /** @description Request rate limit exceeded. */ - RateLimited: { + /** @description BeatAPI processing is temporarily unavailable or did not complete within the processing window. */ + ProcessingUnavailable: { headers: { - /** @description Seconds to wait before retrying the request. */ - "Retry-After"?: number; [name: string]: unknown; }; - content: { - /** - * @example { - * "error": { - * "code": "rate_limit_exceeded", - * "message": "Too many polling requests. Poll every 5-10 seconds.", - * "request_id": "req_xxx", - * "retry_after_seconds": 12 - * } - * } - */ - "application/json": components["schemas"]["Error"]; + content: { + /** + * @example { + * "error": { + * "code": "processing_unavailable", + * "message": "Task processing is temporarily unavailable.", + * "request_id": "req_xxx" + * } + * } + */ + "application/json": components["schemas"]["Error"]; + }; + }; + }; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + listTextModels: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OpenAI-compatible model list */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "object": "list", + * "data": [ + * { + * "id": "gpt-5.6-sol", + * "object": "model", + * "created": 1788220800, + * "owned_by": "beatapi" + * }, + * { + * "id": "gpt-5.6-terra", + * "object": "model", + * "created": 1788220800, + * "owned_by": "beatapi" + * }, + * { + * "id": "gpt-5.6-luna", + * "object": "model", + * "created": 1788220800, + * "owned_by": "beatapi" + * }, + * { + * "id": "claude-fable-5-1", + * "object": "model", + * "created": 1788220800, + * "owned_by": "beatapi" + * } + * ] + * } + */ + "application/json": components["schemas"]["TextModelList"]; + }; + }; + /** @description Invalid or missing BeatAPI API key */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Text API is not enabled for this environment */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Request rate limit exceeded */ + 429: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + createTextResponse: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + /** + * @example { + * "model": "gpt-5.6-sol", + * "input": "Design a resilient webhook retry strategy for a payments API.", + * "reasoning": { + * "effort": "medium" + * }, + * "stream": true + * } + */ + "application/json": components["schemas"]["TextPassthroughRequest"]; + }; + }; + responses: { + /** @description OpenAI-compatible JSON response or server-sent event stream */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TextPassthroughResponse"]; + "text/event-stream": string; + }; + }; + /** @description Invalid or missing BeatAPI API key */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Insufficient BeatAPI USD balance */ + 402: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Rate limit or settlement backlog */ + 429: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Text gateway could not complete the request */ + 502: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Text service is temporarily unavailable */ + 503: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + createChatCompletion: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + /** + * @example { + * "model": "gpt-5.6-terra", + * "messages": [ + * { + * "role": "user", + * "content": "Summarize the attached product requirements into an implementation plan." + * } + * ], + * "stream": true + * } + */ + "application/json": components["schemas"]["TextPassthroughRequest"]; + }; + }; + responses: { + /** @description OpenAI-compatible JSON response or server-sent event stream */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TextPassthroughResponse"]; + "text/event-stream": string; + }; + }; + /** @description Invalid or missing BeatAPI API key */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Insufficient BeatAPI USD balance */ + 402: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Rate limit or settlement backlog */ + 429: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Text gateway could not complete the request */ + 502: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Text service is temporarily unavailable */ + 503: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + createMessage: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + /** + * @example { + * "model": "gpt-5.6-luna", + * "max_tokens": 1024, + * "messages": [ + * { + * "role": "user", + * "content": "Classify this support request and return JSON." + * } + * ] + * } + */ + "application/json": components["schemas"]["TextPassthroughRequest"]; + }; + }; + responses: { + /** @description Anthropic-compatible JSON response or server-sent event stream */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TextPassthroughResponse"]; + "text/event-stream": string; + }; + }; + /** @description Invalid or missing BeatAPI API key */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Insufficient BeatAPI USD balance */ + 402: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Rate limit or settlement backlog */ + 429: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Text gateway could not complete the request */ + 502: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Text service is temporarily unavailable */ + 503: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + generateGeminiCompatibleContent: { + parameters: { + query?: never; + header?: never; + path: { + model: components["schemas"]["TextModelId"]; + action: "generateContent" | "streamGenerateContent"; + }; + cookie?: never; + }; + requestBody: { + content: { + /** + * @example { + * "contents": [ + * { + * "role": "user", + * "parts": [ + * { + * "text": "Explain this architecture decision in three concise bullets." + * } + * ] + * } + * ] + * } + */ + "application/json": { + [key: string]: unknown; + }; + }; + }; + responses: { + /** @description Gemini-compatible JSON response or server-sent event stream */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TextPassthroughResponse"]; + "text/event-stream": string; + }; + }; + /** @description Invalid or missing BeatAPI API key */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Insufficient BeatAPI USD balance */ + 402: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Rate limit or settlement backlog */ + 429: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Text gateway could not complete the request */ + 502: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Text service is temporarily unavailable */ + 503: { + headers: { + [name: string]: unknown; + }; + content?: never; }; }; }; - parameters: never; - requestBodies: never; - headers: never; - pathItems: never; -} -export type $defs = Record; -export interface operations { listWorkflows: { parameters: { query?: never; @@ -813,6 +2826,12 @@ export interface operations { * "object": "workflow", * "name": "Ecommerce Video API", * "description": "Generate product ad videos from product images and a short creative brief." + * }, + * { + * "id": "video-analysis", + * "object": "workflow", + * "name": "Video Analysis API", + * "description": "Analyze an uploaded video with timestamp-aware multimodal reasoning." * } * ] * } @@ -824,77 +2843,319 @@ export interface operations { 429: components["responses"]["RateLimited"]; }; }; - createMusicVideoTask: { + listGenerationModels: { + parameters: { + query?: { + media_type?: "image" | "video"; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Generation model list */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GenerationModelListResponse"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 429: components["responses"]["RateLimited"]; + }; + }; + createImageGenerationTask: { + parameters: { + query?: never; + header?: { + "Idempotency-Key"?: string; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ImageGenerationTaskCreateRequest"]; + }; + }; + responses: { + /** @description Image generation task accepted */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TaskResponse"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + /** @description Insufficient USD balance */ + 402: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Idempotency key conflicts with another request body */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + 429: components["responses"]["RateLimited"]; + }; + }; + createVideoGenerationTask: { + parameters: { + query?: never; + header?: { + "Idempotency-Key"?: string; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["VideoGenerationTaskCreateRequest"]; + }; + }; + responses: { + /** @description Video generation task accepted */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TaskResponse"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + /** @description Insufficient USD balance */ + 402: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Idempotency key conflicts with another request body */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + 429: components["responses"]["RateLimited"]; + }; + }; + listEffects: { + parameters: { + query?: { + output_type?: "image" | "video"; + category?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Active Effect catalog */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["EffectListResponse"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 429: components["responses"]["RateLimited"]; + }; + }; + getEffect: { parameters: { query?: never; header?: never; + path: { + effect_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Effect definition and immutable current version contract */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["EffectResponse"]; + }; + }; + /** @description Effect is unknown or not currently published. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + createEffectTask: { + parameters: { + query?: never; + header?: { + "Idempotency-Key"?: string; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + /** + * @example { + * "effect_id": "video-muscle-max", + * "images": [ + * "https://media.beatapi.io/samples/portrait.png" + * ], + * "options": { + * "resolution": "720p", + * "duration": 12 + * } + * } + */ + "application/json": { + /** + * @description Stable published Effect ID from `GET /v1/effects`. + * @example video-muscle-max + */ + effect_id: string; + /** @description Optional immutable version. Omit to use the current published version. */ + effect_version?: number; + /** @description Public HTTPS input images in the order required by the selected Effect version. Read `GET /v1/effects/{effect_id}` for the exact count and accepted media rules; upload local files with `POST /v1/files`. */ + images: string[]; + /** @description Optional controls supported by the selected Effect version. Omit unsupported controls; the catalog is the source of truth. */ + options?: { + /** @description Requested output aspect ratio when the selected Effect exposes this option. */ + aspect_ratio?: string; + /** @description Requested output resolution when the selected Effect exposes this option. */ + resolution?: string; + /** @description Requested video duration in seconds when the selected Effect exposes this option. */ + duration?: number; + /** @description Include background music when supported by the selected Effect. */ + bgm?: boolean; + /** @description Optional deterministic seed when supported by the selected Effect. */ + seed?: number; + }; + }; + }; + }; + responses: { + /** @description Effect task accepted */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "data": { + * "id": "task_effect123", + * "object": "task", + * "task_kind": "effect", + * "capability_id": "video-muscle-max", + * "capability_version": 1, + * "effect_id": "video-muscle-max", + * "effect_version": 1, + * "status": "queued", + * "stage": "queued", + * "created_at": 1782210000, + * "updated_at": 1782210000, + * "completed_at": null, + * "output": null, + * "usage": { + * "credits_reserved": 1.2, + * "credits_charged": 1.2, + * "credits_settled": 0, + * "credits_refunded": 0 + * }, + * "request_id": "req_effect123", + * "error_code": null, + * "error_message": null + * } + * } + */ + "application/json": components["schemas"]["TaskResponse"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; + /** @description Insufficient USD balance. */ + 402: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Effect or requested version is unavailable. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Idempotency key conflicts with another request body. */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + 429: components["responses"]["RateLimited"]; + }; + }; + createVideoAnalysisTask: { + parameters: { + query?: never; + header?: { + /** + * @description Optional retry key. Reusing the same key with the same normalized request returns the accepted task. + * @example video-analysis-cus_123-01 + */ + "Idempotency-Key"?: string; + }; path?: never; cookie?: never; }; requestBody: { content: { - /** - * @example { - * "images": [ - * "https://media.beatapi.io/samples/neon-singer.png" - * ], - * "audio_url": "https://media.beatapi.io/samples/neon-singer-preview.mp3", - * "prompt": "Neon rooftop performance with metro cutaways and cinematic light trails.", - * "language": "en", - * "quality": "standard", - * "resolution": "720p", - * "compose_mode": "auto" - * } - */ - "application/json": { - /** @description 1-7 public HTTPS image URLs. Use png, jpg, jpeg, or webp images; each image should be 50 MB or smaller, with aspect ratio from 1:4 to 4:1. /v1/files uploads are checked before use; third-party URLs may be rejected during processing if invalid. */ - images: string[]; - /** - * Format: uri - * @description Public HTTPS audio URL. Use mp3, wav, aac, or m4a; file size should be 50 MB or smaller and duration must be 10-180 seconds. - */ - audio_url: string; - /** @description Optional creative prompt, at most 3000 characters. */ - prompt?: string; - /** @enum {string} */ - language?: "en" | "zh"; - lip_sync?: boolean; - /** - * Format: uri - * @description Public HTTPS image URL for lip-sync face reference. Use a clear, front-facing close-up face reference. - */ - lip_ref_url?: string; - /** @description Optional style phrase, at most 200 characters. */ - style?: string; - /** - * @default standard - * @enum {string} - */ - quality?: "standard" | "high"; - /** @enum {string} */ - aspect_ratio?: "1:1" | "16:9" | "9:16" | "4:3" | "3:4"; - /** - * @default 720p - * @enum {string} - */ - resolution?: "540p" | "720p" | "1080p"; - add_subtitle?: boolean; - /** @example #FFFFFF */ - subtitle_color?: string; - /** Format: uri */ - srt_url?: string; - /** @description Billing fallback when audio duration cannot be detected. It must be 10-180 seconds and cannot override a detected audio duration. */ - duration?: number; - /** - * @default auto - * @enum {string} - */ - compose_mode?: "auto" | "manual"; - }; + "application/json": components["schemas"]["VideoAnalysisTaskCreateRequest"]; }; }; responses: { - /** @description Task accepted */ + /** @description Analysis accepted for asynchronous processing. */ 201: { headers: { [name: string]: unknown; @@ -903,26 +3164,26 @@ export interface operations { /** * @example { * "data": { - * "id": "task_8K2qA", + * "id": "task_va8K2qA", * "object": "task", - * "workflow": "music-video", + * "task_kind": "workflow", + * "capability_id": "video-analysis", + * "capability_version": 1, + * "workflow": "video-analysis", * "status": "queued", * "stage": "queued", - * "storyboard": { - * "shots": [] - * }, - * "created_at": 1782210000, - * "updated_at": 1782210000, + * "created_at": 1787385600, + * "updated_at": 1787385600, * "completed_at": null, * "output": null, * "usage": { - * "credits_reserved": 75, - * "credits_charged": 75, - * "billable_duration_seconds": 15, + * "credits_reserved": 0.01, + * "credits_charged": 0.01, + * "billable_duration_seconds": 60, * "credits_settled": 0, * "credits_refunded": 0 * }, - * "request_id": "req_abc123", + * "request_id": "req_va123", * "error_code": null, * "error_message": null * } @@ -933,6 +3194,57 @@ export interface operations { }; 400: components["responses"]["BadRequest"]; 401: components["responses"]["Unauthorized"]; + /** @description Account balance is not sufficient for the reserved analysis envelope. */ + 402: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Idempotency key conflicts with another request body. */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + 429: components["responses"]["RateLimited"]; + }; + }; + createMusicVideoTask: { + parameters: { + query?: never; + header?: { + /** + * @description Optional retry key. Reusing the same key with the same request body returns the accepted task; reusing it with a different body returns `409 idempotency_conflict`. + * @example mv-create-cus_123-01 + */ + "Idempotency-Key"?: string; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["MusicVideoTaskCreateRequest"]; + }; + }; + responses: { + /** @description Task accepted */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TaskResponse"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 401: components["responses"]["Unauthorized"]; /** @description Account balance is not sufficient for this task. */ 402: { headers: { @@ -951,6 +3263,24 @@ export interface operations { "application/json": components["schemas"]["Error"]; }; }; + /** @description The Idempotency-Key was reused with a different body or while another request with that key is still being processed. */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "idempotency_conflict", + * "message": "This Idempotency-Key was already used with a different request body.", + * "request_id": "req_xxx" + * } + * } + */ + "application/json": components["schemas"]["Error"]; + }; + }; /** @description User concurrency exceeded. */ 429: { headers: { @@ -974,7 +3304,13 @@ export interface operations { editMusicVideoShot: { parameters: { query?: never; - header?: never; + header?: { + /** + * @description Optional retry key. Reusing the same key for this task, shot, and request body returns the accepted task without charging the USD amount again; changing any of them returns `409 idempotency_conflict`. + * @example music-edit-task_8K2qA-shot_xxx-01 + */ + "Idempotency-Key"?: string; + }; path: { /** @example task_8K2qA */ task_id: string; @@ -988,26 +3324,12 @@ export interface operations { /** * @example { * "prompt": "Night city chorus with brighter face lighting.", - * "duration": 5, - * "quality": "standard", - * "resolution": "720p" + * "images": [ + * "https://media.beatapi.io/samples/stage.png" + * ] * } */ - "application/json": { - prompt: string; - /** @default 5 */ - duration?: number; - /** - * @default standard - * @enum {string} - */ - quality?: "standard" | "high"; - /** - * @default 720p - * @enum {string} - */ - resolution?: "540p" | "720p" | "1080p"; - }; + "application/json": components["schemas"]["EditMusicVideoShotRequest"]; }; }; responses: { @@ -1022,6 +3344,15 @@ export interface operations { }; 400: components["responses"]["BadRequest"]; 401: components["responses"]["Unauthorized"]; + /** @description Account balance is not sufficient for this shot edit. */ + 402: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; /** @description Task or shot not found. */ 404: { headers: { @@ -1031,6 +3362,18 @@ export interface operations { "application/json": components["schemas"]["Error"]; }; }; + /** @description The Idempotency-Key was reused for a different task, shot, or request body, or the same request is still being processed. */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + 429: components["responses"]["RateLimited"]; + 500: components["responses"]["InternalError"]; + 502: components["responses"]["ProcessingUnavailable"]; }; }; getMusicVideoShotMedia: { @@ -1088,12 +3431,21 @@ export interface operations { "application/json": components["schemas"]["Error"]; }; }; + 429: components["responses"]["RateLimited"]; + 500: components["responses"]["InternalError"]; + 502: components["responses"]["ProcessingUnavailable"]; }; }; composeMusicVideoTask: { parameters: { query?: never; - header?: never; + header?: { + /** + * @description Optional retry key. Reusing the same key for this task and request body returns the accepted task without charging the $1 compose amount again; changing either returns `409 idempotency_conflict`. + * @example music-compose-task_8K2qA-01 + */ + "Idempotency-Key"?: string; + }; path: { /** @example task_8K2qA */ task_id: string; @@ -1127,6 +3479,15 @@ export interface operations { }; 400: components["responses"]["BadRequest"]; 401: components["responses"]["Unauthorized"]; + /** @description Account balance is not sufficient for this compose operation. */ + 402: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; /** @description Task or shot not found. */ 404: { headers: { @@ -1136,12 +3497,30 @@ export interface operations { "application/json": components["schemas"]["Error"]; }; }; + /** @description The Idempotency-Key was reused for a different task or request body, or the same request is still being processed. */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + 429: components["responses"]["RateLimited"]; + 500: components["responses"]["InternalError"]; + 502: components["responses"]["ProcessingUnavailable"]; }; }; createEcommerceVideoTask: { parameters: { query?: never; - header?: never; + header?: { + /** + * @description Optional retry key. Reusing the same key with the same request body returns the accepted task; reusing it with a different body returns `409 idempotency_conflict`. + * @example ecommerce-create-cus_123-01 + */ + "Idempotency-Key"?: string; + }; path?: never; cookie?: never; }; @@ -1158,12 +3537,21 @@ export interface operations { * } */ "application/json": { + /** @description Primary product or scene image first, followed by up to six additional public HTTPS PNG, JPEG, or WebP product or lifestyle images. Upload local files with `POST /v1/files` and use the returned `data.url`. */ images: string[]; + /** @description Required target output duration in seconds and the basis for USD calculation. Allowed range is 10-60 seconds. */ duration: number; + /** @description Optional creative direction, audience, product benefit, offer, tone, scenes, or call to action. Maximum 2000 characters. */ prompt?: string; - /** @enum {string} */ + /** + * @description Target output placement. Use 16:9 for landscape, 9:16 for vertical social, or 1:1 for square placements; set explicitly for stable layout. + * @enum {string} + */ aspect_ratio?: "16:9" | "9:16" | "1:1"; - /** @enum {string} */ + /** + * @description Dialogue and narration language. Use `en` for English or `zh` for Chinese; set explicitly when the prompt contains mixed languages. + * @enum {string} + */ language?: "en" | "zh"; }; }; @@ -1180,6 +3568,9 @@ export interface operations { * "data": { * "id": "task_p9Lm2", * "object": "task", + * "task_kind": "workflow", + * "capability_id": "ecommerce-video", + * "capability_version": 1, * "workflow": "ecommerce-video", * "status": "queued", * "stage": "queued", @@ -1188,8 +3579,8 @@ export interface operations { * "completed_at": null, * "output": null, * "usage": { - * "credits_reserved": 225, - * "credits_charged": 225, + * "credits_reserved": 4.5, + * "credits_charged": 4.5, * "billable_duration_seconds": 15, * "credits_settled": 0, * "credits_refunded": 0 @@ -1223,6 +3614,24 @@ export interface operations { "application/json": components["schemas"]["Error"]; }; }; + /** @description The Idempotency-Key was reused with a different body or while another request with that key is still being processed. */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "idempotency_conflict", + * "message": "This Idempotency-Key was already used with a different request body.", + * "request_id": "req_xxx" + * } + * } + */ + "application/json": components["schemas"]["Error"]; + }; + }; /** @description User concurrency exceeded. */ 429: { headers: { @@ -1281,6 +3690,7 @@ export interface operations { parameters: { query?: never; header: { + /** @example rts-create-cus_123-01 */ "Idempotency-Key": string; }; path?: never; @@ -1300,9 +3710,14 @@ export interface operations { * } */ "application/json": { - /** @enum {integer} */ + /** + * @description Required maximum live session duration in seconds. The USD amount is reserved for the selected 15, 60, or 300 second tier. + * @enum {integer} + */ max_duration_seconds: 15 | 60 | 300; + /** @description Exact browser origins allowed to use the short-lived session secret. */ allowed_origins: string[]; + /** @description Optional server-defined string metadata for your own correlation. Up to 20 keys; keys are at most 64 characters and values at most 256 characters. */ metadata?: { [key: string]: string; }; @@ -1316,12 +3731,36 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["RealtimeSessionResponse"]; + /** + * @example { + * "data": { + * "id": "rts_8K2qA", + * "object": "realtime.session", + * "status": "ready", + * "client_secret": "brt_live_example_short_lived_secret", + * "expires_at": "2026-08-12T10:01:00.000Z", + * "max_duration_seconds": 60, + * "allowed_origins": [ + * "https://app.example.com" + * ], + * "credits": { + * "reserved": 1.2, + * "settled": 0, + * "refunded": 0 + * }, + * "request_id": "req_abc123", + * "created_at": "2026-08-12T10:00:00.000Z", + * "connected_at": null, + * "closed_at": null + * } + * } + */ + "application/json": components["schemas"]["RealtimeSessionCreateResponse"]; }; }; 400: components["responses"]["BadRequest"]; 401: components["responses"]["Unauthorized"]; - /** @description Insufficient credits */ + /** @description Insufficient USD balance */ 402: { headers: { [name: string]: unknown; @@ -1434,29 +3873,60 @@ export interface operations { * @example { * "data": { * "object": "usage", - * "credit_balance": 1080, + * "credit_balance": 21.6, * "total_tasks": 12, - * "credits_settled": 720, - * "credits_refunded": 450, + * "credits_settled": 14.4, + * "credits_refunded": 9, * "concurrency": { * "limit": 2, * "active": 1 * }, * "realtime": { * "sessions": 3, - * "credits": 90, + * "credits": 1.8, * "active": 1 * }, * "by_workflow": [ * { * "workflow": "music-video", * "tasks": 8, - * "credits_settled": 480 + * "credits_settled": 9.6 * }, * { * "workflow": "ecommerce-video", * "tasks": 4, - * "credits_settled": 240 + * "credits_settled": 4.8 + * } + * ], + * "by_capability": [ + * { + * "task_kind": "image", + * "capability_id": "seedream-5-pro", + * "tasks": 3, + * "credits_settled": 0.42 + * }, + * { + * "task_kind": "video", + * "capability_id": "veo-3.1", + * "tasks": 2, + * "credits_settled": 14 + * } + * ], + * "by_model": [ + * { + * "media_type": "image", + * "model": "seedream-5-pro", + * "tasks": 3, + * "credits_settled": 0.42 + * } + * ], + * "by_api_key": [ + * { + * "api_key_id": "key_abc123", + * "title": "Production", + * "key_prefix": "sk_live_abcd", + * "tasks": 12, + * "credits_settled": 14.4 * } * ] * } @@ -1471,7 +3941,10 @@ export interface operations { uploadFile: { parameters: { query?: never; - header?: never; + header: { + /** @description Exact request-body length in bytes. For multipart uploads this includes multipart framing overhead. */ + "Content-Length": number; + }; path?: never; cookie?: never; }; @@ -1483,6 +3956,16 @@ export interface operations { /** @enum {string} */ purpose?: "input"; }; + "image/png": string; + "image/jpeg": string; + "image/webp": string; + "audio/mpeg": string; + "audio/wav": string; + "audio/aac": string; + "audio/mp4": string; + "video/mp4": string; + "video/quicktime": string; + "application/x-subrip": string; }; }; responses: { @@ -1513,6 +3996,8 @@ export interface operations { }; 400: components["responses"]["BadRequest"]; 401: components["responses"]["Unauthorized"]; + 429: components["responses"]["RateLimited"]; + 500: components["responses"]["InternalError"]; }; }; listWebhookEndpoints: { @@ -1557,6 +4042,8 @@ export interface operations { }; }; 401: components["responses"]["Unauthorized"]; + 429: components["responses"]["RateLimited"]; + 500: components["responses"]["InternalError"]; }; }; createWebhookEndpoint: { @@ -1578,9 +4065,14 @@ export interface operations { * } */ "application/json": { - /** Format: uri */ + /** + * Format: uri + * @description Public HTTPS callback URL that accepts BeatAPI task events. Do not use localhost or a private-network URL. + */ url: string; + /** @description Optional internal label for identifying the endpoint in your account. */ description?: string; + /** @description Task events to deliver. Omit to subscribe to both `task.succeeded` and `task.failed`. */ events?: ("task.succeeded" | "task.failed")[]; }; }; @@ -1615,6 +4107,8 @@ export interface operations { }; 400: components["responses"]["BadRequest"]; 401: components["responses"]["Unauthorized"]; + 429: components["responses"]["RateLimited"]; + 500: components["responses"]["InternalError"]; }; }; getWebhookEndpoint: { @@ -1666,6 +4160,8 @@ export interface operations { "application/json": components["schemas"]["Error"]; }; }; + 429: components["responses"]["RateLimited"]; + 500: components["responses"]["InternalError"]; }; }; deleteWebhookEndpoint: { @@ -1698,6 +4194,17 @@ export interface operations { }; }; 401: components["responses"]["Unauthorized"]; + /** @description Webhook endpoint not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + 429: components["responses"]["RateLimited"]; + 500: components["responses"]["InternalError"]; }; }; updateWebhookEndpoint: { @@ -1718,7 +4225,10 @@ export interface operations { * } */ "application/json": { - /** Format: uri */ + /** + * Format: uri + * @description Public HTTPS callback URL that accepts BeatAPI task events. Do not use localhost or a private-network URL. + */ url?: string; description?: string; /** @enum {string} */ @@ -1739,6 +4249,84 @@ export interface operations { }; 400: components["responses"]["BadRequest"]; 401: components["responses"]["Unauthorized"]; + /** @description Webhook endpoint not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + 429: components["responses"]["RateLimited"]; + 500: components["responses"]["InternalError"]; + }; + }; + receiveBeatApiTaskEvent: { + parameters: { + query?: never; + header: { + "x-beatapi-event": "task.succeeded" | "task.failed"; + "x-beatapi-timestamp": string; + "x-beatapi-signature": string; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + /** + * @example { + * "id": "evt_123", + * "event": "task.succeeded", + * "created_at": 1782210300, + * "data": { + * "id": "task_8K2qA", + * "object": "task", + * "task_kind": "video", + * "capability_id": "seedance-2.5", + * "capability_version": null, + * "media_type": "video", + * "model": "seedance-2.5", + * "status": "succeeded", + * "stage": "succeeded", + * "created_at": 1782210000, + * "updated_at": 1782210300, + * "completed_at": 1782210300, + * "output": { + * "media": [ + * { + * "type": "video", + * "url": "https://media.beatapi.io/outputs/task_8K2qA/0.mp4", + * "mime_type": "video/mp4" + * } + * ], + * "r2_url": "https://media.beatapi.io/outputs/task_8K2qA/0.mp4" + * }, + * "usage": { + * "credits_reserved": 1.55, + * "credits_charged": 1.55, + * "billable_duration_seconds": 5, + * "credits_settled": 1.55, + * "credits_refunded": 0 + * }, + * "request_id": "req_abc123", + * "error_code": null, + * "error_message": null + * } + * } + */ + "application/json": components["schemas"]["WebhookEvent"]; + }; + }; + responses: { + /** @description Event accepted */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; }; } diff --git a/package-lock.json b/package-lock.json index 9f398af..481edd2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -978,9 +978,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", - "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", + "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", "dev": true, "funding": [ { @@ -1153,9 +1153,9 @@ } }, "node_modules/hono": { - "version": "4.12.30", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.30.tgz", - "integrity": "sha512-emn+JoJjrN9YTpRDS5it/UI2SO9BAE37T6I3d963RxcZ81G9A4pr2SZTEiiaiKbzx+NKRg5BZ89fCL7gCJCUog==", + "version": "4.13.3", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.3.tgz", + "integrity": "sha512-r8AO2mYHoLxSHkgafNeC/BXyb2vWRxD3jem4Ts+ptav8oTG5FIRifAjuJEmZI4bSvvc2ns0GxmIYiZnHqN3mMw==", "dev": true, "license": "MIT", "engines": { @@ -1208,9 +1208,9 @@ "license": "ISC" }, "node_modules/ip-address": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", - "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.5.0.tgz", + "integrity": "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==", "dev": true, "license": "MIT", "engines": { @@ -1444,9 +1444,9 @@ } }, "node_modules/qs": { - "version": "6.15.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", - "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { diff --git a/package.json b/package.json index a2cc04d..f1fb1f0 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "beatapi-codex-plugin", "version": "0.2.0", "private": true, - "description": "Codex plugin for BeatAPI async and realtime AI video APIs.", + "description": "Codex plugin for BeatAPI image, video, Effect, workflow, and Realtime APIs.", "type": "module", "scripts": { "build": "node scripts/build-mcp.mjs", diff --git a/scripts/build-mcp.mjs b/scripts/build-mcp.mjs index 50ae371..cec00bc 100644 --- a/scripts/build-mcp.mjs +++ b/scripts/build-mcp.mjs @@ -1,4 +1,5 @@ import { build } from "esbuild"; +import { createHash } from "node:crypto"; import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; @@ -20,9 +21,33 @@ const result = await build({ const output = result.outputFiles?.[0]?.contents; if (!output) throw new Error("esbuild did not produce the MCP server bundle."); +function bundleMismatchDetails(current, generated) { + generated = Buffer.from(generated); + const limit = Math.min(current.length, generated.length); + let offset = 0; + while (offset < limit && current[offset] === generated[offset]) offset += 1; + const line = current.subarray(0, offset).toString("utf8").split("\n").length; + const start = Math.max(0, offset - 120); + const end = offset + 240; + const digest = (value) => createHash("sha256").update(value).digest("hex"); + return [ + `first difference: byte ${offset}, line ${line}`, + `committed: ${current.length} bytes, sha256 ${digest(current)}`, + `generated: ${generated.length} bytes, sha256 ${digest(generated)}`, + `committed snippet: ${JSON.stringify(current.subarray(start, end).toString("utf8"))}`, + `generated snippet: ${JSON.stringify(generated.subarray(start, end).toString("utf8"))}`, + ].join("\n"); +} + if (check) { - if (!existsSync(outfile) || !readFileSync(outfile).equals(output)) { - throw new Error("mcp/server.mjs is stale. Run npm run build."); + if (!existsSync(outfile)) { + throw new Error("mcp/server.mjs is missing. Run npm run build."); + } + const current = readFileSync(outfile); + if (!current.equals(output)) { + throw new Error( + `mcp/server.mjs is stale. Run npm run build.\n${bundleMismatchDetails(current, output)}`, + ); } console.log("MCP server bundle is current."); } else { diff --git a/skills/beatapi-video/SKILL.md b/skills/beatapi-video/SKILL.md index 232de6d..315cac5 100644 --- a/skills/beatapi-video/SKILL.md +++ b/skills/beatapi-video/SKILL.md @@ -1,9 +1,9 @@ --- name: beatapi-video -description: Create, monitor, and troubleshoot BeatAPI Music Video, Ecommerce Video, and Realtime Video sessions through bundled BeatAPI MCP tools when available or the official BeatAPI CLI as a fallback. Use when a user wants to generate an AI video, create or inspect a realtime browser session, upload workflow media, check credits and concurrency, manage storyboard shots, retrieve hosted output, configure webhooks, or diagnose a BeatAPI API error. +description: Create, monitor, and troubleshoot BeatAPI text, image, video, Effect, Music Video, Ecommerce Video, Video Analysis, and Realtime tasks through bundled BeatAPI MCP tools when available or the official BeatAPI CLI as a fallback. Use when a user explicitly wants a BeatAPI model or needs to generate media, run a published Effect, analyze video, upload media, inspect tasks, check balance and concurrency, configure webhooks, or diagnose a BeatAPI API error. --- -# BeatAPI Video +# BeatAPI Agent Toolkit Treat the bundled OpenAPI snapshot as the exact API contract. @@ -13,41 +13,59 @@ Prefer the bundled BeatAPI MCP tools when `beatapi_check_setup` is available. Use `beatapi_*` tools for the complete workflow and do not shell out to the CLI for the same operation. -When BeatAPI MCP tools are unavailable, fall back to the official `beatapi` CLI. -The Skills-only distribution requires Node.js 20.19+ or 22.12+ and -`npm install --global beatapi`. +When BeatAPI MCP tools are unavailable, fall back to the official `beatapi` CLI +for commands it supports, or use the bundled OpenAPI contract from trusted +server-side code. The Skills-only distribution requires Node.js 20.19+ or +22.12+ and `npm install --global beatapi`. ## Protect the account - Use the customer's existing BeatAPI account and API key. -- Read credentials only through the MCP setup tool, `beatapi auth`, or - `BEATAPI_API_KEY`. +- Read credentials only through host plugin configuration, the MCP setup tool, + `beatapi auth`, or `BEATAPI_API_KEY`. - Never request a key in chat, pass it as a command argument, print it, or place it in JSON, source files, logs, screenshots, or issue text. -- Treat task creation, shot editing, and composition as paid mutations. -- Consider an explicit request to generate or edit authorization for that - operation. Ask before spending credits only when the request is ambiguous, +- Treat text, image, video, Effect, workflow, Video Analysis, Realtime, + shot-editing, and composition creation as paid mutations. +- Consider an explicit request to generate, analyze, or edit authorization for + that operation. Ask before spending only when the request is ambiguous, material settings are missing, or the operation expands beyond the request. -- Never describe a queued or processing task as a completed video. +- Never describe a queued or processing task as completed output. ## Establish readiness 1. With MCP, call `beatapi_check_setup`. If configured, use its usage result; otherwise follow its exact next step. -2. Without MCP, check `beatapi --version`, then run `beatapi auth status`. -3. If the CLI is missing, instruct the user to install it; install it only when +2. If the host shows a plugin **Configure** action, store `BEATAPI_API_KEY` + there. This keeps the secret outside chat and repository + files. Configure `BEATAPI_BASE_URL` only for an authorized custom endpoint. +3. Without MCP, check `beatapi --version`, then run `beatapi auth status`. +4. If the CLI is missing, instruct the user to install it; install it only when the user has authorized environment changes. -4. If authentication is absent, ask the user to run `beatapi auth login` in a - terminal or set `BEATAPI_API_KEY`. Do not ask them to paste the key into the - conversation. -5. Before a paid operation, call `beatapi_get_usage` or run `beatapi usage`. - Check both credit balance and active concurrency. - -Skip credential checks for anonymous `beatapi_list_workflows` or -`beatapi workflows list`. - -## Choose the workflow - +5. If authentication is absent, ask the user to run `beatapi auth login` in a + terminal or set `BEATAPI_API_KEY` in the host environment. Do not ask them + to paste the key into the conversation. +6. Before a paid operation, call `beatapi_get_usage` or run `beatapi usage`. + Check both USD balance and active concurrency. + +Skip credential checks for anonymous workflow, generation-model, and Effect +discovery. Text-model discovery requires authentication. + +## Choose the capability + +- Choose text generation only when the user explicitly asks for a BeatAPI text + model or explicitly asks to use BeatAPI for text. Do not intercept ordinary + writing, summarization, or chat requests. Use `beatapi_list_text_models` + before model selection and `beatapi_create_text_response` with `stream: false`. +- Choose Image generation for one hosted still image. Choose Video generation + for one hosted model-specific video. Read + [generation-and-effects.md](references/generation-and-effects.md) before + selecting a model or request shape. +- Choose an Effect only after listing and reading its current published input + contract. Effects can return an image or video. +- Choose Video Analysis when the user wants structured analysis of a public + video URL. Use `beatapi_analyze_video`; the result follows the shared async + task lifecycle. - Choose Music Video when the user supplies audio plus 1-7 visual references. - Choose automatic Music Video composition unless the user wants to inspect, select, reorder, or edit storyboard shots. @@ -59,16 +77,16 @@ Skip credential checks for anonymous `beatapi_list_workflows` or session. Read [realtime-video.md](references/realtime-video.md) first. The agent may manage the server-side session but does not own camera permission, WebRTC negotiation, or browser rendering. -- Do not force unrelated video editing, transcription, generic image - generation, or non-BeatAPI API design tasks into this Skill. +- Do not force unrelated editing, transcription, ordinary writing, or + non-BeatAPI API design tasks into this Skill. Read [credits-and-limits.md](references/credits-and-limits.md) when estimating cost or validating media and generation settings. ## Prepare inputs -1. Inspect local paths and public URLs before spending credits. -2. Upload each supported local image, audio file, or SRT subtitle with +1. Inspect local paths and public URLs before spending. +2. Upload each supported local image, audio file, video, or SRT subtitle with `beatapi_upload_file`. With the CLI fallback: ```bash @@ -84,6 +102,44 @@ cost or validating media and generation settings. Reject unsupported media, private-network URLs, localhost URLs, data URLs, and unknown fields instead of guessing. +## Execute text generation + +1. Confirm the user explicitly selected BeatAPI text generation. +2. Call `beatapi_list_text_models` and choose only a returned model ID. +3. Call `beatapi_create_text_response` with the requested input and + `stream: false`. The plugin does not expose a streaming transport. +4. Return the provider-compatible response without claiming an async media + task was created. + +## Execute image, video, or Effect generation + +1. Read [generation-and-effects.md](references/generation-and-effects.md). +2. Discover the current model or Effect before selecting it. +3. Copy the matching image, video, or Effect template to a temporary file. +4. Validate the exact model-specific or Effect-version-specific fields against + the bundled OpenAPI contract. +5. With MCP, call `beatapi_create_image`, `beatapi_create_video`, or + `beatapi_create_effect`. With a CLI version that supports these commands: + + ```bash + beatapi images create --file /tmp/beatapi-image.json + beatapi videos create --file /tmp/beatapi-video.json + beatapi effects create --file /tmp/beatapi-effect.json \ + --idempotency-key effect_request_123 + ``` + +6. Preserve the task ID and wait through the shared task endpoint. + +## Execute Video Analysis + +1. Ensure the input is a public HTTPS video URL, uploading a local file first. +2. Validate `prompt`, optional `analysis_depth`, and output-token limits against + the bundled OpenAPI contract. +3. Call `beatapi_analyze_video`, preserve the task ID, and wait with + `beatapi_wait_for_task`. +4. Return analysis only from a succeeded task. Preserve request and error IDs + on failure. + ## Execute automatic Music Video 1. Copy `assets/music-video.auto.json` to a temporary working file. @@ -120,7 +176,12 @@ unknown fields instead of guessing. - Inspect one task with `beatapi_get_task` or `beatapi tasks get TASK_ID`. - Discover workflows with `beatapi_list_workflows` or `beatapi workflows list`. -- Inspect balance and concurrency with `beatapi_get_usage` or `beatapi usage`. +- Discover generation model aliases with `beatapi_list_generation_models` or + `beatapi models list`; discover Effects with `beatapi_list_effects` or + `beatapi effects list`. +- Discover authenticated text models with `beatapi_list_text_models`. +- Inspect USD balance and concurrency with `beatapi_get_usage` or + `beatapi usage`. - Manage webhook endpoints with the `beatapi_*_webhook` tools or `beatapi webhooks list|create|get|update|delete`. - Read [api-workflows.md](references/api-workflows.md) for the exact MCP, CLI, @@ -153,14 +214,15 @@ unknown fields instead of guessing. Return: -- workflow and task ID; +- capability and task ID when the operation is async; - final or actionable status; - hosted output URL(s) only when present; -- credits charged, settled, or refunded when useful; +- USD amount charged, settled, or refunded when useful; compatibility response + fields can still use `credits_*` names; - `request_id`, `error_code`, and `error_message` for failures; - the next required action for `storyboard_ready` or `requires_action`. -Call a generation complete only when status is `succeeded` and +Call async media generation complete only when status is `succeeded` and `output.media[]` contains hosted media. Treat `GET /v1/tasks/{task_id}` as the source of truth even when webhooks are configured. @@ -169,7 +231,7 @@ source of truth even when webhooks are configured. Read [errors-and-recovery.md](references/errors-and-recovery.md) before retrying. In particular: -- do not retry authentication, validation, insufficient-credit, or +- do not retry authentication, validation, insufficient-balance, or concurrency errors unchanged; - honor `Retry-After` for rate limits; - bound retries for network and retryable server failures; diff --git a/skills/beatapi-video/agents/openai.yaml b/skills/beatapi-video/agents/openai.yaml index ba49804..a3e4906 100644 --- a/skills/beatapi-video/agents/openai.yaml +++ b/skills/beatapi-video/agents/openai.yaml @@ -1,7 +1,7 @@ interface: - display_name: "BeatAPI Video" - short_description: "Manage BeatAPI async and realtime video APIs" - default_prompt: "Use $beatapi-video to create an AI video and return the verified hosted result." + display_name: "BeatAPI Agent Toolkit" + short_description: "Manage BeatAPI text, media, workflow, and realtime APIs" + default_prompt: "Use $beatapi-video to choose the current BeatAPI capability, execute the requested work, and return a verified result." policy: allow_implicit_invocation: true diff --git a/skills/beatapi-video/assets/effect-task.json b/skills/beatapi-video/assets/effect-task.json new file mode 100644 index 0000000..d471857 --- /dev/null +++ b/skills/beatapi-video/assets/effect-task.json @@ -0,0 +1,6 @@ +{ + "effect_id": "replace-with-effect-id", + "images": [ + "https://replace-with-public-input.example/image.png" + ] +} diff --git a/skills/beatapi-video/assets/image-generation.json b/skills/beatapi-video/assets/image-generation.json new file mode 100644 index 0000000..dcee7b4 --- /dev/null +++ b/skills/beatapi-video/assets/image-generation.json @@ -0,0 +1,4 @@ +{ + "model": "replace-with-discovered-model-id", + "prompt": "Replace with the requested image generation instructions." +} diff --git a/skills/beatapi-video/assets/video-generation.json b/skills/beatapi-video/assets/video-generation.json new file mode 100644 index 0000000..bea2f81 --- /dev/null +++ b/skills/beatapi-video/assets/video-generation.json @@ -0,0 +1,4 @@ +{ + "model": "replace-with-discovered-model-id", + "prompt": "Replace with the requested video generation instructions." +} diff --git a/skills/beatapi-video/evals/evals.json b/skills/beatapi-video/evals/evals.json index 0958668..fb9b23d 100644 --- a/skills/beatapi-video/evals/evals.json +++ b/skills/beatapi-video/evals/evals.json @@ -122,6 +122,71 @@ "Does not claim browser media success from ready status" ], "files": [] + }, + { + "id": 10, + "prompt": "Use a current BeatAPI image model and these two reference images to generate a 16:9 campaign still. Go ahead and create it.", + "expected_output": "Should discover the current generation catalog, let the selected model contract determine the accepted fields, upload or validate both references, check USD balance and concurrency, create one image task, poll it, and report only hosted image output.", + "assertions": [ + "Discovers generation models", + "Uses a current returned model ID", + "Validates model-specific image inputs", + "Treats generation as a paid mutation", + "Requires hosted image output before success" + ], + "files": ["reference-one.png", "reference-two.png"] + }, + { + "id": 11, + "prompt": "Use a current BeatAPI video model to make a 10-second video using these image and audio references.", + "expected_output": "Should discover the current catalog, select only a returned model ID, inspect and upload local references, validate the exact model request variant and reference dependencies, create one video task, and wait for hosted output.", + "assertions": [ + "Discovers generation models", + "Uses a current returned model ID", + "Validates reference input combinations", + "Does not expose internal provider routing", + "Waits for hosted video output" + ], + "files": ["character.png", "motion.mp4", "voice.mp3"] + }, + { + "id": 12, + "prompt": "Show me the current portrait Effects, then run the one I choose on this image.", + "expected_output": "Should list active Effects without authentication, present publication-safe choices, wait for the user's Effect selection, read that Effect's current version contract, upload and validate the image, use a stable idempotency key for the paid create call, and poll the shared task endpoint.", + "assertions": [ + "Lists active Effects before creation", + "Waits for the user to choose an Effect", + "Reads the selected Effect contract", + "Uses a stable idempotency key", + "Does not infer internal templates or providers" + ], + "files": ["portrait.png"] + }, + { + "id": 13, + "prompt": "I explicitly asked for BeatAPI text. List the available text models and use one to summarize this launch note.", + "expected_output": "Should verify authentication, list BeatAPI text models, choose only a returned ID, call the non-streaming BeatAPI Responses surface, and return the provider-compatible response. It must not activate for ordinary writing requests without explicit BeatAPI intent.", + "assertions": [ + "Requires explicit BeatAPI text intent", + "Lists authenticated text models", + "Uses only a returned model ID", + "Forces stream false", + "Does not expose the API key" + ], + "files": [] + }, + { + "id": 14, + "prompt": "Analyze this product-demo video with BeatAPI and identify the key scenes and usability issues.", + "expected_output": "Should upload a local video or validate its public HTTPS URL, check setup and usage, create a Video Analysis task with the user's prompt, wait on the shared task endpoint, and report analysis only from a succeeded task.", + "assertions": [ + "Chooses BeatAPI Video Analysis", + "Uses a public HTTPS video URL", + "Treats analysis as a paid mutation", + "Waits for the shared task result", + "Preserves request and error IDs on failure" + ], + "files": ["product-demo.mp4"] } ] } diff --git a/skills/beatapi-video/references/api-workflows.md b/skills/beatapi-video/references/api-workflows.md index 213fe66..bff66c9 100644 --- a/skills/beatapi-video/references/api-workflows.md +++ b/skills/beatapi-video/references/api-workflows.md @@ -2,7 +2,9 @@ Base URL: `https://api.beatapi.io` -Authentication: Bearer API key for every endpoint except `GET /v1/workflows`. +Authentication: Bearer API key for mutations and account data. Workflow, +generation-model, and Effect discovery endpoints are anonymous. Text-model +discovery requires authentication. Prefer bundled MCP tools when available. Otherwise use the CLI so credential handling and output separation stay consistent. @@ -10,6 +12,15 @@ handling and output separation stay consistent. | --- | --- | --- | --- | | Check setup | `beatapi_check_setup` | `beatapi auth status` | `GET /v1/usage` | | Discover workflows | `beatapi_list_workflows` | `beatapi workflows list` | `GET /v1/workflows` | +| Discover text models | `beatapi_list_text_models` | Client/OpenAPI | `GET /v1/models` | +| Create text response | `beatapi_create_text_response` | Client/OpenAPI | `POST /v1/responses` | +| Discover generation models | `beatapi_list_generation_models` | `beatapi models list` | `GET /v1/media/models` | +| Create image task | `beatapi_create_image` | `beatapi images create --file INPUT` | `POST /v1/images/tasks` | +| Create video task | `beatapi_create_video` | `beatapi videos create --file INPUT` | `POST /v1/videos/tasks` | +| List Effects | `beatapi_list_effects` | `beatapi effects list` | `GET /v1/effects` | +| Read Effect | `beatapi_get_effect` | `beatapi effects get EFFECT` | `GET /v1/effects/{effect_id}` | +| Create Effect task | `beatapi_create_effect` | `beatapi effects create --file INPUT` | `POST /v1/effects/tasks` | +| Analyze video | `beatapi_analyze_video` | Client/OpenAPI | `POST /v1/video-analysis/tasks` | | Check usage | `beatapi_get_usage` | `beatapi usage` | `GET /v1/usage` | | Upload local media | `beatapi_upload_file` | `beatapi files upload PATH` | `POST /v1/files` | | Create Music Video | `beatapi_create_music_video` | `beatapi music-video create --file INPUT` | `POST /v1/music-video/tasks` | diff --git a/skills/beatapi-video/references/beatapi.openapi.yaml b/skills/beatapi-video/references/beatapi.openapi.yaml index 65e1dc9..a988a34 100644 --- a/skills/beatapi-video/references/beatapi.openapi.yaml +++ b/skills/beatapi-video/references/beatapi.openapi.yaml @@ -6,11 +6,15 @@ info: name: BeatAPI Terms of Service url: https://beatapi.io/terms-of-service description: | - BeatAPI provides async video workflows and short-lived Realtime Video - Sessions behind one BeatAPI-native API. Async integrations create a task, - poll until it finishes, then read the hosted video URL from `output.media`. - Realtime browser integrations create a Session with the same Bearer API key, - then pass only the returned BeatAPI `client_secret` to `@beatapi/realtime`. + BeatAPI provides one API key for public text models, image generation, video generation, + video analysis, Effects, asynchronous video workflows, and short-lived Realtime Video Sessions. + + For asynchronous operations, create a task, poll the shared task endpoint or receive webhook events, + and read hosted output URLs from `output.media`. For Realtime, create a Session on a trusted server + with your Bearer API key and pass only the returned short-lived `client_secret` to the browser. + + Customer balances and usage are USD-denominated. Compatibility fields such + as `credit_balance` and `credits_reserved` remain in the API; 1 Credit = $1 USD. ## 5 minute Quick Start @@ -18,7 +22,7 @@ info: 2. Create an API key in [Dashboard → API Keys](https://beatapi.io/dashboard/apikeys) and send it as `Authorization: Bearer `. - Credit packs are available from + USD balance top-ups are available from [Dashboard → Billing](https://beatapi.io/dashboard/billing). 3. Use public HTTPS URLs for input media. If your files are local, upload them with `POST /v1/files` first. @@ -27,7 +31,8 @@ info: 6. Add webhooks later if you do not want to poll. ```bash - export BEATAPI_API_KEY="sk_your_key" + read -rsp "BeatAPI API key: " BEATAPI_API_KEY && echo + export BEATAPI_API_KEY curl https://api.beatapi.io/v1/workflows @@ -69,15 +74,15 @@ info: Public workflow inputs must use HTTPS URLs that are reachable from the public internet. Localhost, private network URLs, and data URLs are rejected. - Use `POST /v1/files` for local images, audio, or subtitles. + Use `POST /v1/files` for local images, audio, subtitles, or Motion Control reference videos. - Each verified new user account starts with 50 welcome credits valid for 14 days and - 1 active processing concurrency. Lifetime paid credit purchases unlock higher limits: + Each verified new user account starts with a $2 welcome balance that never expires and + 1 active processing concurrency. Lifetime paid purchases unlock higher limits: $10+ = 2, $100+ = 5, $1,000+ = 10, $5,000+ = 15, and $20,000+ = 30. `GET /v1/usage` returns current usage totals, concurrency limit, and active processing task count. Active concurrency measures tasks that are currently using BeatAPI processing resources. storyboard_ready and requires_action - tasks can have settled credits but do not count toward active processing + tasks can have settled USD usage but do not count toward active processing concurrency. ## Webhooks are optional @@ -95,8 +100,16 @@ tags: description: Discover the workflow IDs available for task creation. - name: Music Video description: Create music video tasks from images, audio, and optional creative controls. + - name: Effects + description: Discover versioned effects and create image or video effect tasks. + - name: Generation + description: Discover BeatAPI generation models and create image or video tasks. + - name: Text + description: Discover enabled text models and call them through OpenAI, Anthropic, or Gemini-compatible request formats. - name: Ecommerce Video description: Create product ad video tasks from product images and duration. + - name: Video Analysis + description: Analyze uploaded videos with standard or deep multimodal reasoning. - name: Tasks description: Poll task status and read output URLs. - name: Usage @@ -107,6 +120,70 @@ tags: description: Upload local assets and use the returned HTTPS URL as workflow input. - name: Webhooks description: Manage optional completion callbacks. +webhooks: + taskCompleted: + post: + operationId: receiveBeatApiTaskEvent + x-fern-ignore: true + tags: [Webhooks] + summary: Receive a BeatAPI task completion event + description: | + BeatAPI sends this request to each active endpoint subscribed to the event. + Verify `x-beatapi-signature` against the exact request body and use polling + as the source of truth if delivery is delayed or fails. + security: [] + parameters: + - in: header + name: x-beatapi-event + required: true + schema: { type: string, enum: [task.succeeded, task.failed] } + - in: header + name: x-beatapi-timestamp + required: true + schema: { type: string } + - in: header + name: x-beatapi-signature + required: true + schema: { type: string } + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/WebhookEvent' } + example: + id: evt_123 + event: task.succeeded + created_at: 1782210300 + data: + id: task_8K2qA + object: task + task_kind: video + capability_id: seedance-2.5 + capability_version: null + media_type: video + model: seedance-2.5 + status: succeeded + stage: succeeded + created_at: 1782210000 + updated_at: 1782210300 + completed_at: 1782210300 + output: + media: + - type: video + url: https://media.beatapi.io/outputs/task_8K2qA/0.mp4 + mime_type: video/mp4 + r2_url: https://media.beatapi.io/outputs/task_8K2qA/0.mp4 + usage: + credits_reserved: 1.55 + credits_charged: 1.55 + billable_duration_seconds: 5 + credits_settled: 1.55 + credits_refunded: 0 + request_id: req_abc123 + error_code: null + error_message: null + responses: + '200': { description: Event accepted } components: securitySchemes: BearerAuth: @@ -114,14 +191,61 @@ components: scheme: bearer bearerFormat: sk_xxx description: 'Send your API key as `Authorization: Bearer `.' + ApiKeyHeader: + type: apiKey + in: header + name: x-api-key + description: Anthropic-compatible API key header. Use a BeatAPI API key. + GoogleApiKeyHeader: + type: apiKey + in: header + name: x-goog-api-key + description: Gemini-compatible API key header. Use a BeatAPI API key. + GoogleApiKeyQuery: + type: apiKey + in: query + name: key + description: Gemini SDK compatibility only. Prefer the x-goog-api-key header when possible. schemas: + TextModelId: + type: string + description: Public text model id exposed by BeatAPI. Call GET /v1/models to discover the models enabled for your environment. + TextModel: + type: object + additionalProperties: false + required: [id, object, created, owned_by] + properties: + id: { $ref: '#/components/schemas/TextModelId' } + object: { type: string, const: model } + created: { type: integer, example: 1788220800 } + owned_by: { type: string, const: beatapi } + TextModelList: + type: object + additionalProperties: false + required: [object, data] + properties: + object: { type: string, const: list } + data: + type: array + items: { $ref: '#/components/schemas/TextModel' } + TextPassthroughRequest: + type: object + description: SDK-compatible text request. BeatAPI preserves supported provider-format fields and streams the matching response format back. + required: [model] + properties: + model: { $ref: '#/components/schemas/TextModelId' } + additionalProperties: true + TextPassthroughResponse: + type: object + description: Response body in the selected SDK-compatible wire format. + additionalProperties: true Workflow: type: object required: [id, object, name, description] properties: id: type: string - enum: [music-video, ecommerce-video] + enum: [music-video, ecommerce-video, video-analysis] example: music-video object: type: string @@ -144,17 +268,22 @@ components: example: shot_xxx index: type: integer + description: Zero-based shot order in the storyboard. example: 0 status: $ref: '#/components/schemas/TaskStatus' + description: Current lifecycle state for this storyboard shot. duration_seconds: type: integer + description: Planned or generated shot duration in seconds. example: 5 prompt: type: string + description: Creative instruction used to generate this shot. example: Opening lyric shot. lyric_text: type: string + description: Lyric segment aligned with this shot when available. example: Intro media: type: object @@ -162,19 +291,24 @@ components: properties: type: type: string + description: Hosted media type for the materialized shot. example: video url: type: string format: uri + description: BeatAPI-hosted HTTPS URL for the materialized shot. example: https://media.beatapi.io/outputs/task_8K2qA/shots/0.mp4 mime_type: type: string + description: MIME type of the hosted shot media. example: video/mp4 created_at: type: integer + description: Unix timestamp when the shot record was created. example: 1782210000 updated_at: type: integer + description: Unix timestamp when the shot record last changed. example: 1782210300 Storyboard: type: object @@ -182,6 +316,7 @@ components: properties: shots: type: array + description: Ordered Music Video storyboard shots. The array may be empty before storyboard generation completes. items: $ref: '#/components/schemas/StoryboardShot' ShotMedia: @@ -230,46 +365,88 @@ components: required: [credits_reserved, credits_settled, credits_refunded, credits_charged] properties: credits_reserved: - type: integer - description: BeatAPI customer credits reserved for this task. + type: number + format: double + multipleOf: 0.01 + description: USD amount reserved for this task. The compatibility field name is retained; 1 Credit equals $1 USD. credits_charged: - type: integer - description: BeatAPI customer credits charged when the task or operation is accepted. + type: number + format: double + multipleOf: 0.01 + description: USD amount charged when the task or operation is accepted. billable_duration_seconds: type: integer - description: Server-detected or request-declared billable duration used for credit calculation. + description: Server-detected or request-declared billable duration used for USD calculation. credits_settled: - type: integer - description: BeatAPI customer credits settled after successful work. + type: number + format: double + multipleOf: 0.01 + description: USD amount settled after successful work. credits_refunded: - type: integer - description: BeatAPI customer credits refunded after failed eligible work. + type: number + format: double + multipleOf: 0.01 + description: USD amount refunded after failed eligible work. Task: type: object - required: [id, object, workflow, status, stage, created_at, updated_at, completed_at, output, usage, request_id, error_code, error_message] + required: [id, object, task_kind, capability_id, capability_version, status, stage, created_at, updated_at, completed_at, output, usage, request_id, error_code, error_message] properties: id: type: string + description: Stable BeatAPI task ID used for polling and support. example: task_8K2qA object: type: string enum: [task] + description: Object discriminator; always `task`. + task_kind: + type: string + enum: [workflow, effect, image, video] + description: Public task family that determines which capability fields are present. + capability_id: + type: string + description: Stable BeatAPI workflow, Effect, or generation model ID selected when the task was accepted. + capability_version: + type: [integer, 'null'] + description: Immutable capability version used by this task. Legacy workflow rows are returned as version 1. workflow: type: string - enum: [music-video, ecommerce-video] + enum: [music-video, ecommerce-video, video-analysis] + description: Present for workflow tasks; identifies the selected BeatAPI workflow. example: music-video + effect_id: + type: string + description: Present for Effect tasks; stable selected Effect ID. + example: video-muscle-max + effect_version: + type: integer + description: Present for Effect tasks; immutable Effect version used for processing. + example: 1 + media_type: + type: string + enum: [image, video] + description: Present when task_kind is image or video. + model: + type: string + description: Stable BeatAPI model alias. It is independent from internal execution routing. status: $ref: '#/components/schemas/TaskStatus' + description: Current task lifecycle status. Stop polling at `succeeded` or `failed`; Music Video can also require manual action. stage: $ref: '#/components/schemas/TaskStatus' + description: Current processing stage, exposed separately so workflow progress can be tracked. storyboard: $ref: '#/components/schemas/Storyboard' + description: Music Video storyboard metadata when available. created_at: type: integer + description: Unix timestamp when BeatAPI accepted the task. updated_at: type: integer + description: Unix timestamp of the latest task update. completed_at: type: [integer, 'null'] + description: Terminal Unix timestamp, or null while work is in progress. output: description: Output is null until the task succeeds. oneOf: @@ -279,54 +456,155 @@ components: properties: media: type: array + description: BeatAPI-hosted result assets. items: type: object required: [type, url, mime_type] properties: type: type: string - enum: [video] + enum: [image, video] + description: Result asset type. url: type: string format: uri + description: BeatAPI-hosted HTTPS result URL. mime_type: type: string - example: video/mp4 + description: Result asset MIME type. + examples: [video/mp4, image/png, image/jpeg, image/webp] r2_url: type: string format: uri + description: Primary BeatAPI-hosted result URL for clients that need one canonical asset. + - type: object + required: [text, usage, finish_reason] + properties: + text: + type: string + description: Completed video analysis text. + usage: + type: object + description: Measured token usage used for final USD settlement. + required: [input_tokens, output_tokens, total_tokens] + properties: + input_tokens: + type: integer + minimum: 0 + description: Tokens consumed by the prompt and video input. + output_tokens: + type: integer + minimum: 0 + description: Tokens consumed by visible output and model reasoning. + total_tokens: + type: integer + minimum: 0 + description: Total measured input and output tokens. + finish_reason: + type: [string, 'null'] + description: Upstream-compatible completion reason. usage: $ref: '#/components/schemas/TaskUsage' + description: USD reservation, settlement, refund, and optional billable duration for this task. request_id: type: string + description: Correlation ID to retain for logs and BeatAPI support. example: req_abc123 error_code: type: [string, 'null'] + description: Machine-readable terminal failure code, or null when no task failure is recorded. example: processing_timeout error_message: type: [string, 'null'] + description: Human-readable terminal failure detail, or null when no task failure is recorded. + Effect: + type: object + required: [id, object, name, description, output_type, category, tags, input, options, preview, version, status] + properties: + id: { type: string, example: video-muscle-max } + object: { type: string, enum: [effect] } + name: { type: string, example: Muscle Transformation } + description: { type: string } + output_type: { type: string, enum: [image, video] } + category: { type: string, example: transformation } + tags: { type: array, items: { type: string } } + input: + type: object + required: [images_min, images_max, accepted_types] + properties: + images_min: { type: integer, minimum: 1 } + images_max: { type: integer, minimum: 1 } + accepted_types: + type: array + items: { type: string, enum: [image/jpeg, image/png, image/webp] } + max_size_mb: + type: integer + minimum: 1 + description: Maximum downloaded bytes per input image. When omitted, BeatAPI enforces 50 MB. + max_dimension_px: + type: integer + minimum: 1 + description: Maximum decoded width or height. BeatAPI inspects the actual image header before charging. + subject_requirements: { type: array, items: { type: string } } + options: + type: object + properties: + aspect_ratios: { type: array, items: { type: string } } + resolutions: { type: array, items: { type: string } } + duration_seconds: { type: array, items: { type: integer } } + bgm: { type: boolean } + seed: { type: boolean } + preview: + type: object + required: [cover_url, media_url] + properties: + cover_url: { type: [string, 'null'], format: uri } + media_url: { type: [string, 'null'], format: uri } + version: { type: integer, minimum: 1 } + status: { type: string, enum: [testing, active, paused] } + EffectResponse: + type: object + required: [data] + properties: + data: { $ref: '#/components/schemas/Effect' } + EffectListResponse: + type: object + required: [data] + properties: + data: + type: object + required: [object, data] + properties: + object: { type: string, enum: [list] } + data: { type: array, items: { $ref: '#/components/schemas/Effect' } } File: type: object required: [id, object, url, key, mime_type, size_bytes, purpose, created_at] properties: id: type: string + description: Stable uploaded file ID. example: file_3xYz9 object: type: string enum: [file] + description: Object discriminator; always `file`. url: type: string format: uri + description: Long-lived BeatAPI HTTPS URL to use in workflow or model requests. example: https://media.beatapi.io/inputs/file_3xYz9.mp3 key: type: string + description: BeatAPI storage key for support and diagnostics. example: inputs/file_3xYz9.mp3 mime_type: type: string + description: Accepted MIME type detected for the uploaded file. example: audio/mpeg size_bytes: type: integer + description: Uploaded file size in bytes. example: 1048576 audio_duration_seconds: type: number @@ -336,11 +614,29 @@ components: type: string description: Duration detection method used for uploaded audio. example: mp3_frame_scan + video_duration_seconds: + type: number + description: Present for MP4/MOV uploads after server-side container inspection. + example: 15.25 + video_duration_source: + type: string + description: Duration and dimension detection method used for the uploaded video. + example: mp4_boxes + width: + type: integer + description: Detected pixel width for uploaded images and videos. + example: 720 + height: + type: integer + description: Detected pixel height for uploaded images and videos. + example: 1280 purpose: type: string enum: [input] + description: File purpose; currently always `input`. created_at: type: integer + description: Unix timestamp when the file was stored. example: 1782210000 WebhookEndpoint: type: object @@ -348,33 +644,41 @@ components: properties: id: type: string + description: Stable webhook endpoint ID used for get, update, and delete operations. example: wh_9aBcD object: type: string enum: [webhook_endpoint] + description: Object discriminator; always `webhook_endpoint`. url: type: string format: uri + description: Public HTTPS callback URL receiving subscribed task events. example: https://example.com/beatapi-webhook description: type: string + description: Account-defined label for the endpoint. example: Production webhook events: type: array + description: Task event types delivered to this endpoint. items: type: string enum: [task.succeeded, task.failed] status: type: string enum: [active, disabled] + description: Delivery status. Disabled endpoints do not receive events. secret: type: string description: Returned in full only when the endpoint is created. Later responses return a masked value. example: whsec_example_masked created_at: type: integer + description: Unix timestamp when the endpoint was created. updated_at: type: integer + description: Unix timestamp when the endpoint last changed. WebhookEvent: type: object required: [id, event, created_at, data] @@ -406,28 +710,576 @@ components: properties: data: $ref: '#/components/schemas/WorkflowList' + GenerationModel: + type: object + additionalProperties: false + required: [id, object, name, media_type, input_modes] + properties: + id: + type: string + enum: [nano-banana, nano-banana-2, nano-banana-2-lite, nano-banana-pro, gpt-image-2, seedream-5-pro, grok-imagine-image-2.0, minimax-h3, grok-imagine-video-1.5, seedance-2, seedance-2-fast, seedance-2-mini, veo-3.1, seedance-2.5, kling-3, kling-2.6-motion-control, kling-3-motion-control, wan-3.0, wan-3.0-prime, happyhorse-1.0, happyhorse-1.1, minimax-h3-max, minimax-h3-max-turbo] + object: { type: string, enum: [generation_model] } + name: { type: string } + media_type: { type: string, enum: [image, video] } + input_modes: + type: array + items: { type: string, enum: [text, image, frames, reference] } + GenerationModelList: + type: object + required: [object, data] + properties: + object: { type: string, enum: [list] } + data: + type: array + items: { $ref: '#/components/schemas/GenerationModel' } + GenerationModelListResponse: + type: object + required: [data] + properties: + data: { $ref: '#/components/schemas/GenerationModelList' } + ImageGenerationTaskCreateRequest: + oneOf: + - $ref: '#/components/schemas/NanoBananaImageRequest' + - $ref: '#/components/schemas/NanoBanana2ImageRequest' + - $ref: '#/components/schemas/NanoBanana2LiteImageRequest' + - $ref: '#/components/schemas/NanoBananaProImageRequest' + - $ref: '#/components/schemas/GptImage2Request' + - $ref: '#/components/schemas/Seedream5ProImageRequest' + - $ref: '#/components/schemas/GrokImagineImage20Request' + discriminator: + propertyName: model + mapping: + nano-banana: '#/components/schemas/NanoBananaImageRequest' + nano-banana-2: '#/components/schemas/NanoBanana2ImageRequest' + nano-banana-2-lite: '#/components/schemas/NanoBanana2LiteImageRequest' + nano-banana-pro: '#/components/schemas/NanoBananaProImageRequest' + gpt-image-2: '#/components/schemas/GptImage2Request' + seedream-5-pro: '#/components/schemas/Seedream5ProImageRequest' + grok-imagine-image-2.0: '#/components/schemas/GrokImagineImage20Request' + NanoBananaImageRequest: + type: object + additionalProperties: false + required: [model, prompt] + properties: + model: { type: string, const: nano-banana, description: Must be `nano-banana`. } + prompt: { type: string, minLength: 1, maxLength: 5000, description: Generation or image-editing instructions. } + images: + type: array + minItems: 1 + maxItems: 10 + description: Public HTTPS reference-image URLs. Omit for text-to-image. + items: { type: string, format: uri, pattern: '^https://' } + aspect_ratio: + type: string + enum: ['1:1', '9:16', '16:9', '3:4', '4:3', '3:2', '2:3', '5:4', '4:5', '21:9', auto] + default: '1:1' + description: Output image aspect ratio. + output_format: { type: string, enum: [png, jpeg], default: png, description: Output image file format. } + NanoBanana2ImageRequest: + type: object + additionalProperties: false + required: [model, prompt] + properties: + model: { type: string, const: nano-banana-2, description: Must be `nano-banana-2`. } + prompt: { type: string, minLength: 1, maxLength: 5000, description: Generation or image-editing instructions. } + images: + type: array + minItems: 1 + maxItems: 10 + description: Public HTTPS reference-image URLs. Omit for text-to-image. + items: { type: string, format: uri, pattern: '^https://' } + aspect_ratio: + type: string + enum: ['1:1', '9:16', '16:9', '3:4', '4:3', '3:2', '2:3', '5:4', '4:5', '21:9', auto] + default: '1:1' + description: Output image aspect ratio. + resolution: { type: string, enum: [1K, 2K, 4K], default: 1K, description: Output resolution tier. } + output_format: { type: string, enum: [png, jpeg], default: png, description: Output image file format. } + NanoBanana2LiteImageRequest: + type: object + additionalProperties: false + required: [model, prompt] + properties: + model: { type: string, const: nano-banana-2-lite, description: Must be `nano-banana-2-lite`. } + prompt: { type: string, minLength: 1, maxLength: 5000, description: Generation or image-editing instructions. } + images: + type: array + minItems: 1 + maxItems: 10 + description: Public HTTPS reference-image URLs. Omit for text-to-image. + items: { type: string, format: uri, pattern: '^https://' } + aspect_ratio: + type: string + enum: ['1:1', '9:16', '16:9', '3:4', '4:3', '3:2', '2:3', '5:4', '4:5', '21:9', auto] + default: '1:1' + description: Output image aspect ratio. + output_format: { type: string, enum: [png, jpeg], default: png, description: Output image file format. } + NanoBananaProImageRequest: + type: object + additionalProperties: false + required: [model, prompt] + properties: + model: { type: string, const: nano-banana-pro, description: Must be `nano-banana-pro`. } + prompt: { type: string, minLength: 1, maxLength: 5000, description: Generation or image-editing instructions. } + images: + type: array + minItems: 1 + maxItems: 8 + description: Public HTTPS reference-image URLs. Omit for text-to-image. + items: { type: string, format: uri, pattern: '^https://' } + aspect_ratio: + type: string + enum: ['1:1', '2:3', '3:2', '3:4', '4:3', '4:5', '5:4', '9:16', '16:9', '21:9', auto] + default: '1:1' + description: Output image aspect ratio. + resolution: { type: string, enum: [1K, 2K, 4K], default: 1K, description: Output resolution tier. } + output_format: { type: string, enum: [png, jpg], default: png, description: Output image file format. } + GptImage2Request: + type: object + additionalProperties: false + required: [model, prompt] + properties: + model: { type: string, const: gpt-image-2, description: Must be `gpt-image-2`. } + prompt: { type: string, minLength: 1, maxLength: 5000, description: Generation or image-editing instructions. } + images: + type: array + minItems: 1 + maxItems: 16 + description: Public HTTPS reference-image URLs. Omit for text-to-image. + items: { type: string, format: uri, pattern: '^https://' } + aspect_ratio: + type: string + enum: [auto, '1:1', '3:2', '2:3', '4:3', '3:4', '5:4', '4:5', '16:9', '9:16', '2:1', '1:2', '3:1', '1:3', '21:9', '9:21'] + default: auto + description: Output image aspect ratio. + resolution: { type: string, enum: [1K, 2K, 4K], default: 1K, description: Output resolution tier. } + Seedream5ProImageRequest: + type: object + additionalProperties: false + required: [model, prompt] + properties: + model: { type: string, const: seedream-5-pro, description: Must be `seedream-5-pro`. } + prompt: { type: string, minLength: 1, maxLength: 5000, description: Generation or image-editing instructions. } + images: + type: array + minItems: 1 + maxItems: 10 + description: Public HTTPS reference-image URLs. Omit for text-to-image. + items: { type: string, format: uri, pattern: '^https://' } + aspect_ratio: + type: string + enum: [auto, '1:1', '4:3', '3:4', '16:9', '9:16', '3:2', '2:3', '21:9'] + default: '1:1' + description: Output image aspect ratio. + resolution: { type: string, enum: [1K, 2K, 4K], default: 1K, description: Output resolution tier. } + output_format: { type: string, enum: [png, jpeg], default: png, description: Output image file format. } + GrokImagineImage20Request: + type: object + additionalProperties: false + required: [model, prompt] + description: Omit `images` for text-to-image. Supply one to five images for editing; `auto` aspect ratio is available only when images are supplied. + properties: + model: { type: string, const: grok-imagine-image-2.0, description: Must be `grok-imagine-image-2.0`. } + prompt: { type: string, minLength: 1, maxLength: 5000, description: Generation or image-editing instructions. } + images: + type: array + minItems: 1 + maxItems: 5 + description: Public HTTPS reference-image URLs. Omit for text-to-image. + items: { type: string, format: uri, pattern: '^https://' } + aspect_ratio: + type: string + enum: ['1:1', '2:3', '3:2', '16:9', '9:16', auto] + default: '1:1' + description: Output image aspect ratio. `auto` requires at least one image. + VideoGenerationTaskCreateRequest: + oneOf: + - $ref: '#/components/schemas/MinimaxH3VideoRequest' + - $ref: '#/components/schemas/GrokImagineVideo15Request' + - $ref: '#/components/schemas/Seedance2VideoRequest' + - $ref: '#/components/schemas/Seedance2FastVideoRequest' + - $ref: '#/components/schemas/Seedance2MiniVideoRequest' + - $ref: '#/components/schemas/Veo31VideoRequest' + - $ref: '#/components/schemas/Seedance25VideoRequest' + - $ref: '#/components/schemas/Kling3VideoRequest' + - $ref: '#/components/schemas/Kling26MotionControlVideoRequest' + - $ref: '#/components/schemas/Kling3MotionControlVideoRequest' + - $ref: '#/components/schemas/Wan30VideoRequest' + - $ref: '#/components/schemas/Wan30PrimeVideoRequest' + - $ref: '#/components/schemas/HappyHorse10VideoRequest' + - $ref: '#/components/schemas/HappyHorse11VideoRequest' + - $ref: '#/components/schemas/MinimaxH3MaxVideoRequest' + - $ref: '#/components/schemas/MinimaxH3MaxTurboVideoRequest' + discriminator: + propertyName: model + mapping: + minimax-h3: '#/components/schemas/MinimaxH3VideoRequest' + grok-imagine-video-1.5: '#/components/schemas/GrokImagineVideo15Request' + seedance-2: '#/components/schemas/Seedance2VideoRequest' + seedance-2-fast: '#/components/schemas/Seedance2FastVideoRequest' + seedance-2-mini: '#/components/schemas/Seedance2MiniVideoRequest' + veo-3.1: '#/components/schemas/Veo31VideoRequest' + seedance-2.5: '#/components/schemas/Seedance25VideoRequest' + kling-3: '#/components/schemas/Kling3VideoRequest' + kling-2.6-motion-control: '#/components/schemas/Kling26MotionControlVideoRequest' + kling-3-motion-control: '#/components/schemas/Kling3MotionControlVideoRequest' + wan-3.0: '#/components/schemas/Wan30VideoRequest' + wan-3.0-prime: '#/components/schemas/Wan30PrimeVideoRequest' + happyhorse-1.0: '#/components/schemas/HappyHorse10VideoRequest' + happyhorse-1.1: '#/components/schemas/HappyHorse11VideoRequest' + minimax-h3-max: '#/components/schemas/MinimaxH3MaxVideoRequest' + minimax-h3-max-turbo: '#/components/schemas/MinimaxH3MaxTurboVideoRequest' + MinimaxH3VideoRequest: + type: object + additionalProperties: false + required: [model, prompt] + description: '`images` cannot be combined with any `reference_*` input. An audio reference also requires at least one reference image or video.' + properties: + model: { type: string, const: minimax-h3, description: Must be `minimax-h3`. } + prompt: { type: string, minLength: 1, maxLength: 5000, description: Video generation instructions. } + images: { type: array, minItems: 1, maxItems: 2, description: One first-frame image or first- and last-frame images as public HTTPS URLs., items: { type: string, format: uri, pattern: '^https://' } } + reference_images: { type: array, minItems: 1, maxItems: 9, description: Public HTTPS image references for multimodal reference generation., items: { type: string, format: uri, pattern: '^https://' } } + reference_videos: { type: array, minItems: 1, maxItems: 3, description: Public HTTPS video references for multimodal reference generation., items: { type: string, format: uri, pattern: '^https://' } } + reference_audios: { type: array, minItems: 1, maxItems: 3, description: Public HTTPS audio references for multimodal reference generation. Audio also requires at least one reference image or video., items: { type: string, format: uri, pattern: '^https://' } } + duration: { type: integer, minimum: 4, maximum: 15, default: 5, description: Requested output duration in seconds. } + aspect_ratio: + type: string + enum: [adaptive, '21:9', '16:9', '4:3', '1:1', '3:4', '9:16'] + description: Text mode defaults to 16:9 and does not accept adaptive. Frame mode always uses adaptive. Reference mode defaults to adaptive and also accepts a concrete ratio. + resolution: { type: string, enum: [768P, 2K], default: 768P, description: Output resolution tier. } + GrokImagineVideo15Request: + type: object + additionalProperties: false + required: [model, prompt] + description: '`images` accepts one first frame and cannot be combined with `reference_images`. Omit `aspect_ratio` when `images` is supplied. 1080p accepts at most one image.' + properties: + model: { type: string, const: grok-imagine-video-1.5, description: Must be `grok-imagine-video-1.5`. } + prompt: { type: string, minLength: 1, maxLength: 4096, description: Video generation instructions. } + images: { type: array, minItems: 1, maxItems: 1, description: One first-frame image as a public HTTPS URL., items: { type: string, format: uri, pattern: '^https://' } } + reference_images: { type: array, minItems: 1, maxItems: 7, description: One to seven public HTTPS reference images., items: { type: string, format: uri, pattern: '^https://' } } + duration: { type: integer, minimum: 1, maximum: 15, default: 8, description: Requested output duration in seconds. } + aspect_ratio: { type: string, enum: ['1:1', '16:9', '9:16', '3:2', '2:3', auto], default: '16:9', description: Output video aspect ratio. Omit when one first-frame image is supplied. } + resolution: { type: string, enum: [480p, 720p, 1080p], default: 480p, description: Output resolution tier. 1080p accepts at most one image. } + Seedance2VideoRequest: + type: object + additionalProperties: false + required: [model, prompt] + description: '`images` cannot be combined with any `reference_*` input. An audio reference also requires at least one reference image or video.' + properties: + model: { type: string, const: seedance-2, description: Must be `seedance-2`. } + prompt: { type: string, minLength: 1, maxLength: 5000, description: Video generation instructions. } + images: { type: array, minItems: 1, maxItems: 2, description: One first-frame image or first- and last-frame images as public HTTPS URLs., items: { type: string, format: uri, pattern: '^https://' } } + reference_images: { type: array, minItems: 1, maxItems: 9, description: Public HTTPS image references for multimodal reference generation., items: { type: string, format: uri, pattern: '^https://' } } + reference_videos: { type: array, minItems: 1, maxItems: 3, description: Public HTTPS video references for multimodal reference generation., items: { type: string, format: uri, pattern: '^https://' } } + reference_audios: { type: array, minItems: 1, maxItems: 3, description: Public HTTPS audio references. Audio also requires at least one reference image or video., items: { type: string, format: uri, pattern: '^https://' } } + duration: { type: integer, minimum: 4, maximum: 15, default: 5, description: Requested output duration in seconds. } + aspect_ratio: { type: string, enum: [adaptive, '21:9', '16:9', '4:3', '1:1', '3:4', '9:16'], default: adaptive, description: Output video aspect ratio. } + resolution: { type: string, enum: [480p, 720p, 1080p, 4k, 4K], default: 720p, description: Output resolution tier. 4k and 4K are equivalent. 1080p is not supported with reference images. } + generate_audio: { type: boolean, default: true, description: Generate synchronized audio with the video. } + Seedance2FastVideoRequest: + type: object + additionalProperties: false + required: [model, prompt] + description: '`images` cannot be combined with any `reference_*` input. An audio reference also requires at least one reference image or video.' + properties: + model: { type: string, const: seedance-2-fast, description: Must be `seedance-2-fast`. } + prompt: { type: string, minLength: 1, maxLength: 5000, description: Video generation instructions. } + images: { type: array, minItems: 1, maxItems: 2, description: One first-frame image or first- and last-frame images as public HTTPS URLs., items: { type: string, format: uri, pattern: '^https://' } } + reference_images: { type: array, minItems: 1, maxItems: 9, description: Public HTTPS image references for multimodal reference generation., items: { type: string, format: uri, pattern: '^https://' } } + reference_videos: { type: array, minItems: 1, maxItems: 3, description: Public HTTPS video references for multimodal reference generation., items: { type: string, format: uri, pattern: '^https://' } } + reference_audios: { type: array, minItems: 1, maxItems: 3, description: Public HTTPS audio references. Audio also requires at least one reference image or video., items: { type: string, format: uri, pattern: '^https://' } } + duration: { type: integer, minimum: 4, maximum: 15, default: 5, description: Requested output duration in seconds. } + aspect_ratio: { type: string, enum: [adaptive, '21:9', '16:9', '4:3', '1:1', '3:4', '9:16'], default: adaptive, description: Output video aspect ratio. } + resolution: { type: string, enum: [480p, 720p], default: 720p, description: Output resolution tier. } + generate_audio: { type: boolean, default: true, description: Generate synchronized audio with the video. } + Seedance2MiniVideoRequest: + type: object + additionalProperties: false + required: [model, prompt] + description: 'Low-cost Seedance 2.0 route. `images` cannot be combined with any `reference_*` input. Generated audio is not supported.' + properties: + model: { type: string, const: seedance-2-mini, description: Must be `seedance-2-mini`. } + prompt: { type: string, minLength: 1, maxLength: 5000, description: Video generation instructions. } + images: { type: array, minItems: 1, maxItems: 2, description: One first-frame image or first- and last-frame images as public HTTPS URLs., items: { type: string, format: uri, pattern: '^https://' } } + reference_images: { type: array, minItems: 1, maxItems: 9, description: Public HTTPS image references for multimodal reference generation., items: { type: string, format: uri, pattern: '^https://' } } + reference_videos: { type: array, minItems: 1, maxItems: 3, description: Public HTTPS video references for multimodal reference generation., items: { type: string, format: uri, pattern: '^https://' } } + reference_audios: { type: array, minItems: 1, maxItems: 3, description: Public HTTPS audio references. Audio also requires at least one reference image or video., items: { type: string, format: uri, pattern: '^https://' } } + duration: { type: integer, minimum: 4, maximum: 15, default: 5, description: Requested output duration in seconds. } + aspect_ratio: { type: string, enum: [adaptive, '21:9', '16:9', '4:3', '1:1', '3:4', '9:16'], default: adaptive, description: Output video aspect ratio. } + resolution: { type: string, enum: [480p, 720p], default: 720p, description: Output resolution tier. } + Veo31VideoRequest: + allOf: + - oneOf: + - $ref: '#/components/schemas/Veo31TextOrFrameVideoRequest' + - $ref: '#/components/schemas/Veo31ReferenceVideoRequest' + Veo31TextOrFrameVideoRequest: + type: object + additionalProperties: false + required: [model, prompt] + description: | + Veo 3.1 text or first/last-frame generation. Output is fixed at 8 seconds + and defaults to Quality at 720p. Price depends on quality and resolution. + properties: + model: { type: string, const: veo-3.1, description: Must be `veo-3.1`. } + prompt: { type: string, minLength: 1, maxLength: 5000, description: Video generation instructions. } + images: { type: array, minItems: 1, maxItems: 2, description: One first-frame image or first- and last-frame images as public HTTPS URLs., items: { type: string, format: uri, pattern: '^https://' } } + aspect_ratio: { type: string, enum: ['16:9', '9:16', auto], default: '16:9', description: Output video aspect ratio. } + resolution: { type: string, enum: [720p, 1080p, 4k, 4K], default: 720p, description: Output video resolution. 4k and 4K are equivalent. Price depends on quality and resolution. } + quality: { type: string, enum: [Quality, Fast, Lite], default: Quality, description: Text or frame generation tier. } + watermark: { type: string, description: Optional watermark text forwarded to the selected model. } + enable_translation: { type: boolean, description: Allow prompt translation before generation. } + Veo31ReferenceVideoRequest: + type: object + additionalProperties: false + required: [model, prompt, reference_images] + description: | + Veo 3.1 reference-image generation. Output is fixed at 8 seconds and + supports the Fast or Lite tier, defaulting to Fast at 720p. Price depends + on quality and resolution. + properties: + model: { type: string, const: veo-3.1, description: Must be `veo-3.1`. } + prompt: { type: string, minLength: 1, maxLength: 5000, description: Video generation instructions. } + reference_images: { type: array, minItems: 1, maxItems: 3, description: Public HTTPS reference images., items: { type: string, format: uri, pattern: '^https://' } } + aspect_ratio: { type: string, enum: ['16:9', '9:16', auto], default: '16:9', description: Output video aspect ratio. } + resolution: { type: string, enum: [720p, 1080p, 4k, 4K], default: 720p, description: Output video resolution. 4k and 4K are equivalent. Price depends on quality and resolution. } + quality: { type: string, enum: [Fast, Lite], default: Fast, description: Reference-image generation tier. } + watermark: { type: string, description: Optional watermark text forwarded to the selected model. } + enable_translation: { type: boolean, description: Allow prompt translation before generation. } + Seedance25VideoRequest: + type: object + additionalProperties: false + required: [model, prompt] + description: '`images` cannot be combined with any `reference_*` input. Audio-only reference generation is supported.' + properties: + model: { type: string, const: seedance-2.5, description: Must be `seedance-2.5`. } + prompt: { type: string, minLength: 1, maxLength: 5000, description: Video generation instructions. } + images: { type: array, minItems: 1, maxItems: 2, description: One first-frame image or first- and last-frame images as public HTTPS URLs., items: { type: string, format: uri, pattern: '^https://' } } + reference_images: { type: array, minItems: 1, maxItems: 30, description: Public HTTPS image references for multimodal reference generation., items: { type: string, format: uri, pattern: '^https://' } } + reference_videos: { type: array, minItems: 1, maxItems: 10, description: Public HTTPS video references for multimodal reference generation., items: { type: string, format: uri, pattern: '^https://' } } + reference_audios: { type: array, minItems: 1, maxItems: 10, description: Public HTTPS audio references. Audio-only reference generation is supported., items: { type: string, format: uri, pattern: '^https://' } } + duration: { type: integer, minimum: 4, maximum: 30, default: 5, description: Requested output duration in seconds. } + aspect_ratio: { type: string, enum: [adaptive, '21:9', '16:9', '4:3', '1:1', '3:4', '9:16'], default: adaptive, description: Output video aspect ratio. } + resolution: { type: string, enum: [480p, 720p, 1080p], default: 720p, description: Output resolution tier. 480p and 1080p are priced separately; see the pricing page. } + generate_audio: { type: boolean, default: true, description: Generate synchronized audio with the video. } + seed: { type: integer, minimum: -1, maximum: 4294967295, default: -1, description: Reproducibility seed. Use -1 for a random seed. } + KlingShot: + type: object + additionalProperties: false + required: [prompt, duration] + properties: + prompt: { type: string, minLength: 1, maxLength: 500, description: Instructions for this shot. } + duration: { type: integer, minimum: 1, maximum: 12, description: Shot duration in seconds. All shot durations must sum to the task duration. } + KlingElement: + type: object + additionalProperties: false + required: [name, element_input_urls] + description: Use 2-4 image URLs or one video URL. A video element may include one audio URL and a 3-8 second segment in milliseconds. + properties: + name: { type: string, minLength: 1, description: Stable name used to reference this element in the prompt. } + description: { type: string, description: Optional description of the subject or object. } + element_input_urls: + type: array + minItems: 1 + maxItems: 4 + description: Two to four image URLs, or one video URL. + items: { type: string, format: uri, pattern: '^https://' } + element_input_audio_urls: + type: array + minItems: 1 + maxItems: 1 + description: Optional audio URL used with a video element. + items: { type: string, format: uri, pattern: '^https://' } + start_time: { type: integer, minimum: 0, maximum: 30000, description: Video element segment start time in milliseconds. } + end_time: { type: integer, minimum: 0, maximum: 30000, description: Video element segment end time in milliseconds. The segment must be 3-8 seconds. } + Kling3VideoRequest: + type: object + additionalProperties: false + required: [model, prompt] + description: Multi-shot mode accepts one first-frame image, requires `multi_prompt`, and defaults sound to true. Shot durations must sum to `duration`. + properties: + model: { type: string, const: kling-3, description: Must be `kling-3`. } + prompt: { type: string, minLength: 1, maxLength: 5000, description: Video generation instructions. } + images: { type: array, minItems: 1, maxItems: 2, description: One first-frame image or first- and last-frame images as public HTTPS URLs. Multi-shot mode accepts exactly one., items: { type: string, format: uri, pattern: '^https://' } } + duration: { type: integer, minimum: 3, maximum: 15, default: 5, description: Requested output duration in seconds. } + aspect_ratio: + type: string + enum: ['16:9', '9:16', '1:1'] + description: Defaults to 16:9 for text generation. Omit it with frame images to adapt to the input aspect ratio. + resolution: { type: string, enum: [std, pro, 4K], default: pro, description: Output quality tier. } + sound: { type: boolean, description: Generate synchronized sound. Defaults to true in multi-shot mode. } + multi_shots: { type: boolean, default: false, description: Enable storyboard-style multi-shot generation. } + multi_prompt: + type: array + minItems: 1 + maxItems: 5 + description: Shot definitions required when `multi_shots=true`. + items: { $ref: '#/components/schemas/KlingShot' } + elements: + type: array + maxItems: 3 + description: Up to three reusable subject or object references. + items: { $ref: '#/components/schemas/KlingElement' } + Kling26MotionControlVideoRequest: + type: object + additionalProperties: false + required: [model, images, reference_videos] + description: | + Transfer motion from one uploaded 3–30 second MP4/MOV video to one + uploaded character image. Both URLs must come from `/v1/files` for the + current BeatAPI account. BeatAPI detects the reference-video duration + server-side and reserves USD at the selected per-second rate, rounding + fractional seconds up. The image must be 10 MB or smaller; the video + may be up to 100 MB. + properties: + model: { type: string, const: kling-2.6-motion-control, description: Must be `kling-2.6-motion-control`. } + prompt: { type: string, maxLength: 2500, description: Optional motion or scene guidance. } + images: + type: array + minItems: 1 + maxItems: 1 + description: Exactly one character-image URL returned by the current account's `/v1/files` upload. + items: { type: string, format: uri, pattern: '^https://' } + reference_videos: + type: array + minItems: 1 + maxItems: 1 + description: Exactly one 3–30 second MP4/MOV URL returned by the current account's `/v1/files` upload. Its detected duration determines billing. + items: { type: string, format: uri, pattern: '^https://' } + resolution: { type: string, enum: [720p, 1080p], default: 720p, description: Output resolution and per-second price tier. } + character_orientation: { type: string, enum: [image, video], default: image, description: Image orientation supports motion videos up to 10 seconds; video orientation supports up to 30 seconds. } + Kling3MotionControlVideoRequest: + type: object + additionalProperties: false + required: [model, images, reference_videos] + description: | + Kling 3.0 motion transfer using exactly one uploaded image and one + uploaded 3–30 second MP4/MOV. Both assets must exceed 340 px in width + and height and use an aspect ratio from 2:5 to 5:2. BeatAPI detects the + reference-video duration server-side and reserves USD at the selected + per-second rate, rounding fractional seconds up. + properties: + model: { type: string, const: kling-3-motion-control, description: Must be `kling-3-motion-control`. } + prompt: { type: string, maxLength: 2500, description: Optional motion or scene guidance. } + images: + type: array + minItems: 1 + maxItems: 1 + description: Exactly one character-image URL returned by the current account's `/v1/files` upload; maximum 10 MB. + items: { type: string, format: uri, pattern: '^https://' } + reference_videos: + type: array + minItems: 1 + maxItems: 1 + description: Exactly one 3–30 second MP4/MOV URL returned by the current account's `/v1/files` upload; maximum 100 MB. Its detected duration determines billing. + items: { type: string, format: uri, pattern: '^https://' } + resolution: { type: string, enum: [720p, 1080p], default: 720p, description: Output resolution and per-second price tier. } + character_orientation: { type: string, enum: [image, video], default: image, description: Image orientation supports motion videos up to 10 seconds; video orientation supports up to 30 seconds. } + background_source: { type: string, enum: [input_video, input_image], default: input_video, description: Preserve the background from the motion video or character image. } + Wan30VideoRequest: + type: object + additionalProperties: false + required: [model, prompt] + description: 'Renders 2–30 seconds in a single pass. `images` starts the render from a picture; reference videos and audio travel alongside it.' + properties: + model: { type: string, const: wan-3.0, description: Must be `wan-3.0`. } + prompt: { type: string, minLength: 1, maxLength: 5000, description: Video generation instructions. } + images: { type: array, minItems: 1, maxItems: 10, description: Public HTTPS images. One starts an image-to-video render; more are used as visual references., items: { type: string, format: uri, pattern: '^https://' } } + reference_videos: { type: array, minItems: 1, maxItems: 5, description: Public HTTPS video references. A request that carries one is billed at 1.5x., items: { type: string, format: uri, pattern: '^https://' } } + reference_audios: { type: array, minItems: 1, maxItems: 5, description: Public HTTPS audio references., items: { type: string, format: uri, pattern: '^https://' } } + duration: { type: integer, minimum: 2, maximum: 30, default: 5, description: Requested output duration in seconds. Any whole number in range; there is no long-clip surcharge. } + aspect_ratio: { type: string, enum: ['16:9', '9:16', '1:1', '4:3', '3:4'], default: '16:9', description: Output video aspect ratio. } + resolution: { type: string, enum: [480p, 720p, 1080p], default: 720p, description: Output resolution tier. Price scales with it. } + Wan30PrimeVideoRequest: + type: object + additionalProperties: false + required: [model, prompt] + description: 'Renders 2–30 seconds in a single pass. `images` starts the render from a picture; reference videos and audio travel alongside it.' + properties: + model: { type: string, const: wan-3.0-prime, description: Must be `wan-3.0-prime`. } + prompt: { type: string, minLength: 1, maxLength: 5000, description: Video generation instructions. } + images: { type: array, minItems: 1, maxItems: 10, description: Public HTTPS images. One starts an image-to-video render; more are used as visual references., items: { type: string, format: uri, pattern: '^https://' } } + reference_videos: { type: array, minItems: 1, maxItems: 5, description: Public HTTPS video references. A request that carries one is billed at 1.5x., items: { type: string, format: uri, pattern: '^https://' } } + reference_audios: { type: array, minItems: 1, maxItems: 5, description: Public HTTPS audio references., items: { type: string, format: uri, pattern: '^https://' } } + duration: { type: integer, minimum: 2, maximum: 30, default: 5, description: Requested output duration in seconds. Any whole number in range; there is no long-clip surcharge. } + aspect_ratio: { type: string, enum: ['16:9', '9:16', '1:1', '4:3', '3:4'], default: '16:9', description: Output video aspect ratio. } + resolution: { type: string, enum: [480p, 720p, 1080p], default: 720p, description: Output resolution tier. Price scales with it. } + HappyHorse10VideoRequest: + type: object + additionalProperties: false + required: [model, prompt, images] + description: 'Image to video only — this model publishes no text-to-video mode, so `images` is required.' + properties: + model: { type: string, const: happyhorse-1.0, description: Must be `happyhorse-1.0`. } + prompt: { type: string, minLength: 1, maxLength: 5000, description: 'What should happen in the shot — the motion, the expression, the camera.' } + images: { type: array, minItems: 1, maxItems: 9, description: Source images as public HTTPS URLs. At least one is required., items: { type: string, format: uri, pattern: '^https://' } } + duration: { type: integer, minimum: 3, maximum: 15, default: 5, description: Requested output duration in seconds. } + aspect_ratio: { type: string, enum: ['16:9', '9:16', '1:1', '4:3', '3:4'], default: '16:9', description: Output video aspect ratio. } + resolution: { type: string, enum: [720p, 1080p], default: 720p, description: Output resolution tier. Price scales with it. } + HappyHorse11VideoRequest: + type: object + additionalProperties: false + required: [model, prompt, images] + description: 'Image to video only — this model publishes no text-to-video mode, so `images` is required.' + properties: + model: { type: string, const: happyhorse-1.1, description: Must be `happyhorse-1.1`. } + prompt: { type: string, minLength: 1, maxLength: 5000, description: 'What should happen in the shot — the motion, the expression, the camera.' } + images: { type: array, minItems: 1, maxItems: 9, description: Source images as public HTTPS URLs. At least one is required., items: { type: string, format: uri, pattern: '^https://' } } + duration: { type: integer, minimum: 3, maximum: 15, default: 5, description: Requested output duration in seconds. } + aspect_ratio: { type: string, enum: ['16:9', '9:16', '1:1', '4:3', '3:4'], default: '16:9', description: Output video aspect ratio. } + resolution: { type: string, enum: [720p, 1080p], default: 720p, description: Output resolution tier. Price scales with it. } + MinimaxH3MaxVideoRequest: + type: object + additionalProperties: false + required: [model, prompt] + description: 'Text to video, or image to video when `images` carries a first frame. A second image becomes the last frame. Output tops out at 768P — MiniMax H3 renders 2K for less per second.' + properties: + model: { type: string, const: minimax-h3-max, description: Must be `minimax-h3-max`. } + prompt: { type: string, minLength: 1, maxLength: 5000, description: 'What should happen in the shot — the motion, the expression, the camera.' } + images: { type: array, minItems: 1, maxItems: 2, description: 'Public HTTPS images. One starts the render from a first frame; a second becomes the last frame, in first-to-last order.', items: { type: string, format: uri, pattern: '^https://' } } + duration: { type: integer, minimum: 1, default: 5, description: 'Requested output duration in seconds. Billed per second at the rate for the chosen resolution. If a duration is unsupported, the API returns `400`.' } + resolution: { type: string, enum: ['480P', '768P'], default: '768P', description: 'Output resolution. Upper-case P, and the only two values this model accepts. Price scales with it.' } + seed: { type: integer, minimum: 0, description: Reuse a seed to re-render the same motion. A random seed is chosen when omitted. } + MinimaxH3MaxTurboVideoRequest: + type: object + additionalProperties: false + required: [model, prompt] + description: 'H3 Max on a faster stack — the same request contract and the same modes, roughly 2.5x quicker, at half the per-second rate.' + properties: + model: { type: string, const: minimax-h3-max-turbo, description: Must be `minimax-h3-max-turbo`. } + prompt: { type: string, minLength: 1, maxLength: 5000, description: 'What should happen in the shot — the motion, the expression, the camera.' } + images: { type: array, minItems: 1, maxItems: 2, description: 'Public HTTPS images. One starts the render from a first frame; a second becomes the last frame, in first-to-last order.', items: { type: string, format: uri, pattern: '^https://' } } + duration: { type: integer, minimum: 1, default: 5, description: 'Requested output duration in seconds. Billed per second at the rate for the chosen resolution. If a duration is unsupported, the API returns `400`.' } + resolution: { type: string, enum: ['480P', '768P'], default: '768P', description: 'Output resolution. Upper-case P, and the only two values this model accepts. Price scales with it.' } + seed: { type: integer, minimum: 0, description: Reuse a seed to re-render the same motion. A random seed is chosen when omitted. } TaskResponse: type: object required: [data] properties: data: $ref: '#/components/schemas/Task' + description: Accepted or current BeatAPI task state. Usage: type: object - required: [object, credit_balance, total_tasks, credits_settled, credits_refunded, concurrency, by_workflow] + required: [object, credit_balance, total_tasks, credits_settled, credits_refunded, concurrency, by_workflow, by_capability, by_model, by_api_key] properties: object: type: string enum: [usage] credit_balance: - type: integer - description: Current credit balance. It may be negative. + type: number + format: double + multipleOf: 0.01 + description: Current USD balance. The compatibility field name is retained; 1 Credit equals $1 USD. The balance may be negative. total_tasks: type: integer credits_settled: - type: integer + type: number + format: double + multipleOf: 0.01 credits_refunded: - type: integer + type: number + format: double + multipleOf: 0.01 concurrency: type: object required: [limit, active] @@ -437,20 +1289,68 @@ components: example: 2 active: type: integer - description: Active processing tasks currently using BeatAPI processing resources. Music Video storyboard_ready and requires_action tasks can have settled credits without counting toward this value. + description: Active processing tasks currently using BeatAPI processing resources. Music Video storyboard_ready and requires_action tasks can have settled USD usage without counting toward this value. by_workflow: type: array + description: Compatibility view containing workflow tasks only. Image, video, and Effect tasks are reported under by_capability instead. items: type: object required: [workflow, tasks, credits_settled] properties: workflow: type: string - enum: [music-video, ecommerce-video] + enum: [music-video, ecommerce-video, video-analysis] tasks: type: integer credits_settled: + type: number + format: double + multipleOf: 0.01 + by_capability: + type: array + items: + type: object + required: [task_kind, capability_id, tasks, credits_settled] + properties: + task_kind: + type: string + enum: [workflow, effect, image, video] + capability_id: + type: string + tasks: type: integer + credits_settled: + type: number + format: double + multipleOf: 0.01 + by_model: + type: array + items: + type: object + required: [media_type, model, tasks, credits_settled] + properties: + media_type: + type: string + enum: [image, video] + model: + type: string + tasks: + type: integer + credits_settled: + type: number + format: double + multipleOf: 0.01 + by_api_key: + type: array + items: + type: object + required: [api_key_id, title, key_prefix, tasks, credits_settled] + properties: + api_key_id: { type: string } + title: { type: string } + key_prefix: { type: string } + tasks: { type: integer } + credits_settled: { type: number, format: double, multipleOf: 0.01 } realtime: type: object required: [sessions, credits, active] @@ -459,8 +1359,10 @@ components: type: integer description: Total BeatAPI realtime sessions for this account. credits: - type: integer - description: Credits settled by connected realtime sessions. + type: number + format: double + multipleOf: 0.01 + description: USD amount settled by connected realtime sessions. active: type: integer description: Realtime sessions in ready, connecting, or active state. @@ -470,6 +1372,238 @@ components: properties: data: $ref: '#/components/schemas/Usage' + VideoAnalysisTaskCreateRequest: + type: object + additionalProperties: false + required: [video_url, prompt] + properties: + video_url: + type: string + format: uri + description: BeatAPI-hosted MP4 or MOV input URL returned by POST /v1/files for the current account. Maximum verified duration is 600 seconds. + prompt: + type: string + minLength: 1 + maxLength: 12000 + description: Analysis instruction. Ask for timestamped output when temporal precision matters. + analysis_depth: + type: string + enum: [standard, deep] + default: standard + description: Standard is the default low-cost route; deep uses the higher-reasoning route. + max_output_tokens: + type: integer + minimum: 256 + maximum: 8192 + default: 2048 + description: Requested answer budget. Provider-reported output usage can include hidden reasoning tokens above this value; BeatAPI records the variance for audit and settles actual reported usage within the task reservation. + MusicVideoTaskCreateRequest: + oneOf: + - $ref: '#/components/schemas/StandardMusicVideoTaskCreateRequest' + - $ref: '#/components/schemas/PremiumMusicVideoTaskCreateRequest' + discriminator: + propertyName: mv_tier + mapping: + standard: '#/components/schemas/StandardMusicVideoTaskCreateRequest' + premium: '#/components/schemas/PremiumMusicVideoTaskCreateRequest' + StandardMusicVideoTaskCreateRequest: + type: object + required: [images, audio_url] + allOf: + - if: + required: [lip_sync] + properties: + lip_sync: { const: true } + then: + required: [lip_ref_url] + properties: + lip_ref_url: {} + not: + anyOf: + - required: [mv_mode] + properties: { mv_mode: {} } + - required: [lip_ref_urls] + properties: { lip_ref_urls: {} } + properties: + mv_tier: + type: string + enum: [standard] + default: standard + description: May be omitted to preserve the backwards-compatible Standard contract. + images: + type: array + minItems: 1 + maxItems: 7 + description: Standard scene images. Provide 1-7 public HTTPS PNG, JPEG, or WebP URLs; place the primary subject or opening scene first. Upload local files through `POST /v1/files` and use the returned `data.url`. + items: { type: string, format: uri } + audio_url: + type: string + format: uri + description: Public HTTPS audio URL; Standard audio must be 10-180 seconds. + prompt: { type: string, maxLength: 3000, description: "Optional creative direction for story, setting, performance, camera, lighting, and pacing. Maximum 3000 characters." } + language: { type: string, enum: [en, zh], description: Dialogue and lyric language used by the Standard workflow. } + quality: { type: string, enum: [standard, high], default: standard, description: Generation quality tier. High quality is unavailable at 540p. } + style: { type: string, maxLength: 200, description: "Optional concise visual style, such as cinematic, anime, documentary, or fashion editorial." } + aspect_ratio: { type: string, enum: ['1:1', '16:9', '9:16', '4:3', '3:4'], description: Target output placement. Set explicitly for the destination player or social feed. } + resolution: { type: string, enum: [540p, 720p, 1080p], default: 720p, description: Output resolution. 540p cannot be combined with high quality or lip sync. } + lip_sync: + type: boolean + default: false + description: Generate lip-synchronized performance. When true, `lip_ref_url` is required. + lip_ref_url: + type: string + format: uri + description: Public HTTPS close-up, front-facing face image used for Standard lip sync. + add_subtitle: { type: boolean, default: false, description: Burn generated or supplied subtitles into the final video. } + subtitle_color: { type: string, pattern: '^#[0-9A-Fa-f]{6}$', example: '#FFFFFF', description: Subtitle text color as a six-digit hexadecimal value. Used when subtitles are enabled. } + srt_url: { type: string, format: uri, description: Optional public HTTPS `.srt` subtitle file. Upload a local subtitle through `POST /v1/files`. } + duration: + type: integer + minimum: 10 + maximum: 180 + description: Billing fallback only; detected audio duration wins. + compose_mode: + type: string + enum: [auto, manual] + default: auto + description: Auto composes the final Music Video; manual pauses at `requires_action` so shots can be reviewed or edited before compose. + PremiumMusicVideoTaskCreateRequest: + allOf: + - oneOf: + - $ref: '#/components/schemas/PremiumMusicVideoSingTaskCreateRequest' + - $ref: '#/components/schemas/PremiumMusicVideoSingPerformTaskCreateRequest' + - $ref: '#/components/schemas/PremiumMusicVideoDanceTaskCreateRequest' + - $ref: '#/components/schemas/PremiumMusicVideoPerformTaskCreateRequest' + discriminator: + propertyName: mv_mode + mapping: + sing: '#/components/schemas/PremiumMusicVideoSingTaskCreateRequest' + sing_perform: '#/components/schemas/PremiumMusicVideoSingPerformTaskCreateRequest' + dance: '#/components/schemas/PremiumMusicVideoDanceTaskCreateRequest' + perform: '#/components/schemas/PremiumMusicVideoPerformTaskCreateRequest' + PremiumMusicVideoTaskRequestBase: + type: object + required: [mv_tier, mv_mode, audio_url] + not: + anyOf: + - required: [quality] + properties: { quality: {} } + - required: [language] + properties: { language: {} } + - required: [lip_sync] + properties: { lip_sync: {} } + - required: [lip_ref_url] + properties: { lip_ref_url: {} } + - required: [srt_url] + properties: { srt_url: {} } + - required: [compose_mode] + properties: { compose_mode: {} } + properties: + mv_tier: { type: string, enum: [premium], description: Selects the Premium Music Video workflow and its mode-specific inputs. } + mv_mode: { type: string, enum: [sing, sing_perform, dance, perform], description: Premium performance mode. Sing modes require `lip_ref_urls`; dance and perform require exactly six `images`. } + audio_url: + type: string + format: uri + description: Public HTTPS audio URL; Premium audio must be 10-300 seconds. + prompt: { type: string, maxLength: 3000, description: "Optional creative direction for story, setting, performance, camera, lighting, and pacing. Maximum 3000 characters." } + style: { type: string, maxLength: 200 } + aspect_ratio: { type: string, enum: ['1:1', '16:9', '9:16', '4:3', '3:4'], description: Target output placement. Set explicitly for the destination player or social feed. } + resolution: + type: string + enum: [720p] + default: 720p + description: Premium output is fixed to 720p. + add_subtitle: { type: boolean, default: false, description: Burn generated subtitles into the final video. } + subtitle_color: { type: string, pattern: '^#[0-9A-Fa-f]{6}$', example: '#FFFFFF', description: Subtitle text color as a six-digit hexadecimal value. Used when subtitles are enabled. } + duration: + type: integer + minimum: 10 + maximum: 300 + description: Premium billing fallback only; detected audio duration wins. + PremiumMusicVideoSingTaskCreateRequest: + allOf: + - $ref: '#/components/schemas/PremiumMusicVideoTaskRequestBase' + - type: object + required: [lip_ref_urls] + properties: + mv_mode: { type: string, enum: [sing] } + images: + type: array + minItems: 0 + maxItems: 6 + description: Optional Premium scene images for sing mode. Provide up to six public HTTPS PNG, JPEG, or WebP URLs. + items: { type: string, format: uri } + lip_ref_urls: + type: array + minItems: 1 + maxItems: 2 + description: Required for sing mode. One or two public HTTPS close-up, front-facing face images for lip synchronization. + items: { type: string, format: uri } + PremiumMusicVideoSingPerformTaskCreateRequest: + allOf: + - $ref: '#/components/schemas/PremiumMusicVideoTaskRequestBase' + - type: object + required: [lip_ref_urls] + properties: + mv_mode: { type: string, enum: [sing_perform] } + images: + type: array + minItems: 0 + maxItems: 6 + description: Optional Premium scene images for sing and perform mode. Provide up to six public HTTPS PNG, JPEG, or WebP URLs. + items: { type: string, format: uri } + lip_ref_urls: + type: array + minItems: 1 + maxItems: 2 + description: Required for sing and perform mode. One or two public HTTPS close-up, front-facing face images for lip synchronization. + items: { type: string, format: uri } + PremiumMusicVideoDanceTaskCreateRequest: + allOf: + - $ref: '#/components/schemas/PremiumMusicVideoTaskRequestBase' + - type: object + required: [images] + not: + required: [lip_ref_urls] + properties: { lip_ref_urls: {} } + properties: + mv_mode: { type: string, enum: [dance] } + images: + type: array + minItems: 6 + maxItems: 6 + description: Required for dance mode. Provide exactly six public HTTPS PNG, JPEG, or WebP scene images. + items: { type: string, format: uri } + PremiumMusicVideoPerformTaskCreateRequest: + allOf: + - $ref: '#/components/schemas/PremiumMusicVideoTaskRequestBase' + - type: object + required: [images] + not: + required: [lip_ref_urls] + properties: { lip_ref_urls: {} } + properties: + mv_mode: { type: string, enum: [perform] } + images: + type: array + minItems: 6 + maxItems: 6 + description: Required for perform mode. Provide exactly six public HTTPS PNG, JPEG, or WebP scene images. + items: { type: string, format: uri } + EditMusicVideoShotRequest: + type: object + additionalProperties: false + required: [prompt] + properties: + prompt: + type: string + maxLength: 3000 + images: + type: array + minItems: 0 + maxItems: 6 + description: Premium tasks only. Optional replacement scene images; an empty array is treated as omitted. Standard tasks reject this field. + items: { type: string, format: uri } RealtimeSession: type: object required: [id, object, status, expires_at, max_duration_seconds, allowed_origins, credits, request_id, created_at, connected_at, closed_at] @@ -477,41 +1611,66 @@ components: id: type: string pattern: '^rts_' + description: Stable Realtime Session ID used to inspect or close the session. object: type: string enum: [realtime.session] + description: Object discriminator; always `realtime.session`. status: type: string enum: [ready, connecting, active, closed, failed, expired] description: Active means BeatAPI accepted the first billing heartbeat after remote output began. - client_secret: - type: string - description: Returned only by POST. Give this short-lived BeatAPI secret to the browser SDK; never give the browser an sk_ API key. - pattern: '^brt_live_' expires_at: type: string format: date-time + description: Time when the unconnected short-lived session credential expires. max_duration_seconds: type: integer enum: [15, 60, 300] + description: Maximum selected live duration and billing tier in seconds. allowed_origins: type: array - items: { type: string, format: uri } + description: Exact browser origins authorized to use this Session. + items: + type: string + format: uri + pattern: '^(https://[A-Za-z0-9.-]+(?::[0-9]+)?|http://(?:localhost|127\\.0\\.0\\.1|\\[::1\\])(?::[0-9]+)?)$' + description: Exact browser origin. Use HTTPS in production; HTTP is accepted only for localhost development. + example: https://app.example.com credits: type: object required: [reserved, settled, refunded] + description: USD reservation, settlement, and refund lifecycle for this Realtime Session. Compatibility field names are retained. properties: - reserved: { type: integer } - settled: { type: integer } - refunded: { type: integer } + reserved: { type: number, format: double, multipleOf: 0.01, description: USD amount reserved when the Session is created. } + settled: { type: number, format: double, multipleOf: 0.01, description: USD amount settled after the first accepted billing heartbeat. } + refunded: { type: number, format: double, multipleOf: 0.01, description: USD amount refunded if the Session ends without billing activation. } request_id: type: string - created_at: { type: string, format: date-time } + description: Correlation ID to retain for logs and BeatAPI support. + created_at: { type: string, format: date-time, description: Time when the Session was created. } connected_at: type: [string, 'null'] format: date-time description: Time of the first accepted BeatAPI billing heartbeat; null before billing activation. - closed_at: { type: [string, 'null'], format: date-time } + closed_at: { type: [string, 'null'], format: date-time, description: "Time when the Session closed, or null while it remains open." } + RealtimeSessionCreated: + allOf: + - $ref: '#/components/schemas/RealtimeSession' + - type: object + required: [client_secret] + properties: + client_secret: + type: string + description: Short-lived BeatAPI browser credential returned only by POST. Never expose an sk_ API key to the browser. + pattern: '^brt_live_' + RealtimeSessionCreateResponse: + type: object + required: [data] + properties: + data: + $ref: '#/components/schemas/RealtimeSessionCreated' + description: Created Realtime Session including the one-time short-lived browser credential. RealtimeSessionResponse: type: object required: [data] @@ -524,6 +1683,7 @@ components: properties: data: $ref: '#/components/schemas/File' + description: Uploaded file metadata and the public HTTPS URL to use in later requests. WebhookEndpointList: type: object required: [object, data] @@ -547,6 +1707,7 @@ components: properties: data: $ref: '#/components/schemas/WebhookEndpoint' + description: Created or retrieved webhook endpoint. Public API responses return the full signing secret at creation and mask it afterward; authenticated dashboard owners can explicitly reveal it again. DeleteResponse: type: object required: [data] @@ -565,10 +1726,12 @@ components: properties: error: type: object + description: Structured BeatAPI error. Use `code` for program logic and retain `request_id` for support. required: [code, message, request_id] properties: code: type: string + description: Stable machine-readable error code. enum: - bad_request - unauthorized @@ -578,6 +1741,7 @@ components: - idempotency_conflict - user_concurrency_exceeded - rate_limit_exceeded + - content_policy_violation - processing_unavailable - processing_failed - processing_timeout @@ -592,8 +1756,10 @@ components: - internal_error message: type: string + description: Human-readable detail intended for logs and debugging. request_id: type: string + description: Correlation ID to retain for BeatAPI support. retry_after_seconds: type: integer description: Present on retryable rate-limit or capacity responses when the client should wait before retrying. @@ -637,7 +1803,214 @@ components: message: Too many polling requests. Poll every 5-10 seconds. request_id: req_xxx retry_after_seconds: 12 + InternalError: + description: BeatAPI could not complete the request because of an internal or storage failure. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: + code: internal_error + message: Internal error. Contact support with the request_id if the problem continues. + request_id: req_xxx + ProcessingUnavailable: + description: BeatAPI processing is temporarily unavailable or did not complete within the processing window. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: + code: processing_unavailable + message: Task processing is temporarily unavailable. + request_id: req_xxx paths: + /v1/models: + get: + operationId: listTextModels + tags: [Text] + summary: List available text models + description: Returns the text models currently enabled for this BeatAPI environment in OpenAI list format. + security: + - BearerAuth: [] + - ApiKeyHeader: [] + - GoogleApiKeyHeader: [] + responses: + '200': + description: OpenAI-compatible model list + content: + application/json: + schema: { $ref: '#/components/schemas/TextModelList' } + example: + object: list + data: + - id: gpt-5.6-sol + object: model + created: 1788220800 + owned_by: beatapi + - id: gpt-5.6-terra + object: model + created: 1788220800 + owned_by: beatapi + - id: gpt-5.6-luna + object: model + created: 1788220800 + owned_by: beatapi + - id: claude-fable-5-1 + object: model + created: 1788220800 + owned_by: beatapi + '401': { description: Invalid or missing BeatAPI API key } + '404': { description: Text API is not enabled for this environment } + '429': { description: Request rate limit exceeded } + + /v1/responses: + post: + operationId: createTextResponse + tags: [Text] + summary: Create a text response + description: Recommended OpenAI-compatible surface for reasoning, tools, structured outputs, and streaming. + security: + - BearerAuth: [] + - ApiKeyHeader: [] + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/TextPassthroughRequest' } + example: + model: gpt-5.6-sol + input: Design a resilient webhook retry strategy for a payments API. + reasoning: { effort: medium } + stream: true + responses: + '200': + description: OpenAI-compatible JSON response or server-sent event stream + content: + application/json: + schema: { $ref: '#/components/schemas/TextPassthroughResponse' } + text/event-stream: + schema: { type: string } + '401': { description: Invalid or missing BeatAPI API key } + '402': { description: Insufficient BeatAPI USD balance } + '429': { description: Rate limit or settlement backlog } + '502': { description: Text gateway could not complete the request } + '503': { description: Text service is temporarily unavailable } + + /v1/chat/completions: + post: + operationId: createChatCompletion + tags: [Text] + summary: Create a text chat completion + description: OpenAI Chat Completions-compatible endpoint for existing SDK integrations. + security: + - BearerAuth: [] + - ApiKeyHeader: [] + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/TextPassthroughRequest' } + example: + model: gpt-5.6-terra + messages: + - role: user + content: Summarize the attached product requirements into an implementation plan. + stream: true + responses: + '200': + description: OpenAI-compatible JSON response or server-sent event stream + content: + application/json: + schema: { $ref: '#/components/schemas/TextPassthroughResponse' } + text/event-stream: + schema: { type: string } + '401': { description: Invalid or missing BeatAPI API key } + '402': { description: Insufficient BeatAPI USD balance } + '429': { description: Rate limit or settlement backlog } + '502': { description: Text gateway could not complete the request } + '503': { description: Text service is temporarily unavailable } + + /v1/messages: + post: + operationId: createMessage + tags: [Text] + summary: Create an Anthropic-compatible message + description: Anthropic Messages-compatible endpoint. Send the BeatAPI key with x-api-key or Bearer authentication. + security: + - ApiKeyHeader: [] + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/TextPassthroughRequest' } + example: + model: gpt-5.6-luna + max_tokens: 1024 + messages: + - role: user + content: Classify this support request and return JSON. + responses: + '200': + description: Anthropic-compatible JSON response or server-sent event stream + content: + application/json: + schema: { $ref: '#/components/schemas/TextPassthroughResponse' } + text/event-stream: + schema: { type: string } + '401': { description: Invalid or missing BeatAPI API key } + '402': { description: Insufficient BeatAPI USD balance } + '429': { description: Rate limit or settlement backlog } + '502': { description: Text gateway could not complete the request } + '503': { description: Text service is temporarily unavailable } + + /v1beta/models/{model}:{action}: + post: + operationId: generateGeminiCompatibleContent + tags: [Text] + summary: Generate text content with a Gemini-compatible request + description: Gemini-compatible endpoint for generateContent and streamGenerateContent. The BeatAPI key is removed before forwarding. + security: + - GoogleApiKeyHeader: [] + - GoogleApiKeyQuery: [] + - BearerAuth: [] + parameters: + - in: path + name: model + required: true + schema: { $ref: '#/components/schemas/TextModelId' } + - in: path + name: action + required: true + schema: { type: string, enum: [generateContent, streamGenerateContent] } + requestBody: + required: true + content: + application/json: + schema: + type: object + additionalProperties: true + example: + contents: + - role: user + parts: + - text: Explain this architecture decision in three concise bullets. + responses: + '200': + description: Gemini-compatible JSON response or server-sent event stream + content: + application/json: + schema: { $ref: '#/components/schemas/TextPassthroughResponse' } + text/event-stream: + schema: { type: string } + '401': { description: Invalid or missing BeatAPI API key } + '402': { description: Insufficient BeatAPI USD balance } + '429': { description: Rate limit or settlement backlog } + '502': { description: Text gateway could not complete the request } + '503': { description: Text service is temporarily unavailable } + /v1/workflows: get: operationId: listWorkflows @@ -646,53 +2019,596 @@ paths: summary: List launch workflows security: [] responses: - '200': - description: Workflow list + '200': + description: Workflow list + content: + application/json: + schema: + $ref: '#/components/schemas/WorkflowListResponse' + example: + data: + object: list + data: + - id: music-video + object: workflow + name: Music Video API + description: Generate short music video clips from audio, lyrics, and visual direction. + - id: ecommerce-video + object: workflow + name: Ecommerce Video API + description: Generate product ad videos from product images and a short creative brief. + - id: video-analysis + object: workflow + name: Video Analysis API + description: Analyze an uploaded video with timestamp-aware multimodal reasoning. + '429': + $ref: '#/components/responses/RateLimited' + + /v1/media/models: + get: + operationId: listGenerationModels + tags: [Generation] + summary: List BeatAPI image and video generation models + description: Returns stable BeatAPI model aliases and public input modes. Internal execution routing is not part of this contract. + security: [] + parameters: + - in: query + name: media_type + schema: { type: string, enum: [image, video] } + responses: + '200': + description: Generation model list + content: + application/json: + schema: { $ref: '#/components/schemas/GenerationModelListResponse' } + '400': { $ref: '#/components/responses/BadRequest' } + '429': { $ref: '#/components/responses/RateLimited' } + + /v1/images/tasks: + post: + operationId: createImageGenerationTask + tags: [Generation] + summary: Create an image generation task + description: | + Creates one asynchronous image task. Select the model-specific request + contract with `model`, save the returned `data.id`, and poll + `GET /v1/tasks/{task_id}` until the task succeeds or fails. + security: [{ BearerAuth: [] }] + parameters: + - in: header + name: Idempotency-Key + required: false + schema: { type: string, maxLength: 255 } + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/ImageGenerationTaskCreateRequest' } + examples: + Nano Banana: + summary: Nano Banana + value: + model: nano-banana + prompt: Editorial product photograph on a warm stone pedestal. + aspect_ratio: '1:1' + output_format: png + Nano Banana 2: + summary: Nano Banana 2 at 2K + value: + model: nano-banana-2 + prompt: Editorial campaign image with crisp product typography. + aspect_ratio: '4:5' + resolution: 2K + output_format: png + Nano Banana 2 Lite: + summary: Nano Banana 2 Lite at 1K + value: + model: nano-banana-2-lite + prompt: Fast social product visual on a clean studio background. + aspect_ratio: '1:1' + output_format: jpeg + Nano Banana Pro: + summary: Nano Banana Pro + value: + model: nano-banana-pro + prompt: Place the supplied product in a premium editorial studio scene. + images: ['https://media.beatapi.io/samples/smart-bottle.png'] + aspect_ratio: '4:5' + resolution: 2K + output_format: png + GPT Image 2: + summary: GPT Image 2 + value: + model: gpt-image-2 + prompt: Create a clean campaign image from the supplied product reference. + images: ['https://media.beatapi.io/samples/smart-bottle.png'] + aspect_ratio: '1:1' + resolution: 2K + Seedream 5 Pro: + summary: Seedream 5 Pro + value: + model: seedream-5-pro + prompt: Recompose the product as a cinematic storefront campaign image. + images: ['https://media.beatapi.io/samples/smart-bottle.png'] + Grok Imagine Image 2.0: + summary: Grok Imagine Image 2.0 + value: + model: grok-imagine-image-2.0 + prompt: Turn the supplied product into a bold launch campaign visual. + images: ['https://media.beatapi.io/samples/smart-bottle.png'] + aspect_ratio: '16:9' + responses: + '201': + description: Image generation task accepted + content: + application/json: + schema: { $ref: '#/components/schemas/TaskResponse' } + examples: + Nano Banana Pro: + summary: Nano Banana Pro + value: + data: + id: task_8K2qA + object: task + task_kind: image + capability_id: nano-banana-pro + capability_version: null + media_type: image + model: nano-banana-pro + status: queued + stage: queued + created_at: 1782210000 + updated_at: 1782210000 + completed_at: null + output: null + usage: + credits_reserved: 0.09 + credits_charged: 0.09 + credits_settled: 0 + credits_refunded: 0 + request_id: req_abc123 + error_code: null + error_message: null + '400': { $ref: '#/components/responses/BadRequest' } + '401': { $ref: '#/components/responses/Unauthorized' } + '402': { description: Insufficient USD balance, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } + '409': { description: Idempotency key conflicts with another request body, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } + '429': { $ref: '#/components/responses/RateLimited' } + + /v1/videos/tasks: + post: + operationId: createVideoGenerationTask + tags: [Generation] + summary: Create a video generation task + description: | + Creates one asynchronous video task. Select the model-specific request + contract with `model`, save the returned `data.id`, and poll + `GET /v1/tasks/{task_id}` until the task succeeds or fails. + security: [{ BearerAuth: [] }] + parameters: + - in: header + name: Idempotency-Key + required: false + schema: { type: string, maxLength: 255 } + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/VideoGenerationTaskCreateRequest' } + examples: + MiniMax H3: + summary: MiniMax H3 + value: + model: minimax-h3 + prompt: A slow cinematic push through a misty mountain village at dawn. + duration: 5 + aspect_ratio: '16:9' + resolution: 768P + Seedance 2: + summary: Seedance 2 + value: + model: seedance-2 + prompt: A handheld tracking shot through a crowded neon night market. + duration: 8 + aspect_ratio: '16:9' + resolution: 1080p + generate_audio: true + Seedance 2 Fast: + summary: Seedance 2 Fast + value: + model: seedance-2-fast + prompt: A fast cinematic production draft through a neon night market. + duration: 5 + aspect_ratio: '16:9' + resolution: 720p + generate_audio: true + Seedance 2 Mini: + summary: Seedance 2 Mini + value: + model: seedance-2-mini + prompt: A low-cost storyboard draft for a product reveal. + duration: 5 + aspect_ratio: '16:9' + resolution: 720p + Veo 3.1 Quality: + summary: Veo 3.1 Quality + value: + model: veo-3.1 + prompt: A cinematic aerial reveal of a quiet coastal village at sunrise. + aspect_ratio: '16:9' + resolution: 720p + quality: Quality + Veo 3.1 Fast: + summary: Veo 3.1 Fast + value: + model: veo-3.1 + prompt: A fast cinematic product reveal with natural camera motion. + aspect_ratio: '16:9' + resolution: 1080p + quality: Fast + Veo 3.1 Lite: + summary: Veo 3.1 Lite + value: + model: veo-3.1 + prompt: A concise storyboard-ready product reveal. + aspect_ratio: '16:9' + resolution: 4k + quality: Lite + Veo 3.1 Reference Fast: + summary: Veo 3.1 Reference Fast + value: + model: veo-3.1 + prompt: Create a cohesive cinematic scene using the supplied visual references. + reference_images: + - https://media.beatapi.io/samples/neon-singer.png + - https://media.beatapi.io/samples/smart-bottle.png + aspect_ratio: '16:9' + resolution: 720p + quality: Fast + Seedance 2.5: + summary: Seedance 2.5 + value: + model: seedance-2.5 + prompt: A cinematic tracking shot through a rain-lit night market. + duration: 5 + aspect_ratio: '16:9' + resolution: 720p + generate_audio: true + seed: -1 + Kling 3: + summary: Kling 3 + value: + model: kling-3 + prompt: A dramatic product reveal with a slow orbiting camera move. + duration: 5 + aspect_ratio: '16:9' + resolution: pro + sound: true + Kling 2.6 Motion Control: + summary: Kling 2.6 Motion Control + value: + model: kling-2.6-motion-control + prompt: Keep the character identity stable while following the reference motion. + images: + - https://media.beatapi.io/inputs/character.png + reference_videos: + - https://media.beatapi.io/inputs/motion.mp4 + resolution: 720p + character_orientation: video + Kling 3.0 Motion Control: + summary: Kling 3.0 Motion Control + value: + model: kling-3-motion-control + prompt: Preserve the character and transfer the full-body motion precisely. + images: + - https://media.beatapi.io/inputs/character.png + reference_videos: + - https://media.beatapi.io/inputs/motion.mp4 + resolution: 1080p + character_orientation: image + background_source: input_video + responses: + '201': + description: Video generation task accepted + content: + application/json: + schema: { $ref: '#/components/schemas/TaskResponse' } + examples: + MiniMax H3: + summary: MiniMax H3 + value: + data: + id: task_8K2qA + object: task + task_kind: video + capability_id: minimax-h3 + capability_version: null + media_type: video + model: minimax-h3 + status: queued + stage: queued + created_at: 1782210000 + updated_at: 1782210000 + completed_at: null + output: null + usage: + credits_reserved: 0.2 + credits_charged: 0.2 + billable_duration_seconds: 5 + credits_settled: 0 + credits_refunded: 0 + request_id: req_abc123 + error_code: null + error_message: null + '400': { $ref: '#/components/responses/BadRequest' } + '401': { $ref: '#/components/responses/Unauthorized' } + '402': { description: Insufficient USD balance, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } + '409': { description: Idempotency key conflicts with another request body, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } } + '429': { $ref: '#/components/responses/RateLimited' } + + /v1/effects: + get: + operationId: listEffects + tags: [Effects] + summary: List active Effects + security: [] + description: Returns only versioned Effects that have passed BeatAPI publication gates. Internal integration names, template ids, costs, and execution context are never exposed. + parameters: + - in: query + name: output_type + schema: { type: string, enum: [image, video] } + - in: query + name: category + schema: { type: string } + responses: + '200': + description: Active Effect catalog + content: + application/json: + schema: { $ref: '#/components/schemas/EffectListResponse' } + '400': { $ref: '#/components/responses/BadRequest' } + '429': { $ref: '#/components/responses/RateLimited' } + + /v1/effects/{effect_id}: + get: + operationId: getEffect + tags: [Effects] + summary: Get an active Effect + security: [] + parameters: + - in: path + name: effect_id + required: true + schema: { type: string } + responses: + '200': + description: Effect definition and immutable current version contract + content: + application/json: + schema: { $ref: '#/components/schemas/EffectResponse' } + '404': + description: Effect is unknown or not currently published. + content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } + + /v1/effects/tasks: + post: + operationId: createEffectTask + tags: [Effects] + summary: Create an Effect task + security: [{ BearerAuth: [] }] + description: | + Creates an asynchronous image or video Effect task. Read the catalog + first: image count, accepted input types, output resolution/duration, + and execution contract are fixed by the selected Effect version. Send + an `Idempotency-Key`; an exact replay returns the + accepted task before remote input URLs are revalidated, while a changed + body returns `idempotency_conflict`. + + The USD amount is reserved atomically when accepted, settled on success, and + fully refunded after a definite processing failure. An uncertain create + result is not blindly retried and never switches integrations automatically. + parameters: + - in: header + name: Idempotency-Key + required: false + schema: { type: string, maxLength: 255 } + requestBody: + required: true + content: + application/json: + schema: + type: object + additionalProperties: false + required: [effect_id, images] + properties: + effect_id: { type: string, example: video-muscle-max, description: Stable published Effect ID from `GET /v1/effects`. } + effect_version: + type: integer + minimum: 1 + description: Optional immutable version. Omit to use the current published version. + images: + type: array + minItems: 1 + maxItems: 7 + description: Public HTTPS input images in the order required by the selected Effect version. Read `GET /v1/effects/{effect_id}` for the exact count and accepted media rules; upload local files with `POST /v1/files`. + items: { type: string, format: uri } + options: + type: object + additionalProperties: false + description: Optional controls supported by the selected Effect version. Omit unsupported controls; the catalog is the source of truth. + properties: + aspect_ratio: { type: string, description: Requested output aspect ratio when the selected Effect exposes this option. } + resolution: { type: string, description: Requested output resolution when the selected Effect exposes this option. } + duration: { type: integer, description: Requested video duration in seconds when the selected Effect exposes this option. } + bgm: { type: boolean, description: Include background music when supported by the selected Effect. } + seed: { type: integer, description: Optional deterministic seed when supported by the selected Effect. } + example: + effect_id: video-muscle-max + images: ['https://media.beatapi.io/samples/portrait.png'] + options: { resolution: 720p, duration: 12 } + responses: + '201': + description: Effect task accepted + content: + application/json: + schema: { $ref: '#/components/schemas/TaskResponse' } + example: + data: + id: task_effect123 + object: task + task_kind: effect + capability_id: video-muscle-max + capability_version: 1 + effect_id: video-muscle-max + effect_version: 1 + status: queued + stage: queued + created_at: 1782210000 + updated_at: 1782210000 + completed_at: null + output: null + usage: { credits_reserved: 1.2, credits_charged: 1.2, credits_settled: 0, credits_refunded: 0 } + request_id: req_effect123 + error_code: null + error_message: null + '400': { $ref: '#/components/responses/BadRequest' } + '401': { $ref: '#/components/responses/Unauthorized' } + '402': + description: Insufficient USD balance. + content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } + '404': + description: Effect or requested version is unavailable. + content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } + '409': + description: Idempotency key conflicts with another request body. + content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } + '429': { $ref: '#/components/responses/RateLimited' } + + /v1/video-analysis/tasks: + post: + operationId: createVideoAnalysisTask + tags: [Video Analysis] + x-apidog-folder: Video Analysis API + summary: Analyze an uploaded video + security: + - BearerAuth: [] + parameters: + - in: header + name: Idempotency-Key + required: false + schema: { type: string } + description: Optional retry key. Reusing the same key with the same normalized request returns the accepted task. + example: video-analysis-cus_123-01 + description: | + Analyze one MP4 or MOV previously uploaded with `POST /v1/files` by the + current BeatAPI account. `standard` is the default low-cost route; + `deep` uses the higher-reasoning route. BeatAPI reserves an estimate from + verified video duration and the output budget, then settles from actual + input and output token usage. Standard costs $0.36 per 1M input tokens and + $1.60 per 1M output tokens; Deep costs $0.72 per 1M input tokens and $5.00 + per 1M output tokens. Each completed task is rounded up to the nearest + $0.01 because the shared USD balance settles in cents. Save `data.id` and poll the shared Task + endpoint if the task remains queued for processing capacity. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/VideoAnalysisTaskCreateRequest' + examples: + standard: + summary: Timestamped standard analysis + value: + video_url: https://media.beatapi.io/uploads/input.mp4 + prompt: Return a timestamped action summary and identify every scene transition. + analysis_depth: standard + max_output_tokens: 2048 + deep: + summary: Deep motion review + value: + video_url: https://media.beatapi.io/uploads/input.mp4 + prompt: Inspect body motion, contact, continuity, and fast transitions with timestamps. + analysis_depth: deep + max_output_tokens: 4096 + responses: + '201': + description: Analysis accepted for asynchronous processing. content: application/json: schema: - $ref: '#/components/schemas/WorkflowListResponse' + $ref: '#/components/schemas/TaskResponse' example: data: - object: list - data: - - id: music-video - object: workflow - name: Music Video API - description: Generate short music video clips from audio, lyrics, and visual direction. - - id: ecommerce-video - object: workflow - name: Ecommerce Video API - description: Generate product ad videos from product images and a short creative brief. - '429': - $ref: '#/components/responses/RateLimited' + id: task_va8K2qA + object: task + task_kind: workflow + capability_id: video-analysis + capability_version: 1 + workflow: video-analysis + status: queued + stage: queued + created_at: 1787385600 + updated_at: 1787385600 + completed_at: null + output: null + usage: + credits_reserved: 0.01 + credits_charged: 0.01 + billable_duration_seconds: 60 + credits_settled: 0 + credits_refunded: 0 + request_id: req_va123 + error_code: null + error_message: null + '400': { $ref: '#/components/responses/BadRequest' } + '401': { $ref: '#/components/responses/Unauthorized' } + '402': + description: Account balance is not sufficient for the reserved analysis envelope. + content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } + '409': + description: Idempotency key conflicts with another request body. + content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } + '429': { $ref: '#/components/responses/RateLimited' } /v1/music-video/tasks: post: operationId: createMusicVideoTask tags: [Music Video] x-apidog-folder: Music Video API/Create Video - summary: Create Music Video + summary: Create a Music Video workflow task security: - BearerAuth: [] + parameters: + - in: header + name: Idempotency-Key + required: false + schema: { type: string } + description: Optional retry key. Reusing the same key with the same request body returns the accepted task; reusing it with a different body returns `409 idempotency_conflict`. + example: mv-create-cus_123-01 description: | - Music Video requires public HTTPS image URLs and a public HTTPS audio URL. - Prompt, language, quality, style, lip reference, subtitle, and format - controls are optional. BeatAPI detects the audio duration before task - creation and charges the detected billable duration at the selected - per-second customer-credit rate. If audio duration cannot be detected, - `duration` is used as the billing fallback. + Music Video requires public HTTPS media URLs. Requests that omit + `mv_tier` use `standard` and preserve the existing API behavior. + Premium retains the configured integration host but uses a distinct + execution path selected internally by BeatAPI. + Shared controls include prompt, aspect ratio, subtitles, and the tier's + billing fallback. Language, quality, `lip_sync`, `lip_ref_url`, + `srt_url`, and `compose_mode` are Standard-only. Premium uses `mv_mode` + plus `style` and mode-specific images or `lip_ref_urls`. BeatAPI detects audio duration before task + creation and records the billable duration in Task usage. If audio + duration cannot be detected, `duration` is used as the billing fallback. Input limits: - - Images must contain 1-7 public HTTPS URLs. + - Standard images must contain 1-7 public HTTPS URLs. + - Premium `sing` and `sing_perform` accept 0-6 scene images and require 1-2 `lip_ref_urls`. + - Premium `dance` and `perform` require exactly 6 scene images. - Use png, jpg, jpeg, or webp images; each image should be 50 MB or smaller. - Image aspect ratio should be between 1:4 and 4:1. - - Audio must be a public HTTPS mp3, wav, aac, or m4a URL between 10 and 180 seconds. + - Standard audio must be 10-180 seconds; Premium audio must be 10-300 seconds and contain vocals or lyrics rather than instrumental-only audio. - The audio file should be 50 MB or smaller. - `prompt` is optional and must be at most 3000 characters. - - `lip_ref_url`, when provided, must be a public HTTPS image URL. Use a clear, front-facing close-up face reference for best lip-sync results. - - `srt_url`, when provided, must point to an `.srt` subtitle file. - - `duration` is only a billing fallback when BeatAPI cannot detect the audio length; it must be 10-180 seconds and cannot override a detected audio duration. + - Standard `lip_sync=true` requires `lip_ref_url`. It must be a public HTTPS image URL showing a clear, front-facing close-up face. + - Standard `srt_url`, when provided, must point to an `.srt` subtitle file. + - `duration` is only a billing fallback when BeatAPI cannot detect the audio length; Standard accepts 10-180 seconds and Premium accepts 10-300 seconds. It cannot override a detected audio duration. BeatAPI validates URL shape, text limits, enum values, combination limits, and audio duration at task creation. Files uploaded through @@ -700,15 +2616,6 @@ paths: Third-party media URLs must follow the same media requirements and may be rejected during processing if invalid. - Customer pricing: - - MV 540p standard: 4 credits/s - - MV 720p standard: 5 credits/s - - MV 1080p standard: 6 credits/s - - lip_sync add-on: +2 credits/s - - MV 720p high: 16 credits/s - - MV 1080p high: 18 credits/s - - Ecommerce Video 1080p: 15 credits/s - Combination limits: - `quality=high` is not supported with `resolution=540p`. - `lip_sync=true` is not supported with `resolution=540p`. @@ -724,76 +2631,23 @@ paths: content: application/json: schema: - type: object - required: [images, audio_url] - properties: - images: - type: array - minItems: 1 - maxItems: 7 - description: 1-7 public HTTPS image URLs. Use png, jpg, jpeg, or webp images; each image should be 50 MB or smaller, with aspect ratio from 1:4 to 4:1. /v1/files uploads are checked before use; third-party URLs may be rejected during processing if invalid. - items: - type: string - format: uri - audio_url: - type: string - format: uri - description: Public HTTPS audio URL. Use mp3, wav, aac, or m4a; file size should be 50 MB or smaller and duration must be 10-180 seconds. - prompt: - type: string - maxLength: 3000 - description: Optional creative prompt, at most 3000 characters. - language: - type: string - enum: [en, zh] - lip_sync: - type: boolean - lip_ref_url: - type: string - format: uri - description: Public HTTPS image URL for lip-sync face reference. Use a clear, front-facing close-up face reference. - style: - type: string - maxLength: 200 - description: Optional style phrase, at most 200 characters. - quality: - type: string - enum: [standard, high] - default: standard - aspect_ratio: - type: string - enum: ['1:1', '16:9', '9:16', '4:3', '3:4'] - resolution: - type: string - enum: [540p, 720p, 1080p] - default: 720p - add_subtitle: - type: boolean - subtitle_color: - type: string - pattern: '^#[0-9A-Fa-f]{6}$' - example: '#FFFFFF' - srt_url: - type: string - format: uri - duration: - type: integer - minimum: 10 - maximum: 180 - description: Billing fallback when audio duration cannot be detected. It must be 10-180 seconds and cannot override a detected audio duration. - compose_mode: - type: string - enum: [auto, manual] - default: auto - example: - images: - - https://media.beatapi.io/samples/neon-singer.png - audio_url: https://media.beatapi.io/samples/neon-singer-preview.mp3 - prompt: Neon rooftop performance with metro cutaways and cinematic light trails. - language: en - quality: standard - resolution: 720p - compose_mode: auto + $ref: '#/components/schemas/MusicVideoTaskCreateRequest' + examples: + standard_backwards_compatible: + summary: Standard music video + value: + mv_tier: standard + images: ['https://media.beatapi.io/samples/neon-singer.png'] + audio_url: https://media.beatapi.io/samples/neon-singer-preview.mp3 + resolution: 720p + premium_sing: + value: { mv_tier: premium, mv_mode: sing, images: [], lip_ref_urls: ['https://media.beatapi.io/samples/singer.png'], audio_url: 'https://media.beatapi.io/samples/song.mp3', resolution: 720p } + premium_sing_perform: + value: { mv_tier: premium, mv_mode: sing_perform, images: ['https://media.beatapi.io/samples/stage.png'], lip_ref_urls: ['https://media.beatapi.io/samples/singer.png'], audio_url: 'https://media.beatapi.io/samples/song.mp3', resolution: 720p } + premium_dance: + value: { mv_tier: premium, mv_mode: dance, images: ['https://media.beatapi.io/1.png', 'https://media.beatapi.io/2.png', 'https://media.beatapi.io/3.png', 'https://media.beatapi.io/4.png', 'https://media.beatapi.io/5.png', 'https://media.beatapi.io/6.png'], audio_url: 'https://media.beatapi.io/samples/song.mp3', resolution: 720p } + premium_perform: + value: { mv_tier: premium, mv_mode: perform, images: ['https://media.beatapi.io/1.png', 'https://media.beatapi.io/2.png', 'https://media.beatapi.io/3.png', 'https://media.beatapi.io/4.png', 'https://media.beatapi.io/5.png', 'https://media.beatapi.io/6.png'], audio_url: 'https://media.beatapi.io/samples/song.mp3', resolution: 720p } responses: '201': description: Task accepted @@ -801,28 +2655,34 @@ paths: application/json: schema: $ref: '#/components/schemas/TaskResponse' - example: - data: - id: task_8K2qA - object: task - workflow: music-video - status: queued - stage: queued - storyboard: - shots: [] - created_at: 1782210000 - updated_at: 1782210000 - completed_at: null - output: null - usage: - credits_reserved: 75 - credits_charged: 75 - billable_duration_seconds: 15 - credits_settled: 0 - credits_refunded: 0 - request_id: req_abc123 - error_code: null - error_message: null + examples: + standard_backwards_compatible: + summary: Standard music video + value: + data: + id: task_8K2qA + object: task + task_kind: workflow + capability_id: music-video + capability_version: 1 + workflow: music-video + status: queued + stage: queued + storyboard: + shots: [] + created_at: 1782210000 + updated_at: 1782210000 + completed_at: null + output: null + usage: + credits_reserved: 1.5 + credits_charged: 1.5 + billable_duration_seconds: 15 + credits_settled: 0 + credits_refunded: 0 + request_id: req_abc123 + error_code: null + error_message: null '400': $ref: '#/components/responses/BadRequest' '401': @@ -838,6 +2698,17 @@ paths: code: insufficient_credits message: Account balance is not sufficient for this task. request_id: req_xxx + '409': + description: The Idempotency-Key was reused with a different body or while another request with that key is still being processed. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: + code: idempotency_conflict + message: This Idempotency-Key was already used with a different request body. + request_id: req_xxx '429': description: User concurrency exceeded. content: @@ -855,13 +2726,16 @@ paths: operationId: editMusicVideoShot tags: [Music Video] x-apidog-folder: Music Video API/Advanced Editing - summary: Edit Shot + summary: Edit a Music Video storyboard shot security: - BearerAuth: [] description: | Edit one storyboard shot using its BeatAPI `shot_id`. This operation - charges BeatAPI customer credits using the selected quality/resolution - rate and the shot duration. Default shot duration is 5 seconds. + charges the customer USD balance using the applicable task tier and shot + duration. Standard edits accept only `prompt`. Premium edits accept + `prompt` plus up to 6 optional replacement `images`. Generation quality, + resolution, and shot duration are inherited from the original task and + are not editable request fields. When the edit finishes, BeatAPI stores the edited shot media and exposes it on that shot. The existing final Music Video is not replaced until you call compose with the selected shot ids. @@ -878,35 +2752,21 @@ paths: schema: type: string example: shot_xxx + - in: header + name: Idempotency-Key + required: false + schema: { type: string } + description: Optional retry key. Reusing the same key for this task, shot, and request body returns the accepted task without charging the USD amount again; changing any of them returns `409 idempotency_conflict`. + example: music-edit-task_8K2qA-shot_xxx-01 requestBody: required: true content: application/json: schema: - type: object - required: [prompt] - properties: - prompt: - type: string - maxLength: 3000 - duration: - type: integer - minimum: 1 - maximum: 180 - default: 5 - quality: - type: string - enum: [standard, high] - default: standard - resolution: - type: string - enum: [540p, 720p, 1080p] - default: 720p + $ref: '#/components/schemas/EditMusicVideoShotRequest' example: prompt: Night city chorus with brighter face lighting. - duration: 5 - quality: standard - resolution: 720p + images: ['https://media.beatapi.io/samples/stage.png'] responses: '202': description: Shot edit accepted @@ -918,19 +2778,37 @@ paths: $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' + '402': + description: Account balance is not sufficient for this shot edit. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '409': + description: The Idempotency-Key was reused for a different task, shot, or request body, or the same request is still being processed. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' '404': description: Task or shot not found. content: application/json: schema: $ref: '#/components/schemas/Error' + '429': + $ref: '#/components/responses/RateLimited' + '500': + $ref: '#/components/responses/InternalError' + '502': + $ref: '#/components/responses/ProcessingUnavailable' /v1/music-video/tasks/{task_id}/shots/{shot_id}/media: post: operationId: getMusicVideoShotMedia tags: [Music Video] x-apidog-folder: Music Video API/Advanced Editing - summary: Get Shot Media + summary: Retrieve a Music Video storyboard shot media URL security: - BearerAuth: [] description: | @@ -989,18 +2867,24 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + '429': + $ref: '#/components/responses/RateLimited' + '500': + $ref: '#/components/responses/InternalError' + '502': + $ref: '#/components/responses/ProcessingUnavailable' /v1/music-video/tasks/{task_id}/compose: post: operationId: composeMusicVideoTask tags: [Music Video] x-apidog-folder: Music Video API/Advanced Editing - summary: Compose Video + summary: Compose a Music Video task from selected shots security: - BearerAuth: [] description: | Compose selected BeatAPI storyboard shots into the final Music Video. - This operation charges a fixed 1 BeatAPI customer credit. + This operation charges a fixed $1 USD. parameters: - in: path name: task_id @@ -1008,6 +2892,12 @@ paths: schema: type: string example: task_8K2qA + - in: header + name: Idempotency-Key + required: false + schema: { type: string } + description: Optional retry key. Reusing the same key for this task and request body returns the accepted task without charging the $1 compose amount again; changing either returns `409 idempotency_conflict`. + example: music-compose-task_8K2qA-01 requestBody: required: true content: @@ -1034,22 +2924,51 @@ paths: $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' + '402': + description: Account balance is not sufficient for this compose operation. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '409': + description: The Idempotency-Key was reused for a different task or request body, or the same request is still being processed. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' '404': description: Task or shot not found. content: application/json: schema: $ref: '#/components/schemas/Error' + '429': + $ref: '#/components/responses/RateLimited' + '500': + $ref: '#/components/responses/InternalError' + '502': + $ref: '#/components/responses/ProcessingUnavailable' /v1/ecommerce-video/tasks: post: operationId: createEcommerceVideoTask tags: [Ecommerce Video] x-apidog-folder: Ecommerce Video API - summary: Create Ecommerce Video + summary: Create an Ecommerce Video workflow task security: - BearerAuth: [] - description: Ecommerce Video requires product images and an explicit output duration. + parameters: + - in: header + name: Idempotency-Key + required: false + schema: { type: string } + description: Optional retry key. Reusing the same key with the same request body returns the accepted task; reusing it with a different body returns `409 idempotency_conflict`. + example: ecommerce-create-cus_123-01 + description: | + Ecommerce Video creates a complete product video from public HTTPS product or + lifestyle images, an explicit duration, and optional creative direction. Upload + local images with `POST /v1/files`, save the returned Task ID, and poll + `GET /v1/tasks/{task_id}` until the task succeeds or fails. requestBody: required: true content: @@ -1062,6 +2981,7 @@ paths: type: array minItems: 1 maxItems: 7 + description: Primary product or scene image first, followed by up to six additional public HTTPS PNG, JPEG, or WebP product or lifestyle images. Upload local files with `POST /v1/files` and use the returned `data.url`. items: type: string format: uri @@ -1069,21 +2989,25 @@ paths: type: integer minimum: 10 maximum: 60 + description: Required target output duration in seconds and the basis for USD calculation. Allowed range is 10-60 seconds. prompt: type: string maxLength: 2000 + description: Optional creative direction, audience, product benefit, offer, tone, scenes, or call to action. Maximum 2000 characters. aspect_ratio: type: string enum: ['16:9', '9:16', '1:1'] + description: Target output placement. Use 16:9 for landscape, 9:16 for vertical social, or 1:1 for square placements; set explicitly for stable layout. language: type: string enum: [en, zh] + description: Dialogue and narration language. Use `en` for English or `zh` for Chinese; set explicitly when the prompt contains mixed languages. example: images: - https://media.beatapi.io/samples/smart-bottle.png duration: 15 prompt: Fast product launch ad for paid social. - aspect_ratio: 9:16 + aspect_ratio: '9:16' responses: '201': description: Task accepted @@ -1095,6 +3019,9 @@ paths: data: id: task_p9Lm2 object: task + task_kind: workflow + capability_id: ecommerce-video + capability_version: 1 workflow: ecommerce-video status: queued stage: queued @@ -1103,8 +3030,8 @@ paths: completed_at: null output: null usage: - credits_reserved: 225 - credits_charged: 225 + credits_reserved: 4.5 + credits_charged: 4.5 billable_duration_seconds: 15 credits_settled: 0 credits_refunded: 0 @@ -1126,6 +3053,17 @@ paths: code: insufficient_credits message: Account balance is not sufficient for this task. request_id: req_xxx + '409': + description: The Idempotency-Key was reused with a different body or while another request with that key is still being processed. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: + code: idempotency_conflict + message: This Idempotency-Key was already used with a different request body. + request_id: req_xxx '429': description: User concurrency exceeded. content: @@ -1168,6 +3106,9 @@ paths: data: id: task_8K2qA object: task + task_kind: workflow + capability_id: music-video + capability_version: 1 workflow: music-video status: queued stage: queued @@ -1178,8 +3119,8 @@ paths: completed_at: null output: null usage: - credits_reserved: 75 - credits_charged: 75 + credits_reserved: 1.5 + credits_charged: 1.5 billable_duration_seconds: 15 credits_settled: 0 credits_refunded: 0 @@ -1192,6 +3133,9 @@ paths: data: id: task_8K2qA object: task + task_kind: workflow + capability_id: music-video + capability_version: 1 workflow: music-video status: storyboard_ready stage: storyboard_ready @@ -1210,10 +3154,10 @@ paths: updated_at: 1782210300 output: null usage: - credits_reserved: 75 - credits_charged: 75 + credits_reserved: 1.5 + credits_charged: 1.5 billable_duration_seconds: 15 - credits_settled: 75 + credits_settled: 1.5 credits_refunded: 0 request_id: req_abc123 error_code: null @@ -1224,6 +3168,9 @@ paths: data: id: task_8K2qA object: task + task_kind: workflow + capability_id: music-video + capability_version: 1 workflow: music-video status: failed stage: failed @@ -1234,11 +3181,11 @@ paths: completed_at: 1782210600 output: null usage: - credits_reserved: 75 - credits_charged: 75 + credits_reserved: 1.5 + credits_charged: 1.5 billable_duration_seconds: 15 credits_settled: 0 - credits_refunded: 75 + credits_refunded: 1.5 request_id: req_abc123 error_code: processing_timeout error_message: Task waited too long for platform capacity. @@ -1262,9 +3209,9 @@ paths: security: - BearerAuth: [] description: | - Reserve credits and allocate a short-lived BeatAPI realtime session. Send a unique + Reserve the selected USD amount and allocate a short-lived BeatAPI realtime session. Send a unique `Idempotency-Key`; retries with the same user, key, and body return the same session - and deterministic short-lived `client_secret` without reserving credits or capacity + and deterministic short-lived `client_secret` without reserving funds or capacity twice. The browser receives only that BeatAPI secret and connects with `@beatapi/realtime`. @@ -1280,7 +3227,8 @@ paths: - in: header name: Idempotency-Key required: true - schema: { type: string, maxLength: 128 } + schema: { type: string, minLength: 1, maxLength: 128 } + example: rts-create-cus_123-01 requestBody: required: true content: @@ -1292,14 +3240,22 @@ paths: max_duration_seconds: type: integer enum: [15, 60, 300] + description: Required maximum live session duration in seconds. The USD amount is reserved for the selected 15, 60, or 300 second tier. allowed_origins: type: array minItems: 1 maxItems: 10 - items: { type: string, format: uri } + description: Exact browser origins allowed to use the short-lived session secret. + items: + type: string + format: uri + pattern: '^(https://[A-Za-z0-9.-]+(?::[0-9]+)?|http://(?:localhost|127\\.0\\.0\\.1|\\[::1\\])(?::[0-9]+)?)$' + description: Exact browser origin. Use HTTPS in production; HTTP is accepted only for localhost development. + example: https://app.example.com metadata: type: object maxProperties: 20 + description: Optional server-defined string metadata for your own correlation. Up to 20 keys; keys are at most 64 characters and values at most 256 characters. propertyNames: { maxLength: 64 } additionalProperties: { type: string, maxLength: 256 } example: @@ -1311,11 +3267,25 @@ paths: description: Realtime session created content: application/json: - schema: { $ref: '#/components/schemas/RealtimeSessionResponse' } + schema: { $ref: '#/components/schemas/RealtimeSessionCreateResponse' } + example: + data: + id: rts_8K2qA + object: realtime.session + status: ready + client_secret: brt_live_example_short_lived_secret + expires_at: '2026-08-12T10:01:00.000Z' + max_duration_seconds: 60 + allowed_origins: ['https://app.example.com'] + credits: { reserved: 1.2, settled: 0, refunded: 0 } + request_id: req_abc123 + created_at: '2026-08-12T10:00:00.000Z' + connected_at: null + closed_at: null '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '402': - description: Insufficient credits + description: Insufficient USD balance content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } '409': description: Idempotency conflict @@ -1379,24 +3349,44 @@ paths: example: data: object: usage - credit_balance: 1080 + credit_balance: 21.6 total_tasks: 12 - credits_settled: 720 - credits_refunded: 450 + credits_settled: 14.4 + credits_refunded: 9 concurrency: limit: 2 active: 1 realtime: sessions: 3 - credits: 90 + credits: 1.8 active: 1 by_workflow: - workflow: music-video tasks: 8 - credits_settled: 480 + credits_settled: 9.6 - workflow: ecommerce-video tasks: 4 - credits_settled: 240 + credits_settled: 4.8 + by_capability: + - task_kind: image + capability_id: seedream-5-pro + tasks: 3 + credits_settled: 0.42 + - task_kind: video + capability_id: veo-3.1 + tasks: 2 + credits_settled: 14 + by_model: + - media_type: image + model: seedream-5-pro + tasks: 3 + credits_settled: 0.42 + by_api_key: + - api_key_id: key_abc123 + title: Production + key_prefix: sk_live_abcd + tasks: 12 + credits_settled: 14.4 '401': $ref: '#/components/responses/Unauthorized' @@ -1409,28 +3399,43 @@ paths: security: - BearerAuth: [] description: | - Use file upload when your images, audio, or subtitle files are not + Use file upload when your images, videos, audio, or subtitle files are not already hosted at public HTTPS URLs. The returned HTTPS URL can be used - directly in `images`, `audio_url`, or `srt_url`. + directly in `images`, `reference_videos`, `audio_url`, or `srt_url`. Limits: - - Maximum file size is 50 MB. + - Images, audio, and subtitles: maximum 50 MB. + - Motion videos: maximum 100 MB. - Images: `png`, `jpg`, `jpeg`, `webp` (`image/png`, `image/jpeg`, `image/webp`). - Audio: `mp3`, `wav`, `aac`, `m4a` (`audio/mpeg`, `audio/wav`, `audio/aac`, `audio/mp4`). - - Audio uploads must be 10-180 seconds. + - Audio uploads must be 10-300 seconds. The selected Music Video tier applies its own task limit: Standard 10-180 seconds; Premium 10-300 seconds. + - Motion videos: `mp4`, `mov` (`video/mp4`, `video/quicktime`), 3-30 seconds. Duration and dimensions are detected during upload. - Subtitles: `srt` (`application/x-subrip`; multipart uploads may use `text/plain` only when the filename ends in `.srt`). - - PDF, generic text files, octet-stream uploads, videos, and zip files + - PDF, generic text files, octet-stream uploads, Matroska videos, and zip files are not supported for launch. - Send either multipart form-data with a `file` field, or send the raw file body with the asset `Content-Type`. + - `Content-Length` is required and is validated before BeatAPI buffers + the request body; chunked uploads without a declared length are rejected. - Returned URLs are HTTPS and long-lived for launch. - Uploaded audio files are duration-checked during upload. The response includes `audio_duration_seconds` when the uploaded asset is audio. + - Uploaded videos are signature-, duration-, and dimension-checked. The + response includes `video_duration_seconds`, `width`, and `height`. - Workflow task inputs still require public HTTPS URLs. Localhost, private network URLs, and data URLs are not accepted. + parameters: + - in: header + name: Content-Length + required: true + description: Exact request-body length in bytes. For multipart uploads this includes multipart framing overhead. + schema: + type: integer + minimum: 1 + maximum: 105906176 requestBody: required: true content: @@ -1445,6 +3450,26 @@ paths: purpose: type: string enum: [input] + image/png: + schema: { type: string, format: binary } + image/jpeg: + schema: { type: string, format: binary } + image/webp: + schema: { type: string, format: binary } + audio/mpeg: + schema: { type: string, format: binary } + audio/wav: + schema: { type: string, format: binary } + audio/aac: + schema: { type: string, format: binary } + audio/mp4: + schema: { type: string, format: binary } + video/mp4: + schema: { type: string, format: binary } + video/quicktime: + schema: { type: string, format: binary } + application/x-subrip: + schema: { type: string, format: binary } responses: '201': description: File uploaded @@ -1468,6 +3493,10 @@ paths: $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' + '429': + $ref: '#/components/responses/RateLimited' + '500': + $ref: '#/components/responses/InternalError' /v1/webhooks: get: @@ -1499,6 +3528,10 @@ paths: updated_at: 1782210000 '401': $ref: '#/components/responses/Unauthorized' + '429': + $ref: '#/components/responses/RateLimited' + '500': + $ref: '#/components/responses/InternalError' post: operationId: createWebhookEndpoint tags: [Webhooks] @@ -1507,8 +3540,9 @@ paths: security: - BearerAuth: [] description: | - The signing secret is returned only once at creation time. Store it - securely. Later responses return a masked secret. + The public API returns the signing secret in full at creation time. Store + it securely; later public API responses return a masked secret. An + authenticated dashboard owner can explicitly reveal the secret again. BeatAPI sends these headers with each delivery: - `x-beatapi-event`: `task.succeeded` or `task.failed` @@ -1572,8 +3606,9 @@ paths: ``` Reject old timestamps to prevent replay attacks. A 5 minute window is - recommended. Failed deliveries are retried at most 3 times with fixed - backoff windows of 1 minute, 5 minutes, and 15 minutes. Polling + recommended. A delivery is attempted at most 3 times total: the initial + request plus up to 2 retries, with fixed backoff windows of 1 minute and + 5 minutes. Polling `GET /v1/tasks/{task_id}` remains the source of truth. requestBody: required: true @@ -1586,10 +3621,14 @@ paths: url: type: string format: uri + pattern: '^https://' + description: Public HTTPS callback URL that accepts BeatAPI task events. Do not use localhost or a private-network URL. description: type: string + description: Optional internal label for identifying the endpoint in your account. events: type: array + description: Task events to deliver. Omit to subscribe to both `task.succeeded` and `task.failed`. items: type: string enum: [task.succeeded, task.failed] @@ -1618,6 +3657,10 @@ paths: $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' + '429': + $ref: '#/components/responses/RateLimited' + '500': + $ref: '#/components/responses/InternalError' /v1/webhooks/{id}: get: @@ -1660,6 +3703,10 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + '429': + $ref: '#/components/responses/RateLimited' + '500': + $ref: '#/components/responses/InternalError' patch: operationId: updateWebhookEndpoint tags: [Webhooks] @@ -1684,6 +3731,8 @@ paths: url: type: string format: uri + pattern: '^https://' + description: Public HTTPS callback URL that accepts BeatAPI task events. Do not use localhost or a private-network URL. description: type: string status: @@ -1707,6 +3756,16 @@ paths: $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' + '404': + description: Webhook endpoint not found. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '429': + $ref: '#/components/responses/RateLimited' + '500': + $ref: '#/components/responses/InternalError' delete: operationId: deleteWebhookEndpoint tags: [Webhooks] @@ -1734,3 +3793,13 @@ paths: deleted: true '401': $ref: '#/components/responses/Unauthorized' + '404': + description: Webhook endpoint not found. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '429': + $ref: '#/components/responses/RateLimited' + '500': + $ref: '#/components/responses/InternalError' diff --git a/skills/beatapi-video/references/credits-and-limits.md b/skills/beatapi-video/references/credits-and-limits.md index 593e9a1..a8dae01 100644 --- a/skills/beatapi-video/references/credits-and-limits.md +++ b/skills/beatapi-video/references/credits-and-limits.md @@ -1,52 +1,49 @@ -# Credits and launch limits +# USD balance and launch limits Use the bundled OpenAPI contract as the source of truth if these launch values change. -## Customer credit rates +## Customer balance -| Workflow/control | Credits | -| --- | ---: | -| Music Video 540p standard | 4 per second | -| Music Video 720p standard | 5 per second | -| Music Video 1080p standard | 6 per second | -| Music Video lip-sync add-on | +2 per second | -| Music Video 720p high | 16 per second | -| Music Video 1080p high | 18 per second | -| Ecommerce Video 1080p | 15 per second | -| Manual composition | 1 fixed credit | +Customer balances and usage are USD-denominated. Compatibility fields retain +names such as `credit_balance`, `credits_reserved`, `credits_charged`, +`credits_settled`, and `credits_refunded`; 1 Credit = $1 USD. Treat these as +decimal USD amounts, not integer token counts. -Shot editing uses the selected Music Video quality/resolution rate multiplied -by shot duration. Default shot duration is 5 seconds. +Do not use legacy per-second credit tables. Text, image, video, workflow, +Realtime, Video Analysis, and Effect prices depend on the selected public +contract and can change. Check the current public documentation, then read the +exact reserved or charged amount from the response. Manual Music Video +composition is the fixed public exception documented in the current contract. + +Do not promise a welcome balance or promotional amount without checking the +current public contract. For Music Video creation, BeatAPI bills detected audio duration. Use the -request `duration` only as a 10-180 second fallback when duration detection -fails; it cannot override a detected duration. +request `duration` only as a documented fallback when duration detection fails; +it cannot override a detected duration. ## Input limits -- Music Video: 1-7 public HTTPS images. -- Images: PNG, JPG/JPEG, or WEBP, at most 50 MB each, aspect ratio 1:4 to 4:1. -- Audio: MP3, WAV, AAC, or M4A, at most 50 MB, 10-180 seconds. -- Subtitle input: public HTTPS SRT. -- Prompt: at most 3000 characters. -- Optional style phrase: at most 200 characters. -- Ecommerce Video duration and enum constraints must match - `beatapi.openapi.yaml`. - -Reject localhost, private-network, data, and non-HTTPS URLs. Upload supported -local inputs with `beatapi files upload`. +- Follow the exact media count, format, size, duration, and aspect-ratio limits + in `beatapi.openapi.yaml` for the selected capability. +- Model-specific image and video constraints can differ; discover the current + model catalog and validate the matching OpenAPI request variant. +- Video Analysis requires a public HTTPS video URL. +- Reject localhost, private-network, data, and non-HTTPS URLs. +- Upload supported local inputs with `beatapi files upload` or + `beatapi_upload_file`. ## Preflight -Run `beatapi usage` before paid work. Check: +Run `beatapi usage` or call `beatapi_get_usage` before paid work. Check: -- `credit_balance`; +- `credit_balance` as a USD amount; - `concurrency.limit`; - `concurrency.active`. Do not treat a sufficient balance as proof that concurrency is available. -Storyboard-ready and requires-action tasks can have settled credits without +Storyboard-ready and requires-action tasks can have settled charges without counting as active processing. ## Realtime sessions @@ -54,9 +51,9 @@ counting as active processing. - Allowed maximum durations are 15, 60, and 300 seconds. - Every create request requires 1-10 exact HTTPS `allowed_origins` and an `Idempotency-Key`. -- Creation reserves credits; billing activates only after the first accepted +- Creation reserves USD balance; billing activates only after the first accepted heartbeat after remote output begins. Inspect `credits.reserved`, `credits.settled`, and `credits.refunded` on the session. - A Realtime session has its own capacity rules. Treat `realtime_capacity_unavailable` and `user_concurrency_exceeded` as signals to - close/wait, not as reasons to retry in a tight loop. + close or wait, not as reasons to retry in a tight loop. diff --git a/skills/beatapi-video/references/generation-and-effects.md b/skills/beatapi-video/references/generation-and-effects.md new file mode 100644 index 0000000..9140cbe --- /dev/null +++ b/skills/beatapi-video/references/generation-and-effects.md @@ -0,0 +1,39 @@ +# Generation models and Effects + +Use stable BeatAPI model IDs only. Discover the current catalog before choosing +a model; never expose or invent internal provider routes. + +## Image and video models + +Call `beatapi_list_generation_models` or `GET /v1/media/models` before +generation. Choose only an ID from the current response, then consult +`beatapi.openapi.yaml` for the exact request variant and validation rules. + +Model availability evolves independently from this Skill. Do not hardcode a +closed model list in agent logic, and do not substitute a similar model without +the user's approval. Preserve the selected model ID exactly. + +Copy `assets/image-generation.json` or `assets/video-generation.json` to a +temporary request. Add only fields accepted by that model's schema. In +particular, do not combine frame images with reference inputs when the selected +model forbids it, and satisfy any audio-reference dependency before creating a +paid task. + +## Effects + +Effects are versioned published capabilities, not model aliases. + +1. List active Effects with `beatapi_list_effects` or `beatapi effects list`. +2. Read the selected Effect with `beatapi_get_effect` or + `beatapi effects get EFFECT_ID`. +3. Validate image count, MIME types, dimensions, subject requirements, and + supported options against that returned version. +4. Copy `assets/effect-task.json`, preserve the selected `effect_id`, and omit + unsupported options. +5. Create with `beatapi_create_effect` or `beatapi effects create --file ...`. + Use one stable idempotency key for retries of the same request. +6. Poll the shared task endpoint and require hosted output before reporting + completion. + +The catalog exposes only publication-safe fields. Never infer provider names, +template IDs, internal costs, or execution context. diff --git a/submission/release-notes.md b/submission/release-notes.md index 4b91421..94123d7 100644 --- a/submission/release-notes.md +++ b/submission/release-notes.md @@ -1,8 +1,12 @@ -# BeatAPI 0.2.0 +# BeatAPI next -Realtime-aligned Skills-only submission. +Unified generation and Effect API update. - Creates and monitors BeatAPI Music Video and Ecommerce Video workflows. +- Discovers four image and seven video model aliases and creates model-specific + image and video tasks. +- Discovers published versioned Effects, validates their current input contract, + and creates Effect tasks. - Creates, reads, and closes short-lived Realtime Video sessions while keeping the browser client secret out of model-visible output. - Handles local media upload, manual storyboard review, shot operations, @@ -11,5 +15,5 @@ Realtime-aligned Skills-only submission. the official CLI without placing credentials in conversations. - Requires the globally installed `beatapi` CLI for Skills-only hosts that do not supply BeatAPI MCP tools. -- Matches the current BeatAPI OpenAPI `1.0.0-launch` Realtime baseline. -- Includes six positive and three negative review cases. +- Matches the current BeatAPI OpenAPI `1.0.0-launch` unified API baseline. +- Includes model, Effect, workflow, Realtime, security, and recovery review cases. diff --git a/test/mcp-e2e.test.ts b/test/mcp-e2e.test.ts index e842bd4..9df522a 100644 --- a/test/mcp-e2e.test.ts +++ b/test/mcp-e2e.test.ts @@ -11,13 +11,19 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js" const root = resolve(import.meta.dirname, ".."); test("bundled stdio MCP serves BeatAPI tools and protects credentials", async () => { - const requests: Array<{ method: string; path: string; authorization?: string }> = []; + const requests: Array<{ + method: string; + path: string; + authorization?: string; + body?: string; + }> = []; const httpServer = createServer(async (request, response) => { const chunks: Buffer[] = []; for await (const chunk of request) chunks.push(Buffer.from(chunk)); requests.push({ method: request.method ?? "GET", path: request.url ?? "/", + body: Buffer.concat(chunks).toString("utf8"), ...(request.headers.authorization ? { authorization: request.headers.authorization } : {}), @@ -35,6 +41,71 @@ test("bundled stdio MCP serves BeatAPI tools and protects credentials", async () ); return; } + if (request.url === "/v1/models") { + response.end( + JSON.stringify({ + object: "list", + data: [ + { + id: "gpt-5.6-sol", + object: "model", + created: 1, + owned_by: "beatapi", + }, + ], + }), + ); + return; + } + if (request.url === "/v1/responses") { + response.end( + JSON.stringify({ + id: "resp_test", + object: "response", + model: "gpt-5.6-sol", + output_text: "Launch summary", + }), + ); + return; + } + if (request.url === "/v1/media/models") { + response.end( + JSON.stringify({ + data: { + object: "list", + data: [ + { + id: "nano-banana", + object: "generation_model", + name: "Nano Banana", + media_type: "image", + input_modes: ["text"], + }, + ], + }, + }), + ); + return; + } + if (request.url === "/v1/effects") { + response.end( + JSON.stringify({ + data: { + object: "list", + data: [{ id: "video-muscle-max", object: "effect" }], + }, + }), + ); + return; + } + if (request.url === "/v1/effects/video-muscle-max") { + response.end( + JSON.stringify({ + data: { id: "video-muscle-max", object: "effect", version: 1 }, + }), + ); + return; + } if (request.url === "/v1/usage") { response.end( JSON.stringify({ @@ -84,6 +155,44 @@ test("bundled stdio MCP serves BeatAPI tools and protects credentials", async () ); return; } + if (request.url === "/v1/video-analysis/tasks") { + response.statusCode = 201; + response.end( + JSON.stringify({ + data: { + id: "task_video_analysis", + object: "task", + task_kind: "workflow", + capability_id: "video-analysis", + capability_version: 1, + status: "queued", + }, + }), + ); + return; + } + if ( + request.url === "/v1/images/tasks" || + request.url === "/v1/videos/tasks" || + request.url === "/v1/effects/tasks" + ) { + response.statusCode = 201; + response.end( + JSON.stringify({ + data: { + id: `task_${request.url.split("/")[2]}`, + object: "task", + task_kind: request.url.includes("images") + ? "image" + : request.url.includes("videos") + ? "video" + : "effect", + status: "queued", + }, + }), + ); + return; + } if (request.url === "/v1/realtime/sessions" && request.method === "POST") { response.statusCode = 201; response.end( @@ -178,7 +287,7 @@ test("bundled stdio MCP serves BeatAPI tools and protects credentials", async () try { await client.connect(transport); const listed = await client.listTools(); - assert.equal(listed.tools.length, 19); + assert.equal(listed.tools.length, 28); assert.ok(listed.tools.every((tool) => !/api[_-]?key/i.test(JSON.stringify(tool.inputSchema)))); const workflows = await client.callTool({ @@ -190,6 +299,110 @@ test("bundled stdio MCP serves BeatAPI tools and protects credentials", async () "music-video", ); + const textModels = await client.callTool({ + name: "beatapi_list_text_models", + arguments: {}, + }); + assert.equal( + (textModels.structuredContent as { result: Array<{ id: string }> }).result[0]?.id, + "gpt-5.6-sol", + ); + + const textResponse = await client.callTool({ + name: "beatapi_create_text_response", + arguments: { + model: "gpt-5.6-sol", + request: { input: "Summarize this launch note" }, + }, + }); + assert.equal( + (textResponse.structuredContent as { result: { id: string } }).result.id, + "resp_test", + ); + assert.equal( + JSON.parse( + requests.find((request) => request.path === "/v1/responses")?.body ?? "{}", + ).stream, + false, + ); + + const models = await client.callTool({ + name: "beatapi_list_generation_models", + arguments: {}, + }); + assert.equal( + (models.structuredContent as { result: Array<{ id: string }> }).result[0]?.id, + "nano-banana", + ); + + const imageTask = await client.callTool({ + name: "beatapi_create_image", + arguments: { + model: "future-image-model", + parameters: { prompt: "Editorial still", aspect_ratio: "16:9" }, + }, + }); + assert.equal( + (imageTask.structuredContent as { result: { id: string } }).result.id, + "task_images", + ); + assert.deepEqual( + JSON.parse( + requests.find((request) => request.path === "/v1/images/tasks")?.body ?? "{}", + ), + { + model: "future-image-model", + prompt: "Editorial still", + aspect_ratio: "16:9", + }, + ); + + const effects = await client.callTool({ + name: "beatapi_list_effects", + arguments: {}, + }); + assert.equal( + (effects.structuredContent as { result: Array<{ id: string }> }).result[0]?.id, + "video-muscle-max", + ); + + const effect = await client.callTool({ + name: "beatapi_get_effect", + arguments: { effect_id: "video-muscle-max" }, + }); + assert.equal( + (effect.structuredContent as { result: { version: number } }).result.version, + 1, + ); + + const effectTask = await client.callTool({ + name: "beatapi_create_effect", + arguments: { + effect_id: "video-muscle-max", + images: ["https://media.example.com/portrait.png"], + idempotency_key: "effect-mcp-test", + }, + }); + assert.equal( + (effectTask.structuredContent as { result: { id: string } }).result.id, + "task_effects", + ); + + const analysisTask = await client.callTool({ + name: "beatapi_analyze_video", + arguments: { + video_url: "https://media.example.com/product-demo.mp4", + prompt: "Identify the key scenes", + analysis_depth: "deep", + max_output_tokens: 2048, + idempotency_key: "analysis-mcp-test", + }, + }); + assert.equal( + (analysisTask.structuredContent as { result: { id: string } }).result.id, + "task_video_analysis", + ); + const musicTask = await client.callTool({ name: "beatapi_create_music_video", arguments: { @@ -302,7 +515,13 @@ test("bundled stdio MCP serves BeatAPI tools and protects credentials", async () ); const authenticatedRequests = requests.filter( - (request) => request.path !== "/v1/workflows", + (request) => + request.path !== "/v1/workflows" && + request.path !== "/v1/media/models" && + !( + request.method === "GET" && + request.path.startsWith("/v1/effects") + ), ); assert.ok( authenticatedRequests.every( @@ -313,6 +532,18 @@ test("bundled stdio MCP serves BeatAPI tools and protects credentials", async () requests.find((request) => request.path === "/v1/workflows")?.authorization, undefined, ); + assert.equal( + requests.find((request) => request.path === "/v1/media/models")?.authorization, + undefined, + ); + assert.ok( + requests + .filter( + (request) => + request.method === "GET" && request.path.startsWith("/v1/effects"), + ) + .every((request) => request.authorization === undefined), + ); } finally { await client.close().catch(() => undefined); await transport.close().catch(() => undefined); @@ -444,6 +675,7 @@ test("setup reports a missing CLI login as an actionable configuration state", a assert.equal(result.configured, false); assert.equal(result.setup_reason, "authentication_required"); assert.match(result.next_step, /beatapi auth login/); + assert.match(result.next_step, /Configure.*BEATAPI_API_KEY/i); } finally { await client.close().catch(() => undefined); await transport.close().catch(() => undefined); diff --git a/test/plugin.test.ts b/test/plugin.test.ts index ae588f8..d28b7d4 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -10,6 +10,15 @@ const root = resolve(import.meta.dirname, ".."); const expectedToolNames = [ "beatapi_check_setup", "beatapi_list_workflows", + "beatapi_list_text_models", + "beatapi_create_text_response", + "beatapi_list_generation_models", + "beatapi_create_image", + "beatapi_create_video", + "beatapi_list_effects", + "beatapi_get_effect", + "beatapi_create_effect", + "beatapi_analyze_video", "beatapi_get_usage", "beatapi_upload_file", "beatapi_create_music_video", @@ -47,6 +56,10 @@ test("marks read, write, paid, and destructive tools accurately", () => { for (const name of [ "beatapi_check_setup", "beatapi_list_workflows", + "beatapi_list_text_models", + "beatapi_list_generation_models", + "beatapi_list_effects", + "beatapi_get_effect", "beatapi_get_usage", "beatapi_get_task", "beatapi_wait_for_task", @@ -59,6 +72,11 @@ test("marks read, write, paid, and destructive tools accurately", () => { for (const name of [ "beatapi_create_music_video", + "beatapi_create_text_response", + "beatapi_create_image", + "beatapi_create_video", + "beatapi_create_effect", + "beatapi_analyze_video", "beatapi_edit_music_video_shot", "beatapi_compose_music_video", "beatapi_create_ecommerce_video", @@ -87,6 +105,36 @@ test("marks read, write, paid, and destructive tools accurately", () => { } }); +test("generation tools accept current and future model IDs through one stable shape", () => { + const byName = new Map(toolDefinitions.map((tool) => [tool.name, tool])); + const image = byName.get("beatapi_create_image")?.inputSchema.safeParse({ + model: "future-image-model", + parameters: { prompt: "Editorial still", aspect_ratio: "16:9" }, + }); + const video = byName.get("beatapi_create_video")?.inputSchema.safeParse({ + model: "future-video-model", + parameters: { prompt: "Slow dolly in", duration: 7 }, + }); + + assert.equal(image?.success, true); + assert.equal(video?.success, true); +}); + +test("text responses are non-streaming and require explicit BeatAPI intent", () => { + const tool = toolDefinitions.find( + (definition) => definition.name === "beatapi_create_text_response", + ); + + assert.match(tool?.description ?? "", /only.*explicit.*BeatAPI/i); + assert.equal( + tool?.inputSchema.safeParse({ + model: "gpt-5.6-sol", + request: { input: "Summarize this note", stream: true }, + }).success, + false, + ); +}); + test("plugin manifest wires the skill, local MCP, and production assets", async () => { const manifest = JSON.parse( await readFile(resolve(root, ".codex-plugin/plugin.json"), "utf8"),