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

# PDF to Video

> Turn a report, paper or guide in PDF into a narrated video

## Overview

PDF to Video extracts the text of a PDF, turns it into a narration split into scenes, reads it with a voice and puts a visual under each scene. It works like [Article to Video](/api-reference/video/article-to-video), with a PDF instead of a web page.

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

* `pdfUrl`: a public `https://` link to the file, up to **20 MB**. Hooked downloads it itself: a private or local address (or a redirect to one), a file over 20 MB or anything that is not a PDF answers `400`.
* `pdfBase64`: the file itself, base64-encoded, up to **10 MB** once decoded (a `data:application/pdf;base64,` prefix is accepted). Add `pdfFileName` to name the project after the file.

Not both. The PDF needs a **text layer**: a scanned or image-only PDF (or one with a password) answers `400` before anything is charged. Run it through OCR first.

**The narration.** `summaryType` is `summarize` (default: a short summary, steered by `customPrompt`), `summarize_long` (a detailed narration) or `key_as_is` (the document as close to the original as possible; the video is as long as it needs). With the first two, `targetDuration` sets the length (default 30 seconds). The narration is written in the document's own language.

**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. For gameplay only, see [PDF to Brainrot](/api-reference/video/pdf-to-brainrot).

<Note>
  The request answers in a few seconds with a `projectId` and status `processing`: the PDF is downloaded and its text checked in the request, and splitting it into scenes and making the video happen after that. What can be refused up front (an invalid body, a link that is not a PDF, a PDF without a text layer, missing keys, too few credits) is still answered with a 400 or 402 and nothing is created. If the scenes cannot be written later, the project becomes `failed` with the reason in `message`, the credits come back and your `webhook` gets the failure.
</Note>

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

***

