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 (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 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 (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 (POST /v1/uploads). Start the upload of a 20 MB video.
# 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')// 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;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
{
"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 (PUT /v1/uploads/{uploadId}/parts/{partNumber}). Send the first part.
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.binimport { 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);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
{
"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 (GET /v1/uploads/{uploadId}). Check which parts arrived.
curl "https://api.gpt-video.com/v1/uploads/$UPLOAD_ID" \
-H "Authorization: Bearer $GPT_VIDEO_API_KEY"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);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
{
"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 (POST /v1/uploads/{uploadId}/complete). Complete an upload.
# 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')// 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;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
{
"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 (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 (POST /v1/projects) to make clips from it. Turn a long video into clips 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.
#!/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."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.`);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.")