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

# Clone a Voice

> Clone a voice from a recording and use it in any narrated video

## Overview

Creates a voice of your own from a recording, the same way **Voice cloning** works in the dashboard. Send a public `https` link to the sample in `audioUrl`. The answer is the new voice: pass its `id` as `voiceId` in any create endpoint, in [Generate Speech](/api-reference/voice/speech) or in [Speech to Speech](/api-reference/voice/speech-to-speech). It also shows up in [List Voices](/api-reference/voice/list) with `isCustom: true`.

<Info>
  **The sample:** MP3, WAV, M4A, AAC, OGG, FLAC or WEBM, **5 to 90 seconds**, up to **10 MB**. Use one speaker and as little background noise as you can (what is left is removed). The type is read from the file itself, not from the URL.
</Info>

What happens in the request (a few seconds):

1. The sample is downloaded. Local, private-network and plain `http` addresses are refused with `400`, and every redirect is checked the same way.
2. The voice is cloned and saved to your team, with the language, gender, age, use case and accent you give (they label the voice in the dashboard; `language` also sets the accents you can pick).
3. The voice records a short introduction in its own language. That sample becomes its `templateUrl`.

Cloning is free, but it uses one of your plan's **custom voices** (cloned and designed voices share the quota; Pro includes 1). [Delete a Voice](/api-reference/voice/delete) frees one. A managed team needs a live plan.

<Warning>
  `consent` must be `true`. By sending it you confirm you have the rights to upload and clone this voice, as the dashboard asks before cloning.
</Warning>

<Note>
  **BYOK teams** clone in their own ElevenLabs account, not here (`403`, `code: "byok"`). Voices in that account appear in [List Voices](/api-reference/voice/list) and can be used as `voiceId` right away.
</Note>