<RequestExample>
  ```bash cURL theme={null}
  curl -X POST "https://api.hooked.so/v1/project/create/pdf-to-video" \
    -H "x-api-key: your_api_key_here" \
    -H "Content-Type: application/json" \
    -d '{
      "pdfUrl": "https://example.com/reports/annual-report-2025.pdf",
      "voiceId": "1004",
      "summaryType": "summarize",
      "targetDuration": 60,
      "customPrompt": "Focus on the three biggest numbers",
      "mediaType": "ai-images",
      "presetSettings": { "preset": "cinematic" },
      "aspectRatio": "ratio_9_16",
      "caption": { "preset": "tiktok" },
      "webhook": "https://example.com/hooked/webhook?token=YOUR_SECRET",
      "metadata": { "reportId": "2025-annual" }
    }'
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch("https://api.hooked.so/v1/project/create/pdf-to-video", {
    method: "POST",
    headers: {
      "x-api-key": "your_api_key_here",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      pdfUrl: "https://example.com/reports/annual-report-2025.pdf",
      voiceId: "1004",
      summaryType: "summarize",
      targetDuration: 60,
      customPrompt: "Focus on the three biggest numbers",
      mediaType: "ai-images",
      presetSettings: { preset: "cinematic" },
      aspectRatio: "ratio_9_16",
      caption: { preset: "tiktok" },
      webhook: "https://example.com/hooked/webhook?token=YOUR_SECRET",
      metadata: { reportId: "2025-annual" },
    }),
  });

  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/pdf-to-video",
      headers={"x-api-key": "your_api_key_here"},
      json={
          "pdfUrl": "https://example.com/reports/annual-report-2025.pdf",
          "voiceId": "1004",
          "summaryType": "summarize",
          "targetDuration": 60,
          "customPrompt": "Focus on the three biggest numbers",
          "mediaType": "ai-images",
          "presetSettings": {"preset": "cinematic"},
          "aspectRatio": "ratio_9_16",
          "caption": {"preset": "tiktok"},
          "webhook": "https://example.com/hooked/webhook?token=YOUR_SECRET",
          "metadata": {"reportId": "2025-annual"},
      },
  )

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

### Sending the file itself

For a PDF that is not online (up to 10 MB), send it base64-encoded:

<CodeGroup>
  ```bash cURL theme={null}
  # The JSON body is written to a file: a PDF is too long for the command line
  printf '{"pdfBase64":"%s","pdfFileName":"handbook.pdf","voiceId":"1004","summaryType":"key_as_is"}' \
    "$(base64 -w0 handbook.pdf)" > body.json

  curl -X POST "https://api.hooked.so/v1/project/create/pdf-to-video" \
    -H "x-api-key: your_api_key_here" \
    -H "Content-Type: application/json" \
    --data-binary @body.json
  ```

  ```javascript Node.js theme={null}
  import { readFileSync } from "node:fs";

  const body = {
    pdfBase64: readFileSync("handbook.pdf").toString("base64"),
    pdfFileName: "handbook.pdf",
    voiceId: "1004",
    summaryType: "key_as_is",
  };

  const response = await fetch("https://api.hooked.so/v1/project/create/pdf-to-video", {
    method: "POST",
    headers: { "x-api-key": "your_api_key_here", "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
  ```

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

  with open("handbook.pdf", "rb") as f:
      pdf_base64 = base64.b64encode(f.read()).decode()

  body = {"pdfBase64": pdf_base64, "pdfFileName": "handbook.pdf", "voiceId": "1004", "summaryType": "key_as_is"}

  response = requests.post(
      "https://api.hooked.so/v1/project/create/pdf-to-video",
      headers={"x-api-key": "your_api_key_here"},
      json=body,
  )
  ```
</CodeGroup>

### More request bodies

<CodeGroup>
  ```json AI video clips theme={null}
  {
    "pdfUrl": "https://example.com/papers/ocean-currents.pdf",
    "voiceId": "1004",
    "summaryType": "summarize_long",
    "targetDuration": 90,
    "mediaType": "ai-videos",
    "presetSettings": { "preset": "realistic", "quality": "pro" }
  }
  ```

  ```json Your own media theme={null}
  {
    "pdfUrl": "https://example.com/catalog/spring-2026.pdf",
    "voiceId": "1004",
    "mediaType": "media",
    "media": ["cm4x9k2p10001ab12cd34ef56", "cm4x9k2p10002ab12cd34ef56"],
    "aspectRatio": "ratio_1_1"
  }
  ```
</CodeGroup>

***

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

  ```json 400 theme={null}
  {
    "success": false,
    "message": "pdf: The PDF has no readable text. Scanned or image-only PDFs are not supported: export it with a text layer (OCR) first."
  }
  ```

  ```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, ElevenLabs. Add them in Settings → AI keys.",
    "errorCode": "MISSING_CREDENTIALS",
    "missingProviders": ["openrouter", "elevenlabs"],
    "details": {}
  }
  ```

  ```json 402 credits theme={null}
  {
    "success": false,
    "errorCode": "INSUFFICIENT_CREDITS",
    "message": "Not enough credits. Needed: 80, Available: 45",
    "creditsNeeded": 80,
    "creditsAvailable": 45
  }
  ```

  ```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 starts as `processing` while the text is split into scenes; if that 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 PDF itself is not kept: only its text, turned into scenes, and the `pdfUrl` it came from. See [Webhooks](/guides/webhooks) for every payload.

***

## Credits and your own keys

**Teams on Hooked's keys (managed):** the project is priced from `targetDuration`, the media type and the quality or model. The PDF is read first, so a file that cannot be read costs nothing; then the credits are taken. If the balance is short, the request answers `402 INSUFFICIENT_CREDITS` with `creditsNeeded` and `creditsAvailable` and nothing is created. If the project fails later, 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 (summary, images, video clips, captions).
* **ElevenLabs**: for the voiceover. Not needed in `speaking` mode (`ai-videos` with a talking preset), where the video model speaks.
* **Gemini**: when `presetSettings.aiModel` is `gemini_omni_flash`.

No Firecrawl key: Hooked downloads and reads the PDF itself. 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 PDF: `pdfUrl: Required: the https URL of the PDF (or the file itself in pdfBase64)`, `pdfUrl: Send either pdfUrl or pdfBase64, not both`, `pdfUrl: Must be an https URL`, `pdfUrl: Must be a public https URL (…)` (a private or local address, or a redirect to one), `pdfUrl: The PDF is larger than 20 MB`, `pdfUrl: The PDF could not be downloaded (HTTP 404)`, `pdfUrl: The file at this URL is not a PDF`, `pdfBase64: The PDF is larger than 10 MB …`, `pdfBase64: The file is not a PDF`, `pdf: The PDF has no readable text …` (scanned), `pdf: The PDF is password-protected …`. The rest as in [Article to Video](/api-reference/video/article-to-video#errors): `summaryType`, `customPrompt`, `voiceId`, `musicId`, `mediaType`, `media`, `presetSettings`, `gameplaySettings`, `caption`, `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. |
| `500` | `{ "success": false, "message": "Failed to create PDF to Video project: …" }` | The text could not be summarised, or 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="PDF to Video example" icon="book" href="/examples/pdf-to-video">
    More requests for this format
  </Card>

  <Card title="PDF to Brainrot" icon="gamepad" href="/api-reference/video/pdf-to-brainrot">
    The same over gameplay
  </Card>

  <Card title="Article to Video" icon="newspaper" href="/api-reference/video/article-to-video">
    From a web page instead
  </Card>
</CardGroup>


## OpenAPI

````yaml POST /v1/project/create/pdf-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/pdf-to-video:
    post:
      tags:
        - Videos
      summary: Create PDF to Video
      description: >-
        Turns a PDF into a narrated video: the text is extracted, summarised (or
        kept as is) into scenes, voiced and illustrated. Send the PDF as a link
        (`pdfUrl`) or inline (`pdfBase64`). The PDF needs a text layer: a
        scanned or image-only PDF, or one with a password, answers 400 before
        anything is charged. Answers in a few seconds with a `projectId` and
        status `processing`: the PDF is downloaded and its text checked in the
        request, and the scenes are written after the response, then the video
        is generated and rendered. If the scenes cannot be written, the project
        becomes `failed` with the reason in `message`, the credits are refunded
        and the failure webhook is sent. BYOK: needs OpenRouter and ElevenLabs
        (no ElevenLabs for a speaking cast); `aiModel: "gemini_omni_flash"` also
        needs Gemini. No Firecrawl key: the PDF is read by Hooked.


        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: createPdfToVideo
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - voiceId
              properties:
                pdfUrl:
                  type: string
                  format: uri
                  maxLength: 2000
                  description: >-
                    A public https link to the PDF (up to 20 MB). Hooked
                    downloads it: private or local addresses, redirects to them,
                    files over 20 MB and anything that is not a PDF answer 400.
                    Send this or `pdfBase64`, not both.
                pdfBase64:
                  type: string
                  description: >-
                    The PDF file itself, base64-encoded (up to 10 MB once
                    decoded; a `data:application/pdf;base64,` prefix is
                    accepted). For bigger files use `pdfUrl`. Send this or
                    `pdfUrl`, not both.
                pdfFileName:
                  type: string
                  maxLength: 500
                  description: >-
                    The file name, used to name the project. Taken from `pdfUrl`
                    when omitted.
                voiceId:
                  type: string
                  minLength: 1
                  maxLength: 30
                  description: >-
                    The narrator: a library voice from /v1/voice/list or one of
                    your team's custom voices. An ID that is neither answers 400
                    (`voiceId: Voice "<id>" not found.`).
                summaryType:
                  type: string
                  enum:
                    - summarize
                    - summarize_long
                    - key_as_is
                  default: summarize
                  description: >-
                    How the text becomes the narration:

                    - `summarize`: a short summary of the key points, about
                    `targetDuration` seconds long.

                    - `summarize_long`: a detailed narration with supporting
                    details, about `targetDuration` seconds long.

                    - `key_as_is`: the text as close to the original as
                    possible, adapted for speech. The video is as long as the
                    text needs; `targetDuration` is ignored.
                targetDuration:
                  type: integer
                  minimum: 10
                  maximum: 600
                  default: 30
                  description: >-
                    Approximate length of the video in seconds (10-600). Not
                    used with `key_as_is`. It also sets the price of the
                    project.
                customPrompt:
                  type: string
                  maxLength: 2000
                  description: >-
                    `summarize` only: extra instructions for the summary, e.g.
                    the angle or the audience (max 2,000 characters). Sent with
                    another `summaryType`, it answers 400.
                mediaType:
                  type: string
                  enum:
                    - ai-images
                    - ai-videos
                    - media
                    - gameplay
                  description: >-
                    Where the visuals come from:

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

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

                    - `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: true
                      description: >-
                        `ai-videos` only: chain the clips frame to frame (each
                        clip starts on the last frame of the one before).
                    characterMode:
                      type: string
                      enum:
                        - narrator
                        - speaking
                      default: narrator
                      description: >-
                        `ai-videos` only. `narrator`: a voiceover over the
                        visuals. `speaking`: the characters say the lines
                        themselves with the video model's own audio (Veo 3 Fast,
                        unless `aiModel` is another model that makes sound).
                        `speaking` needs `mediaType: "ai-videos"` and the
                        `talking-fruit`, `talking-objects` or `talking-organs`
                        preset; any other `speaking` request answers 400. A
                        speaking cast has no narrator, captions or stickers, so
                        `audio`, `caption` and `addStickers` do not apply, and
                        BYOK teams need no ElevenLabs key.
                    voiceProfile:
                      type: string
                      description: >-
                        Only with `characterMode: "speaking"`: a short
                        description of how the characters sound.
                    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.
                    useCharacterReferenceImage:
                      type: boolean
                      default: false
                      description: >-
                        `ai-images` with `characterIds` only: generate from the
                        saved characters' reference images.
                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 source
                    when omitted.
                musicId:
                  type: string
                  maxLength: 30
                  description: >-
                    Background music: a track from /v1/music/list or your team's
                    own uploaded music. An ID that is neither answers 400
                    (`musicId: Music "<id>" not found.`).
                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
                  description: >-
                    Two-letter ISO 639-1 code of the narration. The narration is
                    written in the document's own language, detected when
                    omitted; this does not translate it.
                caption:
                  type: object
                  description: >-
                    Burned-in captions of the narration. Not used with
                    `motion-graphics` (it typesets its own copy) or a speaking
                    cast.
                  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.'
                audio:
                  type: object
                  description: >-
                    Narrator voice settings (ElevenLabs). Any field you leave
                    out keeps its default. Not used by a speaking cast.
                  properties:
                    speed:
                      type: number
                      minimum: 0.7
                      maximum: 1.2
                      default: 1
                      description: Speaking speed.
                    stability:
                      type: number
                      minimum: 0
                      maximum: 1
                      default: 0.5
                      description: Higher is steadier, lower is more expressive.
                    similarityBoost:
                      type: number
                      minimum: 0
                      maximum: 1
                      default: 0.75
                      description: How closely the voice sticks to the original.
                addStickers:
                  type: boolean
                  default: false
                  description: >-
                    Add emoji/GIF stickers anchored to the captions. Not used
                    with `motion-graphics` or a speaking cast.
                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.
            example:
              pdfUrl: https://example.com/reports/annual-report-2025.pdf
              voiceId: '1'
              summaryType: summarize
              targetDuration: 60
              mediaType: ai-images
              presetSettings:
                preset: cinematic
              aspectRatio: ratio_9_16
              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: PDF 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.