---
title: "Create a voiceover | GPT-Video API reference"
description: "Starts reading a script aloud with a synthetic voice, and returns the tool run that makes the audio file."
url: "https://www.gpt-video.com/developers/api/create-voiceover"
operation: "create_voiceover"
method: "POST"
path: "/v1/voiceovers"
scope: "spend"
---

Do not assume endpoints have parameters other than the ones explicitly specified.

# Create a voiceover

Operation id `create_voiceover`. Request `POST /v1/voiceovers`.

Starts reading a script aloud with a synthetic voice, and returns the tool run that makes the audio file.

Beta. This endpoint is supported and can gain new fields. If a change would break your code, we tell you before it takes effect and list it in the changelog.

## About

Call this to turn a script into an MP3 file read by a synthetic voice. Choose the voice with list_voices (`GET /v1/reference/voices`): it must speak the language you send. The call answers 202 at once with the run, queued, and a Location to follow; read it with get_tool_run (`GET /v1/tool-runs/{toolRunId}`) (with ?wait= to hold the request) until it is done, then download the file from its downloadUrl. The file is also an asset of the account, with origin voice. Always send an Idempotency-Key, the same one on every retry: a retry of the same request returns the same run and never uses a second tool run.

## Example use

A team that makes explainer videos sends a script and a voice chosen with list_voices (`GET /v1/reference/voices`). It follows the run with get_tool_run (`GET /v1/tool-runs/{toolRunId}`) and uses the MP3 as the sound of its next video.

## Endpoint

```text
POST https://api.gpt-video.com/v1/voiceovers
```

- Scope `spend`.
- Rate limit 10 requests a minute for each key and 20 for each account, shared by every `spend` call. See [rate limits](/developers/rate-limits.md).
- `Idempotency-Key` is required.
- It starts work that runs on its own. The answer comes back before the work is done.

## Parameters

### Headers

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | string | Required | This operation spends, so the key is required. Create it once, before the request, and send the same key on every retry. From 1 to 255 visible characters, such as a UUID. See [idempotency](/developers/pagination-and-idempotency.md). |

### Body

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `script` | string | Required | The text to read aloud, up to 5000 characters. From 1 to 5000 characters. |
| `voice` | string | Required | The voice that reads the script, as list_voices (`GET /v1/reference/voices`) names it, like en-aria. It must speak the language of the script. One of `en-aria`, `en-jenny`, `en-guy`, `en-christopher`, `it-elsa`, `it-isabella`, `it-diego`, `it-giuseppe`. |
| `language` | string | Required | The language of the script: en for English, it for Italian. One of `en`, `it`. |
| `title` | string | Optional | A name for the run and its file. Leave it out to use the start of the script. From 1 to 120 characters. |

Send only the fields listed here. Any other field is refused with UNKNOWN_PARAMETER.

## Request

Read a script with an English voice.

```bash title="curl"
# Create the key once. Send this same key again if you retry this request.
IDEMPOTENCY_KEY=$(uuidgen)
curl -X POST "https://api.gpt-video.com/v1/voiceovers" \
  -H "Authorization: Bearer $GPT_VIDEO_API_KEY" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "script": "Meet the editor that turns one long video into ten short clips.",
    "voice": "en-aria",
    "language": "en",
    "title": "Launch voiceover"
  }'
```

```ts title="TypeScript"
// Create the key once. Send this same key again if you retry this request.
const idempotencyKey = crypto.randomUUID();
const res = await fetch("https://api.gpt-video.com/v1/voiceovers", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.GPT_VIDEO_API_KEY}`,
    "Idempotency-Key": idempotencyKey,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "script": "Meet the editor that turns one long video into ten short clips.",
    "voice": "en-aria",
    "language": "en",
    "title": "Launch voiceover"
  }),
});
const data = await res.json();
console.log(data);
```

```python title="Python"
import os
import uuid

import requests

