---
title: "Write the story of a fake text project | GPT-Video API"
description: "Replaces the story of a fake text project with the one you send, or writes its first story, and rebuilds its video."
url: "https://www.gpt-video.com/developers/api/update-fake-text-story"
operation: "update_fake_text_story"
method: "PUT"
path: "/v1/projects/{projectId}/story"
scope: "clips:write"
---

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

# Write the story of a fake text project

Operation id `update_fake_text_story`. Request `PUT /v1/projects/{projectId}/story`.

Replaces the story of a fake text project with the one you send, or writes its first story, and rebuilds its video.

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

Send the whole story as you want it: the title, the two people, the voices and every line in order. Send the line ids you read with get_fake_text_story (`GET /v1/projects/{projectId}/story`): a line sent with its id keeps its voice while its text does not change. Lines sent without an id get a new id each time you send the story, and a line with a new id has no voice until create_fake_text_voices (`POST /v1/projects/{projectId}/story/voices`) makes it. A project without a story gets this one as its first, and nothing is used. The language of the story is the language of the project. The answer is the story as it is saved.

## Example use

A team that writes its own chat scripts sends the whole story as one object, with the two people and every line. It then calls create_fake_text_voices (`POST /v1/projects/{projectId}/story/voices`), which voices only the lines that are new or changed.

## Endpoint

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

- Scope `clips:write`.
- Rate limit 60 requests a minute for each key and 120 for each account, shared by every `write` call. See [rate limits](/developers/rate-limits.md).
- No `Idempotency-Key`.

## Parameters

### Path

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

### Body

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `title` | string | Required | The title of the story, also the title of its video. From 1 to 200 characters. Matches `\S`. |
| `contactName` | string | Optional | The name shown at the top of the chat. Without it the chat shows left.name. From 1 to 60 characters. Matches `\S`. |
| `left` | object | Required | The other person in the chat, whose bubbles are on the left. |
| `left.name` | string | Required | The name of the other person in the chat. From 1 to 60 characters. Matches `\S`. |
| `right` | object | Required | The person whose phone shows the chat, whose bubbles are on the right. |
| `right.name` | string | Required | The name of the person whose phone shows the chat. From 1 to 60 characters. Matches `\S`. |
| `voices` | object | Optional | The voice of each side. A side you leave out keeps its voice. |
| `voices.left` | string or null | Optional | A voice id as list_voices (`GET /v1/reference/voices`) names it, in the language of the project. null gives this side the default voice of the language; leaving it out keeps the voice this side has. From 1 to 40 characters. |
| `voices.right` | string or null | Optional | A voice id as list_voices (`GET /v1/reference/voices`) names it, in the language of the project. null gives this side the default voice of the language; leaving it out keeps the voice this side has. From 1 to 40 characters. |
| `messages` | array of objects | Required | The lines of the chat in the order they appear, from 1 to 60. From 1 to 60 items. |
| `messages[].id` | string | Optional | The id of a line you read with get_fake_text_story (`GET /v1/projects/{projectId}/story`). Send it to keep the line and its voice; leave it out for a new line. Matches `^[A-Za-z0-9_-]{1,64}$`. |
| `messages[].side` | string | Required | Who sends the line: left is the other person, right is the person whose phone shows the chat. One of `left`, `right`. |
| `messages[].text` | string | Required | The words of the line, up to 300 characters. From 1 to 300 characters. Matches `\S`. |

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

## Request

Change a line and add one.

```bash title="curl"
curl -X PUT "https://api.gpt-video.com/v1/projects/prj_ba8b07a88bfb14585459/story" \
  -H "Authorization: Bearer $GPT_VIDEO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "The surprise party",
    "left": {
      "name": "Sam"
    },
    "right": {
      "name": "Me"
    },
    "voices": {
      "left": "en-guy",
      "right": "en-aria"
    },
    "messages": [
      {
        "id": "msg_e09afef44e32faf0bcb7",
        "side": "left",
        "text": "Are you free on Saturday night?"
      },
      {
        "id": "msg_568316dbc0cca81d3b24",
        "side": "right",
        "text": "Maybe. Why do you ask?"
      },
      {
        "id": "msg_556466181cf09d6bf0a2",
        "side": "left",
        "text": "No reason. Wear something nice."
      },
      {
        "side": "right",
        "text": "Should I be worried?"
      }
    ]
  }'
```

```ts title="TypeScript"
const res = await fetch("https://api.gpt-video.com/v1/projects/prj_ba8b07a88bfb14585459/story", {
  method: "PUT",
  headers: {
    "Authorization": `Bearer ${process.env.GPT_VIDEO_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "title": "The surprise party",
    "left": {
      "name": "Sam"
    },
    "right": {
      "name": "Me"
    },
    "voices": {
      "left": "en-guy",
      "right": "en-aria"
    },
    "messages": [
      {
        "id": "msg_e09afef44e32faf0bcb7",
        "side": "left",
        "text": "Are you free on Saturday night?"
      },
      {
        "id": "msg_568316dbc0cca81d3b24",
        "side": "right",
        "text": "Maybe. Why do you ask?"
      },
      {
        "id": "msg_556466181cf09d6bf0a2",
        "side": "left",
        "text": "No reason. Wear something nice."
      },
      {
        "side": "right",
        "text": "Should I be worried?"
      }
    ]
  }),
});
const data = await res.json();
console.log(data);
```

```python title="Python"
import os

