Edit a timeline
A Vibe Editing project turns a whole video into an edit that you change with a list of operations. You can cut and reorder segments, add texts and b-roll, set captions and change the shape. This guide prepares an edit, changes it twice and exports it as an MP4 file.
Follow the steps in order. A step that needs an id or a number 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 and a video that is already uploaded. Upload a video file shows how to upload one.
Find your video
list_assets (GET /v1/assets). List your videos.
# This command needs jq to read the answer.
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 assetId = 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()
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, so the first one is the video you uploaded last. The example keeps its id in assetId. If you uploaded something else since, use the id the upload guide gave you.
Create the project
create_vibe_edit_project (POST /v1/projects/vibe-edit). Create a Vibe Editing project.
# 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)
PROJECT_ID=$(curl -sS -X POST "https://api.gpt-video.com/v1/projects/vibe-edit" \
-H "Authorization: Bearer $GPT_VIDEO_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d '{
"sourceAssetId": "'"$ASSET_ID"'",
"language": "en",
"captionStyle": "word-pop",
"aspect": "9:16",
"title": "Product demo, edited"
}' | jq -r '.project.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/vibe-edit", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.GPT_VIDEO_API_KEY}`,
"Idempotency-Key": idempotencyKey,
"Content-Type": "application/json",
},
body: JSON.stringify({
"sourceAssetId": assetId,
"language": "en",
"captionStyle": "word-pop",
"aspect": "9:16",
"title": "Product demo, edited"
}),
});
const data = await res.json();
const projectId = data.project.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(
"https://api.gpt-video.com/v1/projects/vibe-edit",
headers={
"Authorization": f"Bearer {os.environ['GPT_VIDEO_API_KEY']}",
"Idempotency-Key": idempotency_key,
},
json={
"sourceAssetId": asset_id,
"language": "en",
"captionStyle": "word-pop",
"aspect": "9:16",
"title": "Product demo, edited"
},
)
data = r.json()
project_id = data["project"]["id"]Example response
POST /v1/projects/vibe-edit
{
"project": {
"id": "prj_6e2b9d14a7c3f0581e2d",
"kind": "vibeedit",
"title": "Product demo, edited",
"status": "draft",
"sourceAssetId": "ast_8e21b0d94c6f4a17b358",
"compositionId": null,
"settings": null,
"missingSettings": [],
"splitScreen": null,
"streamer": null,
"redditStory": null,
"fakeText": null,
"vibeEdit": {
"sourceAssetId": "ast_8e21b0d94c6f4a17b358",
"language": "en",
"captionStyle": "word-pop",
"aspect": "9:16",
"vocabulary": null,
"prepared": false
},
"errorMessage": null,
"createdAt": "2026-09-01T11:00:00.000Z",
"updatedAt": "2026-09-01T11:00:00.000Z"
}
}The body sets the language of the video, the caption style and the shape the edit starts with. aspect can keep the shape of the video or make it vertical, square or wide. get_setting_options (GET /v1/reference/setting-options) lists the caption styles. This call uses no credits.
The example creates the Idempotency-Key once, before the request. If the request times out, send it again with the same key and you get the same project back. A new key would create a second project.
Check the price
estimate_analysis (POST /v1/projects/{projectId}/analyze/estimate). Price an analysis before starting it.
curl -X POST "https://api.gpt-video.com/v1/projects/$PROJECT_ID/analyze/estimate" \
-H "Authorization: Bearer $GPT_VIDEO_API_KEY"const res = await fetch(`https://api.gpt-video.com/v1/projects/${projectId}/analyze/estimate`, {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.GPT_VIDEO_API_KEY}`,
},
});
const data = await res.json();
console.log(data);import os
import requests
r = requests.post(
f"https://api.gpt-video.com/v1/projects/{project_id}/analyze/estimate",
headers={
"Authorization": f"Bearer {os.environ['GPT_VIDEO_API_KEY']}",
},
)
data = r.json()
print(data)Example response
POST /v1/projects/prj_7c2e9a4f1b3d4e8096a5/analyze/estimate
{
"estimate": {
"credits": 30,
"creditsToCharge": 30,
"sourceDurationSec": 1800,
"runningJobId": null,
"creditsAvailable": 640,
"planRequired": false,
"enough": true,
"payer": "plan",
"bonusRemaining": null
}
}estimate.credits is the price of preparing the edit. It depends on the length of the video. estimate.enough says whether the account can pay it now. If it is false, stop and read Handle running out of credits.
Prepare the edit
prepare_timeline (POST /v1/projects/{projectId}/timeline/prepare). Prepare an edit.
# 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/timeline/prepare" \
-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}/timeline/prepare`, {
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}/timeline/prepare",
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_37573e451e0ff7f863c8/timeline/prepare
{
"job": {
"id": "job_d41c7e0a95b3f26c8e17",
"kind": "analysis",
"status": "queued",
"progress": 0,
"errorMessage": null,
"projectId": "prj_37573e451e0ff7f863c8",
"compositionId": null,
"exportId": null,
"assetId": null,
"toolRunId": null,
"turnId": null,
"createdAt": "2026-09-01T11:10:00.000Z",
"startedAt": null,
"finishedAt": null
},
"creditsCharged": 2,
"reused": false,
"paidWith": "plan"
}The video is transcribed and analyzed once, and the edit is built from it. The credits are charged when the preparation starts. The answer comes back at once with the job.
An edit is prepared once and is never charged again. Calling this on a prepared edit answers CONFLICT. If the request times out, send it again with the same Idempotency-Key. A retry never charges twice.
Wait for the preparation
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, read the edit. When it is failed, errorMessage says what happened. Wait for long jobs explains this loop.
Read the edit
get_timeline (GET /v1/projects/{projectId}/timeline). Get an edit.
# This command needs jq to read the answer.
RESPONSE=$(curl -sS "https://api.gpt-video.com/v1/projects/$PROJECT_ID/timeline" \
-H "Authorization: Bearer $GPT_VIDEO_API_KEY")
REVISION=$(printf '%s' "$RESPONSE" | jq -r '.timeline.revision')
FIRST_SEGMENT_ID=$(printf '%s' "$RESPONSE" | jq -r '.timeline.segments[0].id')
SECOND_SEGMENT_ID=$(printf '%s' "$RESPONSE" | jq -r '.timeline.segments[1].id')const res = await fetch(`https://api.gpt-video.com/v1/projects/${projectId}/timeline`, {
method: "GET",
headers: {
"Authorization": `Bearer ${process.env.GPT_VIDEO_API_KEY}`,
},
});
const data = await res.json();
const revision = data.timeline.revision;
const firstSegmentId = data.timeline.segments[0].id;
const secondSegmentId = data.timeline.segments[1].id;import os
import requests
r = requests.get(
f"https://api.gpt-video.com/v1/projects/{project_id}/timeline",
headers={
"Authorization": f"Bearer {os.environ['GPT_VIDEO_API_KEY']}",
},
)
data = r.json()
revision = data["timeline"]["revision"]
first_segment_id = data["timeline"]["segments"][0]["id"]
second_segment_id = data["timeline"]["segments"][1]["id"]Example response
GET /v1/projects/prj_b9c1f9c7873dcef51a57/timeline
{
"timeline": {
"id": "tml_bbe061171a8229f8daed",
"projectId": "prj_b9c1f9c7873dcef51a57",
"compositionId": "cmp_cfbdd85eaaff301fa997",
"revision": 1,
"durationSec": 60,
"aspect": "9:16",
"reframe": "off",
"captions": {
"enabled": true,
"style": "bold-karaoke"
},
"music": null,
"fadeInSec": 0,
"fadeOutSec": 0,
"clips": [
{
"assetId": "ast_3f9a1c7e5b2d4086a1c4",
"title": "Interview with a founder",
"durationSec": 1800,
"state": "ready"
}
],
"segments": [
{
"id": "seg_7279fbb508f4a22e65a4",
"assetId": "ast_3f9a1c7e5b2d4086a1c4",
"sourceStartSec": 120,
"sourceEndSec": 150,
"zoom": 1,
"muted": false,
"volume": 1,
"outputStartSec": 0,
"outputEndSec": 30
},
{
"id": "seg_a9dcee59c1fb9edb6751",
"assetId": "ast_3f9a1c7e5b2d4086a1c4",
"sourceStartSec": 300,
"sourceEndSec": 330,
"zoom": 1,
"muted": false,
"volume": 1,
"outputStartSec": 30,
"outputEndSec": 60
}
],
"texts": [
{
"id": "ovl_49f5239b9cd73496225d",
"text": "Paid before the product",
"preset": "headline",
"outputStartSec": 0,
"durationSec": 3,
"xPercent": null,
"yPercent": null
}
],
"broll": [
{
"id": "brl_f44d18e008ec4009bf10",
"assetId": "ast_8ac5d8b97546145cdbf6",
"outputStartSec": 12,
"durationSec": 4,
"fit": "cover"
}
],
"diagnostics": []
}
}The edit lists its videos, its segments, its texts, its b-roll and its captions. Every change you send names the revision you read here. If the edit changed after that, nothing is written and the answer is CONFLICT. Read the edit again and send your change with the new revision.
Segments, texts and b-roll are named by the ids this answer shows. The example keeps the ids of the first two segments. An edit with only one segment has no second id, so choose the ids that fit your edit.
Times come in two bases. sourceStartSec and sourceEndSec are seconds of the source video. outputStartSec and the other output times are seconds of the video that comes out. If NOT_READY comes back, the preparation is still running. Wait the seconds in Retry-After and read again.
Cut the edit
apply_timeline_operations (POST /v1/projects/{projectId}/timeline/operations). Cut a segment in two and remove another.
# 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)
RESPONSE=$(curl -sS -X POST "https://api.gpt-video.com/v1/projects/$PROJECT_ID/timeline/operations" \
-H "Authorization: Bearer $GPT_VIDEO_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d '{
"expectedRevision": '"$REVISION"',
"operations": [
{
"op": "split_segment",
"segmentId": "'"$FIRST_SEGMENT_ID"'",
"atSourceSec": 135
},
{
"op": "remove_segments",
"segmentIds": [
"'"$SECOND_SEGMENT_ID"'"
]
}
]
}')
REVISION=$(printf '%s' "$RESPONSE" | jq -r '.timeline.revision')
COMPOSITION_ID=$(printf '%s' "$RESPONSE" | jq -r '.compositionId')// 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}/timeline/operations`, {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.GPT_VIDEO_API_KEY}`,
"Idempotency-Key": idempotencyKey,
"Content-Type": "application/json",
},
body: JSON.stringify({
"expectedRevision": revision,
"operations": [
{
"op": "split_segment",
"segmentId": firstSegmentId,
"atSourceSec": 135
},
{
"op": "remove_segments",
"segmentIds": [
secondSegmentId
]
}
]
}),
});
const data = await res.json();
const revision = data.timeline.revision;
const compositionId = data.compositionId;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}/timeline/operations",
headers={
"Authorization": f"Bearer {os.environ['GPT_VIDEO_API_KEY']}",
"Idempotency-Key": idempotency_key,
},
json={
"expectedRevision": revision,
"operations": [
{
"op": "split_segment",
"segmentId": first_segment_id,
"atSourceSec": 135
},
{
"op": "remove_segments",
"segmentIds": [
second_segment_id
]
}
]
},
)
data = r.json()
revision = data["timeline"]["revision"]
composition_id = data["compositionId"]Example response
POST /v1/projects/prj_b9c1f9c7873dcef51a57/timeline/operations
{
"timeline": {
"id": "tml_bbe061171a8229f8daed",
"projectId": "prj_b9c1f9c7873dcef51a57",
"compositionId": "cmp_cfbdd85eaaff301fa997",
"revision": 2,
"durationSec": 30,
"aspect": "9:16",
"reframe": "off",
"captions": {
"enabled": true,
"style": "bold-karaoke"
},
"music": null,
"fadeInSec": 0,
"fadeOutSec": 0,
"clips": [
{
"assetId": "ast_3f9a1c7e5b2d4086a1c4",
"title": "Interview with a founder",
"durationSec": 1800,
"state": "ready"
}
],
"segments": [
{
"id": "seg_7279fbb508f4a22e65a4",
"assetId": "ast_3f9a1c7e5b2d4086a1c4",
"sourceStartSec": 120,
"sourceEndSec": 135,
"zoom": 1,
"muted": false,
"volume": 1,
"outputStartSec": 0,
"outputEndSec": 15
},
{
"id": "seg_5a0e3c8f1d7b42e69c13",
"assetId": "ast_3f9a1c7e5b2d4086a1c4",
"sourceStartSec": 135,
"sourceEndSec": 150,
"zoom": 1,
"muted": false,
"volume": 1,
"outputStartSec": 15,
"outputEndSec": 30
}
],
"texts": [
{
"id": "ovl_49f5239b9cd73496225d",
"text": "Paid before the product",
"preset": "headline",
"outputStartSec": 0,
"durationSec": 3,
"xPercent": null,
"yPercent": null
}
],
"broll": [
{
"id": "brl_f44d18e008ec4009bf10",
"assetId": "ast_8ac5d8b97546145cdbf6",
"outputStartSec": 12,
"durationSec": 4,
"fit": "cover"
}
],
"diagnostics": []
},
"compositionId": "cmp_cfbdd85eaaff301fa997",
"warnings": []
}The operations apply in order, as one change and one new revision, or not at all. This request splits the first segment at a time you set in atSourceSec, which must fall inside that segment, and removes the second segment. Change the operations to the cuts you want. The reference lists every operation in apply_timeline_operations (POST /v1/projects/{projectId}/timeline/operations).
The answer is the edit after the change. warnings says what the change adjusted. compositionId is the composition the edit compiles to, which you export later. The example keeps the new revision, so the next change can use it.
Each change uses no credits. Adding another video to the edit costs credits and has its own call, add_timeline_clip (POST /v1/projects/{projectId}/timeline/clips).
Change the captions and the shape
apply_timeline_operations (POST /v1/projects/{projectId}/timeline/operations). Change the captions, a word and the shape.
# 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)
REVISION=$(curl -sS -X POST "https://api.gpt-video.com/v1/projects/$PROJECT_ID/timeline/operations" \
-H "Authorization: Bearer $GPT_VIDEO_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d '{
"expectedRevision": '"$REVISION"',
"operations": [
{
"op": "set_captions",
"style": "word-pop",
"options": {
"uppercase": true,
"maxWordsPerGroup": 3
}
},
{
"op": "replace_caption_word",
"from": "customer",
"to": "client"
},
{
"op": "set_aspect",
"aspect": "1:1"
},
{
"op": "set_reframe",
"mode": "center"
}
]
}' | jq -r '.timeline.revision')// 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}/timeline/operations`, {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.GPT_VIDEO_API_KEY}`,
"Idempotency-Key": idempotencyKey,
"Content-Type": "application/json",
},
body: JSON.stringify({
"expectedRevision": revision,
"operations": [
{
"op": "set_captions",
"style": "word-pop",
"options": {
"uppercase": true,
"maxWordsPerGroup": 3
}
},
{
"op": "replace_caption_word",
"from": "customer",
"to": "client"
},
{
"op": "set_aspect",
"aspect": "1:1"
},
{
"op": "set_reframe",
"mode": "center"
}
]
}),
});
const data = await res.json();
const revision = data.timeline.revision;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}/timeline/operations",
headers={
"Authorization": f"Bearer {os.environ['GPT_VIDEO_API_KEY']}",
"Idempotency-Key": idempotency_key,
},
json={
"expectedRevision": revision,
"operations": [
{
"op": "set_captions",
"style": "word-pop",
"options": {
"uppercase": True,
"maxWordsPerGroup": 3
}
},
{
"op": "replace_caption_word",
"from": "customer",
"to": "client"
},
{
"op": "set_aspect",
"aspect": "1:1"
},
{
"op": "set_reframe",
"mode": "center"
}
]
},
)
data = r.json()
revision = data["timeline"]["revision"]Example response
POST /v1/projects/prj_b9c1f9c7873dcef51a57/timeline/operations
{
"timeline": {
"id": "tml_bbe061171a8229f8daed",
"projectId": "prj_b9c1f9c7873dcef51a57",
"compositionId": "cmp_cfbdd85eaaff301fa997",
"revision": 2,
"durationSec": 60,
"aspect": "1:1",
"reframe": "center",
"captions": {
"enabled": true,
"style": "word-pop"
},
"music": null,
"fadeInSec": 0,
"fadeOutSec": 0,
"clips": [
{
"assetId": "ast_3f9a1c7e5b2d4086a1c4",
"title": "Interview with a founder",
"durationSec": 1800,
"state": "ready"
}
],
"segments": [
{
"id": "seg_7279fbb508f4a22e65a4",
"assetId": "ast_3f9a1c7e5b2d4086a1c4",
"sourceStartSec": 120,
"sourceEndSec": 150,
"zoom": 1,
"muted": false,
"volume": 1,
"outputStartSec": 0,
"outputEndSec": 30
},
{
"id": "seg_a9dcee59c1fb9edb6751",
"assetId": "ast_3f9a1c7e5b2d4086a1c4",
"sourceStartSec": 300,
"sourceEndSec": 330,
"zoom": 1,
"muted": false,
"volume": 1,
"outputStartSec": 30,
"outputEndSec": 60
}
],
"texts": [
{
"id": "ovl_49f5239b9cd73496225d",
"text": "Paid before the product",
"preset": "headline",
"outputStartSec": 0,
"durationSec": 3,
"xPercent": null,
"yPercent": null
}
],
"broll": [
{
"id": "brl_f44d18e008ec4009bf10",
"assetId": "ast_8ac5d8b97546145cdbf6",
"outputStartSec": 12,
"durationSec": 4,
"fit": "cover"
}
],
"diagnostics": []
},
"compositionId": "cmp_cfbdd85eaaff301fa997",
"warnings": []
}This change sets the caption style, replaces a word in the captions, changes the shape of the edit and sets its framing. Change the values to yours. It uses the revision that the cut answered. If you send an old revision, the answer is CONFLICT. Read the edit with get_timeline (GET /v1/projects/{projectId}/timeline) and send the change again.
To see earlier versions of the edit, call list_timeline_revisions (GET /v1/projects/{projectId}/timeline/revisions). To go back to one, call restore_timeline (POST /v1/projects/{projectId}/timeline/restore).
Export the edit
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.
If the export answers NOT_READY, wait the seconds in the Retry-After header and send the same request again with the same Idempotency-Key.
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.