Skip to content

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.

  • status is queued, running, done, failed or cancelled. The first two mean the work is going on. The last three are final.
  • progress goes from 0 to 1.
  • errorMessage says why a job failed or stopped. It is null otherwise.
  • kind says what the job does, for example analysis, build or export.
  • toolRunId and turnId give 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.

Request example
# 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

HTTP 200
{
  "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.

Request example
# 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

HTTP 202
{
  "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.

Request example
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

HTTP 200
{
  "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 Authorization header. 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.downloadUrlExpiresAt gives 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.

LimitValue
Items in a page (limit)From 1 to 100, 20 by default
Idempotency-Key length255 characters
How long an Idempotency-Key is kept24 hours
Longest wait25 seconds
Requests with wait open at once for each account10
A download link works for15 minutes
Size of an upload part8 MiB
Parts in an upload1024
Largest upload8 GiB
Largest JSON body1 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.