---
title: "Upload a video file with the GPT-Video API"
description: "Send a video to GPT-Video in parts. Create the upload, send each part, resume after a failure and complete it to get the id of the video."
url: "https://www.gpt-video.com/developers/guides/upload-a-video"
---

# Upload a video file

A video file goes to GPT-Video in parts. You start an upload, send each part, and complete the upload when every part has arrived. The answer gives you the id of the video, which [create_project](/developers/api/create-project.md) (`POST /v1/projects`) takes as the source of a project.

The steps below show one call each. The script at the end of the page does the whole upload. It sends again the parts that fail, and it can resume after a restart.

## Before you start

The examples read your key from the `GPT_VIDEO_API_KEY` variable, as [Authentication](/developers/authentication.md) explains. The key needs the `clips:write` scope. The steps use variables, so an id from one answer feeds the next call.

GPT-Video reads MP4, MOV, MKV, WebM, AVI and MPEG-TS files, and MP4 and MOV work best. Convert a video in another format before you upload it. [create_upload](/developers/api/create-upload.md) (`POST /v1/uploads`) lists the largest file you can send.

Uploading uses no credits. The file counts toward the storage of your account.

## 1. Start the upload

Send the name of the file and its exact size in bytes. The answer says how many parts to send in `partCount`, and how big each part is in `partSizeBytes`. Every part has that size except the last one, which holds the rest of the file. Read both values from the answer every time.

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

Keep the upload id. You need it for every other step, and to resume the upload later.

## 2. Send the parts

Send each part as raw bytes, with `Content-Type: application/octet-stream` and no JSON around them. Part 1 starts at byte 0 of the file, part 2 starts at byte `partSizeBytes`, and so on. Parts can go in any order and in parallel. Sending a part again replaces it.

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

The example sends a file called `part-1.bin` that holds the first part. The script at the end reads each part straight from the video instead.

## 3. Check which parts arrived

When a part fails, or your program restarts, read the upload. `receivedParts` lists the parts that arrived whole. Send again only the parts that are missing from it.

[`get_upload`](/developers/api/get-upload.md) (`GET /v1/uploads/{uploadId}`). Check which parts arrived.

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

```ts title="TypeScript"
const res = await fetch(`https://api.gpt-video.com/v1/uploads/${uploadId}`, {
  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/uploads/{upload_id}",
    headers={
        "Authorization": f"Bearer {os.environ['GPT_VIDEO_API_KEY']}",
    },
)
data = r.json()
print(data)
```

**Example response**

`GET /v1/uploads/upl_e7b04c92a15d4f36b8c7`

```json title="HTTP 200"
{
  "upload": {
    "id": "upl_e7b04c92a15d4f36b8c7",
    "filename": "teaser.mp4",
    "contentType": "video/mp4",
    "sizeBytes": 4096,
    "partSizeBytes": 8388608,
    "partCount": 1,
    "receivedParts": [
      1
    ],
    "createdAt": "2026-09-01T10:40:00.000Z",
    "expiresAt": "2026-09-01T16:41:00.000Z"
  }
}
```

An upload with no activity is removed when its `expiresAt` passes. Every part that arrives moves that time later.

## 4. Complete the upload

When `receivedParts` lists every part, complete the upload. GPT-Video checks the file and turns it into a video of your account, called 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
  }
}
```

Send an `Idempotency-Key` with this call. After a success the upload no longer exists, so a retry without a key answers `NOT_FOUND`. A retry with the same key returns the same asset.

If parts are missing, the answer is `CONFLICT` and the message names them. Send them, then complete the upload again. If the file is not a video GPT-Video can read, the answer is `VALIDATION_FAILED` and the upload stays open. Cancel it with [cancel_upload](/developers/api/cancel-upload.md) (`DELETE /v1/uploads/{uploadId}`), and upload the file again as MP4 or MOV.

## 5. Use the video

`assetId` now holds the id of the video. Pass it as `sourceAssetId` to [create_project](/developers/api/create-project.md) (`POST /v1/projects`) to make clips from it. [Turn a long video into clips](/developers/guides/clips-from-a-video.md) goes on from there.

## The whole upload in one script

The script starts the upload and prints its id. It sends every part, reads the upload again, and sends the parts that did not arrive, for up to three rounds. Then it completes the upload and prints the id of the asset.

If the script stops halfway, run it again with the `UPLOAD_ID` variable set to the id it printed. It reads the upload with `get_upload` (`GET /v1/uploads/{uploadId}`) and sends only the parts that are missing.

