Write the story of a fake text project
update_fake_text_storyPUT/v1/projects/{projectId}/storyBeta
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
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
writecall. See rate limits. - 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.
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?"
}
]
}'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);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){
"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
{
"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
{
"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."
}
]
}{
"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
{
"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?"
}
]
}{
"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
{
"title": "A story",
"left": {
"name": "Sam"
},
"right": {
"name": "Me"
},
"messages": [
{
"side": "left",
"text": "Hi"
}
]
}{
"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 | 404 | See the error reference. | Change the request, then send it again. |
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, UNKNOWN_PARAMETER, UNAUTHORIZED, SCOPE_MISSING, PAYLOAD_TOO_LARGE, RATE_LIMITED, INTERNAL, UNAVAILABLE.
Cost
No credits are used.
Related operations
get_fake_text_story(GET /v1/projects/{projectId}/story) Get the story of a fake text projectcreate_fake_text_voices(POST /v1/projects/{projectId}/story/voices) Make the voices of a fake text storycreate_export(POST /v1/compositions/{compositionId}/exports) Export a compositioncreate_fake_text_project(POST /v1/projects/fake-text) Create a fake text projectgenerate_fake_text_story(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.