---
title: "Wait for long jobs in the GPT-Video API"
description: "Analyses, builds and exports run as jobs. Follow one with get_job (GET /v1/jobs/{jobId}) and the wait parameter, then read the result when it ends."
url: "https://www.gpt-video.com/developers/jobs"
---

# 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](/developers/api/get-job.md) (`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`](/developers/api/list-projects.md) (`GET /v1/projects`). List the projects.

```bash title="curl"
# 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')
```

```ts title="TypeScript"
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;
```

```python title="Python"
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`

```json title="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`](/developers/api/analyze-project.md) (`POST /v1/projects/{projectId}/analyze`). Analyze a project.

```bash title="curl"
# 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')
```

```ts title="TypeScript"
// 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;
```

```python title="Python"
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`

```json title="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`](/developers/api/get-job.md) (`GET /v1/jobs/{jobId}`). Wait for a job to finish.

```bash title="curl"
curl "https://api.gpt-video.com/v1/jobs/$JOB_ID?wait=25" \
  -H "Authorization: Bearer $GPT_VIDEO_API_KEY"
```

```ts title="TypeScript"
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);
```

```python title="Python"
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`

```json title="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](/developers/api/get-export.md) (`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.

| 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](/developers/webhooks.md).

## 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](/developers/pagination-and-idempotency.md) explains the key.
