---
title: "Make a fake text video with the GPT-Video API"
description: "Create a fake text project, write the chat story, make the voices and export the video. Each step is one call to the GPT-Video API."
url: "https://www.gpt-video.com/developers/guides/fake-text-video"
---

# Make a fake text video

A fake text video shows a chat story on a phone screen, with a voice for each line and a video playing behind it. This guide creates the project, writes the story, makes the voices and exports the video as an MP4 file.

Follow the steps in order. A step that needs an id from an earlier answer reads it from a variable that the step before filled in. Change the values in the request bodies to your own. The examples read your key from the `GPT_VIDEO_API_KEY` variable, as [Authentication](/developers/authentication.md) explains.

You need a key. A video for the background is optional, and you can upload one with [Upload a video file](/developers/guides/upload-a-video.md).

## Choose the background video

[`list_assets`](/developers/api/list-assets.md) (`GET /v1/assets`). List your videos.

```bash title="curl"
# This command needs jq to read the answer.
BACKGROUND_ASSET_ID=$(curl -sS "https://api.gpt-video.com/v1/assets" \
  -H "Authorization: Bearer $GPT_VIDEO_API_KEY" | jq -r '.data[0].id')
```

```ts title="TypeScript"
const res = await fetch("https://api.gpt-video.com/v1/assets", {
  method: "GET",
  headers: {
    "Authorization": `Bearer ${process.env.GPT_VIDEO_API_KEY}`,
  },
});
const data = await res.json();
const backgroundAssetId = data.data[0].id;
```

```python title="Python"
import os

import requests

r = requests.get(
    "https://api.gpt-video.com/v1/assets",
    headers={
        "Authorization": f"Bearer {os.environ['GPT_VIDEO_API_KEY']}",
    },
)
data = r.json()
background_asset_id = data["data"][0]["id"]
```

**Example response**

`GET /v1/assets`

```json title="HTTP 200"
{
  "data": [
    {
      "id": "ast_8e21b0d94c6f4a17b358",
      "kind": "video",
      "title": "Product demo",
      "filename": "demo.mp4",
      "origin": "upload",
      "mimeType": "video/mp4",
      "sizeBytes": 50331648,
      "durationSec": 120,
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "hasAudio": true,
      "createdAt": "2026-09-01T09:05:00.000Z",
      "deletesAt": "2026-09-08T09:05:00.000Z"
    },
    {
      "id": "ast_3f9a1c7e5b2d4086a1c4",
      "kind": "video",
      "title": "Interview with a founder",
      "filename": "interview.mp4",
      "origin": "upload",
      "mimeType": "video/mp4",
      "sizeBytes": 734003200,
      "durationSec": 1800,
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "hasAudio": true,
      "createdAt": "2026-09-01T09:00:00.000Z",
      "deletesAt": null
    }
  ],
  "hasMore": false,
  "nextCursor": null
}
```

The list shows your videos with the newest first. The example keeps the first one as the video that plays behind the chat. If you want another video behind the chat, use its id instead. A project can also have no background. Send `null` for `backgroundAssetId` in the next step.

## Create the project

[`create_fake_text_project`](/developers/api/create-fake-text-project.md) (`POST /v1/projects/fake-text`). Create a fake text project.

```bash title="curl"
# This command needs jq to read the answer.
PROJECT_ID=$(curl -sS -X POST "https://api.gpt-video.com/v1/projects/fake-text" \
  -H "Authorization: Bearer $GPT_VIDEO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "theme": "imessage",
    "language": "en",
    "voiceover": true,
    "backgroundAssetId": "'"$BACKGROUND_ASSET_ID"'",
    "title": "Group chat drama"
  }' | jq -r '.project.id')
```

```ts title="TypeScript"
const res = await fetch("https://api.gpt-video.com/v1/projects/fake-text", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.GPT_VIDEO_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "theme": "imessage",
    "language": "en",
    "voiceover": true,
    "backgroundAssetId": backgroundAssetId,
    "title": "Group chat drama"
  }),
});
const data = await res.json();
const projectId = data.project.id;
```

```python title="Python"
import os

import requests

r = requests.post(
    "https://api.gpt-video.com/v1/projects/fake-text",
    headers={
        "Authorization": f"Bearer {os.environ['GPT_VIDEO_API_KEY']}",
    },
    json={
        "theme": "imessage",
        "language": "en",
        "voiceover": True,
        "backgroundAssetId": background_asset_id,
        "title": "Group chat drama"
    },
)
data = r.json()
project_id = data["project"]["id"]
```

**Example response**

`POST /v1/projects/fake-text`

