openapi: 3.0.3
info:
  title: Vivideo API
  version: "1.0.0"
  description: |
    The Vivideo public API lets you generate AI videos programmatically —
    the same engine behind https://app.vivideo.ai.

    **Base URL:** `https://api.vivideo.ai`

    **Authentication:** every endpoint (except `GET /v1`) requires an API key
    sent as `Authorization: Bearer vv_live_...` (or `X-API-Key`). Create keys in
    the Vivideo app under **Settings → More → API Keys**
    (https://app.vivideo.ai/account/api-keys).

    **Generation is asynchronous:** `POST /v1/videos` returns immediately with a
    video id; poll `GET /v1/videos/{id}` (respect `poll_interval_seconds`) or
    register a webhook to be notified on `video.completed` / `video.failed`.

    **Free accounts** get one free video project: the first eligible
    `POST /v1/videos` returns `status: "preview"` with a blurred preview image
    instead of rendering. After upgrading at https://app.vivideo.ai/upgrade,
    call `POST /v1/videos/{id}/render` — the first video renders free.

    **Errors** always use one envelope: `{"error": {"code", "message",
    "retryable", "request_id", "links", ...}}`. `429` responses carry a
    `Retry-After` header.
  contact:
    url: https://developers.vivideo.ai
servers:
  - url: https://api.vivideo.ai
security:
  - ApiKeyBearer: []
tags:
  - name: Account
  - name: Videos
  - name: Avatars
  - name: Voices
  - name: Brand kits
  - name: Folders
  - name: Webhooks

paths:
  /v1:
    get:
      operationId: getApiIndex
      summary: API index (no auth required)
      security: []
      tags: [Account]
      responses:
        "200":
          description: Service metadata and endpoint list.
          content:
            application/json:
              schema:
                type: object
                properties:
                  name: { type: string, example: Vivideo API }
                  version: { type: string, example: v1 }
                  documentation: { type: string, example: https://developers.vivideo.ai }
                  endpoints:
                    type: array
                    items: { type: string }

  /v1/me:
    get:
      operationId: getAccount
      summary: Account, plan and credit balance
      description: Use this for credit checks and premium checks before creating videos.
      tags: [Account]
      responses:
        "200":
          description: The account behind the API key.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Account" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/models:
    get:
      operationId: listModels
      summary: Available video models and credit rates
      tags: [Videos]
      responses:
        "200":
          description: Manual-mode models plus the auto-generate pipeline.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { type: string, example: list }
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Model" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/videos:
    post:
      operationId: createVideo
      summary: Create a video (async)
      description: |
        Starts a video generation. Premium accounts are charged credits
        atomically and rendering starts immediately (`status: "processing"`).
        Free accounts receive a one-time preview (`status: "preview"`) with an
        upgrade link instead of a render.

        Send an `Idempotency-Key` header so network retries can never create or
        charge a duplicate video. Identical concurrent requests without one are
        rejected with `409 duplicate_request`.
      tags: [Videos]
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - $ref: "#/components/schemas/CreateVideoAuto"
                - $ref: "#/components/schemas/CreateVideoManual"
              discriminator:
                propertyName: mode
            examples:
              auto:
                summary: Auto-Generate (AI avatar video)
                value:
                  mode: auto
                  prompt: "A 30-second product intro for our meal-planning app, upbeat and friendly"
                  duration_seconds: 30
                  avatar_id: "Abigail_expressive_2024112501"
                  voice_id: "1bd001e7e50f421d891986aad5158bc8"
                  brand_kit_id: "br_1a2b3c4d"
                  orientation: landscape
              manual:
                summary: Manual mode (direct model)
                value:
                  mode: manual
                  model: veo31_fast
                  prompt: "A golden retriever surfing a wave at sunset, cinematic"
                  duration_seconds: 8
                  resolution: 1080p
                  aspect_ratio: "16:9"
      responses:
        "201":
          description: Video accepted (processing) or free preview created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Video" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "403": { $ref: "#/components/responses/UpgradeRequired" }
        "409":
          description: Duplicate request (`duplicate_request`) or an Idempotency-Key still in flight (`idempotency_in_flight`).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorEnvelope" }
        "429": { $ref: "#/components/responses/RateLimited" }
    get:
      operationId: listVideos
      summary: List videos
      tags: [Videos]
      parameters:
        - name: folder_id
          in: query
          schema: { type: string }
          description: Filter to one folder. Defaults to all of the account's videos.
        - name: status
          in: query
          schema: { type: string, enum: [queued, processing, completed, failed, preview] }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
      responses:
        "200":
          description: Videos, newest first.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { type: string, example: list }
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Video" }
                  count: { type: integer }
                  total: { type: integer }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/videos/{video_id}:
    get:
      operationId: getVideo
      summary: Video status and result (poll this)
      description: |
        Poll while `status` is `queued`/`processing` (respect
        `poll_interval_seconds`, typically 5s). Terminal states: `completed`
        (with `video_url`) and `failed` (credits auto-refunded). Free previews
        stay in `preview` until rendered after an upgrade.
      tags: [Videos]
      parameters:
        - $ref: "#/components/parameters/VideoId"
      responses:
        "200":
          description: Current state of the video.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Video" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/videos/{video_id}/render:
    post:
      operationId: renderVideo
      summary: Render a free preview after upgrading
      description: |
        Renders a `status: "preview"` video for real once the account is
        premium. The first video after upgrading is free (comped); later renders
        charge the normal credit price. Idempotent — if the video is already
        rendering or done, the current state is returned without a second charge.
      tags: [Videos]
      parameters:
        - $ref: "#/components/parameters/VideoId"
      responses:
        "200":
          description: Render started (or current state if already rendered).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Video" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "403": { $ref: "#/components/responses/UpgradeRequired" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/avatars:
    get:
      operationId: listAvatars
      summary: List avatars (catalogue + your own)
      tags: [Avatars]
      parameters:
        - name: view
          in: query
          schema: { type: string, enum: [characters, catalogue], default: characters }
          description: "`characters` groups looks by character; `catalogue` returns the flat avatar list."
        - name: search
          in: query
          schema: { type: string }
        - name: gender
          in: query
          schema: { type: string, enum: [female, male, unspecified] }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 200 }
      responses:
        "200":
          description: Avatar characters or catalogue entries; `owned` lists your generated avatars.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { type: string, example: list }
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Avatar" }
                  owned:
                    type: array
                    items: { $ref: "#/components/schemas/Avatar" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createAvatar
      summary: Generate a new avatar (async)
      description: |
        `kind: "prompt"` designs an AI avatar from a text description (no real
        person). `kind: "photo"` builds an avatar from a photo and requires
        `consent: true` plus a `file_url` or `asset_id`. Training runs in the
        background — poll `GET /v1/avatars/{id}`. Free accounts may create one
        avatar; more requires premium.
      tags: [Avatars]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [kind, name]
              properties:
                kind: { type: string, enum: [prompt, photo] }
                name: { type: string, maxLength: 80 }
                prompt: { type: string, maxLength: 1000, description: "For kind=prompt." }
                file_url: { type: string, description: "For kind=photo: https URL of the photo." }
                asset_id: { type: string, description: "Alternative to file_url: an uploaded asset id." }
                consent: { type: boolean, description: "Required true for kind=photo." }
                reference_images:
                  type: array
                  items: { type: string }
                  maxItems: 3
      responses:
        "201":
          description: Avatar creation started.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Avatar" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/UpgradeRequired" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/avatars/{avatar_id}:
    get:
      operationId: getAvatar
      summary: Avatar training status
      tags: [Avatars]
      parameters:
        - name: avatar_id
          in: path
          required: true
          schema: { type: string }
          description: The `id` (avatar_ref, `av_...`) from POST /v1/avatars.
      responses:
        "200":
          description: Training status; use it in videos once `ready` is true.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Avatar" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/voices:
    get:
      operationId: listVoices
      summary: List voices
      tags: [Voices]
      parameters:
        - name: language
          in: query
          schema: { type: string, example: English }
        - name: gender
          in: query
          schema: { type: string }
        - name: search
          in: query
          schema: { type: string }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
      responses:
        "200":
          description: Public voice catalogue plus your own voices.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { type: string, example: list }
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Voice" }
                  owned:
                    type: array
                    items: { $ref: "#/components/schemas/Voice" }
                  total_public: { type: integer }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/voices/speech:
    post:
      operationId: generateSpeech
      summary: Generate a speech sample (TTS)
      description: Hear a voice say a line before using it in a video. Voice cloning is not available via the API.
      tags: [Voices]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [text, voice_id]
              properties:
                text: { type: string, maxLength: 300 }
                voice_id: { type: string }
      responses:
        "200":
          description: Generated audio sample.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { type: string, example: speech }
                  audio_url: { type: string }
                  duration_seconds: { type: number }
                  voice_id: { type: string }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/brand-kits:
    get:
      operationId: listBrandKits
      summary: List brand kits
      tags: [Brand kits]
      responses:
        "200":
          description: The account's brand kits.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { type: string, example: list }
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/BrandKit" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      operationId: createBrandKit
      summary: Create a brand kit
      description: Free accounts may create one brand kit; more requires premium. Apply a kit to auto-mode videos via `brand_kit_id`.
      tags: [Brand kits]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string, maxLength: 80 }
                logo_asset_id: { type: string }
                logo_url: { type: string }
                primary_color: { type: string, example: "#7C5CFF" }
                secondary_color: { type: string }
                tertiary_color: { type: string }
                accent_color: { type: string }
                title_font: { type: string, maxLength: 60 }
                body_font: { type: string, maxLength: 60 }
                motion_style:
                  type: string
                  enum: [elevated, friendly, minimal, professional, playful]
                brand_identity: { type: string, maxLength: 2000 }
      responses:
        "201":
          description: Brand kit created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BrandKit" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/UpgradeRequired" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/brand-kits/from-url:
    post:
      operationId: brandKitFromUrl
      summary: Draft a brand kit from a website URL
      description: |
        Extracts logo, colors, fonts and identity from a public https website
        and returns a DRAFT (nothing is saved). Review it, then create it with
        POST /v1/brand-kits.
      tags: [Brand kits]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: { type: string, example: "https://yourcompany.com" }
      responses:
        "200":
          description: Draft brand kit extracted from the site.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BrandKit" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/brand-kits/{brand_id}:
    delete:
      operationId: deleteBrandKit
      summary: Delete a brand kit
      tags: [Brand kits]
      parameters:
        - name: brand_id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Brand kit deleted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string }
                  object: { type: string, example: brand_kit }
                  deleted: { type: boolean }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/uploads:
    post:
      operationId: createUpload
      summary: Get a presigned upload URL (public file → https URL)
      description: >
        Returns a presigned S3 `upload_url` and the resulting public `public_url`.
        PUT your file bytes to `upload_url` with header `Content-Type: <content_type>`,
        then use `public_url` as `image_url` / `first_frame_url` (manual videos),
        `file_url` (avatar photos) or `logo_url` (brand logos).
      tags: [Assets]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [filename, content_type]
              properties:
                filename: { type: string, example: photo.png }
                content_type: { type: string, example: image/png }
      responses:
        "201":
          description: Presigned upload created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Upload" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/assets:
    get:
      operationId: listAssets
      summary: List your HeyGen assets
      tags: [Assets]
      responses:
        "200":
          description: Your uploaded assets.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { type: string, example: list }
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Asset" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      operationId: createAsset
      summary: Create a HeyGen asset upload (for auto-mode file_asset_ids)
      description: >
        Returns a presigned `upload_url` + `asset_id`. PUT your file bytes to
        `upload_url` with the returned `upload_headers`, then call
        POST /v1/assets/{id}/complete. Use `asset_id` as an auto-mode
        `file_asset_ids` entry, an avatar `asset_id`, or a brand `logo_asset_id`.
      tags: [Assets]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [filename, content_type, size_bytes]
              properties:
                filename: { type: string, example: reference.png }
                content_type: { type: string, example: image/png }
                size_bytes: { type: integer, example: 20480 }
      responses:
        "201":
          description: Asset upload created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Asset" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/assets/{asset_id}:
    delete:
      operationId: deleteAsset
      summary: Delete an asset
      tags: [Assets]
      parameters:
        - name: asset_id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Asset deleted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string }
                  object: { type: string, example: asset }
                  deleted: { type: boolean }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/assets/{asset_id}/complete:
    post:
      operationId: completeAsset
      summary: Finalize an asset after uploading its bytes
      tags: [Assets]
      parameters:
        - name: asset_id
          in: path
          required: true
          schema: { type: string, description: "The asset id (as_...) from POST /v1/assets." }
      responses:
        "200":
          description: Asset finalized (status ready or processing).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Asset" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/folders:
    get:
      operationId: listFolders
      summary: List folders
      description: Folders organize videos (projects). Pass a folder's id to `GET /v1/videos?folder_id=` or when creating videos.
      tags: [Folders]
      responses:
        "200":
          description: The account's folders.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { type: string, example: list }
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Folder" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/webhooks:
    get:
      operationId: listWebhooks
      summary: List webhook endpoints
      tags: [Webhooks]
      responses:
        "200":
          description: Registered endpoints (secrets are never re-shown).
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { type: string, example: list }
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/WebhookEndpoint" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      operationId: createWebhook
      summary: Register a webhook endpoint
      description: |
        Registers a public **https** URL for `video.completed` / `video.failed`
        events. The response includes the signing `secret` (shown once). Verify
        deliveries via the `Vivideo-Signature: t=<ts>,v1=<hmac>` header, where
        `v1 = hex(hmac_sha256(secret, "<ts>.<raw body>"))`. Deliveries retry
        with backoff (1m, 5m, 30m, 2h, 6h); dedupe on the event `id`. Max 5
        active endpoints per account.
      tags: [Webhooks]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: { type: string, example: "https://example.com/vivideo-webhook" }
                events:
                  type: array
                  items: { type: string, enum: [video.completed, video.failed] }
      responses:
        "201":
          description: Endpoint registered — store the secret now.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WebhookEndpoint" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/webhooks/{webhook_id}:
    delete:
      operationId: deleteWebhook
      summary: Delete a webhook endpoint
      tags: [Webhooks]
      parameters:
        - name: webhook_id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Endpoint deleted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string }
                  object: { type: string, example: webhook_endpoint }
                  deleted: { type: boolean }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

