> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hooked.so/llms.txt
> Use this file to discover all available pages before exploring further.

# Music to Video

> Turn a song into a music video whose visuals follow the lyrics, cut to the beat

## Overview

Music to Video listens to a song, transcribes its lyrics and builds a music video around them: a visual for each part of the song, cut to the beat, with the lyrics as captions and, if you want, an audio-reactive sound wave. The song plays as the soundtrack.

**Sending the song.** Send one of:

* `audioUrl`: a public `https://` link to the audio file itself (MP3, M4A, AAC, WAV or OGG), up to **10 MB**, the same limit as an upload in the dashboard. Hooked downloads it and reads its type from the file. A private or local address (or a redirect to one), a bigger file, a page instead of a file (a Spotify, YouTube or Suno song page) or anything that is not audio answers `400`. A Suno CDN link (`https://cdn1.suno.ai/<id>.mp3`) is a file and works.
* `musicId`: a track from [`GET /v1/music/list`](/api-reference/music/list) or one of your team's own audio files.

Not both. The song is copied into your team's media library as a new audio file, so it counts toward your storage: a team without room for it gets `403` before anything is created.

<Warning>
  The video follows the **lyrics**. A song without vocals (most of the library tracks are instrumental) cannot be transcribed: the project fails, the credits come back and your `webhook` gets the failure.
</Warning>

**The visuals.** `mediaType` as in [Script to Video](/api-reference/video/script-to-video): `ai-images` (default), `ai-videos`, `media` (your own library files) or `gameplay`. Motion graphics are not available for this format. `visualGuidelines` steers the AI visuals (a setting, a palette, what to avoid) and `characterIds` keeps the same people across the video. With `ai-videos`, `isContinuous` chains each clip to the last frame of the one before; it is off by default, as in the dashboard.

<Note>
  The request answers with a `projectId` and status `processing` once the song is downloaded and checked. The transcription, the beat analysis, the media and the render happen after that. What can be refused up front (an invalid body, a link that is not audio, missing keys, too few credits, no storage left) is answered with a 400, 402 or 403 and nothing is created.
</Note>

```
POST https://api.hooked.so/v1/project/create/music-to-video
```

***

<RequestExample>
  ```bash cURL theme={null}
  curl -X POST "https://api.hooked.so/v1/project/create/music-to-video" \
    -H "x-api-key: your_api_key_here" \
    -H "Content-Type: application/json" \
    -d '{
      "audioUrl": "https://example.com/music/summer-nights.mp3",
      "mediaType": "ai-images",
      "presetSettings": { "preset": "anime" },
      "visualGuidelines": "A road trip along the coast at sunset, warm colors",
      "addSoundWave": true,
      "aspectRatio": "ratio_9_16",
      "caption": { "preset": "tiktok" },
      "webhook": "https://example.com/hooked/webhook?token=YOUR_SECRET",
      "metadata": { "trackId": "summer-nights" }
    }'
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch("https://api.hooked.so/v1/project/create/music-to-video", {
    method: "POST",
    headers: {
      "x-api-key": "your_api_key_here",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      audioUrl: "https://example.com/music/summer-nights.mp3",
      mediaType: "ai-images",
      presetSettings: { preset: "anime" },
      visualGuidelines: "A road trip along the coast at sunset, warm colors",
      addSoundWave: true,
      aspectRatio: "ratio_9_16",
      caption: { preset: "tiktok" },
      webhook: "https://example.com/hooked/webhook?token=YOUR_SECRET",
      metadata: { trackId: "summer-nights" },
    }),
  });

  const { data } = await response.json();
  console.log(data.projectId, data.status); // "processing"
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      "https://api.hooked.so/v1/project/create/music-to-video",
      headers={"x-api-key": "your_api_key_here"},
      json={
          "audioUrl": "https://example.com/music/summer-nights.mp3",
          "mediaType": "ai-images",
          "presetSettings": {"preset": "anime"},
          "visualGuidelines": "A road trip along the coast at sunset, warm colors",
          "addSoundWave": True,
          "aspectRatio": "ratio_9_16",
          "caption": {"preset": "tiktok"},
          "webhook": "https://example.com/hooked/webhook?token=YOUR_SECRET",
          "metadata": {"trackId": "summer-nights"},
      },
  )

  print(response.json()["data"]["projectId"])
  ```