```json title="HTTP 201"
{
  "project": {
    "id": "prj_3c0f8e1d5a9b4e2786d1",
    "kind": "faketext",
    "title": "Group chat drama",
    "status": "draft",
    "sourceAssetId": null,
    "compositionId": null,
    "settings": null,
    "missingSettings": [],
    "splitScreen": null,
    "streamer": null,
    "redditStory": null,
    "fakeText": {
      "theme": "imessage",
      "language": "en",
      "voiceover": true,
      "backgroundAssetId": "ast_8ac5d8b97546145cdbf6",
      "storyId": null
    },
    "vibeEdit": null,
    "errorMessage": null,
    "createdAt": "2026-09-01T11:00:00.000Z",
    "updatedAt": "2026-09-01T11:00:00.000Z"
  }
}
```

The look, the language, the voices setting and the background are chosen here. `theme` is `imessage` or `whatsapp`. `language` is `en` or `it`, and it is the language of the story and of its voices. `voiceover` says whether the lines get voices. You can change these choices later with [update_fake_text_project](/developers/api/update-fake-text-project.md) (`PATCH /v1/projects/{projectId}/fake-text`).

The project starts without a story. This call uses no credits.

## Write the story

[`update_fake_text_story`](/developers/api/update-fake-text-story.md) (`PUT /v1/projects/{projectId}/story`). Write the first story of a project.

```bash title="curl"
curl -X PUT "https://api.gpt-video.com/v1/projects/$PROJECT_ID/story" \
  -H "Authorization: Bearer $GPT_VIDEO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "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."
      }
    ]
  }'
```

```ts title="TypeScript"
const res = await fetch(`https://api.gpt-video.com/v1/projects/${projectId}/story`, {
  method: "PUT",
  headers: {
    "Authorization": `Bearer ${process.env.GPT_VIDEO_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "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."
      }
    ]
  }),
});
const data = await res.json();
console.log(data);
```

```python title="Python"
import os

import requests

r = requests.put(
    f"https://api.gpt-video.com/v1/projects/{project_id}/story",
    headers={
        "Authorization": f"Bearer {os.environ['GPT_VIDEO_API_KEY']}",
    },
    json={
        "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."
            }
        ]
    },
)
data = r.json()
print(data)
```

**Example response**

`PUT /v1/projects/prj_ba8b07a88bfb14585459/story`

```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 story has a title, the two people in the chat and the lines in order. `left` is the other person, whose bubbles are on the left. `right` is the person whose phone shows the chat. Each line has a `side` and its `text`. The answer gives every line an id.

This call replaces the whole story, so send it complete each time. To keep the voice of a line you did not change, send back the line ids that [get_fake_text_story](/developers/api/get-fake-text-story.md) (`GET /v1/projects/{projectId}/story`) shows. A line sent without an id is a new line. It has no voice until you make one. Writing the story uses no credits.

To pick a voice for each person, send `voices` with ids from [list_voices](/developers/api/list-voices.md) (`GET /v1/reference/voices?language=en`). Without `voices`, each side gets the default voice of the language. A voice must be in the language of the project.

To have GPT-Video write the story from a short brief instead, call [generate_fake_text_story](/developers/api/generate-fake-text-story.md) (`POST /v1/projects/{projectId}/story/generate`). It answers with a job that you follow with `get_job` (`GET /v1/jobs/{jobId}`), and it uses a fake text script from your daily allowance. The new story replaces the story the project had.

## Make the voices

[`create_fake_text_voices`](/developers/api/create-fake-text-voices.md) (`POST /v1/projects/{projectId}/story/voices`). Make the voices of the story.

```bash title="curl"
# This command needs jq to read the answer.
# Create the key once. Send this same key again if you retry this request.
IDEMPOTENCY_KEY=$(uuidgen)
JOB_ID=$(curl -sS -X POST "https://api.gpt-video.com/v1/projects/$PROJECT_ID/story/voices" \
  -H "Authorization: Bearer $GPT_VIDEO_API_KEY" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" | jq -r '.job.id')
```

```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/${projectId}/story/voices`, {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.GPT_VIDEO_API_KEY}`,
    "Idempotency-Key": idempotencyKey,
  },
});
const data = await res.json();
const jobId = data.job.id;
```

```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(
    f"https://api.gpt-video.com/v1/projects/{project_id}/story/voices",
    headers={
        "Authorization": f"Bearer {os.environ['GPT_VIDEO_API_KEY']}",
        "Idempotency-Key": idempotency_key,
    },
)
data = r.json()
job_id = data["job"]["id"]
```

**Example response**

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

```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"
}
```

Only the lines without a voice are voiced: new lines, lines whose words changed and the lines of a side whose voice changed. Making the voices uses a fake text script from your daily allowance. When the API bonus pays, it uses a bonus credit too. [API pricing](/developers/pricing.md) shows what the bonus pays and the daily limits.

The answer is a job to follow. If voices are already being made for the story, you get that job back, `reused` is true and nothing is used. If this request times out, send it again with the same `Idempotency-Key`. A retry never uses a second script.

The call is refused with `CONFLICT` when the project has no story yet, when `voiceover` is off or when every line already has its voice. The message says which.

## Wait for the voices

[`get_job`](/developers/api/get-job.md) (`GET /v1/jobs/{jobId}`). Wait for a job to finish.

```bash title="curl"
curl "https://api.gpt-video.com/v1/jobs/$JOB_ID?wait=25" \
  -H "Authorization: Bearer $GPT_VIDEO_API_KEY"
