Quickstart
This page takes you from an API key to an MP4 clip on your disk. Each call is shown in curl, TypeScript and Python. The steps pass ids to each other through variables, so run them in order.
The curl examples read the answers with jq. The Python examples use the requests package. The TypeScript examples use top-level await and run in Node 20 or later as ES modules. There is no official SDK yet. The OpenAPI document works with code generators.
Before you start
- Create a GPT-Video account and confirm your email address.
- Open Developers in the GPT-Video app, at
https://www.gpt-video.com/app/developers. - Create a key with the scopes
clips:read,clips:writeandspend. The key is shown once, so copy it then. - Put the key in an environment variable called
GPT_VIDEO_API_KEY.
The examples read the key from that variable.
The API bonus
API bonus: 50 credits. No card needed.
An account can get the API bonus the first time it uses the API. We give a limited number each day. If none are left, a call on another day can get it.
To get it, the account needs a confirmed email address and a password, or a Google sign-in. Addresses from throwaway email services do not qualify. Each person gets it once.
API bonus credits can be spent only through the API. They expire 30 days after you get them. While the bonus pays, the limits in this table apply.
| Limit while the API bonus pays | Value |
|---|---|
| Exports a day | 5 |
| Longest export | 600 seconds |
| Editor Assistant turns a day | 20 |
| Tool runs a day | 10 |
| Fake text scripts a day | 10 |
| Jobs running at once | 1 |
After the bonus, the API needs a Creator or Studio plan. Without one, a call that spends through the API answers PLAN_REQUIRED when the bonus cannot pay for it. When a daily limit of the bonus is used up, it answers DAILY_LIMIT_REACHED until 00:00 UTC. Pricing has the plans, and API pricing has what each call uses.
We can pause the API bonus. While it is paused, a call it would pay for answers PLAN_REQUIRED. The Developers page in the app shows whether you have the bonus, and why not if you do not.
1. Check the key
Call get_me (GET /v1/me) first. It uses no credits and tells you whether the key works and which scopes it has.
get_me (GET /v1/me). Check a key before using it.
curl "https://api.gpt-video.com/v1/me" \
-H "Authorization: Bearer $GPT_VIDEO_API_KEY"const res = await fetch("https://api.gpt-video.com/v1/me", {
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(
"https://api.gpt-video.com/v1/me",
headers={
"Authorization": f"Bearer {os.environ['GPT_VIDEO_API_KEY']}",
},
)
data = r.json()
print(data){
"method": "tools/call",
"params": {
"name": "get_me",
"arguments": {}
}
}Example response
GET /v1/me
{
"key": {
"id": "mck_5f2c9e1a7b3d8046",
"scopes": [
"clips:read",
"clips:write",
"spend"
]
},
"account": {
"plan": "creator"
}
}If key.scopes does not include spend, the key cannot start an analysis or an export. Create a key that has it.
2. Check the balance
get_credits (GET /v1/credits) shows the credits of the plan, the top-up and the API bonus.
get_credits (GET /v1/credits). Check the balance with the API bonus.
curl "https://api.gpt-video.com/v1/credits" \
-H "Authorization: Bearer $GPT_VIDEO_API_KEY"const res = await fetch("https://api.gpt-video.com/v1/credits", {
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(
"https://api.gpt-video.com/v1/credits",
headers={
"Authorization": f"Bearer {os.environ['GPT_VIDEO_API_KEY']}",
},
)
data = r.json()
print(data){
"method": "tools/call",
"params": {
"name": "get_credits",
"arguments": {}
}
}Example response
GET /v1/credits
{
"plan": "free",
"planRequired": true,
"credits": {
"period": "2026-09",
"periodEndsAt": "2026-10-01T00:00:00.000Z",
"included": 0,
"remaining": 0,
"remainingFromPlan": 0,
"topUp": 0
},
"bonus": {
"status": "active",
"granted": 50,
"remaining": 47,
"grantedAt": "2026-09-01T08:00:00.000Z",
"expiresAt": "2026-10-01T08:00:00.000Z",
"resumesAt": null
},
"dailyLimits": {
"resetsAt": "2026-09-02T00:00:00.000Z",
"exports": {
"limit": 5,
"used": 1,
"remaining": 4
},
"assistantTurns": {
"limit": 20,
"used": 1,
"remaining": 19
},
"toolRuns": {
"limit": 10,
"used": 0,
"remaining": 10
},
"fakeTextScripts": {
"limit": 10,
"used": 0,
"remaining": 10
},
"assistantTurnsThroughApi": null
}
}When bonus.status is active, the bonus pays for calls through the API before the credits of the plan. When bonus is null, the account has no API bonus. The section above says who can get it.
3. Upload the video
An upload has three calls. Start the upload with the name of your file, its exact size in bytes and its type.
create_upload (POST /v1/uploads). Start the upload of a 20 MB 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)
UPLOAD_ID=$(curl -sS -X POST "https://api.gpt-video.com/v1/uploads" \
-H "Authorization: Bearer $GPT_VIDEO_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d '{
"filename": "keynote.mp4",
"sizeBytes": 20971520,
"contentType": "video/mp4"
}' | jq -r '.upload.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/uploads", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.GPT_VIDEO_API_KEY}`,
"Idempotency-Key": idempotencyKey,
"Content-Type": "application/json",
},
body: JSON.stringify({
"filename": "keynote.mp4",
"sizeBytes": 20971520,
"contentType": "video/mp4"
}),
});
const data = await res.json();
const uploadId = data.upload.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/uploads",
headers={
"Authorization": f"Bearer {os.environ['GPT_VIDEO_API_KEY']}",
"Idempotency-Key": idempotency_key,
},
json={
"filename": "keynote.mp4",
"sizeBytes": 20971520,
"contentType": "video/mp4"
},
)
data = r.json()
upload_id = data["upload"]["id"]Example response
POST /v1/uploads
{
"upload": {
"id": "upl_9c4e1a7d2b5f4e6083b1",
"filename": "keynote.mp4",
"contentType": "video/mp4",
"sizeBytes": 20971520,
"partSizeBytes": 8388608,
"partCount": 3,
"receivedParts": [],
"createdAt": "2026-09-01T10:30:00.000Z",
"expiresAt": "2026-09-01T16:30:00.000Z"
}
}The answer gives partSizeBytes and partCount. Split the file into parts of partSizeBytes bytes. The last part holds the rest of the file. Send each part with its number, starting from 1, as the raw bytes of the body.
upload_part (PUT /v1/uploads/{uploadId}/parts/{partNumber}). Send the first part.
curl -X PUT "https://api.gpt-video.com/v1/uploads/$UPLOAD_ID/parts/1" \
-H "Authorization: Bearer $GPT_VIDEO_API_KEY" \
-H "Content-Type: application/octet-stream" \
--data-binary @part-1.binimport { readFile } from "node:fs/promises";
const res = await fetch(`https://api.gpt-video.com/v1/uploads/${uploadId}/parts/1`, {
method: "PUT",
headers: {
"Authorization": `Bearer ${process.env.GPT_VIDEO_API_KEY}`,
"Content-Type": "application/octet-stream",
},
body: await readFile("part-1.bin"),
});
const data = await res.json();
console.log(data);import os
import requests
r = requests.put(
f"https://api.gpt-video.com/v1/uploads/{upload_id}/parts/1",
headers={
"Authorization": f"Bearer {os.environ['GPT_VIDEO_API_KEY']}",
"Content-Type": "application/octet-stream",
},
data=open("part-1.bin", "rb").read(),
)
data = r.json()
print(data)Example response
PUT /v1/uploads/upl_5d2a8f1e6c3b4079e0a1/parts/1
{
"upload": {
"id": "upl_5d2a8f1e6c3b4079e0a1",
"filename": "keynote.mp4",
"contentType": "video/mp4",
"sizeBytes": 20971520,
"partSizeBytes": 8388608,
"partCount": 3,
"receivedParts": [
1
],
"createdAt": "2026-09-01T10:30:00.000Z",
"expiresAt": "2026-09-01T16:31:00.000Z"
}
}Send every part from 1 to partCount the same way. Upload a video file has a loop that sends all the parts and resumes after an error. When every part has arrived, complete the upload. It turns the file into an asset.
complete_upload (POST /v1/uploads/{uploadId}/complete). Complete an upload.
# 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)
ASSET_ID=$(curl -sS -X POST "https://api.gpt-video.com/v1/uploads/$UPLOAD_ID/complete" \
-H "Authorization: Bearer $GPT_VIDEO_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Product teaser"
}' | jq -r '.asset.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/uploads/${uploadId}/complete`, {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.GPT_VIDEO_API_KEY}`,
"Idempotency-Key": idempotencyKey,
"Content-Type": "application/json",
},
body: JSON.stringify({
"title": "Product teaser"
}),
});
const data = await res.json();
const assetId = data.asset.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/uploads/{upload_id}/complete",
headers={
"Authorization": f"Bearer {os.environ['GPT_VIDEO_API_KEY']}",
"Idempotency-Key": idempotency_key,
},
json={
"title": "Product teaser"
},
)
data = r.json()
asset_id = data["asset"]["id"]Example response
POST /v1/uploads/upl_e7b04c92a15d4f36b8c7/complete
{
"asset": {
"id": "ast_5b0e7c3a9d1f4286b7e4",
"kind": "video",
"title": "Product teaser",
"filename": "teaser.mp4",
"origin": "upload",
"mimeType": "video/mp4",
"sizeBytes": 4096,
"durationSec": 42.5,
"width": 1920,
"height": 1080,
"fps": 30,
"hasAudio": true,
"createdAt": "2026-09-01T10:42:00.000Z",
"deletesAt": null
}
}4. Create a project
A project holds the settings of the analysis. get_setting_options (GET /v1/reference/setting-options) lists the values each setting accepts, such as the languages, the objectives and the caption styles.
get_setting_options (GET /v1/reference/setting-options). Read the options before setting up a project.
curl "https://api.gpt-video.com/v1/reference/setting-options" \
-H "Authorization: Bearer $GPT_VIDEO_API_KEY"const res = await fetch("https://api.gpt-video.com/v1/reference/setting-options", {
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(
"https://api.gpt-video.com/v1/reference/setting-options",
headers={
"Authorization": f"Bearer {os.environ['GPT_VIDEO_API_KEY']}",
},
)
data = r.json()
print(data){
"method": "tools/call",
"params": {
"name": "get_setting_options",
"arguments": {}
}
}Example response
GET /v1/reference/setting-options
{
"languages": [
{
"id": "auto",
"name": "Detect automatically"
},
{
"id": "it",
"name": "Italian"
},
{
"id": "en",
"name": "English"
}
],
"objectives": [
{
"id": "viral",
"name": "Mixed",
"description": "Moments with a strong opening and a clear payoff, on any topic."
},
{
"id": "educational",
"name": "Educational",
"description": "Lessons, methods and figures that a viewer would keep to watch again."
},
{
"id": "entertaining",
"name": "Entertaining",
"description": "Jokes, stories and moments nobody saw coming."
},
{
"id": "emotional",
"name": "Emotional",
"description": "Personal stories, moments of doubt and words that move people."
},
{
"id": "controversial",
"name": "Controversial",
"description": "Strong opinions and positions that go against what most people think."
},
{
"id": "howto",
"name": "How-to",
"description": "Practical steps that show how to do one thing."
},
{
"id": "business",
"name": "Business",
"description": "Lessons about running a company, with numbers, decisions and mistakes."
}
],
"captionStyles": [
{
"id": "bold-karaoke",
"name": "Bold karaoke",
"description": "Bold uppercase words, and the word being spoken changes color."
},
{
"id": "word-pop",
"name": "Word pop",
"description": "One large word at a time, each with a short entrance animation."
},
{
"id": "boxed",
"name": "Boxed",
"description": "The sentence sits in a dark box, and the word being spoken is highlighted."
},
{
"id": "outline",
"name": "Outline",
"description": "White words with a black outline, readable on any background."
}
],
"reframeModes": [
{
"id": "face",
"name": "Follow the face",
"description": "The vertical frame follows the face of the speaker, and stays in the middle when no face is found."
},
{
"id": "center",
"name": "Center",
"description": "The vertical frame stays in the middle of the video."
}
],
"clipCount": {
"min": 1,
"max": 100
},
"minDurationSec": {
"min": 5,
"max": 180
},
"maxDurationSec": {
"min": 10,
"max": 300
},
"layouts": [
{
"id": "50-50",
"name": "Half and half",
"description": "The main video and the background video each take half of the screen."
},
{
"id": "60-40",
"name": "Main video larger",
"description": "The main video takes more of the screen than the background video."
}
],
"themes": [
{
"id": "imessage",
"name": "iMessage",
"description": "The chat looks like a conversation in Apple Messages."
},
{
"id": "whatsapp",
"name": "WhatsApp",
"description": "The chat looks like a conversation in WhatsApp."
}
],
"aspects": [
{
"id": "source",
"name": "Same as the video",
"description": "The edit keeps the shape of its source video."
},
{
"id": "9:16",
"name": "Vertical 9:16",
"description": "A tall frame that fills a phone screen held upright."
},
{
"id": "1:1",
"name": "Square 1:1",
"description": "A square frame, as wide as it is tall."
},
{
"id": "16:9",
"name": "Horizontal 16:9",
"description": "A wide frame, the shape of most videos made for a computer screen."
}
],
"videoTypes": [
{
"id": "story",
"name": "Story",
"description": "A story read by a voice over a background video."
},
{
"id": "fake_text",
"name": "Fake text",
"description": "A chat conversation shown one message at a time."
},
{
"id": "split_screen",
"name": "Split screen",
"description": "A talking video above a background video."
},
{
"id": "any",
"name": "Any",
"description": "Ideas for any kind of short video."
}
]
}Create the project on the asset, with a value for every setting.
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"
}
}When project.missingSettings is empty, the project is ready to analyze. Otherwise, set the settings it lists with update_project (PATCH /v1/projects/{projectId}).
5. Check the price
estimate_analysis (POST /v1/projects/{projectId}/analyze/estimate) tells you what the analysis will use before you start it. It uses no credits.
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
}
}Read estimate.creditsToCharge and estimate.enough. estimate.payer says who would pay: api_bonus for the API bonus, plan for the credits of the account.
6. Start the analysis
The analysis spends credits, so the request needs an Idempotency-Key header. Create the key once, before you send the request. If the request fails or times out, send it again with the same key. GPT-Video then answers with the first result and does not charge twice. A new key is a new charge.
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 answer is 202 with a job. creditsCharged and paidWith say what the analysis used and who paid.
7. Wait for the analysis
Read the job with wait=25. The request stays open for up to 25 seconds while the job is still working, and answers as soon as it ends.
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"
}
}Repeat the call while job.status is queued or running. If an answer comes back at once with the job still working, wait a few seconds before you read it again. When job.status is done, the clips are ready. When it is failed, job.errorMessage says why. Jobs has more on waiting.
8. Accept a clip
List the clips that wait for your decision. They come in the order the analysis ranked them.
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
}Accept the clip. Accepting it makes a composition, the vertical video you export.
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"
}
}9. Export the clip
The export uses one export of the daily limit, or bonus credits when the API bonus pays for it, so it needs an Idempotency-Key too.
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"
}If the answer is NOT_READY, the framing of the clip is still being computed. Wait the seconds in the Retry-After header, then send the same request with the same key.
Wait for the export the same way as for the analysis.
get_export (GET /v1/exports/{exportId}). Wait for an export to finish.
# This command needs jq to read the answer.
DOWNLOAD_URL=$(curl -sS "https://api.gpt-video.com/v1/exports/$EXPORT_ID?wait=25" \
-H "Authorization: Bearer $GPT_VIDEO_API_KEY" | jq -r '.export.downloadUrl')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();
const downloadUrl = data.export.downloadUrl;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()
download_url = data["export"]["downloadUrl"]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
}
}Repeat the call while export.status is queued or rendering. When it is done, export.downloadUrl holds the link to the file.
10. Download the file
Download the file from downloadUrl without the Authorization header. The link is on www.gpt-video.com, and it works only until downloadUrlExpiresAt. Save the file right away. If the link has expired, call get_export (GET /v1/exports/{exportId}) again for a new one.
curl -o clip.mp4 "$DOWNLOAD_URL"import { writeFile } from "node:fs/promises";
const file = await fetch(downloadUrl);
await writeFile("clip.mp4", Buffer.from(await file.arrayBuffer()));import requests
file = requests.get(download_url)
with open("clip.mp4", "wb") as out:
out.write(file.content)When a call fails
Every error has the same body, with error.code, error.message and error.hint. Decide what to do from error.code, never from the HTTP status. A 429 can mean wait a minute or stop until tomorrow. Errors lists every code with the action to take.
Next steps
- Turn a long video into clips goes through the same steps with more clips and more settings.
- Estimate the cost before you spend credits shows how to check the price before each call that spends.
- Use GPT-Video from an AI agent shows how to give these steps to an agent.
- The API reference has a page for every endpoint.