---
title: "Make the voices of a fake text story | GPT-Video API"
description: "Starts making the voice of every line of the story that does not have it yet, and returns the job to follow."
url: "https://www.gpt-video.com/developers/api/create-fake-text-voices"
operation: "create_fake_text_voices"
method: "POST"
path: "/v1/projects/{projectId}/story/voices"
scope: "spend"
---

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

# Make the voices of a fake text story

Operation id `create_fake_text_voices`. Request `POST /v1/projects/{projectId}/story/voices`.

Starts making the voice of every line of the story that does not have it yet, and returns the job to follow.

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 it once the story is as you want it and voiceover is on. Only lines without their voice are voiced: new lines, lines whose words changed, and the lines of a side whose voice changed. The answer is 202 with the job: follow it with get_job (`GET /v1/jobs/{jobId}`), then export the video with create_export (`POST /v1/compositions/{compositionId}/exports`). If voices are already being made for the story, you get that job back and nothing is used (reused is true). A story whose lines all have their voice is refused before anything is used.

## Example use

After the story is final, a script asks for the voice of every line that does not have one yet. It follows the job with get_job (`GET /v1/jobs/{jobId}`) and calls create_export (`POST /v1/compositions/{compositionId}/exports`) when it is done.

## Endpoint

```text
POST https://api.gpt-video.com/v1/projects/{projectId}/story/voices
```

- 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

### Path

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `projectId` | string | Required | The project id. It starts with prj_. Matches `^prj_[0-9a-f]{20}$`. |

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

## Request

Make the voices of the story.

```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/projects/prj_ba8b07a88bfb14585459/story/voices" \
  -H "Authorization: Bearer $GPT_VIDEO_API_KEY" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY"
```

```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/projects/prj_ba8b07a88bfb14585459/story/voices", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.GPT_VIDEO_API_KEY}`,
    "Idempotency-Key": idempotencyKey,
  },
});
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/projects/prj_ba8b07a88bfb14585459/story/voices",
    headers={
        "Authorization": f"Bearer {os.environ['GPT_VIDEO_API_KEY']}",
        "Idempotency-Key": idempotency_key,
    },
)
data = r.json()
print(data)
```

```json title="MCP (Full)"
{
  "method": "tools/call",
  "params": {
    "name": "create_fake_text_voices",
    "arguments": {
      "projectId": "prj_ba8b07a88bfb14585459",
      "confirm": true,
      "idempotencyKey": "REPLACE-WITH-A-NEW-UUID-FOR-THIS-CALL"
    }
  }
}
```

## Response

A successful call answers HTTP 202. Returns a [Job object](/developers/objects.md#job) in `job`.

### Fields to check

| Name | Type | Description |
| --- | --- | --- |
| `job.id` | string | The job id. It starts with job_. |
| `job.status` | string | queued and running are still working; done, failed and cancelled are final. |
| `reused` | boolean | true when making the voices was already running for this project and this is its job, paid by nobody again. |
| `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/jobs/job_7d2c9a4e1f0b4386c5a2`

```json title="HTTP 202"
{
  "job": {
    "status": "queued",
    "progress": 0,
    "errorMessage": null,
    "projectId": "prj_ba8b07a88bfb14585459",
    "compositionId": null,
    "exportId": null,
    "assetId": null,
    "toolRunId": null,
    "turnId": null,
    "createdAt": "2026-09-01T11:10:00.000Z",
    "startedAt": null,
    "finishedAt": null,
    "id": "job_7d2c9a4e1f0b4386c5a2",
    "kind": "voices"
  },
  "reused": false,
  "paidWith": "plan"
}
```

**All response fields**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `job` | Job object | Required | Described in [Job object](/developers/objects.md#job). |
| `reused` | boolean | Required | true when making the voices was already running for this project and this is its job, paid by nobody again. |
| `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**

#### Voices that are already being made

`POST /v1/projects/prj_ba8b07a88bfb14585459/story/voices`

