---
title: "Add a video to an edit | GPT-Video API reference"
description: "Adds another video you uploaded to a prepared edit, as one segment of its whole length, and charges it once."
url: "https://www.gpt-video.com/developers/api/add-timeline-clip"
operation: "add_timeline_clip"
method: "POST"
path: "/v1/projects/{projectId}/timeline/clips"
scope: "spend"
---

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

# Add a video to an edit

Operation id `add_timeline_clip`. Request `POST /v1/projects/{projectId}/timeline/clips`.

Adds another video you uploaded to a prepared edit, as one segment of its whole length, and charges it once.

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

Send the revision you read with get_timeline (`GET /v1/projects/{projectId}/timeline`), the video to add and the segment to put it in front of; leave beforeSegmentId out, or send null, to add it at the end. The video is prepared in the background: until its state is ready it plays but cannot be cut by its words. A video the edit already uses, or already paid for, costs nothing. Retrying with the same Idempotency-Key never pays twice; if the first try was not written, its credits were given back and the retry answers CONFLICT, so send the change again with a new key.

## Example use

A team that cuts a talk filmed with two cameras uploads the second video, adds it before the closing segment with the revision it has read, and waits until its state is ready before cutting it by its words.

## Endpoint

```text
POST https://api.gpt-video.com/v1/projects/{projectId}/timeline/clips
```

- Scope `spend`.
- Rate limit 10 requests a minute for each key and 20 for each account, shared by every `spend` call. See [rate limits](/developers/rate-limits.md).
- `Idempotency-Key` is required.

## Parameters

### Path

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `projectId` | string | Required | The project id. It starts with prj_. Matches `^prj_[0-9a-f]{20}$`. |

### Headers

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | string | Required | This operation spends, so the key is required. Create it once, before the request, and send the same key on every retry. From 1 to 255 visible characters, such as a UUID. See [idempotency](/developers/pagination-and-idempotency.md). |

### Body

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `expectedRevision` | integer | Required | The revision of the edit you read last, from get_timeline (`GET /v1/projects/{projectId}/timeline`) or from the answer of your last change. If the edit changed since then, nothing is written and the answer is CONFLICT. At least 0. |
| `assetId` | string | Required | The video to add: the asset id that complete_upload (`POST /v1/uploads/{uploadId}/complete`) returned. Matches `^ast_n?[0-9a-f]{20}$`. |
| `beforeSegmentId` | string or null | Optional | The segment to put the video in front of. Leave it out, or send null, to add it at the end. Matches `^seg_[0-9a-f]{20}$`. |

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

## Request

Add a video at the end of an edit.

```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/projects/prj_b9c1f9c7873dcef51a57/timeline/clips" \
  -H "Authorization: Bearer $GPT_VIDEO_API_KEY" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "expectedRevision": 1,
    "assetId": "ast_8e21b0d94c6f4a17b358"
  }'
```