Change `keynote.mp4` to the path of your file. The curl version needs `jq`. The TypeScript has no type annotations, so it also runs as JavaScript. Save it as `upload.mjs` and run it with Node 20 or later. The Python version needs the `requests` package.

```bash title="curl"
#!/usr/bin/env bash
# This script needs curl and jq.
set -o pipefail
FILE="keynote.mp4"
API="https://api.gpt-video.com/v1"
AUTH="Authorization: Bearer $GPT_VIDEO_API_KEY"

# 1. Start the upload with the exact size of the file, or read it again to resume.
if [ -n "$UPLOAD_ID" ]; then
  UPLOAD=$(curl -sS --fail-with-body "$API/uploads/$UPLOAD_ID" -H "$AUTH") || { echo "$UPLOAD"; exit 1; }
else
  # Create the key once. Send this same key again if you retry this request.
  IDEMPOTENCY_KEY=$(uuidgen)
  UPLOAD=$(curl -sS --fail-with-body -X POST "$API/uploads" \
    -H "$AUTH" \
    -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
    -H "Content-Type: application/json" \
    -d "$(jq -n --arg name "$FILE" --argjson size "$(wc -c < "$FILE")" '{filename: $name, sizeBytes: $size}')") || { echo "$UPLOAD"; exit 1; }
fi
UPLOAD_ID=$(printf '%s' "$UPLOAD" | jq -r '.upload.id')
PART_SIZE=$(printf '%s' "$UPLOAD" | jq -r '.upload.partSizeBytes')
echo "Upload $UPLOAD_ID. To resume it later, set UPLOAD_ID to this id."

# 2. Send the parts that have not arrived. A part that fails is sent again in the next round.
for ROUND in 1 2 3; do
  MISSING=$(printf '%s' "$UPLOAD" | jq -r '[range(1; .upload.partCount + 1)] - .upload.receivedParts | .[]')
  [ -z "$MISSING" ] && break
  for PART in $MISSING; do
    dd if="$FILE" bs="$PART_SIZE" skip=$((PART - 1)) count=1 2>/dev/null |
      curl -sS --fail-with-body -X PUT "$API/uploads/$UPLOAD_ID/parts/$PART" \
        -H "$AUTH" \
        -H "Content-Type: application/octet-stream" \
        --data-binary @- > /dev/null ||
      echo "Part $PART did not arrive. It is sent again in the next round."
  done
  # Read the upload again. receivedParts lists the parts that arrived.
  UPLOAD=$(curl -sS --fail-with-body "$API/uploads/$UPLOAD_ID" -H "$AUTH") || { echo "$UPLOAD"; exit 1; }
done
MISSING=$(printf '%s' "$UPLOAD" | jq -r '[range(1; .upload.partCount + 1)] - .upload.receivedParts | .[]')
if [ -n "$MISSING" ]; then
  echo "Parts still missing after 3 rounds. Run the script again with UPLOAD_ID=$UPLOAD_ID"
  exit 1
fi

# 3. Complete the upload. The answer holds the asset that create_project takes.
# Create the key once. Send this same key again if you retry this request.
IDEMPOTENCY_KEY=$(uuidgen)
ASSET=$(curl -sS --fail-with-body -X POST "$API/uploads/$UPLOAD_ID/complete" \
  -H "$AUTH" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title": "Keynote"}') || { echo "$ASSET"; exit 1; }
ASSET_ID=$(printf '%s' "$ASSET" | jq -r '.asset.id')
echo "Asset $ASSET_ID is ready."
```

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

const file = "keynote.mp4";
const api = "https://api.gpt-video.com/v1";
const auth = `Bearer ${process.env.GPT_VIDEO_API_KEY}`;

// 1. Start the upload with the exact size of the file, or read it again to resume.
let res;
if (process.env.UPLOAD_ID) {
  res = await fetch(`${api}/uploads/${process.env.UPLOAD_ID}`, { headers: { Authorization: auth } });
} else {
  // Create the key once. Send this same key again if you retry this request.
  const idempotencyKey = crypto.randomUUID();
  res = await fetch(`${api}/uploads`, {
    method: "POST",
    headers: { Authorization: auth, "Idempotency-Key": idempotencyKey, "Content-Type": "application/json" },
    body: JSON.stringify({ filename: file, sizeBytes: (await stat(file)).size }),
  });
}
if (!res.ok) throw new Error(await res.text());
let upload = (await res.json()).upload;
console.log(`Upload ${upload.id}. To resume it later, set UPLOAD_ID to this id.`);

