Skip to content
Create a key

Paste a key to use it in the examples on these pages. The key stays in this browser tab. These pages never send it anywhere.

Turn a long video into clips

This guide starts from a video you uploaded and ends with a vertical MP4 file. You create a project, check the price, run the analysis, accept a clip and export it.

Follow the steps in order. A step that needs an id from an earlier answer reads it from a variable that the step before filled in. Change the values in the request bodies to your own. The examples read your key from the GPT_VIDEO_API_KEY variable, as Authentication explains.

You need a key and a video that is already uploaded. Upload a video file shows how to upload one. Pricing explains what pays for the analysis.

Find your video

list_assets (GET /v1/assets). List your videos.

Request example
# This command needs jq to read the answer.
ASSET_ID=$(curl -sS "https://api.gpt-video.com/v1/assets" \
  -H "Authorization: Bearer $GPT_VIDEO_API_KEY" | jq -r '.data[0].id')
const res = await fetch("https://api.gpt-video.com/v1/assets", {
  method: "GET",
  headers: {
    "Authorization": `Bearer ${process.env.GPT_VIDEO_API_KEY}`,
  },
});
const data = await res.json();
const assetId = data.data[0].id;
import os

import requests

r = requests.get(
    "https://api.gpt-video.com/v1/assets",
    headers={
        "Authorization": f"Bearer {os.environ['GPT_VIDEO_API_KEY']}",
    },
)
data = r.json()
asset_id = data["data"][0]["id"]
Example response

GET /v1/assets

HTTP 200
{
  "data": [
    {
      "id": "ast_8e21b0d94c6f4a17b358",
      "kind": "video",
      "title": "Product demo",
      "filename": "demo.mp4",
      "origin": "upload",
      "mimeType": "video/mp4",
      "sizeBytes": 50331648,
      "durationSec": 120,
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "hasAudio": true,
      "createdAt": "2026-09-01T09:05:00.000Z",
      "deletesAt": "2026-09-08T09:05:00.000Z"
    },
    {
      "id": "ast_3f9a1c7e5b2d4086a1c4",
      "kind": "video",
      "title": "Interview with a founder",
      "filename": "interview.mp4",
      "origin": "upload",
      "mimeType": "video/mp4",
      "sizeBytes": 734003200,
      "durationSec": 1800,
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "hasAudio": true,
      "createdAt": "2026-09-01T09:00:00.000Z",
      "deletesAt": null
    }
  ],
  "hasMore": false,
  "nextCursor": null
}

The list shows your videos with the newest first, so the first one is the video you uploaded last. The example keeps its id in assetId. If you uploaded something else since, use the id the upload guide gave you, or add search with part of the title.

Create a project

create_project (POST /v1/projects). Create a project on an uploaded video.

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

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"
  }
}

A project holds the video and the choices for the analysis: the language, the number of clips, how long a clip may be, the objective, the caption style and the framing. Change them in the body. get_setting_options (GET /v1/reference/setting-options) lists the values each setting accepts.

A setting you leave out is named in missingSettings. Choose it with update_project (PATCH /v1/projects/{projectId}) before you start the analysis. This call uses no credits.

The example creates the Idempotency-Key once, before the request. If the request times out, send it again with the same key and you get the same project back. A new key would create a second project.

Check the price

estimate_analysis (POST /v1/projects/{projectId}/analyze/estimate). Price an analysis before starting it.

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

HTTP 200
{
  "estimate": {
    "credits": 30,
    "creditsToCharge": 30,
    "sourceDurationSec": 1800,
    "runningJobId": null,
    "creditsAvailable": 640,
    "planRequired": false,
    "enough": true,
    "payer": "plan",
    "bonusRemaining": null
  }
}

estimate.credits is the price of the analysis. estimate.enough says whether the account can pay it now, and estimate.payer says who would pay. Nothing is charged and nothing is reserved.

If enough is false, stop here and read Handle running out of credits. Estimate the cost before you spend credits explains every field of the answer.

Start the analysis

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"
}

The credits are charged now. The answer comes back at once with the job to follow. creditsCharged says what this request was charged and paidWith says who paid.