```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/prj_b9c1f9c7873dcef51a57/timeline/clips", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.GPT_VIDEO_API_KEY}`,
    "Idempotency-Key": idempotencyKey,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "expectedRevision": 1,
    "assetId": "ast_8e21b0d94c6f4a17b358"
  }),
});
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/projects/prj_b9c1f9c7873dcef51a57/timeline/clips",
    headers={
        "Authorization": f"Bearer {os.environ['GPT_VIDEO_API_KEY']}",
        "Idempotency-Key": idempotency_key,
    },
    json={
        "expectedRevision": 1,
        "assetId": "ast_8e21b0d94c6f4a17b358"
    },
)
data = r.json()
print(data)
```

```json title="MCP (Full)"
{
  "method": "tools/call",
  "params": {
    "name": "add_timeline_clip",
    "arguments": {
      "projectId": "prj_b9c1f9c7873dcef51a57",
      "expectedRevision": 1,
      "assetId": "ast_8e21b0d94c6f4a17b358",
      "confirm": true,
      "idempotencyKey": "REPLACE-WITH-A-NEW-UUID-FOR-THIS-CALL"
    }
  }
}
```

## Response

A successful call answers HTTP 200. Returns a [Timeline object](/developers/objects.md#timeline) in `timeline`.

### Fields to check

| Name | Type | Description |
| --- | --- | --- |
| `timeline.id` | string | The edit id. It starts with tml_. |
| `timeline.segments[].id` | string | The segment id. It starts with seg_. |
| `timeline.texts[].id` | string | The text id. It starts with ovl_. |
| `timeline.broll[].id` | string | The b-roll id. It starts with brl_. |
| `creditsCharged` | integer | The credits this request was charged: 0 when the edit already had the video, or had paid for it. |
| `paidWith` | string or null | Who paid for this request: plan is the credits or the daily allowance of the account's plan, api_bonus is the API bonus. null when this request paid nothing, because the same work was already running. A retry with the same Idempotency-Key says who paid the first time. |

### Response example

```json title="HTTP 200"
{
  "timeline": {
    "id": "tml_bbe061171a8229f8daed",
    "projectId": "prj_b9c1f9c7873dcef51a57",
    "compositionId": "cmp_cfbdd85eaaff301fa997",
    "revision": 2,
    "durationSec": 180,
    "aspect": "9:16",
    "reframe": "off",
    "captions": {
      "enabled": true,
      "style": "bold-karaoke"
    },
    "music": null,
    "fadeInSec": 0,
    "fadeOutSec": 0,
    "clips": [
      {
        "assetId": "ast_3f9a1c7e5b2d4086a1c4",
        "title": "Interview with a founder",
        "durationSec": 1800,
        "state": "ready"
      },
      {
        "assetId": "ast_8e21b0d94c6f4a17b358",
        "title": "Product demo",
        "durationSec": 120,
        "state": "preparing"
      }
    ],
    "segments": [
      {
        "id": "seg_7279fbb508f4a22e65a4",
        "assetId": "ast_3f9a1c7e5b2d4086a1c4",
        "sourceStartSec": 120,
        "sourceEndSec": 150,
        "zoom": 1,
        "muted": false,
        "volume": 1,
        "outputStartSec": 0,
        "outputEndSec": 30
      },
      {
        "id": "seg_a9dcee59c1fb9edb6751",
        "assetId": "ast_3f9a1c7e5b2d4086a1c4",
        "sourceStartSec": 300,
        "sourceEndSec": 330,
        "zoom": 1,
        "muted": false,
        "volume": 1,
        "outputStartSec": 30,
        "outputEndSec": 60
      },
      {
        "id": "seg_c3f81a6d0e5b9472d1a8",
        "assetId": "ast_8e21b0d94c6f4a17b358",
        "sourceStartSec": 0,
        "sourceEndSec": 120,
        "outputStartSec": 60,
        "outputEndSec": 180,
        "zoom": 1,
        "muted": false,
        "volume": 1
      }
    ],
    "texts": [
      {
        "id": "ovl_49f5239b9cd73496225d",
        "text": "Paid before the product",
        "preset": "headline",
        "outputStartSec": 0,
        "durationSec": 3,
        "xPercent": null,
        "yPercent": null
      }
    ],
    "broll": [
      {
        "id": "brl_f44d18e008ec4009bf10",
        "assetId": "ast_8ac5d8b97546145cdbf6",
        "outputStartSec": 12,
        "durationSec": 4,
        "fit": "cover"
      }
    ],
    "diagnostics": []
  },
  "creditsCharged": 2,
  "paidWith": "plan",
  "clip": {
    "assetId": "ast_8e21b0d94c6f4a17b358",
    "state": "preparing"
  }
}
```

**All response fields**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `timeline` | Timeline object | Required | Described in [Timeline object](/developers/objects.md#timeline). |
| `creditsCharged` | integer | Required | The credits this request was charged: 0 when the edit already had the video, or had paid for it. At least 0. |
| `paidWith` | string or null | Required | Who paid for this request: plan is the credits or the daily allowance of the account's plan, api_bonus is the API bonus. null when this request paid nothing, because the same work was already running. A retry with the same Idempotency-Key says who paid the first time. One of `plan`, `api_bonus`. |
| `clip` | object | Required |   |
| `clip.assetId` | string | Required | The video that was added. |
| `clip.state` | string | Required | ready can be cut, preparing is still being prepared, failed could not be prepared and is not prepared again by itself. One of `ready`, `preparing`, `failed`. |

**More examples**

#### Add a video in front of a segment

`POST /v1/projects/prj_b9c1f9c7873dcef51a57/timeline/clips`

```json title="Request body"
{
  "expectedRevision": 1,
  "assetId": "ast_8e21b0d94c6f4a17b358",
  "beforeSegmentId": "seg_a9dcee59c1fb9edb6751"
}
```

```json title="HTTP 200"
{
  "timeline": {
    "id": "tml_bbe061171a8229f8daed",
    "projectId": "prj_b9c1f9c7873dcef51a57",
    "compositionId": "cmp_cfbdd85eaaff301fa997",
    "revision": 2,
    "durationSec": 180,
    "aspect": "9:16",
    "reframe": "off",
    "captions": {
      "enabled": true,
      "style": "bold-karaoke"
    },
    "music": null,
    "fadeInSec": 0,
    "fadeOutSec": 0,
    "clips": [
      {
        "assetId": "ast_3f9a1c7e5b2d4086a1c4",
        "title": "Interview with a founder",
        "durationSec": 1800,
        "state": "ready"
      },
      {
        "assetId": "ast_8e21b0d94c6f4a17b358",
        "title": "Product demo",
        "durationSec": 120,
        "state": "preparing"
      }
    ],
    "segments": [
      {
        "id": "seg_7279fbb508f4a22e65a4",
        "assetId": "ast_3f9a1c7e5b2d4086a1c4",
        "sourceStartSec": 120,
        "sourceEndSec": 150,
        "zoom": 1,
        "muted": false,
        "volume": 1,
        "outputStartSec": 0,
        "outputEndSec": 30
      },
      {
        "id": "seg_c3f81a6d0e5b9472d1a8",
        "assetId": "ast_8e21b0d94c6f4a17b358",
        "sourceStartSec": 0,
        "sourceEndSec": 120,
        "outputStartSec": 30,
        "outputEndSec": 150,
        "zoom": 1,
        "muted": false,
        "volume": 1
      },
      {
        "id": "seg_a9dcee59c1fb9edb6751",
        "assetId": "ast_3f9a1c7e5b2d4086a1c4",
        "sourceStartSec": 300,
        "sourceEndSec": 330,
        "zoom": 1,
        "muted": false,
        "volume": 1,
        "outputStartSec": 150,
        "outputEndSec": 180
      }
    ],
    "texts": [
      {
        "id": "ovl_49f5239b9cd73496225d",
        "text": "Paid before the product",
        "preset": "headline",
        "outputStartSec": 0,
        "durationSec": 3,
        "xPercent": null,
        "yPercent": null
      }
    ],
    "broll": [
      {
        "id": "brl_f44d18e008ec4009bf10",
        "assetId": "ast_8ac5d8b97546145cdbf6",
        "outputStartSec": 12,
        "durationSec": 4,
        "fit": "cover"
      }
    ],
    "diagnostics": []
  },
  "creditsCharged": 2,
  "paidWith": "plan",
  "clip": {
    "assetId": "ast_8e21b0d94c6f4a17b358",
    "state": "preparing"
  }
}
```

#### The edit changed since you read it

`POST /v1/projects/prj_b9c1f9c7873dcef51a57/timeline/clips`

```json title="Request body"
{
  "expectedRevision": 0,
  "assetId": "ast_8e21b0d94c6f4a17b358"
}
```

```json title="HTTP 409"
{
  "error": {
    "code": "CONFLICT",
    "message": "This edit changed since revision 0; its revision is now 1.",
    "hint": "Read it again with get_timeline, then send the operations with its current revision.",
    "requestId": "req_7d3b0f9a2e6c15d84b7a0e63"
  }
}
```

#### The video does not exist

`POST /v1/projects/prj_b9c1f9c7873dcef51a57/timeline/clips`

```json title="Request body"
{
  "expectedRevision": 1,
  "assetId": "ast_0b1e5c9d3a7f40628e2d"
}
```

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

## Errors

| Code | Status | When | What to do |
| --- | --- | --- | --- |
| [`INSUFFICIENT_CREDITS`](/developers/errors.md#INSUFFICIENT_CREDITS) | 402 | See the error reference. | Stop and tell the user. |
| [`PLAN_REQUIRED`](/developers/errors.md#PLAN_REQUIRED) | 402 | See the error reference. | Stop and tell the user. |
| [`NOT_FOUND`](/developers/errors.md#NOT_FOUND) | 404 | The video does not exist | Change the request, then send it again. |
| [`CONFLICT`](/developers/errors.md#CONFLICT) | 409 | The edit changed since you read it | Read the resource again, then decide. Do not send the same request again. |
| [`NOT_READY`](/developers/errors.md#NOT_READY) | 409 | See the error reference. | Wait for Retry-After, then send the same request again. |
| [`DAILY_LIMIT_REACHED`](/developers/errors.md#DAILY_LIMIT_REACHED) | 429 | See the error reference. | Stop until the limit resets at 00:00 UTC. |

Every call can also answer these errors. [`VALIDATION_FAILED`](/developers/errors.md#VALIDATION_FAILED), [`UNKNOWN_PARAMETER`](/developers/errors.md#UNKNOWN_PARAMETER), [`IDEMPOTENCY_KEY_REQUIRED`](/developers/errors.md#IDEMPOTENCY_KEY_REQUIRED), [`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