// 2. Send the parts that have not arrived. A part that fails is sent again in the next round.
const handle = await open(file);
for (let round = 1; upload.receivedParts.length < upload.partCount; round++) {
  if (round > 3) throw new Error(`Parts still missing after 3 rounds. Run the script again with UPLOAD_ID=${upload.id}`);
  for (let part = 1; part <= upload.partCount; part++) {
    if (upload.receivedParts.includes(part)) continue;
    const start = (part - 1) * upload.partSizeBytes;
    const bytes = new Uint8Array(Math.min(upload.partSizeBytes, upload.sizeBytes - start));
    await handle.read(bytes, 0, bytes.length, start);
    const sent = await fetch(`${api}/uploads/${upload.id}/parts/${part}`, {
      method: "PUT",
      headers: { Authorization: auth, "Content-Type": "application/octet-stream" },
      body: bytes,
    }).catch(() => null);
    if (!sent?.ok) console.log(`Part ${part} did not arrive. It is sent again in the next round.`);
  }
  // Read the upload again. receivedParts lists the parts that arrived.
  res = await fetch(`${api}/uploads/${upload.id}`, { headers: { Authorization: auth } });
  if (!res.ok) throw new Error(await res.text());
  upload = (await res.json()).upload;
}
await handle.close();

// 3. Complete the upload. The answer holds the asset that create_project takes.
// Create the key once. Send this same key again if you retry this request.
const completeKey = crypto.randomUUID();
res = await fetch(`${api}/uploads/${upload.id}/complete`, {
  method: "POST",
  headers: { Authorization: auth, "Idempotency-Key": completeKey, "Content-Type": "application/json" },
  body: JSON.stringify({ title: "Keynote" }),
});
if (!res.ok) throw new Error(await res.text());
const assetId = (await res.json()).asset.id;
console.log(`Asset ${assetId} is ready.`);
```

```python title="Python"
import os
import uuid

import requests

FILE = "keynote.mp4"
API = "https://api.gpt-video.com/v1"
AUTH = {"Authorization": f"Bearer {os.environ['GPT_VIDEO_API_KEY']}"}


def check(r):
    if not r.ok:
        raise RuntimeError(r.text)
    return r.json()


# 1. Start the upload with the exact size of the file, or read it again to resume.
if os.environ.get("UPLOAD_ID"):
    upload = check(requests.get(f"{API}/uploads/{os.environ['UPLOAD_ID']}", headers=AUTH))["upload"]
else:
    # Create the key once. Send this same key again if you retry this request.
    idempotency_key = str(uuid.uuid4())
    upload = check(requests.post(
        f"{API}/uploads",
        headers={**AUTH, "Idempotency-Key": idempotency_key},
        json={"filename": FILE, "sizeBytes": os.path.getsize(FILE)},
    ))["upload"]
print(f"Upload {upload['id']}. To resume it later, set UPLOAD_ID to this id.")

# 2. Send the parts that have not arrived. A part that fails is sent again in the next round.
with open(FILE, "rb") as f:
    rounds = 0
    while len(upload["receivedParts"]) < upload["partCount"]:
        rounds += 1
        if rounds > 3:
            raise RuntimeError(f"Parts still missing after 3 rounds. Run the script again with UPLOAD_ID={upload['id']}")
        for part in range(1, upload["partCount"] + 1):
            if part in upload["receivedParts"]:
                continue
            f.seek((part - 1) * upload["partSizeBytes"])
            try:
                sent = requests.put(
                    f"{API}/uploads/{upload['id']}/parts/{part}",
                    headers={**AUTH, "Content-Type": "application/octet-stream"},
                    data=f.read(upload["partSizeBytes"]),
                ).ok
            except requests.RequestException:
                sent = False
            if not sent:
                print(f"Part {part} did not arrive. It is sent again in the next round.")
        # Read the upload again. receivedParts lists the parts that arrived.
        upload = check(requests.get(f"{API}/uploads/{upload['id']}", headers=AUTH))["upload"]

# 3. Complete the upload. The answer holds the asset that create_project takes.
# Create the key once. Send this same key again if you retry this request.
idempotency_key = str(uuid.uuid4())
asset = check(requests.post(
    f"{API}/uploads/{upload['id']}/complete",
    headers={**AUTH, "Idempotency-Key": idempotency_key},
    json={"title": "Keynote"},
))["asset"]
print(f"Asset {asset['id']} is ready.")
```