components:
  securitySchemes:
    ApiKeyBearer:
      type: http
      scheme: bearer
      bearerFormat: "vv_live_..."
      description: Vivideo API key. Also accepted as an `X-API-Key` header.

  parameters:
    VideoId:
      name: video_id
      in: path
      required: true
      schema: { type: string }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema: { type: string, maxLength: 255 }
      description: Client-chosen key; retries with the same key replay the original response instead of creating a duplicate video.

  responses:
    Unauthorized:
      description: Missing, malformed or revoked API key (`missing_api_key`, `invalid_api_key`, `api_key_revoked`).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }
    BadRequest:
      description: Invalid parameters (`invalid_request`, `unknown_model`, `invalid_url`).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }
    NotFound:
      description: The resource does not exist for this account (`not_found`).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }
    InsufficientCredits:
      description: Not enough credits (`insufficient_credits`); `links.buy_credits` points to the top-up page.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }
    UpgradeRequired:
      description: Premium plan required or the free allowance is used (`upgrade_required`, `free_limit_reached`); `links.upgrade` = https://app.vivideo.ai/upgrade.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }
    RateLimited:
      description: Rate limit hit (`rate_limited`). Honor the `Retry-After` header / `retry_after_seconds` field.
      headers:
        Retry-After:
          schema: { type: integer }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }

  schemas:
    ErrorEnvelope:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message, retryable, request_id]
          properties:
            code:
              type: string
              description: Stable machine-readable code.
              enum: [invalid_request, missing_api_key, invalid_api_key, api_key_revoked,
                     account_not_found, not_found, unknown_model, invalid_url,
                     insufficient_credits, upgrade_required, free_limit_reached,
                     rate_limited, duplicate_request, idempotency_in_flight,
                     payload_too_large, webhook_limit_reached, upstream_error,
                     internal_error, method_not_allowed]
            message: { type: string }
            retryable: { type: boolean }
            request_id: { type: string, example: req_1f2e3d4c5b6a7988 }
            retry_after_seconds: { type: integer }
            details: { type: object, additionalProperties: true }
            links:
              type: object
              additionalProperties: { type: string }
              description: Next steps — may include `upgrade`, `buy_credits`, `manage_keys`, `docs`.

    Account:
      type: object
      properties:
        object: { type: string, example: account }
        email: { type: string }
        plan: { type: string, enum: [free, pro, max, expired] }
        premium: { type: boolean }
        credits: { type: number }
        free_video_available:
          type: boolean
          description: True when this free account can still create its one free preview video.
        first_video_comp_used: { type: boolean }
        links:
          type: object
          additionalProperties: { type: string }

    Model:
      type: object
      properties:
        id: { type: string, example: veo-3.1-fast }
        object: { type: string, example: model }
        name: { type: string }
        brand: { type: string }
        mode: { type: string, enum: [manual, auto] }
        requires_premium: { type: boolean }
        operations:
          type: array
          items: { type: string, enum: [text_to_video, image_to_video, first_last_frame, multiple_images] }
        credits_per_second:
          type: object
          additionalProperties: { type: number }
          description: Keyed by resolution. Estimate — the create response's `credits_charged` is authoritative.
        durations:
          type: array
          items: { type: string, example: "5s" }
        default_duration: { type: string }
        resolutions:
          type: array
          items: { type: string }
        default_resolution: { type: string }
        aspect_ratios:
          type: array
          items: { type: string }
        default_aspect_ratio: { type: string }
        native_audio: { type: boolean }
        supports_negative_prompt: { type: boolean }
        supports_seed: { type: boolean }
        supports_motion: { type: boolean, description: "Vidu movement_amplitude control." }
        supports_cfg_scale: { type: boolean, description: "Kling cfg_scale (prompt adherence) control." }
        styles:
          type: array
          items: { type: string }
          description: "Style presets (e.g. PixVerse anime/3d_animation/…), when the model supports them."

    CreateVideoAuto:
      type: object
      required: [mode, prompt]
      properties:
        mode: { type: string, enum: [auto] }
        prompt: { type: string, maxLength: 5000 }
        duration_seconds: { type: integer, minimum: 5, maximum: 300, default: 30 }
        avatar_id: { type: string, description: "From GET /v1/avatars (optional)." }
        voice_id: { type: string, description: "From GET /v1/voices (optional)." }
        brand_kit_id: { type: string, description: "A brand_ref from GET /v1/brand-kits (optional)." }
        style_id: { type: string, description: "A HeyGen template/style id (optional)." }
        file_asset_ids:
          type: array
          items: { type: string }
          description: "HeyGen asset_ids from POST /v1/assets — reference files the video agent can weave in."
        orientation: { type: string, enum: [landscape, portrait] }
        folder_id: { type: string }
        name: { type: string, maxLength: 60 }

    CreateVideoManual:
      type: object
      required: [mode, model, prompt]
      description: >
        The operation is inferred from the inputs: text-to-video by default;
        image_to_video with image_url; first_last_frame with first_frame_url +
        last_frame_url; multiple_images with image_urls. resolution, duration_seconds,
        aspect_ratio and style are validated against the chosen model's allowed values
        (see GET /v1/models) — a 400 lists what's allowed.
      properties:
        mode: { type: string, enum: [manual] }
        model: { type: string, description: "A model id from GET /v1/models.", example: veo31_fast }
        prompt: { type: string, maxLength: 5000 }
        duration_seconds: { type: integer, description: "Must match one of the model's durations (e.g. 4 for '4s')." }
        resolution: { type: string, description: "Must be one of the model's resolutions." }
        aspect_ratio: { type: string, description: "Must be one of the model's aspect_ratios." }
        negative_prompt: { type: string, description: "Models with supports_negative_prompt." }
        seed: { type: integer, description: "Models with supports_seed." }
        generate_audio: { type: boolean, description: "Models with native_audio." }
        style: { type: string, description: "One of the model's styles (e.g. PixVerse anime)." }
        movement_amplitude: { type: string, description: "Vidu motion control (auto/small/medium/large)." }
        cfg_scale: { type: number, description: "Kling prompt-adherence 0–1." }
        camera_fixed: { type: boolean }
        auto_fix: { type: boolean }
        image_url: { type: string, description: "Switches the request to image_to_video." }
        first_frame_url: { type: string, description: "With last_frame_url → first_last_frame." }
        last_frame_url: { type: string }
        end_image_url: { type: string }
        image_urls:
          type: array
          items: { type: string }
          description: "Reference images → multiple_images (models that support it, e.g. veo31_fast)."
        folder_id: { type: string }
        name: { type: string, maxLength: 60 }

    Video:
      type: object
      properties:
        id: { type: string }
        object: { type: string, example: video }
        mode: { type: string, enum: [auto, manual] }
        model: { type: string, example: veo31_fast }
        status:
          type: string
          enum: [queued, processing, completed, failed, preview]
        name: { type: string }
        folder_id: { type: string }
        created_at: { type: string }
        credits_charged: { type: number }
        poll_interval_seconds: { type: integer, example: 5 }
        video_url: { type: string, description: "Present when status=completed." }
        video_url_no_captions: { type: string }
        caption_url: { type: string }
        thumbnail_url: { type: string }
        duration_seconds: { type: number }
        progress:
          type: object
          properties:
            status: { type: string }
            queue_position: { type: integer, nullable: true }
            percent: { type: number, nullable: true }
        error:
          type: object
          properties:
            code: { type: string, example: generation_failed }
            message: { type: string }
            credits_refunded: { type: boolean }
        preview:
          type: object
          description: Present when status=preview (free-account funnel).
          properties:
            image_url: { type: string, nullable: true, description: "Low-res preview image — display blurred." }
            image_note: { type: string }
            message: { type: string }
            upgrade_url: { type: string, example: https://app.vivideo.ai/upgrade }

    Avatar:
      type: object
      properties:
        id: { type: string, description: "avatar_ref (av_...) for your avatars, or the catalogue avatar_id." }
        object: { type: string, example: avatar }
        name: { type: string }
        kind: { type: string, enum: [prompt, photo] }
        status: { type: string }
        ready: { type: boolean }
        gender: { type: string }
        preview_image_url: { type: string }
        preview_video_url: { type: string }
        looks: { type: array, items: { type: object, additionalProperties: true } }

    Voice:
      type: object
      properties:
        voice_id: { type: string }
        name: { type: string }
        language: { type: string }
        gender: { type: string }
        preview_audio_url: { type: string }

    BrandKit:
      type: object
      properties:
        object: { type: string, example: brand_kit }
        brand_ref: { type: string, example: br_1a2b3c4d, description: "Use as brand_kit_id when creating auto videos." }
        name: { type: string }
        logo_url: { type: string }
        logo_asset_id: { type: string }
        primary_color: { type: string }
        secondary_color: { type: string }
        tertiary_color: { type: string }
        accent_color: { type: string }
        title_font: { type: string }
        body_font: { type: string }
        motion_style: { type: string }
        brand_identity: { type: string }

    Upload:
      type: object
      properties:
        object: { type: string, example: upload }
        upload_url: { type: string, description: "Presigned S3 PUT url." }
        public_url: { type: string, description: "The resulting public https url to reference." }
        expires_in_seconds: { type: integer }
        method: { type: string, example: PUT }

    Asset:
      type: object
      properties:
        id: { type: string, example: as_1a2b3c4d, description: "Our stable asset id (for complete/delete)." }
        object: { type: string, example: asset }
        asset_id: { type: string, description: "HeyGen asset id — use in file_asset_ids / avatar asset_id / brand logo_asset_id." }
        upload_url: { type: string }
        upload_headers: { type: object, additionalProperties: { type: string } }
        status: { type: string, enum: [uploading, processing, ready, deleted] }
        asset_type: { type: string }
        url: { type: string }
        content_type: { type: string }
        size_bytes: { type: integer }

    Folder:
      type: object
      properties:
        id: { type: string }
        object: { type: string, example: folder }
        name: { type: string }
        created_at: { type: string }

    WebhookEndpoint:
      type: object
      properties:
        id: { type: string, example: whk_9f8e7d6c5b4a3210 }
        object: { type: string, example: webhook_endpoint }
        url: { type: string }
        events:
          type: array
          items: { type: string }
        status: { type: string, example: active }
        created_at: { type: integer }
        secret:
          type: string
          description: Signing secret (whsec_...) — returned ONLY at creation.

    WebhookEvent:
      type: object
      description: The JSON body POSTed to your webhook endpoint.
      properties:
        id: { type: string, example: evt_a1b2c3d4e5f60718, description: "Dedupe on this — retries reuse the same id." }
        object: { type: string, example: event }
        type: { type: string, enum: [video.completed, video.failed] }
        created: { type: integer }
        data:
          type: object
          properties:
            video: { $ref: "#/components/schemas/Video" }