import requests

r = requests.put(
    "https://api.gpt-video.com/v1/projects/prj_ba8b07a88bfb14585459/story",
    headers={
        "Authorization": f"Bearer {os.environ['GPT_VIDEO_API_KEY']}",
    },
    json={
        "title": "The surprise party",
        "left": {
            "name": "Sam"
        },
        "right": {
            "name": "Me"
        },
        "voices": {
            "left": "en-guy",
            "right": "en-aria"
        },
        "messages": [
            {
                "id": "msg_e09afef44e32faf0bcb7",
                "side": "left",
                "text": "Are you free on Saturday night?"
            },
            {
                "id": "msg_568316dbc0cca81d3b24",
                "side": "right",
                "text": "Maybe. Why do you ask?"
            },
            {
                "id": "msg_556466181cf09d6bf0a2",
                "side": "left",
                "text": "No reason. Wear something nice."
            },
            {
                "side": "right",
                "text": "Should I be worried?"
            }
        ]
    },
)
data = r.json()
print(data)
```

```json title="MCP (Full)"
{
  "method": "tools/call",
  "params": {
    "name": "update_fake_text_story",
    "arguments": {
      "projectId": "prj_ba8b07a88bfb14585459",
      "title": "The surprise party",
      "left": {
        "name": "Sam"
      },
      "right": {
        "name": "Me"
      },
      "voices": {
        "left": "en-guy",
        "right": "en-aria"
      },
      "messages": [
        {
          "id": "msg_e09afef44e32faf0bcb7",
          "side": "left",
          "text": "Are you free on Saturday night?"
        },
        {
          "id": "msg_568316dbc0cca81d3b24",
          "side": "right",
          "text": "Maybe. Why do you ask?"
        },
        {
          "id": "msg_556466181cf09d6bf0a2",
          "side": "left",
          "text": "No reason. Wear something nice."
        },
        {
          "side": "right",
          "text": "Should I be worried?"
        }
      ]
    }
  }
}
```

## Response

A successful call answers HTTP 200.

### Fields to check

| Name | Type | Description |
| --- | --- | --- |
| `story.id` | string | The story id. It starts with cnv_. |
| `story.messages[].id` | string | The line id. Send it back with update_fake_text_story (`PUT /v1/projects/{projectId}/story`) to keep the line and its voice. |

### Response example

```json title="HTTP 200"
{
  "story": {
    "id": "cnv_a744d5ef6cc005e19285",
    "projectId": "prj_ba8b07a88bfb14585459",
    "title": "The surprise party",
    "contactName": "Sam",
    "language": "en",
    "left": {
      "name": "Sam"
    },
    "right": {
      "name": "Me"
    },
    "voices": {
      "left": "en-guy",
      "right": "en-aria"
    },
    "messages": [
      {
        "id": "msg_e09afef44e32faf0bcb7",
        "side": "left",
        "text": "Are you free on Saturday night?",
        "voiced": true,
        "voiceDurationSec": 1.9
      },
      {
        "id": "msg_568316dbc0cca81d3b24",
        "side": "right",
        "text": "Maybe. Why do you ask?",
        "voiced": false,
        "voiceDurationSec": null
      },
      {
        "id": "msg_556466181cf09d6bf0a2",
        "side": "left",
        "text": "No reason. Wear something nice.",
        "voiced": true,
        "voiceDurationSec": 1.9
      },
      {
        "id": "msg_9b3e1f7c2a5d4860b1c4",
        "side": "right",
        "text": "Should I be worried?",
        "voiced": false,
        "voiceDurationSec": null
      }
    ],
    "voicedCount": 2,
    "createdAt": "2026-08-31T22:12:00.000Z",
    "updatedAt": "2026-09-01T11:05:00.000Z"
  }
}
```

**All response fields**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `story` | object | Required |   |
| `story.id` | string | Required | The story id. It starts with cnv_. |
| `story.projectId` | string | Required |   |
| `story.title` | string | Required |   |
| `story.contactName` | string | Required | The name shown at the top of the chat. |
| `story.language` | string | Required | One of `en`, `it`. |
| `story.left` | object | Required | The other person in the chat. |
| `story.right` | object | Required | The person whose phone shows the chat. |
| `story.voices` | object | Required | The voice of each side, as list_voices (`GET /v1/reference/voices`) names it; null for the default voice of the language, or for a voice that is not available through the API. |
| `story.messages` | array of objects | Required |   |
| `story.voicedCount` | integer | Required | How many lines have their voice. |
| `story.createdAt` | string | Required | An ISO 8601 time in UTC. |
| `story.updatedAt` | string | Required | An ISO 8601 time in UTC. |

**Fields of story.left**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `story.left.name` | string | Required |   |

**Fields of story.right**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `story.right.name` | string | Required |   |

**Fields of story.voices**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `story.voices.left` | string or null | Required |   |
| `story.voices.right` | string or null | Required |   |

**Fields of story.messages[]**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `story.messages[].id` | string | Required | The line id. Send it back with update_fake_text_story (`PUT /v1/projects/{projectId}/story`) to keep the line and its voice. |
| `story.messages[].side` | string | Required | One of `left`, `right`. |
| `story.messages[].text` | string | Required |   |
| `story.messages[].voiced` | boolean | Required | true when the line has its voice. |
| `story.messages[].voiceDurationSec` | number or null | Required | How long the voice of the line lasts, in seconds; null without a voice. |

**More examples**

#### Write the first story of a project

`PUT /v1/projects/prj_ba8b07a88bfb14585459/story`

```json title="Request body"
{
  "title": "The last slice",
  "contactName": "Alex",
  "left": {
    "name": "Alex"
  },
  "right": {
    "name": "Me"
  },
  "messages": [
    {
      "side": "left",
      "text": "Who ate my pizza?"
    },
    {
      "side": "right",
      "text": "Not me."
    }
  ]
}
```

```json title="HTTP 200"
{
  "story": {
    "id": "cnv_5f1e2d3c4b5a6978a8b9",
    "projectId": "prj_ba8b07a88bfb14585459",
    "title": "The last slice",
    "contactName": "Alex",
    "language": "en",
    "left": {
      "name": "Alex"
    },
    "right": {
      "name": "Me"
    },
    "voices": {
      "left": null,
      "right": null
    },
    "messages": [
      {
        "id": "msg_1a6c4e8f0b2d4975a3c8",
        "side": "left",
        "text": "Who ate my pizza?",
        "voiced": false,
        "voiceDurationSec": null
      },
      {
        "id": "msg_7e0b5d3a9c1f4268e4d0",
        "side": "right",
        "text": "Not me.",
        "voiced": false,
        "voiceDurationSec": null
      }
    ],
    "voicedCount": 0,
    "createdAt": "2026-09-01T11:05:00.000Z",
    "updatedAt": "2026-09-01T11:05:00.000Z"
  }
}
```

#### A voice of another language

`PUT /v1/projects/prj_ba8b07a88bfb14585459/story`

```json title="Request body"
{
  "title": "The surprise party",
  "left": {
    "name": "Sam"
  },
  "right": {
    "name": "Me"
  },
  "voices": {
    "left": "it-diego"
  },
  "messages": [
    {
      "id": "msg_e09afef44e32faf0bcb7",
      "side": "left",
      "text": "Are you free on Saturday night?"
    }
  ]
}
```

```json title="HTTP 400"
{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "Voice it-diego is not available in en. Call list_voices to see the voices you can use.",
    "requestId": "req_0d7b3f9a5e2c48614b8e3a7f"
  }
}
```

#### A project that is not a fake text project

`PUT /v1/projects/prj_7c2e9a4f1b3d4e8096a5/story`

```json title="Request body"
{
  "title": "A story",
  "left": {
    "name": "Sam"
  },
  "right": {
    "name": "Me"
  },
  "messages": [
    {
      "side": "left",
      "text": "Hi"
    }
  ]
}
```

```json title="HTTP 409"
{
  "error": {
    "code": "CONFLICT",
    "message": "This project is not a fake text project. Stories exist only in fake text projects.",
    "requestId": "req_9a4e6c1b3d7f45208c6d1e3b"
  }
}
```

## Errors

| Code | Status | When | What to do |
| --- | --- | --- | --- |
| [`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 | A project that is not a fake text project | Read the resource again, then decide. Do not send the same request again. |

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

## Cost

No credits are used.

## Related operations

- [`get_fake_text_story`](/developers/api/get-fake-text-story.md) (`GET /v1/projects/{projectId}/story`) Get the story of a fake text project
- [`create_fake_text_voices`](/developers/api/create-fake-text-voices.md) (`POST /v1/projects/{projectId}/story/voices`) Make the voices of a fake text story
- [`create_export`](/developers/api/create-export.md) (`POST /v1/compositions/{compositionId}/exports`) Export a composition
- [`create_fake_text_project`](/developers/api/create-fake-text-project.md) (`POST /v1/projects/fake-text`) Create a fake text project
- [`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

### What happens if I send the same story twice?

Lines with an id stay the same lines. Lines sent without an id get a new id each time, so send back the ids you read with get_fake_text_story (`GET /v1/projects/{projectId}/story`).

### Which lines need a new voice after I change the story?

Lines whose words or side changed, new lines, and every line of a side whose voice you changed. create_fake_text_voices (`POST /v1/projects/{projectId}/story/voices`) makes only those.

### How long can a story be?

Up to 60 lines of up to 300 characters each.