```

```ts title="TypeScript"
const res = await fetch(`https://api.gpt-video.com/v1/jobs/${jobId}?wait=25`, {
  method: "GET",
  headers: {
    "Authorization": `Bearer ${process.env.GPT_VIDEO_API_KEY}`,
  },
});
const data = await res.json();
console.log(data);
```

```python title="Python"
import os

import requests

r = requests.get(
    f"https://api.gpt-video.com/v1/jobs/{job_id}?wait=25",
    headers={
        "Authorization": f"Bearer {os.environ['GPT_VIDEO_API_KEY']}",
    },
)
data = r.json()
print(data)
```

**Example response**

`GET /v1/jobs/job_6c1e8b4a2f0d4a97e3b5?wait=25`

```json title="HTTP 200"
{
  "job": {
    "id": "job_6c1e8b4a2f0d4a97e3b5",
    "kind": "analysis",
    "status": "done",
    "progress": 1,
    "errorMessage": null,
    "projectId": "prj_7c2e9a4f1b3d4e8096a5",
    "compositionId": null,
    "exportId": null,
    "assetId": null,
    "toolRunId": null,
    "turnId": null,
    "createdAt": "2026-09-01T09:11:00.000Z",
    "startedAt": "2026-09-01T09:12:00.000Z",
    "finishedAt": "2026-09-01T09:40:00.000Z"
  }
}
```

The `wait` value, up to 25 seconds, holds the request until the job ends or the wait time passes. While `status` is `queued` or `running`, send the same request again. When it is `done`, the voices are ready. When it is `failed`, `errorMessage` says what happened, and lines that got their voice before the failure keep it. To voice the rest, call create_fake_text_voices (`POST /v1/projects/{projectId}/story/voices`) again with a new `Idempotency-Key` and do not send the story again: a story sent without the line ids makes every line new, and new lines have no voice. [Wait for long jobs](/developers/jobs.md) explains this loop.

## Find the composition

[`get_project`](/developers/api/get-project.md) (`GET /v1/projects/{projectId}`). Read a project.

```bash title="curl"
# This command needs jq to read the answer.
COMPOSITION_ID=$(curl -sS "https://api.gpt-video.com/v1/projects/$PROJECT_ID" \
  -H "Authorization: Bearer $GPT_VIDEO_API_KEY" | jq -r '.project.compositionId')
```

```ts title="TypeScript"
const res = await fetch(`https://api.gpt-video.com/v1/projects/${projectId}`, {
  method: "GET",
  headers: {
    "Authorization": `Bearer ${process.env.GPT_VIDEO_API_KEY}`,
  },
});
const data = await res.json();
const compositionId = data.project.compositionId;
```

```python title="Python"
import os

import requests