</RequestExample>

### More request bodies

<CodeGroup>
  ```json AI video clips theme={null}
  {
    "audioUrl": "https://example.com/music/midnight-drive.m4a",
    "mediaType": "ai-videos",
    "presetSettings": { "preset": "cinematic", "quality": "pro", "isContinuous": true },
    "aspectRatio": "ratio_16_9",
    "language": "es"
  }
  ```

  ```json Your own media theme={null}
  {
    "audioUrl": "https://example.com/music/tour-anthem.mp3",
    "mediaType": "media",
    "media": ["cm4x9k2p10001ab12cd34ef56", "cm4x9k2p10002ab12cd34ef56"],
    "addSoundWave": false
  }
  ```

  ```json Gameplay theme={null}
  {
    "audioUrl": "https://example.com/music/level-up.mp3",
    "mediaType": "gameplay",
    "gameplaySettings": { "selectedGame": "subway-s", "selectedVideo": "subway-s-3" },
    "caption": { "preset": "beast", "alignment": "middle" }
  }
  ```
</CodeGroup>

***

<ResponseExample>
  ```json 200 theme={null}
  {
    "success": true,
    "data": {
      "projectId": "cm4x9k2p10001ab12cd34ef56",
      "status": "processing"
    },
    "message": "Music to Video successfully created"
  }
  ```

  ```json 400 theme={null}
  {
    "success": false,
    "message": "audioUrl: The file is not an MP3, M4A, AAC, WAV or OGG audio file"
  }
  ```

  ```json 401 theme={null}
  {
    "code": "not_authenticated",
    "message": "Not authenticated",
    "errorCode": "NOT_AUTHENTICATED",
    "details": { "x-api-key": "Header not provided or API Key invalid" }
  }
  ```

  ```json 402 missing keys theme={null}
  {
    "code": "missing_credentials",
    "message": "Your team uses its own provider keys (BYOK) and these are missing or invalid: OpenRouter. Add them in Settings → AI keys.",
    "errorCode": "MISSING_CREDENTIALS",
    "missingProviders": ["openrouter"],
    "details": {}
  }
  ```

  ```json 402 credits theme={null}
  {
    "success": false,
    "errorCode": "INSUFFICIENT_CREDITS",
    "message": "Not enough credits for Music to Video. Needed: 10, Available: 4",
    "creditsNeeded": 10,
    "creditsAvailable": 4
  }
  ```

  ```json 403 theme={null}
  {
    "code": "entitlement_required",
    "error": "entitlement_required",
    "message": "This endpoint requires the \"app\" product.",
    "product": "app"
  }
  ```
</ResponseExample>

***

## What happens next

1. Keep the `projectId`. The project stays `processing` while the song is transcribed and analysed and the visuals are made; if a step fails it becomes `failed`, with the reason in `message` and the credits refunded.
2. Either poll [`GET /v1/project/{projectId}`](/api-reference/project/details) until `video.status` is `COMPLETED` (or the project `status` is `failed`), or pass a `webhook` and wait for the call.
3. Download the file from `video.url` (or `data.url` in the webhook).

The song stays in your media library as an audio file. See [Webhooks](/guides/webhooks) for every payload.

***

## Credits and your own keys

**Teams on Hooked's keys (managed):** every music video costs a base (the beat analysis and the assembly). With `media` or `gameplay` that is all, taken when the project is created. With `ai-images` and `ai-videos` the media is priced once the song has been split into sections: per image by the model, or per clip by its length and model, plus the base. A team without the base gets `402 INSUFFICIENT_CREDITS` and nothing is created; if the balance is short for the media later, the project fails and nothing is taken. If the project fails, the credits are refunded automatically.

**Teams on their own keys (BYOK):** no credits are charged. The team needs these keys in **Settings → AI keys**:

* **OpenRouter**: always (transcription, project name, images, video clips).
* **Gemini**: when `presetSettings.aiModel` is `gemini_omni_flash`.