# Create the key once. Send this same key again if you retry this request.
idempotency_key = str(uuid.uuid4())
r = requests.post(
    "https://api.gpt-video.com/v1/voiceovers",
    headers={
        "Authorization": f"Bearer {os.environ['GPT_VIDEO_API_KEY']}",
        "Idempotency-Key": idempotency_key,
    },
    json={
        "script": "Meet the editor that turns one long video into ten short clips.",
        "voice": "en-aria",
        "language": "en",
        "title": "Launch voiceover"
    },
)
data = r.json()
print(data)
```

```json title="MCP (Full)"
{
  "method": "tools/call",
  "params": {
    "name": "create_voiceover",
    "arguments": {
      "script": "Meet the editor that turns one long video into ten short clips.",
      "voice": "en-aria",
      "language": "en",
      "title": "Launch voiceover",
      "confirm": true,
      "idempotencyKey": "REPLACE-WITH-A-NEW-UUID-FOR-THIS-CALL"
    }
  }
}
```

## Response

A successful call answers HTTP 202. Returns a [Tool run object](/developers/objects.md#tool-run) in `toolRun`.

### Fields to check

| Name | Type | Description |
| --- | --- | --- |
| `toolRun.id` | string | The tool run id. It starts with run_. |
| `toolRun.status` | string | queued and running are still working; done and failed are final. |
| `toolRun.file.downloadUrl` | string or null | A link to download the file, on a read of a finished run; null otherwise. |
| `toolRun.file.downloadUrlExpiresAt` | string or null | An ISO 8601 time in UTC. |
| `toolRun.jobId` | string or null | The job that carries out the run, to follow with get_job (`GET /v1/jobs/{jobId}`); null once it is gone. |
| `paidWith` | string or null | Who paid for this request: plan is the credits or the daily allowance of the account's plan, api_bonus is the API bonus. null when this request paid nothing, because the same work was already running. A retry with the same Idempotency-Key says who paid the first time. |

### Response example

- `Location: https://api.gpt-video.com/v1/tool-runs/run_5e8a1c3f7d2b4096a1e4`

```json title="HTTP 202"
{
  "toolRun": {
    "id": "run_5e8a1c3f7d2b4096a1e4",
    "tool": "voiceover",
    "status": "queued",
    "progress": 0,
    "errorMessage": null,
    "file": null,
    "ideas": null,
    "jobId": "job_2c7f9e1a4d6b48035f9e",
    "createdAt": "2026-09-01T11:00:00.000Z",
    "finishedAt": null,
    "voiceover": {
      "title": "Launch voiceover",
      "script": "Meet the editor that turns one long video into ten short clips.",
      "voice": "en-aria",
      "language": "en"
    },
    "separation": null,
    "brainstorm": null
  },
  "paidWith": "plan"
}
```