r = requests.get(
    f"https://api.gpt-video.com/v1/projects/{project_id}",
    headers={
        "Authorization": f"Bearer {os.environ['GPT_VIDEO_API_KEY']}",
    },
)
data = r.json()
composition_id = data["project"]["compositionId"]
```

**Example response**

`GET /v1/projects/prj_7c2e9a4f1b3d4e8096a5`

```json title="HTTP 200"
{
  "project": {
    "id": "prj_7c2e9a4f1b3d4e8096a5",
    "kind": "autoclip",
    "title": "Founder interview",
    "status": "ready",
    "sourceAssetId": "ast_3f9a1c7e5b2d4086a1c4",
    "compositionId": null,
    "settings": {
      "language": "en",
      "clipCount": 3,
      "minDurationSec": 15,
      "maxDurationSec": 60,
      "objective": "viral",
      "captionStyle": "word-pop",
      "reframe": "face",
      "instructions": null,
      "vocabulary": null
    },
    "missingSettings": [],
    "splitScreen": null,
    "streamer": null,
    "redditStory": null,
    "fakeText": null,
    "vibeEdit": null,
    "errorMessage": null,
    "createdAt": "2026-09-01T09:10:00.000Z",
    "updatedAt": "2026-09-01T09:40:00.000Z",
    "analysisJobId": null
  }
}
```

Once the project has a story, it has a `compositionId`. It is null before that. The composition is the video you export. The example keeps its id.

## Export the video

[`create_export`](/developers/api/create-export.md) (`POST /v1/compositions/{compositionId}/exports`). Export a composition.

```bash title="curl"
# This command needs jq to read the answer.
# Create the key once. Send this same key again if you retry this request.
IDEMPOTENCY_KEY=$(uuidgen)
EXPORT_ID=$(curl -sS -X POST "https://api.gpt-video.com/v1/compositions/$COMPOSITION_ID/exports" \
  -H "Authorization: Bearer $GPT_VIDEO_API_KEY" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" | jq -r '.export.id')
```

```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/compositions/${compositionId}/exports`, {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.GPT_VIDEO_API_KEY}`,
    "Idempotency-Key": idempotencyKey,
  },
});
const data = await res.json();
const exportId = data.export.id;
```

```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(
    f"https://api.gpt-video.com/v1/compositions/{composition_id}/exports",
    headers={
        "Authorization": f"Bearer {os.environ['GPT_VIDEO_API_KEY']}",
        "Idempotency-Key": idempotency_key,
    },
)
data = r.json()
export_id = data["export"]["id"]
```

**Example response**

`POST /v1/compositions/cmp_4e8c2a6f09d14b73a1e5/exports`

```json title="HTTP 202"
{
  "export": {
    "id": "rnd_6d0f2a8c4e1b47a9c3f5",
    "compositionId": "cmp_4e8c2a6f09d14b73a1e5",
    "projectId": "prj_7c2e9a4f1b3d4e8096a5",
    "jobId": "job_8c3f1e5a9d2b4f7c06e1",
    "revision": 0,
    "status": "queued",
    "progress": 0,
    "errorMessage": null,
    "createdAt": "2026-09-01T11:00:00.000Z",
    "finishedAt": null,
    "file": null,
    "downloadUrl": null,
    "downloadUrlExpiresAt": null,
    "fileDeletesAt": null,
    "fileDeletedAt": null
  },
  "paidWith": "plan"
}
```

The answer comes back at once with the export, queued. It has no download link yet. An export counts against your daily export limit and uses no plan credits. When the API bonus pays for it, it uses bonus credits.

Send an `Idempotency-Key`, and use the same one on every retry. A retry of the same request never uses a second export.

## Download the file

[`get_export`](/developers/api/get-export.md) (`GET /v1/exports/{exportId}`). Wait for an export to finish.

```bash title="curl"
curl "https://api.gpt-video.com/v1/exports/$EXPORT_ID?wait=25" \
  -H "Authorization: Bearer $GPT_VIDEO_API_KEY"
```

```ts title="TypeScript"
const res = await fetch(`https://api.gpt-video.com/v1/exports/${exportId}?wait=25`, {
  method: "GET",
  headers: {
    "Authorization": `Bearer ${process.env.GPT_VIDEO_API_KEY}`,
  },
});
const data = await res.json();
console.log(data);
```

```python title="Python"
import os

import requests

r = requests.get(
    f"https://api.gpt-video.com/v1/exports/{export_id}?wait=25",
    headers={
        "Authorization": f"Bearer {os.environ['GPT_VIDEO_API_KEY']}",
    },
)
data = r.json()
print(data)
```

**Example response**

`GET /v1/exports/rnd_0f6b3e9d2c7a4158b4a3?wait=25`

```json title="HTTP 200"
{
  "export": {
    "id": "rnd_0f6b3e9d2c7a4158b4a3",
    "compositionId": "cmp_4e8c2a6f09d14b73a1e5",
    "projectId": "prj_7c2e9a4f1b3d4e8096a5",
    "jobId": "job_a3e9d05c7b1f4e2896d4",
    "revision": 0,
    "status": "done",
    "progress": 1,
    "errorMessage": null,
    "createdAt": "2026-09-01T10:10:00.000Z",
    "finishedAt": "2026-09-01T10:15:00.000Z",
    "file": {
      "mimeType": "video/mp4",
      "sizeBytes": 18874368,
      "durationSec": 30,
      "width": 1080,
      "height": 1920
    },
    "downloadUrl": "https://www.gpt-video.com/media/ast_c47d2e8a19b54f06d2e1?exp=1788258600&sig=9b2e61c04d7f3a58e1b6c2d9f0a4e7b3",
    "downloadUrlExpiresAt": "2026-09-01T10:30:00.000Z",
    "fileDeletesAt": "2026-09-15T10:15:00.000Z",
    "fileDeletedAt": null
  }
}
```

Send the request again while `status` is `queued` or `rendering`. When `status` is `done`, `downloadUrl` is a link to the MP4 file. The link works without your API key, so do not send the key to it. Save the response body as an `.mp4` file.

The link is short-lived, and `downloadUrlExpiresAt` says until when. Read the export again right before you download to get a fresh link. If the export failed, `errorMessage` says why. Export again with a new `Idempotency-Key`.