No ElevenLabs key: nothing is narrated. A missing key answers `402 missing_credentials` with the list in `missingProviders`, before anything is created.

***

## Errors

| Status | Body | When |
| - | - | - |
| `400` | `{ "success": false, "message": "<field>: <reason>" }` | Validation failed. The song: `audioUrl: Required: the https URL of the song (or musicId for a track already in Hooked)`, `audioUrl: Send either audioUrl or musicId, not both`, `audioUrl: Must be an https URL`, `audioUrl: Must be a public https URL (…)` (a private or local address, or a redirect to one), `audioUrl: The audio file is larger than 10 MB`, `audioUrl: The audio file could not be downloaded (HTTP 404)`, `audioUrl: The file is not an MP3, M4A, AAC, WAV or OGG audio file`, `musicId: Music "<id>" not found.`. The rest: `mediaType`, `media`, `presetSettings`, `gameplaySettings`, `caption`, `addSoundWave: Must be true or false`, `visualGuidelines: Must be a string of up to 2,000 characters`, `language: Must be a two-letter ISO 639-1 code`, `webhook: Must be a valid HTTPS URL`, `Invalid JSON body`. |
| `401` | `not_authenticated` | Missing or invalid `x-api-key`. |
| `402` | `missing_credentials` | BYOK team without the keys listed above. |
| `402` | `subscription_required` | Managed team without an active plan. |
| `402` | `INSUFFICIENT_CREDITS` | Managed team without enough credits. |
| `403` | `entitlement_required` | The team does not have the product this endpoint needs. |
| `403` | `{ "success": false, "message": "Insufficient storage. …" }` | Your team's storage has no room for the song. |
| `500` | `{ "success": false, "message": "Failed to create Music to Video project: …" }` | An unexpected error; the credits taken are refunded. |

<Warning>
  Send an [`Idempotency-Key`](/guides/idempotency) header to retry safely: if a request times out or answers 5xx, sending it again with the same key and body returns the first answer instead of creating (and charging) the project twice.
</Warning>

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Get Project" icon="circle-info" href="/api-reference/project/details">
    Follow the project until the video is ready
  </Card>

  <Card title="Music to Video example" icon="book" href="/examples/music-to-video">
    More requests for this format
  </Card>

  <Card title="List Music" icon="music" href="/api-reference/music/list">
    The library tracks you can pass as musicId
  </Card>

  <Card title="Script to Video" icon="film" href="/api-reference/video/script-to-video">
    A narrated video with music in the background
  </Card>
</CardGroup>


## OpenAPI

````yaml POST /v1/project/create/music-to-video
openapi: 3.0.0
info:
  title: Hooked API
  version: 1.0.0
  description: AI Video Generation API
servers:
  - url: https://api.hooked.so
security:
  - ApiKeyAuth: []
tags:
  - name: Videos
    description: Create projects, follow them and render videos
  - name: Account Analysis
    description: Analyze TikTok and YouTube accounts
  - name: Channels
    description: Connected social accounts you publish to
  - name: Publish
    description: Schedule videos to your connected accounts
  - name: Images
    description: 'Image editing: background removal and expansion'
  - name: Media
    description: 'Your media library: list, import from a URL, upload a file'
  - name: Avatars
    description: Your team's own avatars (Actors) and their Hook + Demo reactions
