Jobs
Analyzing a video, building a video and exporting a file take time. The call that starts the work answers at once with a job, and you follow the job until it ends. This page shows how to wait for it and how to download the result.
Which calls start a job
analyze_project (POST /v1/projects/{projectId}/analyze) returns a job. So do the calls that build a split screen, streamer or Reddit story video, and the calls that write a story or make the voices of a fake text project. A tool run, a turn of the Editor Assistant and the framing of a clip each carry the id of their job. An export has a jobId too.
Read a job
Call get_job (GET /v1/jobs/{jobId}) with the job id. The job has these fields that matter for a program.
statusisqueued,running,done,failedorcancelled. The first two mean the work is going on. The last three are final.progressgoes from 0 to 1.errorMessagesays why a job failed or stopped. It isnullotherwise.kindsays what the job does, for exampleanalysis,buildorexport.toolRunIdandturnIdgive the result of a tool run and of an Editor Assistant turn once the job is done.
Stop reading when status is done, failed or cancelled. Then use the ids on the job to read the result, for example the clips of the project after an analysis.
Wait without polling
Add ?wait= to the request, with a number of seconds, up to the longest wait in the table below. The API holds the request open until the job ends or the time passes, and then answers with the job as it is. If the job is not final, send the request again.
An account can keep only a few of these waits open at once. When all of them are in use, a new wait does not start, and you get an immediate answer, the same as wait=0. If an answer comes back at once and the job is still working, pause for a few seconds before you read it again.
The requests below start an analysis and wait for it. They use variables, so each step uses the id of the one before it.
list_projects (GET /v1/projects). List the projects.
# This command needs jq to read the answer.
PROJECT_ID=$(curl -sS "https://api.gpt-video.com/v1/projects" \
-H "Authorization: Bearer $GPT_VIDEO_API_KEY" | jq -r '.data[0].id')const res = await fetch("https://api.gpt-video.com/v1/projects", {
method: "GET",
headers: {
"Authorization": `Bearer ${process.env.GPT_VIDEO_API_KEY}`,
},
});
const data = await res.json();
const projectId = data.data[0].id;import os
import requests
r = requests.get(
"https://api.gpt-video.com/v1/projects",
headers={
"Authorization": f"Bearer {os.environ['GPT_VIDEO_API_KEY']}",
},
)
data = r.json()
project_id = data["data"][0]["id"]Example response
GET /v1/projects
{
"data": [
{
"id": "prj_1d8b6e0c47a24f93b1c2",
"kind": "autoclip",
"title": "Weekly podcast",
"status": "draft",
"sourceAssetId": null,
"compositionId": null,
"settings": {
"language": null,
"clipCount": 5,
"minDurationSec": null,
"maxDurationSec": null,
"objective": "educational",
"captionStyle": null,
"reframe": null,
"instructions": null,
"vocabulary": null
},
"missingSettings": [
"language",
"minDurationSec",
"maxDurationSec",
"captionStyle",
"reframe"
],
"splitScreen": null,
"streamer": null,
"redditStory": null,
"fakeText": null,
"vibeEdit": null,
"errorMessage": null,
"createdAt": "2026-09-01T09:50:00.000Z",
"updatedAt": "2026-09-01T09:50:00.000Z"
},
{
"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"
}
],
"hasMore": false,
"nextCursor": null
}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"
}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"
}
}Wait for an export and download it
To follow an export, use get_export (GET /v1/exports/{exportId}), which also gives the download link. It takes ?wait= in the same way. When export.status is done, export.downloadUrl holds the link to the file.
- Download the file without the
Authorizationheader. The link is signed, and it points to the GPT-Video site and not to the host of the API. - The link works for a short time.
export.downloadUrlExpiresAtgives the exact time. - When the link has expired, call
get_export(GET /v1/exports/{exportId}) again to get a new one. - Save the file as soon as you have it.
An export id starts with rnd_. It is not a job id, so get_job (GET /v1/jobs/{jobId}) does not accept it. Use the jobId of the export if you want its job.
The longest wait, the number of waits an account can keep open and the life of a download link are in this table.
| Limit | Value |
|---|---|
Items in a page (limit) | From 1 to 100, 20 by default |
Idempotency-Key length | 255 characters |
How long an Idempotency-Key is kept | 24 hours |
Longest wait | 25 seconds |
Requests with wait open at once for each account | 10 |
| A download link works for | 15 minutes |
| Size of an upload part | 8 MiB |
| Parts in an upload | 1024 |
| Largest upload | 8 GiB |
| Largest JSON body | 1 MiB |
Webhooks instead of waiting
If your app has a public address, it can receive an event when a job ends, so it does not need to wait. See webhooks.
What to do when a job fails
A job that ends with failed says why in errorMessage. Read it to decide what to change. Starting the work again is a new request, so it needs a new Idempotency-Key. Retrying with the same key is meant for network errors. Pagination and idempotency explains the key.