> ## 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.

# Cinematic Studio

> Turn a story brief into a short film with characters who speak

## Overview

Cinematic Studio writes a short film from a brief. Hooked turns the brief into a storyboard (scenes, cast and dialogue), draws a portrait of each character, draws a start frame for each scene, films every scene with the video model you pick (the characters speak with the model's own audio), then adds subtitles and music and renders the video. It is the same pipeline as **Automatic** mode in the dashboard's Cinematic Studio.

Send `storyBrief`, `durationSeconds` (10 to 120) and `model`. Everything else is optional: `storyTone`, `presetId` (the visual style), `imageModel`, `characterIds` (your characters, from [List Characters](/api-reference/character/list)), `preserveExactStoryText`, `aspectRatio`, `language` (`en` or `es`), `musicId`, `name`, `webhook` and `metadata`.

<Note>
  The request answers in a few seconds with a `projectId` and status `processing`. The storyboard, the portraits, the scenes, the final cut and the render all happen after that. What can be refused up front (an invalid body, a brief about a real person, missing keys, too few credits for the whole run) is still answered with a 400, 402 or 404 and nothing is created. If a later step fails, the project becomes `failed` with the reason in `message` and your `webhook` gets the failure.
</Note>

```
POST https://api.hooked.so/v1/project/create/cinematic
```

***

<RequestExample>
  ```bash cURL theme={null}
  curl -X POST "https://api.hooked.so/v1/project/create/cinematic" \
    -H "x-api-key: your_api_key_here" \
    -H "Content-Type: application/json" \
    -d '{
      "storyBrief": "A lighthouse keeper finds a message in a bottle during a storm and decides to answer it.",
      "durationSeconds": 30,
      "model": "grok_imagine_video",
      "storyTone": "drama",
      "presetId": "cinematic",
      "webhook": "https://example.com/hooked/webhook?token=YOUR_SECRET"
    }'
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch("https://api.hooked.so/v1/project/create/cinematic", {
    method: "POST",
    headers: {
      "x-api-key": "your_api_key_here",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      storyBrief: "A lighthouse keeper finds a message in a bottle during a storm and decides to answer it.",
      durationSeconds: 30,
      model: "grok_imagine_video",
      storyTone: "drama",
      presetId: "cinematic",
      webhook: "https://example.com/hooked/webhook?token=YOUR_SECRET",
    }),
  });

  const { data } = await response.json();
  console.log(data.projectId);
  ```

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

  response = requests.post(
      "https://api.hooked.so/v1/project/create/cinematic",
      headers={"x-api-key": "your_api_key_here"},
      json={
          "storyBrief": "A lighthouse keeper finds a message in a bottle during a storm and decides to answer it.",
          "durationSeconds": 30,
          "model": "grok_imagine_video",
          "storyTone": "drama",
          "presetId": "cinematic",
          "webhook": "https://example.com/hooked/webhook?token=YOUR_SECRET",
      },
  )

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

### More request bodies

<CodeGroup>
  ```json Vertical anime short with music theme={null}
  {
    "storyBrief": "Two rival street racers have to team up when their cars break down in the desert.",
    "durationSeconds": 45,
    "model": "kling_3_0",
    "storyTone": "action",
    "presetId": "anime",
    "aspectRatio": "ratio_9_16",
    "musicId": "1"
  }
  ```

  ```json Your own words, in Spanish theme={null}
  {
    "storyBrief": "Lucía mira por la ventana y dice: \"Mañana me voy\". Su abuelo sonríe y responde: \"Ya lo sabía\".",
    "durationSeconds": 15,
    "model": "veo_3_fast",
    "language": "es",
    "preserveExactStoryText": true
  }
  ```
</CodeGroup>

To cast characters you saved in the dashboard (**Studio → Characters**), get their ids with [List Characters](/api-reference/character/list) (`GET /v1/character/list`), add `"characterIds": ["<characterId>"]` (up to 6) and mention them in the brief as `@Name`. They keep their look, voice and photos; a character without a photo gets a portrait drawn like any other.

***

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

  ```json 400 theme={null}
  {
    "success": false,
    "message": "model: Invalid model \"sora\". Allowed values are: veo_3_fast, veo_3, gemini_omni_flash, grok_imagine_video, kling_3_0, seedance_2_0, seedance_2_0_fast, veo_3_1_lite, seedance_2_0_mini"
  }
  ```

  ```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 credits theme={null}
  {
    "success": false,
    "errorCode": "INSUFFICIENT_CREDITS",
    "message": "Not enough credits. Needed: 260, Available: 100",
    "creditsNeeded": 260,
    "creditsAvailable": 100
  }
  ```

  ```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 403 theme={null}
  {
    "code": "entitlement_required",
    "error": "entitlement_required",
    "message": "This endpoint requires the \"app\" product.",
    "product": "app"
  }
  ```

  ```json 404 theme={null}
  {
    "success": false,
    "code": "feature_not_available",
    "error": "feature_not_available",
    "message": "Cinematic videos are not available on this platform."
  }
  ```
</ResponseExample>

***

## Options

Every value these fields accept is listed by [Get Catalog](/api-reference/catalog/get) with `name=cinematic` (one item per field, with each model's durations, resolutions and aspect ratios), read from the same lists this endpoint validates against.

| Field | Values | Default |
| - | - | - |
| `model` | A video model with native audio, e.g. `veo_3_fast`, `kling_3_0`, `seedance_2_0` | required |
| `storyTone` | e.g. `drama`, `comedy`, `thriller` | `drama` |
| `presetId` | A visual style of `GET /v1/catalog/visual-styles` (e.g. `realistic`, `anime`, `pixar`), or `custom-<id>` for one of your custom styles | `realistic` |
| `imageModel` | An image model, e.g. `gpt_image_2`, `nano_banana_pro` | `gpt_image_2` |
| `aspectRatio` | `ratio_16_9`, `ratio_9_16`; `ratio_1_1` only with the models whose `aspectRatios` include it | `ratio_16_9` |
| `language` | `en`, `es` | `en` |

Seedance models can't film the photorealistic styles (`realistic`, `cinematic`): their face filter refuses realistic people, so that pair answers `400`. Pick another model or style. In `GET /v1/catalog/cinematic`, the `presetId` item lists those styles with the models each one refuses (`excludedModels`).

***

## What happens next

1. **During the request**: the body is checked, the whole run is priced and the brief is checked (no real, identifiable or famous people, no minors). The project is created with status `processing` and the request answers.
2. **In the background**: the storyboard is written and a portrait is drawn for each character; then a start frame is drawn for each scene, one after the other; then every scene is filmed, the subtitles and the music are added and the video is rendered. While the storyboard is being written, the project is `processing` with no scenes and no video yet.
3. Follow the project with [`GET /v1/project/{projectId}`](/api-reference/project/details) until `video.status` is `COMPLETED` (or the project `status` is `failed`), or wait for your `webhook`.

The webhook is called once when the video is ready (`"status": "COMPLETED"` with `data.url`), or once if the project fails (`"status": "FAILED"`, `videoId: null`). See [Webhooks](/guides/webhooks) for every payload.

***

## Credits and your own keys

**Teams on Hooked's keys (managed):**

* **Checked up front.** Before anything is paid, the whole run is priced: a portrait for each library character without a photo, plus one start frame and one clip for each 6 seconds of `durationSeconds` (filmed at 1080p, the same estimate the dashboard shows). If the balance does not cover it, the request answers `402 INSUFFICIENT_CREDITS` with `creditsNeeded` and `creditsAvailable`, and nothing is created or charged. Once the storyboard is written (in the background), the run is priced again with its real scenes and characters; if the balance no longer covers it, the project becomes `failed` ("Not enough credits…" in `message`) before anything is charged, and your webhook gets the failure.
* **Charged in stages**, as each one starts: the portraits once the storyboard is written, the scene start frames right after them, the clips once every frame is ready, and the subtitles once every clip is done (if the balance can't cover the subtitles then, the video completes without them).
* **Refunds on failure.** A portrait, start frame or clip that fails is refunded, and so is every frame or clip that was paid for but never started when the run stops. A portrait that was drawn is a delivered asset: it stays in your media library and is not refunded, even if the video fails later (for example, when the scene frames can't be paid for or queued). Once the frames have started, what was already made (frames, finished clips) stays in your media library and is not refunded.
* **On the project.** `usedCredits` in [Get Project](/api-reference/project/details) is what the run has cost so far: every stage charged, minus what was refunded. It grows as the stages start. On a failed project it is what you kept paying for (the portraits that were drawn).

**Teams on their own keys (BYOK):** no credits are charged. The team needs **OpenRouter** in **Settings → AI keys** (story, portraits, frames and clips), and **Gemini** when `model` is `gemini_omni_flash`. 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. Examples: `storyBrief: Required`, `durationSeconds: Must be at most 120`, `model: Invalid model "…". Allowed values are: …`, `storyTone: Invalid tone "…". Allowed values are: …`, `imageModel: Invalid image model "…". Allowed values are: …`, `presetId: Invalid preset "…". Allowed values are: …` (or `Custom style "custom-…" not found`), `aspectRatio: "…" does not film ratio_1_1. Allowed values are: …`, `characterIds: Character "…" not found.`, `musicId: Music "…" not found.`, `webhook: Must be a valid HTTPS URL`, `Invalid JSON body`, or a brief blocked by the content policy (`storyBrief: Blocked by content policy: …`). |
| `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 for the whole run. |
| `403` | `entitlement_required` | The team does not have the product this endpoint needs. |
| `404` | `feature_not_available` | Cinematic Studio is not offered on this platform. |
| `500` | `{ "success": false, "message": "..." }` | Unexpected error. A run that fails after the answer (the storyboard, a portrait, the start of the scenes) is not an error response: the project becomes `failed` with the reason in `message`, and your webhook is called once. |

<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>

***

## Writing a brief

Say what happens, who is in it and the mood: "A lighthouse keeper finds a message in a bottle during a storm and decides to answer it" gives the storyboard a character, a place and a turn. Write the dialogue in quotes and set `preserveExactStoryText: true` to keep it word for word. Briefs about real, identifiable or famous people are refused; use original characters.

***

## 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="Webhooks" icon="webhook" href="/guides/webhooks">
    Payloads and delivery rules
  </Card>

  <Card title="Cinematic example" icon="clapperboard" href="/examples/cinematic">
    A full request, from brief to video
  </Card>

  <Card title="List Music" icon="music" href="/api-reference/music/list">
    Pick a background track
  </Card>
</CardGroup>


## OpenAPI

````yaml POST /v1/project/create/cinematic
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/cinematic:
    post:
      tags:
        - Videos
      summary: Create Cinematic Studio
      description: >-
        Turns a story brief into a short film with characters who speak: Hooked
        writes the storyboard, casts a portrait per character, draws a start
        frame per scene, films each scene with the chosen video model, adds
        subtitles and music and renders the video. The same pipeline as
        Automatic mode in the dashboard's Cinematic Studio.


        The request answers in a few seconds with a `projectId` and status
        `processing`; the storyboard, the portraits, the scenes, the final cut
        and the render follow in the background. A step that fails there marks
        the project `failed` with the reason in `message` and sends the failure
        webhook once.


        Credits: the whole run (portraits, scene images and clips for
        `durationSeconds`) is checked before anything is created; a short
        balance answers 402 with nothing created. It is checked again once the
        storyboard is written: a balance that no longer covers it fails the
        project before anything is charged. Then each stage is charged as it
        starts, and a stage that fails is refunded; the project's `usedCredits`
        is every stage charged so far minus those refunds (a portrait that was
        drawn is a delivered asset and is not refunded, even if the video fails
        later). BYOK: needs OpenRouter; `model: "gemini_omni_flash"` also needs
        Gemini.


        A body that is not valid JSON answers 400 (`Invalid JSON body`). Create
        endpoints are not idempotent: do not retry a POST on a timeout or 5xx
        without first checking your projects, or you may create (and pay for)
        the video twice.
      operationId: createCinematicVideo
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - storyBrief
                - durationSeconds
                - model
              properties:
                storyBrief:
                  type: string
                  minLength: 1
                  maxLength: 4000
                  description: >-
                    What the film is about (1–4,000 characters): the story, who
                    is in it, the mood. Mention a library character with
                    `@Name`. A brief that recreates a real, identifiable or
                    famous person (or a minor) answers 400.
                durationSeconds:
                  type: integer
                  minimum: 10
                  maximum: 120
                  description: >-
                    Target length in seconds (10–120). The storyboard picks the
                    real scene count; credits are checked up front for one
                    6-second scene per 6 seconds.
                model:
                  type: string
                  enum:
                    - veo_3_fast
                    - veo_3
                    - gemini_omni_flash
                    - grok_imagine_video
                    - kling_3_0
                    - seedance_2_0
                    - seedance_2_0_fast
                    - veo_3_1_lite
                    - seedance_2_0_mini
                  description: >-
                    Video model that films every scene, with its own audio (the
                    cast speaks). Seedance models can't film the photorealistic
                    styles (`realistic`, `cinematic`): that pair answers 400.
                    `gemini_omni_flash` runs on Gemini.
                storyTone:
                  type: string
                  enum:
                    - drama
                    - comedy
                    - action
                    - romance
                    - thriller
                    - documentary
                    - music_video
                    - fantasy
                    - anime
                  default: drama
                  description: 'Genre and pacing of the story. Default: `drama`.'
                presetId:
                  type: string
                  default: realistic
                  description: >-
                    Visual style of every scene and of the cast portraits: one
                    of the style ids (`realistic`, `cinematic`, `anime`,
                    `pixar`, `ghibli-studio`…, the same list as
                    `presetSettings.preset` in Script to Video), or
                    `custom-<id>` for one of your team's custom styles. Default:
                    `realistic`. An unknown id answers 400 (`presetId: Invalid
                    preset "…". Allowed values are: …`).
                  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
                imageModel:
                  type: string
                  enum:
                    - gpt_image_2
                    - nano_banana_pro
                    - nano_banana_2
                    - nano_banana
                    - grok_imagine_image
                    - seedream_5_0
                    - seedream_4_5
                  default: gpt_image_2
                  description: >-
                    Image model that draws each scene's start frame. Default:
                    `gpt_image_2`.
                characterIds:
                  type: array
                  maxItems: 6
                  items:
                    type: string
                  description: >-
                    Up to 6 characters saved in your team's dashboard (Studio →
                    Characters) to cast: their look, voice and photos are kept;
                    mention them in the brief as `@Name`. Without it the
                    storyboard invents the cast. An id that is not your team's
                    answers 400 (`characterIds: Character "…" not found.`).
                preserveExactStoryText:
                  type: boolean
                  default: false
                  description: >-
                    Keep the brief's own dialogue and narration word for word
                    instead of rewriting it.
                aspectRatio:
                  type: string
                  enum:
                    - ratio_16_9
                    - ratio_9_16
                    - ratio_1_1
                  default: ratio_16_9
                  description: >-
                    Default: horizontal. `ratio_1_1` only with Kling and
                    Seedance; a ratio the model does not film answers 400.
                language:
                  type: string
                  enum:
                    - en
                    - es
                  default: en
                  description: Language the story and its dialogue are written in.
                musicId:
                  type: string
                  maxLength: 30
                  description: >-
                    Background music under the whole film: a track from
                    /v1/music/list or your team's own uploaded music. No music
                    when omitted. An ID that is neither answers 400 (`musicId:
                    Music "<id>" not found.`).
                name:
                  type: string
                  maxLength: 100
                  description: >-
                    Project name (max 100 characters). The storyboard's title
                    when omitted.
                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.
            example:
              storyBrief: >-
                A lighthouse keeper finds a message in a bottle during a storm
                and decides to answer it.
              durationSeconds: 30
              model: grok_imagine_video
              storyTone: drama
              presetId: cinematic
              aspectRatio: ratio_16_9
              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: Cinematic successfully created
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/EntitlementRequired'
        '404':
          description: >-
            Cinematic is not available on this platform (a tenant without the
            feature).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NotFound404'
              example:
                success: false
                code: feature_not_available
                error: feature_not_available
                message: Cinematic videos are not available on this platform.
        '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
    NotFound404:
      type: object
      description: >-
        Not found. Another team's resource answers the same way as a missing
        one.
      properties:
        success:
          type: boolean
          enum:
            - false
        message:
          type: string
      required:
        - success
        - message
      example:
        success: false
        message: Project not found
    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.