paths:
  /v1/project/create/music-to-video:
    post:
      tags:
        - Videos
      summary: Create Music to Video
      description: >-
        Turns a song into a music video: the lyrics are transcribed and the
        visuals (AI images or clips, your own media or gameplay) follow them,
        cut to the beat, with the lyrics as captions. Send the song as a direct
        link to the audio file (`audioUrl`, up to 10 MB: MP3, M4A, AAC, WAV or
        OGG) or as `musicId` (a track from /v1/music/list or one of your team's
        own audio files), not both. The song is downloaded and checked in the
        request: a private or local address, a file over the limit or anything
        that is not audio answers 400 before anything is created. It is stored
        in your media library as a new audio file (it counts toward your
        storage). Answers with a `projectId` and status `processing`; the
        transcription, the media and the render run after that. The song needs
        vocals: a track without lyrics fails (the credits come back and the
        failure webhook is sent). BYOK: needs OpenRouter; `aiModel:
        "gemini_omni_flash"` also needs Gemini. No ElevenLabs: nothing is
        narrated.


        A body that is not valid JSON answers 400 (`Invalid JSON body`).
      operationId: createMusicToVideo
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                audioUrl:
                  type: string
                  format: uri
                  maxLength: 2000
                  description: >-
                    A public https link to the song file itself (MP3, M4A, AAC,
                    WAV or OGG, up to 10 MB), e.g. a file in your own storage or
                    a Suno CDN link (`https://cdn1.suno.ai/<id>.mp3`). Hooked
                    downloads it and reads its type from the file: a private or
                    local address (or a redirect to one), a file over 10 MB, a
                    page (Spotify, YouTube, a Suno song page) or anything that
                    is not audio answers 400. Send this or `musicId`, not both.
                musicId:
                  type: string
                  maxLength: 30
                  description: >-
                    Instead of `audioUrl`: a track from /v1/music/list or one of
                    your team's own audio files (up to 25 MB). The file is
                    copied for this video. An ID that is neither answers 400
                    (`musicId: Music "<id>" not found.`). Most library tracks
                    are instrumental: the video follows the lyrics, so use a
                    song with vocals.
                mediaType:
                  type: string
                  enum:
                    - ai-images
                    - ai-videos
                    - media
                    - gameplay
                  description: >-
                    Where the visuals come from:

                    - `ai-images` (default): one AI image per lyric section
                    (`presetSettings`: `preset`, `aiModel`, `characterIds`).

                    - `ai-videos`: one AI video clip per section
                    (`presetSettings`: `preset`, `quality`, `aiModel`,
                    `isContinuous`, `characterIds`).

                    - `media`: your own images and videos from the media library
                    (`media` required).

                    - `gameplay`: a gameplay clip in the background
                    (`gameplaySettings`).


                    `motion-graphics` is not available for this format (400).
                  default: ai-images
                presetSettings:
                  type: object
                  description: >-
                    Visual settings for `ai-images` and `ai-videos`; not used by
                    `media` and `gameplay`. Each field says which media types
                    read it.
                  properties:
                    preset:
                      type: string
                      enum:
                        - realistic
                        - cinematic
                        - anime
                        - real-anime
                        - retro-anime
                        - cyberpunk-anime
                        - ghibli-studio
                        - pixar
                        - cartoon
                        - comic-book
                        - claymation
                        - fantasy
                        - 80s-fantasy-movie
                        - creative
                        - art-style
                        - sketch-black-and-white
                        - sketch-color
                        - japanese-ink
                        - ink-style
                        - haunted-linework
                        - neon-futuristic
                        - pixel-art
                        - collage
                        - lego
                        - technical-blueprints
                        - stickman
                        - nursery-rhyme
                        - south-park
                        - skeleton-3d
                        - fruit-people
                        - talking-fruit
                        - talking-objects
                        - talking-organs
                        - minecraft
                        - gta-v
                        - free-fire
                        - fortnite
                        - roblox
                        - pubg
                        - bitlife
                        - space-marines-40k
                        - bombardiro-crocodilo
                        - tralalero-tralala
                        - demon-slayer
                        - dragon-ball
                        - one-piece
                        - pokemon
                        - naruto
                        - attack-on-titan
                        - final-fantasy
                        - mecha-break
                        - alien-stage
                        - marvel
                        - dc
                        - star-wars
                        - star-trek
                        - harry-potter
                        - lord-of-the-rings
                        - game-of-thrones
                        - doctor-who
                        - sherlock-holmes
                        - stranger-things
                        - squid-game
                        - wednesday
                        - zelda
                        - genshin-impact
                        - dungeons-and-dragons
                        - rick-and-morty
                        - amazing-digital-circus
                      default: realistic
                      description: >-
                        `ai-images` / `ai-videos`: the visual style, or
                        `custom-<id>` for one of your team's custom styles. Any
                        other value answers 400.
                    quality:
                      type: string
                      enum:
                        - base
                        - pro
                        - ultra
                      default: base
                      description: >-
                        `ai-videos`: picks the video model when `aiModel` is not
                        set. Not used by `ai-images` (use `aiModel`).
                    aiModel:
                      type: string
                      enum:
                        - nano_banana
                        - nano_banana_2
                        - nano_banana_pro
                        - seedream_4_5
                        - seedream_5_0
                        - gpt_image_2
                        - grok_imagine_image
                        - seedance_1_5_pro
                        - seedance_2_0
                        - seedance_2_0_mini
                        - seedance_2_0_fast
                        - kling_3_0
                        - veo_3_fast
                        - veo_3
                        - veo_3_1_lite
                        - wan_2_7_i2v
                        - grok_imagine_video
                        - gemini_omni_flash
                      description: >-
                        `ai-images` / `ai-videos`: the model that generates the
                        media, instead of the default. It must match
                        `mediaType`, otherwise the request answers 400
                        (`presetSettings.aiModel: "<model>" does not generate
                        <mediaType>`).

                        - `ai-images`: `nano_banana`, `nano_banana_2`,
                        `nano_banana_pro`, `seedream_4_5`, `seedream_5_0`,
                        `gpt_image_2`, `grok_imagine_image`.

                        - `ai-videos`: `seedance_1_5_pro`, `seedance_2_0`,
                        `seedance_2_0_mini`, `seedance_2_0_fast`, `kling_3_0`,
                        `veo_3_fast`, `veo_3`, `veo_3_1_lite`, `wan_2_7_i2v`,
                        `grok_imagine_video`, `gemini_omni_flash`.


                        BYOK: `gemini_omni_flash` needs the Gemini key.
                    isContinuous:
                      type: boolean
                      default: false
                      description: >-
                        `ai-videos` only: chain the clips frame to frame (each
                        clip starts on the last frame of the one before). Off by
                        default, as in the dashboard.
                    characterIds:
                      type: array
                      items:
                        type: string
                      description: >-
                        `ai-images` / `ai-videos`: IDs of characters saved in
                        your team's Characters library, to keep the same people
                        across scenes.
                gameplaySettings:
                  type: object
                  description: '`mediaType: "gameplay"` only: the background clip.'
                  properties:
                    selectedGame:
                      type: string
                      enum:
                        - minecraft
                        - subway-s
                        - temple-run
                        - gta
                        - fortnite
                        - roblox
                        - free-fire
                        - custom
                      default: minecraft
                      description: >-
                        The game, or `custom` to play a video from your own
                        media library. Any other value answers 400.
                    selectedVideo:
                      type: string
                      default: minecraft-1
                      description: >-
                        The clip, as `<game>-<n>`: `minecraft-1`…`minecraft-9`,
                        `subway-s-1`…`subway-s-11`,
                        `temple-run-1`…`temple-run-8`, `gta-1`…`gta-12`,
                        `fortnite-1`…`fortnite-5`, `roblox-1`, `free-fire-1`. A
                        clip that does not exist plays the game's first clip.
                        With `selectedGame: "custom"`, the ID of a video in your
                        media library (400 when it is not one).
                media:
                  type: array
                  maxItems: 50
                  items:
                    type: string
                    description: Media ID from your library
                  description: >-
                    IDs of images or videos in your team's media library (max
                    50). Required for `mediaType: "media"`; ignored for the
                    other media types. An ID that is not in your library answers
                    400 (`media: Media "<id>" not found`), and so does a media
                    that is not `COMPLETED` yet.
                name:
                  type: string
                  maxLength: 100
                  description: >-
                    Project name (max 100 characters). Generated from the song
                    when omitted.
                aspectRatio:
                  type: string
                  enum:
                    - ratio_9_16
                    - ratio_16_9
                    - ratio_1_1
                  default: ratio_9_16
                  description: Vertical, horizontal or square.
                language:
                  type: string
                  pattern: ^[a-zA-Z]{2}$
                  description: >-
                    Two-letter ISO 639-1 code of the song's lyrics. It guides
                    the transcription; detected from the song when omitted.
                caption:
                  type: object
                  description: Burned-in captions of the lyrics.
                  properties:
                    preset:
                      type: string
                      enum:
                        - default
                        - beast
                        - umi
                        - tiktok
                        - wrap1
                        - wrap2
                        - ariel
                        - hooked
                        - classic
                        - active
                        - bubble
                        - glass
                        - comic
                        - glow
                        - pastel
                        - neon
                        - retroTV
                        - red
                        - marker
                        - modern
                        - blue
                        - vivid
                      default: wrap1
                      description: Caption style. Any other value answers 400.
                    alignment:
                      type: string
                      enum:
                        - top
                        - middle
                        - bottom
                      default: bottom
                      description: Caption position.
                    disabled:
                      type: boolean
                      default: false
                      description: '`true` renders the video without captions.'
                addStickers:
                  type: boolean
                  default: false
                  description: Add emoji/GIF stickers anchored to the lyrics.
                content:
                  type: object
                  description: Branding.
                  properties:
                    brandingLogo:
                      type: object
                      description: >-
                        Your logo over the whole video. Drawn only with
                        `enabled: true` and a `url`.
                      properties:
                        enabled:
                          type: boolean
                          default: false
                        url:
                          type: string
                          description: >-
                            Public URL of the logo image (PNG with transparency
                            works best).
                        position:
                          type: string
                          enum:
                            - top-left
                            - top-right
                            - bottom-left
                            - bottom-right
                          default: bottom-right
                        size:
                          type: number
                          minimum: 20
                          maximum: 200
                          default: 100
                          description: Logo size (20-200).
                webhook:
                  type: string
                  maxLength: 500
                  format: uri
                  description: >-
                    HTTPS URL on a public host, called when the video is ready
                    or the project fails (max 500 characters). Requests are
                    signed: verify the Hooked-Signature header (HMAC-SHA256 of
                    "<t>.<raw body>") with the signing secret from Settings →
                    Webhooks; Hooked-Event and Hooked-Delivery headers name the
                    event and the delivery. No redirects are followed; 10 s
                    timeout; 3 immediate attempts. See the Webhooks guide for
                    the payload.
                metadata:
                  type: object
                  additionalProperties: true
                  description: >-
                    Any JSON object of your own (max 5 KB). Stored on the
                    project and sent back in the webhook payload. The key
                    `automationId` is reserved for dashboard automations and is
                    removed.
                addSoundWave:
                  type: boolean
                  default: true
                  description: >-
                    Overlay an audio-reactive sound wave synced to the song (its
                    style can be changed in the editor).
                visualGuidelines:
                  type: string
                  maxLength: 2000
                  description: >-
                    Extra direction for the AI visuals (`ai-images` /
                    `ai-videos`): a setting, a palette, what to avoid. Max 2,000
                    characters.
            example:
              audioUrl: https://example.com/music/summer-nights.mp3
              mediaType: ai-images
              presetSettings:
                preset: anime
              addSoundWave: true
              aspectRatio: ratio_9_16
              caption:
                preset: tiktok
              webhook: https://yoursite.com/webhook?token=YOUR_SECRET
      responses:
        '200':
          description: >-
            Project created. Follow it with GET /v1/project/{projectId} or wait
            for the webhook.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectCreateResponse'
              example:
                success: true
                data:
                  projectId: clx9p2k4m0001abcd1234efgh
                  status: processing
                message: Music to Video successfully created
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/EntitlementRequired'
        '409':
          $ref: '#/components/responses/IdempotencyInProgress'
        '422':
          $ref: '#/components/responses/IdempotencyMismatch'
        '429':
          $ref: '#/components/responses/RateLimited429'
        '500':
          $ref: '#/components/responses/InternalError'
      security:
        - ApiKeyAuth: []