```json title="HTTP 202"
{
  "job": {
    "status": "running",
    "progress": 0.5,
    "errorMessage": null,
    "projectId": "prj_ba8b07a88bfb14585459",
    "compositionId": null,
    "exportId": null,
    "assetId": null,
    "toolRunId": null,
    "turnId": null,
    "createdAt": "2026-08-31T22:12:00.000Z",
    "startedAt": "2026-08-31T22:13:00.000Z",
    "finishedAt": null,
    "id": "job_8060ba8cd16645d280c1",
    "kind": "voices"
  },
  "reused": true,
  "paidWith": null
}
```

#### Every line already has its voice

`POST /v1/projects/prj_ba8b07a88bfb14585459/story/voices`

```json title="HTTP 409"
{
  "error": {
    "code": "CONFLICT",
    "message": "Every line of this story already has its voice.",
    "requestId": "req_1e6b9d3a0c7f48252d8b4f0e"
  }
}
```

#### A project with no story yet

`POST /v1/projects/prj_ba8b07a88bfb14585459/story/voices`

```json title="HTTP 409"
{
  "error": {
    "code": "CONFLICT",
    "message": "This project has no story yet. Write one with update_fake_text_story, or let GPT-Video write one with generate_fake_text_story.",
    "requestId": "req_4a7c2e9f1b5d40636f0a3c8e"
  }
}
```

#### Voices are turned off

`POST /v1/projects/prj_ba8b07a88bfb14585459/story/voices`

```json title="HTTP 409"
{
  "error": {
    "code": "CONFLICT",
    "message": "Voices are turned off for this project. Turn them on with update_fake_text_project.",
    "requestId": "req_8d3f6b1a4e9c27050a5d8e2c"
  }
}
```

#### A voice that is not available through the API

`POST /v1/projects/prj_ba8b07a88bfb14585459/story/voices`

```json title="HTTP 409"
{
  "error": {
    "code": "CONFLICT",
    "message": "This project uses a voice that is not available through the API. Choose one with update_fake_text_story.",
    "requestId": "req_2b9e5c8d0a3f46171e4c9a6d"
  }
}
```

#### The fake text scripts of the day are used up

`POST /v1/projects/prj_ba8b07a88bfb14585459/story/voices`

```json title="HTTP 429"
{
  "error": {
    "code": "DAILY_LIMIT_REACHED",
    "message": "Daily limit of fake text scripts reached (100). It resets at 00:00 UTC. Upgrade your plan for a higher limit.",
    "requestId": "req_6f2a0d7c3e9b41588b1f6d3a"
  }
}
```

## 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. |
| [`NOT_FOUND`](/developers/errors.md#NOT_FOUND) | 404 | See the error reference. | Change the request, then send it again. |
| [`CONFLICT`](/developers/errors.md#CONFLICT) | 409 | Every line already has its voice | Read the resource again, then decide. Do not send the same request again. |
| [`DAILY_LIMIT_REACHED`](/developers/errors.md#DAILY_LIMIT_REACHED) | 429 | The fake text scripts 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 fake text script 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 work fails.

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

## Related operations

- [`get_job`](/developers/api/get-job.md) (`GET /v1/jobs/{jobId}`) Get a job
- [`get_fake_text_story`](/developers/api/get-fake-text-story.md) (`GET /v1/projects/{projectId}/story`) Get the story of a fake text project
- [`update_fake_text_story`](/developers/api/update-fake-text-story.md) (`PUT /v1/projects/{projectId}/story`) Write the story of a fake text project
- [`create_export`](/developers/api/create-export.md) (`POST /v1/compositions/{compositionId}/exports`) Export a composition
- [`generate_fake_text_story`](/developers/api/generate-fake-text-story.md) (`POST /v1/projects/{projectId}/story/generate`) Write a fake text story with GPT-Video

## Questions

### Which lines get a voice?

Lines that do not have their voice yet: new lines, lines whose words changed, and the lines of a side whose voice changed. Lines that already have their voice are not made again.

### Is the script returned if making the voices fails?

No. Ask for the voices again: lines that got their voice before the failure keep it, and a retry with the same Idempotency-Key never uses a second script.
