Turn a long video into clips
This guide starts from a video you uploaded and ends with a vertical MP4 file. You create a project, check the price, run the analysis, accept a clip and export it.
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 and a video that is already uploaded. Upload a video file shows how to upload one. Pricing explains what pays for the analysis.
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, or add search with part of the title.
Create a project
create_project (POST /v1/projects). Create a project on an uploaded video.
# 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" \
-H "Authorization: Bearer $GPT_VIDEO_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d '{
"sourceAssetId": "'"$ASSET_ID"'",
"title": "Product demo clips",
"settings": {
"language": "en",
"clipCount": 5,
"minDurationSec": 20,
"maxDurationSec": 45,
"objective": "howto",
"captionStyle": "bold-karaoke",
"reframe": "face"
}
}' | 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", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.GPT_VIDEO_API_KEY}`,
"Idempotency-Key": idempotencyKey,
"Content-Type": "application/json",
},
body: JSON.stringify({
"sourceAssetId": assetId,
"title": "Product demo clips",
"settings": {
"language": "en",
"clipCount": 5,
"minDurationSec": 20,
"maxDurationSec": 45,
"objective": "howto",
"captionStyle": "bold-karaoke",
"reframe": "face"
}
}),
});
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",
headers={
"Authorization": f"Bearer {os.environ['GPT_VIDEO_API_KEY']}",
"Idempotency-Key": idempotency_key,
},
json={
"sourceAssetId": asset_id,
"title": "Product demo clips",
"settings": {
"language": "en",
"clipCount": 5,
"minDurationSec": 20,
"maxDurationSec": 45,
"objective": "howto",
"captionStyle": "bold-karaoke",
"reframe": "face"
}
},
)
data = r.json()
project_id = data["project"]["id"]Example response
POST /v1/projects
{
"project": {
"id": "prj_27dd3348d28146ce5a3a",
"kind": "autoclip",
"title": "Product demo clips",
"status": "draft",
"sourceAssetId": "ast_8e21b0d94c6f4a17b358",
"compositionId": null,
"settings": {
"language": "en",
"clipCount": 5,
"minDurationSec": 20,
"maxDurationSec": 45,
"objective": "howto",
"captionStyle": "bold-karaoke",
"reframe": "face",
"instructions": null,
"vocabulary": null
},
"missingSettings": [],
"splitScreen": null,
"streamer": null,
"redditStory": null,
"fakeText": null,
"vibeEdit": null,
"errorMessage": null,
"createdAt": "2026-09-01T11:00:00.000Z",
"updatedAt": "2026-09-01T11:00:00.000Z"
}
}A project holds the video and the choices for the analysis: the language, the number of clips, how long a clip may be, the objective, the caption style and the framing. Change them in the body. get_setting_options (GET /v1/reference/setting-options) lists the values each setting accepts.
A setting you leave out is named in missingSettings. Choose it with update_project (PATCH /v1/projects/{projectId}) before you start the analysis. 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 the analysis. estimate.enough says whether the account can pay it now, and estimate.payer says who would pay. Nothing is charged and nothing is reserved.
If enough is false, stop here and read Handle running out of credits. Estimate the cost before you spend credits explains every field of the answer.
Start the analysis
analyze_project (POST /v1/projects/{projectId}/analyze). Analyze a 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)
JOB_ID=$(curl -sS -X POST "https://api.gpt-video.com/v1/projects/$PROJECT_ID/analyze" \
-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}/analyze`, {
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}/analyze",
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_7c2e9a4f1b3d4e8096a5/analyze
{
"job": {
"id": "job_b582bc8315e372bf9ba3",
"kind": "analysis",
"status": "queued",
"progress": 0,
"errorMessage": null,
"projectId": "prj_7c2e9a4f1b3d4e8096a5",
"compositionId": null,
"exportId": null,
"assetId": null,
"toolRunId": null,
"turnId": null,
"createdAt": "2026-09-01T11:10:00.000Z",
"startedAt": null,
"finishedAt": null
},
"creditsCharged": 30,
"reused": false,
"paidWith": "plan"
}The credits are charged now. The answer comes back at once with the job to follow. creditsCharged says what this request was charged and paidWith says who paid.
If an analysis of the project is already running, you get that job back, reused is true and nothing is charged. If this request times out, send it again with the same Idempotency-Key. A retry never charges twice.
Wait for the analysis
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 holds the request until the job ends or the wait time passes. If status is queued or running, send the same request again. When status is done, go on to the clips. When it is failed or cancelled, errorMessage says what happened. Wait for long jobs explains this loop.
List the clips
list_clips (GET /v1/projects/{projectId}/clips). Only the clips waiting for a decision.
# This command needs jq to read the answer.
CLIP_ID=$(curl -sS "https://api.gpt-video.com/v1/projects/$PROJECT_ID/clips?status=proposed" \
-H "Authorization: Bearer $GPT_VIDEO_API_KEY" | jq -r '.data[0].id')const res = await fetch(`https://api.gpt-video.com/v1/projects/${projectId}/clips?status=proposed`, {
method: "GET",
headers: {
"Authorization": `Bearer ${process.env.GPT_VIDEO_API_KEY}`,
},
});
const data = await res.json();
const clipId = data.data[0].id;import os
import requests
r = requests.get(
f"https://api.gpt-video.com/v1/projects/{project_id}/clips?status=proposed",
headers={
"Authorization": f"Bearer {os.environ['GPT_VIDEO_API_KEY']}",
},
)
data = r.json()
clip_id = data["data"][0]["id"]Example response
GET /v1/projects/prj_7c2e9a4f1b3d4e8096a5/clips?status=proposed
{
"data": [
{
"id": "cnd_2b7f4e91a0c84d35e6f0",
"projectId": "prj_7c2e9a4f1b3d4e8096a5",
"sourceAssetId": "ast_3f9a1c7e5b2d4086a1c4",
"summary": "The founder explains how the first customer paid before the product existed.",
"reason": "A clear claim in the first seconds and a result at the end.",
"tags": [
"startups",
"sales"
],
"createdAt": "2026-09-01T09:40:00.000Z",
"status": "proposed",
"rank": 1,
"startSec": 300,
"endSec": 330,
"durationSec": 30,
"title": "The first customer",
"hook": "Our first customer changed everything",
"score": 81,
"scoreBreakdown": [
{
"factor": "hook",
"label": "Hook (first 3 seconds)",
"score": 8,
"note": "Opens on a concrete claim."
},
{
"factor": "payoff",
"label": "Payoff / ending",
"score": 7,
"note": "Ends on the result."
},
{
"factor": "clarity",
"label": "Clarity",
"score": 6,
"note": "One idea, said plainly."
},
{
"factor": "emotion",
"label": "Emotion",
"score": 8,
"note": "The speaker sounds surprised."
},
{
"factor": "novelty",
"label": "Novelty / surprise",
"score": 7,
"note": "Not the usual advice."
},
{
"factor": "selfContained",
"label": "Self-contained",
"score": 6,
"note": "Needs no context."
},
{
"factor": "quotability",
"label": "Quotability",
"score": 8,
"note": "Has a line people repeat."
},
{
"factor": "dialogue",
"label": "Dialogue (back-and-forth)",
"score": 7,
"note": "A single speaker."
}
],
"transcriptExcerpt": "before we had a product and that changed everything for our team the first customer paid us before we had a product and that changed everything for our team the first customer paid us before we had a product and that changed everything for our team the first customer paid us before we had a product and that changed everything for our team the first customer paid us before we had a product and that",
"social": {
"title": "The first customer",
"caption": "How a first customer paid before there was a product.",
"hashtags": [
"#startup",
"#founder"
]
},
"compositionId": null
}
],
"hasMore": false,
"nextCursor": null
}The clips come in the order the analysis ranked them. Each one has a title, a hook, its place in the source video and a score. The example keeps the first clip.
If hasMore is true, send nextCursor back as cursor to read the next page. Pagination and idempotency shows how. To read one clip on its own, call get_clip (GET /v1/clips/{clipId}).
Accept the clip
accept_clip (POST /v1/clips/{clipId}/accept). Accept a proposed clip.
# This command needs jq to read the answer.
COMPOSITION_ID=$(curl -sS -X POST "https://api.gpt-video.com/v1/clips/$CLIP_ID/accept" \
-H "Authorization: Bearer $GPT_VIDEO_API_KEY" | jq -r '.composition.id')const res = await fetch(`https://api.gpt-video.com/v1/clips/${clipId}/accept`, {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.GPT_VIDEO_API_KEY}`,
},
});
const data = await res.json();
const compositionId = data.composition.id;import os
import requests
r = requests.post(
f"https://api.gpt-video.com/v1/clips/{clip_id}/accept",
headers={
"Authorization": f"Bearer {os.environ['GPT_VIDEO_API_KEY']}",
},
)
data = r.json()
composition_id = data["composition"]["id"]Example response
POST /v1/clips/cnd_2b7f4e91a0c84d35e6f0/accept
{
"clip": {
"id": "cnd_2b7f4e91a0c84d35e6f0",
"projectId": "prj_7c2e9a4f1b3d4e8096a5",
"sourceAssetId": "ast_3f9a1c7e5b2d4086a1c4",
"summary": "The founder explains how the first customer paid before the product existed.",
"reason": "A clear claim in the first seconds and a result at the end.",
"tags": [
"startups",
"sales"
],
"createdAt": "2026-09-01T09:40:00.000Z",
"status": "accepted",
"rank": 1,
"startSec": 300,
"endSec": 330,
"durationSec": 30,
"title": "The first customer",
"hook": "Our first customer changed everything",
"score": 81,
"scoreBreakdown": [
{
"factor": "hook",
"label": "Hook (first 3 seconds)",
"score": 8,
"note": "Opens on a concrete claim."
},
{
"factor": "payoff",
"label": "Payoff / ending",
"score": 7,
"note": "Ends on the result."
},
{
"factor": "clarity",
"label": "Clarity",
"score": 6,
"note": "One idea, said plainly."
},
{
"factor": "emotion",
"label": "Emotion",
"score": 8,
"note": "The speaker sounds surprised."
},
{
"factor": "novelty",
"label": "Novelty / surprise",
"score": 7,
"note": "Not the usual advice."
},
{
"factor": "selfContained",
"label": "Self-contained",
"score": 6,
"note": "Needs no context."
},
{
"factor": "quotability",
"label": "Quotability",
"score": 8,
"note": "Has a line people repeat."
},
{
"factor": "dialogue",
"label": "Dialogue (back-and-forth)",
"score": 7,
"note": "A single speaker."
}
],
"transcriptExcerpt": "before we had a product and that changed everything for our team the first customer paid us before we had a product and that changed everything for our team the first customer paid us before we had a product and that changed everything for our team the first customer paid us before we had a product and that changed everything for our team the first customer paid us before we had a product and that",
"social": {
"title": "The first customer",
"caption": "How a first customer paid before there was a product.",
"hashtags": [
"#startup",
"#founder"
]
},
"compositionId": "cmp_b18e4f2a6c0d4973e5a1"
},
"composition": {
"id": "cmp_b18e4f2a6c0d4973e5a1",
"projectId": "prj_7c2e9a4f1b3d4e8096a5",
"clipId": "cnd_2b7f4e91a0c84d35e6f0",
"kind": "autoclip",
"title": "The first customer",
"revision": 0,
"durationSec": 30,
"width": 1080,
"height": 1920,
"fps": 30,
"source": {
"assetId": "ast_3f9a1c7e5b2d4086a1c4",
"startSec": 300,
"endSec": 330
},
"hook": "Our first customer changed everything",
"captions": {
"style": "word-pop",
"wordCount": 75
},
"framing": {
"status": "tracking",
"jobId": "job_4f0a9c2e7d1b4e3a86c5"
},
"createdAt": "2026-09-01T10:30:00.000Z",
"updatedAt": "2026-09-01T10:30:00.000Z"
}
}Accepting turns the clip into a composition, the vertical video cut from the source with captions and the hook. It uses no credits. Accepting a clip that is already accepted returns the same composition again.
To drop a clip you do not want, call reject_clip (POST /v1/clips/{clipId}/reject).
Wait for the framing
get_composition (GET /v1/compositions/{compositionId}). Get a composition.
curl "https://api.gpt-video.com/v1/compositions/$COMPOSITION_ID" \
-H "Authorization: Bearer $GPT_VIDEO_API_KEY"const res = await fetch(`https://api.gpt-video.com/v1/compositions/${compositionId}`, {
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/compositions/{composition_id}",
headers={
"Authorization": f"Bearer {os.environ['GPT_VIDEO_API_KEY']}",
},
)
data = r.json()
print(data)Example response
GET /v1/compositions/cmp_4e8c2a6f09d14b73a1e5
{
"composition": {
"id": "cmp_4e8c2a6f09d14b73a1e5",
"projectId": "prj_7c2e9a4f1b3d4e8096a5",
"clipId": "cnd_9e04a6c3d12b4f78a5b9",
"kind": "autoclip",
"title": "Paid before the product",
"revision": 0,
"durationSec": 30,
"width": 1080,
"height": 1920,
"fps": 30,
"source": {
"assetId": "ast_3f9a1c7e5b2d4086a1c4",
"startSec": 120,
"endSec": 150
},
"hook": "They paid us before we had a product",
"captions": {
"style": "word-pop",
"wordCount": 75
},
"framing": {
"status": "centered",
"jobId": null
},
"createdAt": "2026-09-01T10:00:00.000Z",
"updatedAt": "2026-09-01T10:00:00.000Z"
}
}Right after you accept a clip, GPT-Video is still following the face in it, and framing.status is tracking. Read the composition again until the status changes. An export asked for earlier answers NOT_READY.
Export the clip
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 framing is not done, the answer is NOT_READY. Wait the seconds in the Retry-After header and send the same request again with the same Idempotency-Key. Asking again while the same version is exporting returns that export and uses nothing.
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 the composition again with a new Idempotency-Key.
Next steps
- Read GPT-Video API errors and what to do next to handle each error code in your program.
- Use rate limits to see how many requests you can send.
- Run the analysis again on the same project when you want a new set of clips. The clips you accepted stay.