**All response fields**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `toolRun` | Tool run object | Required | Described in [Tool run object](/developers/objects.md#tool-run). |
| `paidWith` | string or null | Required | Who paid for this request: plan is the credits or the daily allowance of the account's plan, api_bonus is the API bonus. null when this request paid nothing, because the same work was already running. A retry with the same Idempotency-Key says who paid the first time. One of `plan`, `api_bonus`. |

**More examples**

#### A voice that does not speak the language of the script

`POST /v1/voiceovers`

```json title="Request body"
{
  "script": "Meet the editor that turns one long video into ten short clips.",
  "voice": "it-elsa",
  "language": "en"
}
```

```json title="HTTP 400"
{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "Voice it-elsa is not available in English. Call list_voices to see the voices you can use.",
    "hint": "Fix the request as the message says, then send it again.",
    "requestId": "req_3e9b1d7a5c2f4e8a0d6b9c13"
  }
}
```

#### The tool runs of the day are used up

`POST /v1/voiceovers`

```json title="Request body"
{
  "script": "Meet the editor that turns one long video into ten short clips.",
  "voice": "en-aria",
  "language": "en"
}
```

```json title="HTTP 429"
{
  "error": {
    "code": "DAILY_LIMIT_REACHED",
    "message": "Daily limit of tool runs reached (100). It resets at 00:00 UTC. Upgrade your plan for a higher limit.",
    "hint": "The limit resets at 00:00 UTC. A higher plan raises it: https://www.gpt-video.com/app/billing.",
    "requestId": "req_6a2d9f1c4e8b3a7d0f5c2e94"
  }
}
```

#### Voices through the API are off

`POST /v1/voiceovers`

```json title="Request body"
{
  "script": "Meet the editor that turns one long video into ten short clips.",
  "voice": "en-aria",
  "language": "en"
}
```

- `Retry-After: 60`

```json title="HTTP 503"
{
  "error": {
    "code": "UNAVAILABLE",
    "message": "Making voices through the API is not available right now.",
    "hint": "Retry later.",
    "requestId": "req_9a3d7f1c5e8b2a6d0c4f7e13"
  }
}
```

## Errors

| Code | Status | When | What to do |
| --- | --- | --- | --- |
| [`INSUFFICIENT_CREDITS`](/developers/errors.md#INSUFFICIENT_CREDITS) | 402 | See the error reference. | Stop and tell the user. |
| [`PLAN_REQUIRED`](/developers/errors.md#PLAN_REQUIRED) | 402 | See the error reference. | Stop and tell the user. |
| [`DAILY_LIMIT_REACHED`](/developers/errors.md#DAILY_LIMIT_REACHED) | 429 | The tool runs of the day are used up | Stop until the limit resets at 00:00 UTC. |

Every call can also answer these errors. [`VALIDATION_FAILED`](/developers/errors.md#VALIDATION_FAILED), [`UNKNOWN_PARAMETER`](/developers/errors.md#UNKNOWN_PARAMETER), [`IDEMPOTENCY_KEY_REQUIRED`](/developers/errors.md#IDEMPOTENCY_KEY_REQUIRED), [`UNAUTHORIZED`](/developers/errors.md#UNAUTHORIZED), [`SCOPE_MISSING`](/developers/errors.md#SCOPE_MISSING), [`IDEMPOTENCY_IN_PROGRESS`](/developers/errors.md#IDEMPOTENCY_IN_PROGRESS), [`PAYLOAD_TOO_LARGE`](/developers/errors.md#PAYLOAD_TOO_LARGE), [`IDEMPOTENCY_KEY_REUSED`](/developers/errors.md#IDEMPOTENCY_KEY_REUSED), [`RATE_LIMITED`](/developers/errors.md#RATE_LIMITED), [`INTERNAL`](/developers/errors.md#INTERNAL), [`UNAVAILABLE`](/developers/errors.md#UNAVAILABLE).

## Cost

Uses one tool run of the plan's daily allowance, which resets at 00:00 UTC. When the API bonus pays, it also uses one bonus credit. Nothing is returned if the run fails.

- Paid by the API bonus, it uses 1 bonus credit.
- When the API bonus pays, an account can run 10 tools a day.
- See [API pricing](/developers/pricing.md) for who pays first.

## Related operations

- [`get_tool_run`](/developers/api/get-tool-run.md) (`GET /v1/tool-runs/{toolRunId}`) Get a tool run
- [`list_tool_runs`](/developers/api/list-tool-runs.md) (`GET /v1/tool-runs`) List the tool runs
- [`list_voices`](/developers/api/list-voices.md) (`GET /v1/reference/voices`) List the voices
- [`get_asset`](/developers/api/get-asset.md) (`GET /v1/assets/{assetId}`) Get an asset

## Questions

### What happens if my request times out and I send it again?

Send it again with the same Idempotency-Key. You get the same run back and no second tool run is used from the daily allowance. A new key starts a new run.

### Which voices can I use?

The ones list_voices (`GET /v1/reference/voices`) returns for the language of the script. A voice of another language answers VALIDATION_FAILED, and nothing is used.

### Where is the audio file?

Once the run is done, get_tool_run (`GET /v1/tool-runs/{toolRunId}`) gives its file with a downloadUrl. The file is also an asset of the account, with origin voice, so get_asset (`GET /v1/assets/{assetId}`) and list_assets (`GET /v1/assets`) show it too.

### What does UNAVAILABLE mean here?

Making voices through the API is off for now. Nothing is used: wait the seconds in Retry-After and send the same request again later.