<RequestExample>
  ```bash cURL theme={null}
  curl -X POST "https://api.hooked.so/v1/voice/clone" \
    -H "x-api-key: your_api_key_here" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: clone-founder-voice-1" \
    -d '{
      "name": "Founder voice",
      "audioUrl": "https://cdn.example.com/voices/founder-sample.mp3",
      "consent": true,
      "language": "Spanish",
      "gender": "female",
      "accent": "Castilian",
      "description": "Warm, calm narration"
    }'
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch("https://api.hooked.so/v1/voice/clone", {
    method: "POST",
    headers: {
      "x-api-key": process.env.HOOKED_API_KEY,
      "Content-Type": "application/json",
      "Idempotency-Key": "clone-founder-voice-1",
    },
    body: JSON.stringify({
      name: "Founder voice",
      audioUrl: "https://cdn.example.com/voices/founder-sample.mp3",
      consent: true,
      language: "Spanish",
      gender: "female",
    }),
  });
  const { data: voice } = await response.json();
  // Use voice.id as voiceId in any create endpoint
  console.log(voice.id, voice.name);
  ```

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

  response = requests.post(
      "https://api.hooked.so/v1/voice/clone",
      headers={"x-api-key": os.environ["HOOKED_API_KEY"], "Idempotency-Key": "clone-founder-voice-1"},
      json={
          "name": "Founder voice",
          "audioUrl": "https://cdn.example.com/voices/founder-sample.mp3",
          "consent": True,
          "language": "Spanish",
          "gender": "female",
      },
  )
  voice = response.json()["data"]
  print(voice["id"], voice["name"])
  ```
</RequestExample>

<ResponseExample>
  ```json 201 theme={null}
  {
    "success": true,
    "message": "Voice cloned",
    "data": {
      "id": "cm4xa1b2c0001ab12cd34ef56",
      "voiceId": "Xb7hH8MSUJpSbSDYk0k2",
      "name": "Founder voice",
      "gender": "female",
      "language": "Spanish",
      "accent": "Castilian",
      "country": "ES",
      "age": "middle_aged",
      "templateUrl": "team/public/voice/cm4xa1b2c0001ab12cd34ef56.mp3",
      "thumbnail": "/images/voice-default.png",
      "isCustom": true,
      "source": "custom",
      "ownerProvider": "managed",
      "description": "Warm, calm narration"
    }
  }
  ```

  ```json 403 theme={null}
  {
    "success": false,
    "code": "limit_reached",
    "message": "You've reached your custom voice limit (1). Upgrade your plan to create more voices.",
    "limit": 1,
    "currentCount": 1
  }
  ```
</ResponseExample>

## Errors

| Status | Message / code | Cause |
| - | - | - |
| 400 | `Invalid JSON body` | The body is not JSON. |
| 400 | `consent: Must be true: ...` | `consent` is missing or not `true`. |
| 400 | `name: Must be at least 2 characters` | `name` is shorter than 2 or longer than 100 characters. |
| 400 | `audioUrl: Must be a public https URL` | Not a URL, not `https`, or a local or private address. |
| 400 | `audioUrl: The file could not be downloaded (HTTP 404)` | The link does not answer with the file. |
| 400 | `audioUrl: Not an audio file (...)` | The link is a page, an image, a PDF... |
| 400 | `audioUrl: The file is over 10 MB` | The sample is too large. |
| 400 | `audioUrl: The audio is too short` / `too long` | Shorter than 5 or longer than 90 seconds. |
| 400 | `language: ...`, `accent: ...` | A value the dashboard does not offer; the message lists the valid ones. |
| 401 | `code: "not_authenticated"` | Missing or invalid `x-api-key`. |
| 402 | `code: "subscription_required"` | Managed team without a live plan. |
| 402 | `code: "missing_credentials"` | Your team's ElevenLabs key is missing. |
| 403 | `code: "byok"` | Your team uses its own ElevenLabs account: clone the voice there. |
| 403 | `code: "limit_reached"` | Your plan's custom voices are used up. |
| 500 | `Failed to clone the voice` | The voice provider refused the sample or failed. Nothing is charged. |

Send an `Idempotency-Key` header so a retry after a timeout answers with the first voice instead of cloning it twice.


## OpenAPI

````yaml POST /v1/voice/clone
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/voice/clone:
    post:
      tags: []
      summary: Clone a Voice
      description: >-
        Clones a voice from a recording, like Voice cloning in the dashboard.
        Send a public https link to the sample (`audioUrl`): MP3, WAV, M4A, AAC,
        OGG, FLAC or WEBM, 5 to 90 seconds, up to 10 MB, one speaker with little
        background noise. Hooked downloads it in the request (a private or local
        address, or a redirect to one, answers 400) and reads its type from the
        file. Background noise is removed, the voice is saved to your team and a
        short sample of it, in its language, becomes its `templateUrl`. The
        answer is the new voice: pass its `id` as `voiceId` anywhere (it is
        listed in List Voices with `isCustom: true`). Takes a few seconds.


        Free, but counted against your plan's custom voices (cloned and designed
        together; Pro: 1). It needs a live plan (402 `subscription_required`
        without one). A BYOK team clones in its own ElevenLabs account instead
        (403 `byok`): voices there are listed in List Voices. `consent: true` is
        required: you confirm you have the rights to clone this voice.


        A body that is not valid JSON answers 400 (`Invalid JSON body`).
      operationId: cloneVoice
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
                - audioUrl
                - consent
              properties:
                name:
                  type: string
                  minLength: 2
                  maxLength: 100
                  description: The voice's name in your library.
                audioUrl:
                  type: string
                  format: uri
                  maxLength: 2000
                  description: >-
                    Public https link to the sample file itself: MP3, WAV, M4A,
                    AAC, OGG, FLAC or WEBM, 5 to 90 seconds, up to 10 MB.
                consent:
                  type: boolean
                  enum:
                    - true
                  description: >-
                    Must be `true`: you confirm you have the necessary rights to
                    upload and clone this voice.
                description:
                  type: string
                  maxLength: 500
                  description: A note about the voice, returned in List Voices.
                language:
                  type: string
                  default: English
                  description: >-
                    The sample's language, a name such as `English`, `Spanish`
                    or `Portuguese` (case-insensitive; the dashboard's list of
                    55 languages).
                gender:
                  type: string
                  enum:
                    - male
                    - female
                  description: Case-insensitive. Stored as `Unknown` when omitted.
                age:
                  type: string
                  enum:
                    - young
                    - middle_aged
                    - old
                  description: Case-insensitive. Stored as `unknown` when omitted.
                useCase:
                  type: string
                  enum:
                    - narrative_story
                    - informative_educational
                    - conversational
                    - advertisement
                    - social_media
                    - entertainment_tv
                    - characters_animation
                    - general
                  default: general
                accent:
                  type: string
                  default: Standard
                  description: >-
                    One of the accents the dashboard offers for `language` (for
                    English: `Standard`, `Neutral`, `American`, `British`,
                    `Australian`, ...). Case-insensitive; another value answers
                    400 with the list.
            example:
              name: Founder voice
              audioUrl: https://cdn.example.com/voices/founder-sample.mp3
              consent: true
              language: Spanish
              gender: female
              accent: Castilian
              description: Warm, calm narration
      responses:
        '201':
          description: Voice cloned
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
                  data:
                    $ref: '#/components/schemas/Voice'
              example:
                success: true
                message: Voice cloned
                data:
                  id: cm4xa1b2c0001ab12cd34ef56
                  voiceId: Xb7hH8MSUJpSbSDYk0k2
                  name: Founder voice
                  gender: female
                  language: Spanish
                  accent: Castilian
                  country: ES
                  age: middle_aged
                  templateUrl: team/public/voice/cm4xa1b2c0001ab12cd34ef56.mp3
                  thumbnail: /images/voice-default.png
                  isCustom: true
                  source: custom
                  ownerProvider: managed
                  description: Warm, calm narration
        '400':
          description: Invalid body, voice or audio file
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError400'
              example:
                success: false
                message: 'audioUrl: The audio is too short (3.2 s, at least 5 s)'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          description: >-
            BYOK team, custom voice quota used up, or the team does not have the
            product
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/CustomVoiceDenied403'
                  - $ref: '#/components/schemas/EntitlementRequired403'
              example:
                success: false
                code: limit_reached
                message: >-
                  You've reached your custom voice limit (1). Upgrade your plan
                  to create more voices.
                limit: 1
                currentCount: 1
        '409':
          $ref: '#/components/responses/IdempotencyInProgress'
        '422':
          $ref: '#/components/responses/IdempotencyMismatch'
        '429':
          $ref: '#/components/responses/RateLimited429'
        '500':
          description: Internal server error (anything charged is given back)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error500'
              example:
                success: false
                message: Failed to clone the voice
      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:
    Voice:
      type: object
      properties:
        id:
          type: string
          description: >-
            Pass it as `voiceId`. For library voices it is the ElevenLabs voice
            ID; for your cloned voices it is a Hooked ID.
        voiceId:
          type: string
          description: The ElevenLabs voice ID. The same as `id` for library voices.
        name:
          type: string
          example: Jude
          description: Display name.
        gender:
          type: string
          example: Male
          description: '`Male`, `Female`, ... (`Unknown` when the voice is not labelled).'
        language:
          type: string
          example: English
          description: Language name.
        accent:
          type: string
          nullable: true
          example: British
          description: Accent label (`Standard` for library voices without one).
        country:
          type: string
          nullable: true
          example: GB
          description: Country code derived from the language.
        age:
          type: string
          example: young
          description: >-
            Age label (`unknown` for cloned voices and unlabelled library
            voices).
        templateUrl:
          type: string
          description: Preview audio URL.
        thumbnail:
          type: string
          nullable: true
          description: Thumbnail path.
        isCustom:
          type: boolean
          description: >-
            `true` for your team's own voices (cloned or designed). Delete one
            with DELETE /v1/voice/{voiceId}.
        source:
          type: string
          enum:
            - library
            - custom
          description: '`library` or `custom`.'
        ownerProvider:
          type: string
          enum:
            - managed
            - byok
          description: >-
            Cloned voices only: `managed` (cloned with Hooked's keys) or `byok`
            (cloned with your team's ElevenLabs key). The voice only works while
            the team is in that mode.
        unavailable:
          type: boolean
          description: >-
            Present, and `true`, when the voice cannot be used in the team's
            current mode.
        unavailableReason:
          type: string
          nullable: true
          description: Why the voice is unavailable. Present with `unavailable`.
        description:
          type: string
          description: Present when the voice has one.
      description: A narration voice.
    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'
    CustomVoiceDenied403:
      type: object
      description: >-
        Your team cannot create a custom voice: it uses its own ElevenLabs
        account (`byok`: clone the voice there and it appears in List Voices),
        or the plan's custom voice quota is used up (`limit_reached`).
      properties:
        success:
          type: boolean
          enum:
            - false
        code:
          type: string
          enum:
            - byok
            - limit_reached
        message:
          type: string
        limit:
          type: integer
          description: Custom voices your plan allows (0 on BYOK).
        currentCount:
          type: integer
          description: Custom voices your team has (cloned and designed).
      required:
        - success
        - code
        - message
      example:
        success: false
        code: limit_reached
        message: >-
          You've reached your custom voice limit (1). Upgrade your plan to
          create more voices.
        limit: 1
        currentCount: 1
    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
    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
  responses:
    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
    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
  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.