---
title: "Complete an upload | GPT-Video API reference"
description: "Turns an upload whose parts all arrived into an asset that a project can use as its source."
url: "https://www.gpt-video.com/developers/api/complete-upload"
operation: "complete_upload"
method: "POST"
path: "/v1/uploads/{uploadId}/complete"
scope: "clips:write"
---

Do not assume endpoints have parameters other than the ones explicitly specified.

# Complete an upload

Operation id `complete_upload`. Request `POST /v1/uploads/{uploadId}/complete`.

Turns an upload whose parts all arrived into an asset that a project can use as its source.

Beta. This endpoint is supported and can gain new fields. If a change would break your code, we tell you before it takes effect and list it in the changelog.

## About

Call it once receivedParts lists every part. The file is checked and becomes an asset; pass its id to create_project (`POST /v1/projects`) to analyze it. If parts are missing, nothing changes and the answer names them. The first bytes of the file are checked before anything else: only MP4, MOV, MKV, WebM, AVI and MPEG-TS files go on. A playlist (M3U8), a DASH manifest, a web page or any other text file is refused, and the upload stays open: cancel it with cancel_upload (`DELETE /v1/uploads/{uploadId}`). If the file passes that check but cannot be read as a video, the upload also stays open with all its parts: call complete_upload (`POST /v1/uploads/{uploadId}/complete`) again, or cancel it. After a success the upload no longer exists, so a retry without an Idempotency-Key answers not found; send a key, and a retry with the same key returns the same asset.

## Example use

Once receivedParts lists every part, a script completes the upload with a title for the video. It passes the id of the new asset to create_project (`POST /v1/projects`) to start an Auto Clip project on it.

## Endpoint

```text
POST https://api.gpt-video.com/v1/uploads/{uploadId}/complete
```

- Scope `clips:write`.
- Rate limit 60 requests a minute for each key and 120 for each account, shared by every `write` call. See [rate limits](/developers/rate-limits.md).
- `Idempotency-Key` is optional.

## Parameters

### Path

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `uploadId` | string | Required | The id of the upload, as create_upload (`POST /v1/uploads`) returned it. Matches `^upl_[0-9a-f]{20}$`. |

### Headers

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | string | Optional | Send the same key on every retry of this request. From 1 to 255 visible characters, such as a UUID. See [idempotency](/developers/pagination-and-idempotency.md). |

### Body

The body is optional. Send only the fields you want to change.

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `title` | string | Optional | The title of the video in the app. Leave it out to use the name of the file without its extension. From 1 to 200 characters. |

Send only the fields listed here. Any other field is refused with UNKNOWN_PARAMETER.

## Request

Complete an upload.

```bash title="curl"
# Create the key once. Send this same key again if you retry this request.
IDEMPOTENCY_KEY=$(uuidgen)
curl -X POST "https://api.gpt-video.com/v1/uploads/upl_e7b04c92a15d4f36b8c7/complete" \
  -H "Authorization: Bearer $GPT_VIDEO_API_KEY" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Product teaser"
  }'
```

```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/upl_e7b04c92a15d4f36b8c7/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();
console.log(data);
```

```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/upl_e7b04c92a15d4f36b8c7/complete",
    headers={
        "Authorization": f"Bearer {os.environ['GPT_VIDEO_API_KEY']}",
        "Idempotency-Key": idempotency_key,
    },
    json={
        "title": "Product teaser"
    },
)
data = r.json()
print(data)
```

```json title="MCP (Full)"
{
  "method": "tools/call",
  "params": {
    "name": "complete_upload",
    "arguments": {
      "uploadId": "upl_e7b04c92a15d4f36b8c7",
      "title": "Product teaser"
    }
  }
}
```

## Response

A successful call answers HTTP 201.

### Fields to check

| Name | Type | Description |
| --- | --- | --- |
| `asset.id` | string | The asset id. It starts with ast_. |

### Response example

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

**All response fields**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `asset` | object | Required | The new asset. Its id is what create_project (`POST /v1/projects`) takes as sourceAssetId. |
| `asset.id` | string | Required | The asset id. It starts with ast_. |
| `asset.kind` | string | Required | One of `video`, `audio`, `image`, `font`, `other`. |
| `asset.title` | string | Required |   |
| `asset.filename` | string or null | Required | The name of the file as it was uploaded, when there was one. |
| `asset.origin` | string | Required | upload was sent by you, link was imported from a link, converted is a playable copy made for the analysis, export is the file of an export, voice is the audio of a voiceover, separated is the audio an audio separation kept. One of `upload`, `link`, `converted`, `export`, `voice`, `separated`. |
| `asset.mimeType` | string | Required |   |
| `asset.sizeBytes` | integer | Required |   |
| `asset.durationSec` | number or null | Required |   |
| `asset.width` | integer or null | Required | The width as the video is shown, rotation applied. |
| `asset.height` | integer or null | Required | The height as the video is shown, rotation applied. |
| `asset.fps` | number or null | Required |   |
| `asset.hasAudio` | boolean or null | Required |   |
| `asset.createdAt` | string | Required | An ISO 8601 time in UTC. |
| `asset.deletesAt` | string or null | Required | The first time the file may be deleted to free space: files are kept for a limited time. null when nothing deletes it. |