One credit per started minute of the added video, charged once per video in an edit; adding the same video again costs no credits.

- Paid by the API bonus, it uses the same number of bonus credits.
- See [API pricing](/developers/pricing.md) for who pays first.

## Related operations

- [`get_timeline`](/developers/api/get-timeline.md) (`GET /v1/projects/{projectId}/timeline`) Get an edit
- [`apply_timeline_operations`](/developers/api/apply-timeline-operations.md) (`POST /v1/projects/{projectId}/timeline/operations`) Change an edit
- [`estimate_analysis`](/developers/api/estimate-analysis.md) (`POST /v1/projects/{projectId}/analyze/estimate`) Estimate the credits of an analysis or a build
- [`get_credits`](/developers/api/get-credits.md) (`GET /v1/credits`) Get the credit balance
- [`restore_timeline`](/developers/api/restore-timeline.md) (`POST /v1/projects/{projectId}/timeline/restore`) Restore a revision of an edit

## Questions

### Why do I get CONFLICT with a revision number?

The edit changed after you read it: another call, the Editor Assistant or someone in the GPT-Video app wrote a new revision. Read it again with get_timeline (`GET /v1/projects/{projectId}/timeline`), then send your change with the revision it shows.

### What happens if my request times out and I send it again?

Send it again with the same Idempotency-Key and the same expectedRevision: the retry never pays twice. If the first try charged the video and was not written, the credits were given back and the retry answers CONFLICT; read the edit again and send the change with a new Idempotency-Key.
