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 explains.
You need a key. A video for the background is optional, and you can upload one with Upload a video file.
Choose the background video
list_assets (GET /v1/assets). List your videos.
# 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')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;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
{
"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 (POST /v1/projects/fake-text). Create a fake text project.
# 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')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;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
{
"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 (PATCH /v1/projects/{projectId}/fake-text).
The project starts without a story. This call uses no credits.
Write the story
update_fake_text_story (PUT /v1/projects/{projectId}/story). Write the first story of a project.
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."
}
]
}'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);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
{
"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 (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 (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 (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 (POST /v1/projects/{projectId}/story/voices). Make the voices of the story.
# 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')// 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;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
{
"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 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 (GET /v1/jobs/{jobId}). Wait for a job to finish.
curl "https://api.gpt-video.com/v1/jobs/$JOB_ID?wait=25" \
-H "Authorization: Bearer $GPT_VIDEO_API_KEY"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);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
{
"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 explains this loop.
Find the composition
get_project (GET /v1/projects/{projectId}). Read a project.
# 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')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;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
{
"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 (POST /v1/compositions/{compositionId}/exports). Export a composition.
# 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')// 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;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
{
"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 (GET /v1/exports/{exportId}). Wait for an export to finish.
curl "https://api.gpt-video.com/v1/exports/$EXPORT_ID?wait=25" \
-H "Authorization: Bearer $GPT_VIDEO_API_KEY"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);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
{
"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.