**More examples**

#### Complete an upload with parts missing

`POST /v1/uploads/upl_5d2a8f1e6c3b4079e0a1/complete`

```json title="HTTP 409"
{
  "error": {
    "code": "CONFLICT",
    "message": "Parts 1, 2 and 3 are missing. Send them, then complete the upload again.",
    "requestId": "req_4d9f3b7e1a5c0d8b2f6e9a3c"
  }
}
```

#### Complete an upload that is not a video

`POST /v1/uploads/upl_e7b04c92a15d4f36b8c7/complete`

```json title="HTTP 400"
{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "We could not read this file as a video. If it is a video, call complete_upload again; otherwise cancel this upload with cancel_upload and upload the file again as MP4 or MOV.",
    "hint": "Fix the request as the message says, then send it again.",
    "requestId": "req_1a7c5e9b3d0f6a2c8e4b7d1f"
  }
}
```

#### Complete an upload whose file is a playlist

`POST /v1/uploads/upl_e7b04c92a15d4f36b8c7/complete`

```json title="HTTP 400"
{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "This file is not a video file GPT-Video can read. Cancel this upload with cancel_upload and upload the file again as MP4 or MOV.",
    "hint": "Fix the request as the message says, then send it again.",
    "requestId": "req_6b2e8d4a0c7f3e9b1d5a8c2f"
  }
}
```

#### Complete an upload that does not exist

`POST /v1/uploads/upl_3b6f9e2d7c0a4e51a8d9/complete`

```json title="HTTP 404"
{
  "error": {
    "code": "NOT_FOUND",
    "message": "Upload upl_3b6f9e2d7c0a4e51a8d9 not found",
    "hint": "Check the id. An id that belongs to another account is not found either.",
    "requestId": "req_8f2c6a0e4b9d3f7a1c5e8b2d"
  }
}
```

## Errors

| Code | Status | When | What to do |
| --- | --- | --- | --- |
| [`NOT_FOUND`](/developers/errors.md#NOT_FOUND) | 404 | Complete an upload that does not exist | Change the request, then send it again. |
| [`CONFLICT`](/developers/errors.md#CONFLICT) | 409 | Complete an upload with parts missing | Read the resource again, then decide. Do not send the same request again. |

Every call can also answer these errors. [`VALIDATION_FAILED`](/developers/errors.md#VALIDATION_FAILED), [`UNKNOWN_PARAMETER`](/developers/errors.md#UNKNOWN_PARAMETER), [`UNAUTHORIZED`](/developers/errors.md#UNAUTHORIZED), [`SCOPE_MISSING`](/developers/errors.md#SCOPE_MISSING), [`IDEMPOTENCY_IN_PROGRESS`](/developers/errors.md#IDEMPOTENCY_IN_PROGRESS), [`PAYLOAD_TOO_LARGE`](/developers/errors.md#PAYLOAD_TOO_LARGE), [`IDEMPOTENCY_KEY_REUSED`](/developers/errors.md#IDEMPOTENCY_KEY_REUSED), [`RATE_LIMITED`](/developers/errors.md#RATE_LIMITED), [`INTERNAL`](/developers/errors.md#INTERNAL), [`UNAVAILABLE`](/developers/errors.md#UNAVAILABLE).

## Cost

No credits are used. Analyzing the video later is what uses credits.

## Related operations

- [`create_upload`](/developers/api/create-upload.md) (`POST /v1/uploads`) Start an upload
- [`upload_part`](/developers/api/upload-part.md) (`PUT /v1/uploads/{uploadId}/parts/{partNumber}`) Send a part of an upload
- [`get_upload`](/developers/api/get-upload.md) (`GET /v1/uploads/{uploadId}`) Get an upload
- [`cancel_upload`](/developers/api/cancel-upload.md) (`DELETE /v1/uploads/{uploadId}`) Cancel an upload
- [`get_asset`](/developers/api/get-asset.md) (`GET /v1/assets/{assetId}`) Get an asset
- [`create_project`](/developers/api/create-project.md) (`POST /v1/projects`) Create a project

## Questions

### Does completing an upload start the analysis?

No. It only creates the asset. Create a project with the asset as its source, then analyze the project.

### What if the file is not a readable video?

The answer is a validation error and the upload stays open. If the start of the file is not a video format GPT-Video reads (MP4, MOV, MKV, WebM, AVI or MPEG-TS), calling again gives the same answer: cancel the upload and send the file again as MP4 or MOV. If the format is right but the file could not be read, call complete_upload (`POST /v1/uploads/{uploadId}/complete`) again; if it still fails, cancel the upload and send the file again.