If an analysis of the project is already running, you get that job back, reused is true and nothing is charged. If this request times out, send it again with the same Idempotency-Key. A retry never charges twice.

Wait for the analysis

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"
  }
}

The wait value holds the request until the job ends or the wait time passes. If status is queued or running, send the same request again. When status is done, go on to the clips. When it is failed or cancelled, errorMessage says what happened. Wait for long jobs explains this loop.

List the clips

list_clips (GET /v1/projects/{projectId}/clips). Only the clips waiting for a decision.

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

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
}

The clips come in the order the analysis ranked them. Each one has a title, a hook, its place in the source video and a score. The example keeps the first clip.

If hasMore is true, send nextCursor back as cursor to read the next page. Pagination and idempotency shows how. To read one clip on its own, call get_clip (GET /v1/clips/{clipId}).

Accept the clip

accept_clip (POST /v1/clips/{clipId}/accept). Accept a proposed clip.

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

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"
  }
}

Accepting turns the clip into a composition, the vertical video cut from the source with captions and the hook. It uses no credits. Accepting a clip that is already accepted returns the same composition again.

To drop a clip you do not want, call reject_clip (POST /v1/clips/{clipId}/reject).

Wait for the framing

get_composition (GET /v1/compositions/{compositionId}). Get a composition.

Request example
curl "https://api.gpt-video.com/v1/compositions/$COMPOSITION_ID" \
  -H "Authorization: Bearer $GPT_VIDEO_API_KEY"
const res = await fetch(`https://api.gpt-video.com/v1/compositions/${compositionId}`, {
  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/compositions/{composition_id}",
    headers={
        "Authorization": f"Bearer {os.environ['GPT_VIDEO_API_KEY']}",
    },
)
data = r.json()
print(data)
Example response

GET /v1/compositions/cmp_4e8c2a6f09d14b73a1e5

HTTP 200
{
  "composition": {
    "id": "cmp_4e8c2a6f09d14b73a1e5",
    "projectId": "prj_7c2e9a4f1b3d4e8096a5",
    "clipId": "cnd_9e04a6c3d12b4f78a5b9",
    "kind": "autoclip",
    "title": "Paid before the product",
    "revision": 0,
    "durationSec": 30,
    "width": 1080,
    "height": 1920,
    "fps": 30,
    "source": {
      "assetId": "ast_3f9a1c7e5b2d4086a1c4",
      "startSec": 120,
      "endSec": 150
    },
    "hook": "They paid us before we had a product",
    "captions": {
      "style": "word-pop",
      "wordCount": 75
    },
    "framing": {
      "status": "centered",
      "jobId": null
    },
    "createdAt": "2026-09-01T10:00:00.000Z",
    "updatedAt": "2026-09-01T10:00:00.000Z"
  }
}

Right after you accept a clip, GPT-Video is still following the face in it, and framing.status is tracking. Read the composition again until the status changes. An export asked for earlier answers NOT_READY.

Export the clip

create_export (POST /v1/compositions/{compositionId}/exports). Export a composition.

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

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"
}

The answer comes back at once with the export, queued. It has no download link yet. An export counts against your daily export limit and uses no plan credits. When the API bonus pays for it, it uses bonus credits.

If the framing is not done, the answer is NOT_READY. Wait the seconds in the Retry-After header and send the same request again with the same Idempotency-Key. Asking again while the same version is exporting returns that export and uses nothing.

Download the file

get_export (GET /v1/exports/{exportId}). Wait for an export to finish.

Request example
curl "https://api.gpt-video.com/v1/exports/$EXPORT_ID?wait=25" \
  -H "Authorization: Bearer $GPT_VIDEO_API_KEY"
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();
console.log(data);
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()
print(data)
Example response

GET /v1/exports/rnd_0f6b3e9d2c7a4158b4a3?wait=25

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
  }
}

Send the request again while status is queued or rendering. When status is done, downloadUrl is a link to the MP4 file. The link works without your API key, so do not send the key to it. Save the response body as an .mp4 file.

The link is short-lived, and downloadUrlExpiresAt says until when. Read the export again right before you download to get a fresh link. If the export failed, errorMessage says why. Export the composition again with a new Idempotency-Key.

Next steps