---
title: "Make your first clips with the GPT-Video API"
description: "Go from an API key to a vertical MP4 clip on your disk. Each step is one call, and the steps pass ids to each other through variables."
url: "https://www.gpt-video.com/developers/quickstart"
---

# 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

1. Create a GPT-Video account and confirm your email address.
2. Open Developers in the GPT-Video app, at `https://www.gpt-video.com/app/developers`.
3. Create a key with the scopes `clips:read`, `clips:write` and `spend`. The key is shown once, so copy it then.
4. 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](/pricing) has the plans, and [API pricing](/developers/pricing.md) 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`](/developers/api/get-me.md) (`GET /v1/me`). Check a key before using it.

```bash title="curl"
curl "https://api.gpt-video.com/v1/me" \
  -H "Authorization: Bearer $GPT_VIDEO_API_KEY"
```

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

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

```json title="MCP (Full)"
{
  "method": "tools/call",
  "params": {
    "name": "get_me",
    "arguments": {}
  }
}
```

**Example response**

`GET /v1/me`

```json title="HTTP 200"
{
  "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`](/developers/api/get-credits.md) (`GET /v1/credits`). Check the balance with the API bonus.

```bash title="curl"
curl "https://api.gpt-video.com/v1/credits" \
  -H "Authorization: Bearer $GPT_VIDEO_API_KEY"
```

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

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

```json title="MCP (Full)"
{
  "method": "tools/call",
  "params": {
    "name": "get_credits",
    "arguments": {}
  }
}
```

**Example response**

`GET /v1/credits`

```json title="HTTP 200"
{
  "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`](/developers/api/create-upload.md) (`POST /v1/uploads`). Start the upload of a 20 MB video.

```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)
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')
```

```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/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;
```

```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(
    "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`

```json title="HTTP 201"
{
  "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`](/developers/api/upload-part.md) (`PUT /v1/uploads/{uploadId}/parts/{partNumber}`). Send the first part.

```bash title="curl"
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.bin
```

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

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

```json title="HTTP 200"
{
  "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](/developers/guides/upload-a-video.md) 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`](/developers/api/complete-upload.md) (`POST /v1/uploads/{uploadId}/complete`). Complete an upload.

```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)
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')
```

```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/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;
```

```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/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`

```json title="HTTP 201"
{
  "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`](/developers/api/get-setting-options.md) (`GET /v1/reference/setting-options`). Read the options before setting up a project.

```bash title="curl"
curl "https://api.gpt-video.com/v1/reference/setting-options" \
  -H "Authorization: Bearer $GPT_VIDEO_API_KEY"
```

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

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

```json title="MCP (Full)"
{
  "method": "tools/call",
  "params": {
    "name": "get_setting_options",
    "arguments": {}
  }
}
```

**Example response**

`GET /v1/reference/setting-options`

```json title="HTTP 200"
{
  "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`](/developers/api/create-project.md) (`POST /v1/projects`). Create a project on an uploaded video.

```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)
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')
```

```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", {
  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;
```

```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(
    "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`

```json title="HTTP 201"
{
  "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`](/developers/api/estimate-analysis.md) (`POST /v1/projects/{projectId}/analyze/estimate`). Price an analysis before starting it.

```bash title="curl"
curl -X POST "https://api.gpt-video.com/v1/projects/$PROJECT_ID/analyze/estimate" \
  -H "Authorization: Bearer $GPT_VIDEO_API_KEY"
```

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

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

```json title="HTTP 200"
{
  "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`](/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"
}
```

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`](/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"
  }
}
```

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](/developers/jobs.md) 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`](/developers/api/list-clips.md) (`GET /v1/projects/{projectId}/clips`). Only the clips waiting for a decision.

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

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

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

```json title="HTTP 200"
{
  "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`](/developers/api/accept-clip.md) (`POST /v1/clips/{clipId}/accept`). Accept a proposed clip.

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

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

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

```json title="HTTP 200"
{
  "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`](/developers/api/create-export.md) (`POST /v1/compositions/{compositionId}/exports`). Export a composition.

```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)
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')
```

```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/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;
```

```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/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`

```json title="HTTP 202"
{
  "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`](/developers/api/get-export.md) (`GET /v1/exports/{exportId}`). Wait for an export to finish.

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

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

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

```json title="HTTP 200"
{
  "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.

```bash title="curl"
curl -o clip.mp4 "$DOWNLOAD_URL"
```

```ts title="TypeScript"
import { writeFile } from "node:fs/promises";

const file = await fetch(downloadUrl);
await writeFile("clip.mp4", Buffer.from(await file.arrayBuffer()));
```

```python title="Python"
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](/developers/errors.md) lists every code with the action to take.

## Next steps

- [Turn a long video into clips](/developers/guides/clips-from-a-video.md) goes through the same steps with more clips and more settings.
- [Estimate the cost before you spend credits](/developers/guides/estimate-before-spending.md) shows how to check the price before each call that spends.
- [Use GPT-Video from an AI agent](/developers/ai-agents.md) shows how to give these steps to an agent.
- The [API reference](/developers/api.md) has a page for every endpoint.