components:
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: >-
        Makes the request safe to retry. 1-255 printable ASCII characters, one
        per operation (your job id, or a UUID you store), reused on every retry.
        Within 24 hours the same key with the same body answers with the first
        response and the header `Idempotent-Replayed: true`, without running or
        charging again. Keys are scoped to your team and the endpoint. 5xx
        answers and refusals before anything ran (401, 402, 403, 409, 429) are
        not kept. See [Idempotency](/guides/idempotency).
      schema:
        type: string
        minLength: 1
        maxLength: 255
        pattern: ^[\x20-\x7E]+$
      example: order-1042-video
  schemas:
    ProjectCreateResponse:
      type: object
      properties:
        success:
          type: boolean
          enum:
            - true
        data:
          type: object
          properties:
            projectId:
              type: string
              description: Follow it with GET /v1/project/{projectId}
            status:
              type: string
              enum:
                - draft
                - processing
                - completed
                - failed
              description: >-
                The project status right after creation, usually `draft` or
                `processing`. Follow it with GET /v1/project/{projectId}.
          required:
            - projectId
            - status
        message:
          type: string
      required:
        - success
        - data
        - message
    ValidationError400:
      type: object
      description: >-
        The request is invalid. `message` is `<field>: <reason>` when a field is
        to blame.
      properties:
        success:
          type: boolean
          enum:
            - false
        message:
          type: string
      required:
        - success
        - message
      example:
        success: false
        message: 'webhook: Must be a valid HTTPS URL'
    Error401:
      type: object
      description: Missing or invalid `x-api-key`.
      properties:
        code:
          type: string
          enum:
            - not_authenticated
        message:
          type: string
          example: Not authenticated
        errorCode:
          type: string
          enum:
            - NOT_AUTHENTICATED
        details:
          type: object
          additionalProperties:
            type: string
      example:
        code: not_authenticated
        message: Not authenticated
        errorCode: NOT_AUTHENTICATED
        details:
          x-api-key: Header not provided or API Key invalid
    MissingCredentials402:
      type: object
      description: >-
        BYOK team without the provider keys this request uses. Nothing was
        created or charged. Add the keys in Settings → AI keys.
      properties:
        success:
          type: boolean
          enum:
            - false
          description: Present when raised by the spend check
        code:
          type: string
          enum:
            - missing_credentials
        errorCode:
          type: string
          enum:
            - MISSING_CREDENTIALS
        missingProviders:
          type: array
          items:
            type: string
            enum:
              - openrouter
              - elevenlabs
              - gemini
              - bria
              - firecrawl
        message:
          type: string
        details:
          type: object
      required:
        - code
        - errorCode
        - missingProviders
        - message
      example:
        code: missing_credentials
        message: >-
          Your team uses its own provider keys (BYOK) and these are missing or
          invalid: OpenRouter, ElevenLabs. Add them in Settings → AI keys.
        errorCode: MISSING_CREDENTIALS
        missingProviders:
          - openrouter
          - elevenlabs
        details: {}
    SubscriptionRequired402:
      type: object
      description: Managed team (generates with Hooked's keys) without a live subscription.
      properties:
        success:
          type: boolean
          enum:
            - false
        code:
          type: string
          enum:
            - subscription_required
        errorCode:
          type: string
          enum:
            - SUBSCRIPTION_REQUIRED
        message:
          type: string
      required:
        - success
        - code
        - errorCode
        - message
      example:
        success: false
        code: subscription_required
        errorCode: SUBSCRIPTION_REQUIRED
        message: >-
          Your team generates with Hooked's keys, which are paid by your plan.
          Subscribe to keep generating.
    InsufficientCredits402:
      type: object
      description: >-
        Not enough credits for this request. `creditsNeeded` /
        `creditsAvailable` are included when the estimate is known.
      properties:
        success:
          type: boolean
          enum:
            - false
        errorCode:
          type: string
          enum:
            - INSUFFICIENT_CREDITS
        message:
          type: string
        creditsNeeded:
          type: number
        creditsAvailable:
          type: number
      required:
        - success
        - errorCode
        - message
      example:
        success: false
        errorCode: INSUFFICIENT_CREDITS
        message: Not enough credits to perform this action
        creditsNeeded: 40
        creditsAvailable: 12
    EntitlementRequired403:
      type: object
      description: >-
        Valid key, but the team does not have the product this endpoint belongs
        to.
      properties:
        code:
          type: string
          enum:
            - entitlement_required
        error:
          type: string
          enum:
            - entitlement_required
        message:
          type: string
        product:
          type: string
          example: app
      required:
        - code
        - error
        - message
        - product
      example:
        code: entitlement_required
        error: entitlement_required
        message: This endpoint requires the "app" product.
        product: app
    Error500:
      type: object
      properties:
        success:
          type: boolean
          enum:
            - false
        message:
          type: string
        error:
          type: string
          description: Present on some endpoints
      example:
        success: false
        message: Internal server error
  responses:
    ValidationError:
      description: Validation error (also `Invalid JSON body`)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ValidationError400'
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error401'
    PaymentRequired:
      description: >-
        Payment required: a BYOK team is missing provider keys (nothing was
        created or charged), a managed team has no live subscription, or there
        are not enough credits.
      content:
        application/json:
          schema:
            oneOf:
              - $ref: '#/components/schemas/MissingCredentials402'
              - $ref: '#/components/schemas/SubscriptionRequired402'
              - $ref: '#/components/schemas/InsufficientCredits402'
          examples:
            missingCredentials:
              value:
                code: missing_credentials
                message: >-
                  Your team uses its own provider keys (BYOK) and these are
                  missing or invalid: OpenRouter, ElevenLabs. Add them in
                  Settings → AI keys.
                errorCode: MISSING_CREDENTIALS
                missingProviders:
                  - openrouter
                  - elevenlabs
                details: {}
            subscriptionRequired:
              value:
                success: false
                code: subscription_required
                errorCode: SUBSCRIPTION_REQUIRED
                message: >-
                  Your team generates with Hooked's keys, which are paid by your
                  plan. Subscribe to keep generating.
            insufficientCredits:
              value:
                success: false
                errorCode: INSUFFICIENT_CREDITS
                message: Not enough credits to perform this action
                creditsNeeded: 40
                creditsAvailable: 12
    EntitlementRequired:
      description: The team does not have the product this endpoint needs
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/EntitlementRequired403'
    IdempotencyInProgress:
      description: >-
        A request with the same `Idempotency-Key` is still running. Retry in a
        few seconds.
      content:
        application/json:
          schema:
            type: object
            properties:
              success:
                type: boolean
                enum:
                  - false
              code:
                type: string
              message:
                type: string
              details:
                type: object
                additionalProperties: true
            required:
              - success
              - code
              - message
          example:
            success: false
            code: idempotency_conflict
            message: A request with this Idempotency-Key is still in progress
            details: {}
    IdempotencyMismatch:
      description: >-
        The `Idempotency-Key` was already used with a different body. Use a new
        key for a new request.
      content:
        application/json:
          schema:
            type: object
            properties:
              success:
                type: boolean
                enum:
                  - false
              code:
                type: string
              message:
                type: string
              details:
                type: object
                additionalProperties: true
            required:
              - success
              - code
              - message
          example:
            success: false
            code: idempotency_mismatch
            message: Idempotency-Key was used with a different request
            details: {}
    RateLimited429:
      description: >-
        Your team is over a rate limit: 30 POST requests per minute, 120 other
        requests per minute, or 3 downloads of your URLs running at once.
        Nothing ran and nothing was charged: wait `Retry-After` seconds and send
        the request again. See [Rate
        limits](/guides/error-handling#rate-limits-429).
      headers:
        Retry-After:
          description: Seconds to wait before sending again.
          schema:
            type: integer
        X-RateLimit-Limit:
          description: The limit of the bucket you hit (not sent for the download cap).
          schema:
            type: integer
        X-RateLimit-Remaining:
          description: 'Requests left in this window: 0 (not sent for the download cap).'
          schema:
            type: integer
        X-RateLimit-Reset:
          description: >-
            When the window ends, in Unix seconds (not sent for the download
            cap).
          schema:
            type: integer
      content:
        application/json:
          schema:
            type: object
            properties:
              success:
                type: boolean
                enum:
                  - false
              errorCode:
                type: string
                enum:
                  - RATE_LIMITED
              message:
                type: string
              retryAfterSeconds:
                type: integer
            required:
              - success
              - errorCode
              - message
              - retryAfterSeconds
          example:
            success: false
            errorCode: RATE_LIMITED
            message: >-
              Rate limit exceeded: 30 POST requests per minute per team. Retry
              after 12 s.
            retryAfterSeconds: 12
    InternalError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error500'
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.