---
title: "Change an edit | GPT-Video API reference"
description: "Applies a list of operations to an edit in one change: cuts, order, transitions, zoom, sound, captions, texts, b-roll, music and shape."
url: "https://www.gpt-video.com/developers/api/apply-timeline-operations"
operation: "apply_timeline_operations"
method: "POST"
path: "/v1/projects/{projectId}/timeline/operations"
scope: "clips:write"
---

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

# Change an edit

Operation id `apply_timeline_operations`. Request `POST /v1/projects/{projectId}/timeline/operations`.

Applies a list of operations to an edit in one change: cuts, order, transitions, zoom, sound, captions, texts, b-roll, music and shape.

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`) and from 1 to 50 operations: they apply in order, as one change and one new revision, or not at all. Segments, texts and b-roll are named by the ids get_timeline shows; times of segments are seconds of the source video, times of texts and b-roll are seconds of the video that comes out. Adding a video is not an operation here, because it costs credits: use add_timeline_clip (`POST /v1/projects/{projectId}/timeline/clips`). The answer is the edit after the change, with warnings that say what was adjusted.

## Example use

An editing tool lets a person remove a long pause and add a title text. It sends both as one list of operations with the revision it read, then shows the warnings of the answer next to the new edit.

## Endpoint

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

- 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 |
| --- | --- | --- | --- |
| `projectId` | string | Required | The project id. It starts with prj_. Matches `^prj_[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

| 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. |
| `operations` | array of objects | Required | The operations, from 1 to 50, applied in this order. The op field says which one each is. From 1 to 50 items. |

Each item of `operations` is one of 21 variants, chosen by `op`. Send exactly one variant for each item.

#### op = "split_segment"

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `operations[].op` | string | Required | Always `split_segment`. |
| `operations[].segmentId` | string | Required | The segment id. It starts with seg_. Matches `^seg_[0-9a-f]{20}$`. |
| `operations[].atSourceSec` | number | Required | Where to cut, in seconds of the source video. More than 0. |

#### op = "trim_segment"

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `operations[].op` | string | Required | Always `trim_segment`. |
| `operations[].segmentId` | string | Required | The segment id. It starts with seg_. Matches `^seg_[0-9a-f]{20}$`. |
| `operations[].sourceStartSec` | number | Optional | The new start, in seconds of the source video. At least 0. |
| `operations[].sourceEndSec` | number | Optional | The new end, in seconds of the source video. More than 0. |

#### op = "remove_segments"

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `operations[].op` | string | Required | Always `remove_segments`. |
| `operations[].segmentIds` | array of strings | Required | The segment ids, from 1 to 200. From 1 to 200 items. |

#### op = "move_segment"

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `operations[].op` | string | Required | Always `move_segment`. |
| `operations[].segmentId` | string | Required | The segment id. It starts with seg_. Matches `^seg_[0-9a-f]{20}$`. |
| `operations[].beforeSegmentId` | string or null | Required | The segment to move it in front of; null moves it to the end. Matches `^seg_[0-9a-f]{20}$`. |

#### op = "set_transition"

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `operations[].op` | string | Required | Always `set_transition`. |
| `operations[].segmentId` | string | Required | The segment id. It starts with seg_. Matches `^seg_[0-9a-f]{20}$`. |
| `operations[].kind` | string | Required | One of `dip`, `dissolve`. |
| `operations[].sec` | number | Required | How long the transition into this segment lasts, from 0 to 3 seconds; 0 removes it. From 0 to 3. |

#### op = "set_video_fade"

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `operations[].op` | string | Required | Always `set_video_fade`. |
| `operations[].edge` | string | Required | One of `start`, `end`. |
| `operations[].sec` | number | Required | How long the fade lasts, from 0 to 3 seconds; 0 removes it. From 0 to 3. |

#### op = "set_zoom"

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `operations[].op` | string | Required | Always `set_zoom`. |
| `operations[].segmentIds` | array of strings | Required | The segment ids, from 1 to 200. From 1 to 200 items. |
| `operations[].factor` | number | Required | 1 removes the zoom; up to 1.5. From 1 to 1.5. |
| `operations[].mode` | string | Required | One of `static`, `push`. |

#### op = "set_segment_audio"

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `operations[].op` | string | Required | Always `set_segment_audio`. |
| `operations[].segmentIds` | array of strings | Required | The segment ids, from 1 to 200. From 1 to 200 items. |
| `operations[].volume` | number | Optional | From 0 to 2. |
| `operations[].muted` | boolean | Optional |   |

#### op = "remove_silences"

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `operations[].op` | string | Required | Always `remove_silences`. |
| `operations[].aggressiveness` | string | Required | One of `soft`, `standard`, `aggressive`. |

#### op = "remove_fillers"

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `operations[].op` | string | Required | Always `remove_fillers`. |

#### op = "set_captions"

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `operations[].op` | string | Required | Always `set_captions`. |
| `operations[].enabled` | boolean | Optional |   |
| `operations[].style` | string | Optional | One of `bold-karaoke`, `word-pop`, `boxed`, `outline`. |
| `operations[].options` | object | Optional | The caption options to change. A field you leave out stays as it is. |
| `operations[].options.fontFamily` | string | Optional | The typeface of the captions. One of `Montserrat`, `Anton`, `System`. |
| `operations[].options.fontSize` | integer | Optional | The size of the caption text, from 20 to 200. From 20 to 200. |
| `operations[].options.yPercent` | number | Optional | Where the captions sit, from 0 at the top to 100 at the bottom. From 0 to 100. |
| `operations[].options.color` | string | Optional | The color of the words, as #RRGGBB. Matches `^#[0-9A-Fa-f]{6}$`. |
| `operations[].options.highlightColor` | string | Optional | The color of the word being spoken, as #RRGGBB. Matches `^#[0-9A-Fa-f]{6}$`. |
| `operations[].options.maxWordsPerGroup` | integer | Optional | How many words are on screen at once, at most, from 1 to 12. From 1 to 12. |
| `operations[].options.maxCharsPerGroup` | integer | Optional | How many characters are on screen at once, at most, from 4 to 80. From 4 to 80. |
| `operations[].options.uppercase` | boolean | Optional | true shows the captions in capital letters. |
| `operations[].options.maxGapSec` | number | Optional | The longest pause, in seconds, inside one group of words on screen, from 0.1 to 5. From 0.1 to 5. |

#### op = "replace_caption_word"

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `operations[].op` | string | Required | Always `replace_caption_word`. |
| `operations[].from` | string | Required | From 1 to 200 characters. |
| `operations[].to` | string | Required | From 1 to 200 characters. |
| `operations[].occurrence` | integer | Optional | Which match to replace, from 1; leave it out to replace every match. At least 1. |

#### op = "add_text"

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `operations[].op` | string | Required | Always `add_text`. |
| `operations[].text` | string | Required | From 1 to 200 characters. |
| `operations[].preset` | string | Required | One of `headline`, `hook`, `caption-note`. |
| `operations[].outputStartSec` | number | Required | A time in the video that comes out, in seconds. At least 0. |
| `operations[].durationSec` | number | Required | A length in seconds. More than 0. |
| `operations[].xPercent` | number | Optional | The horizontal position, from 0 to 100. From 0 to 100. |
| `operations[].yPercent` | number | Optional | The vertical position, from 0 to 100. From 0 to 100. |

#### op = "update_text"

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `operations[].op` | string | Required | Always `update_text`. |
| `operations[].textId` | string | Required | The text id. It starts with ovl_. Matches `^ovl_[0-9a-f]{20}$`. |
| `operations[].text` | string | Optional | From 1 to 200 characters. |
| `operations[].preset` | string | Optional | One of `headline`, `hook`, `caption-note`. |
| `operations[].outputStartSec` | number | Optional | A time in the video that comes out, in seconds. At least 0. |
| `operations[].durationSec` | number | Optional | A length in seconds. More than 0. |
| `operations[].xPercent` | number or null | Optional | The horizontal position, from 0 to 100. From 0 to 100. |
| `operations[].yPercent` | number or null | Optional | The vertical position, from 0 to 100. From 0 to 100. |

#### op = "remove_text"

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `operations[].op` | string | Required | Always `remove_text`. |
| `operations[].textId` | string | Required | The text id. It starts with ovl_. Matches `^ovl_[0-9a-f]{20}$`. |

#### op = "add_broll"

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `operations[].op` | string | Required | Always `add_broll`. |
| `operations[].assetId` | string | Required | Matches `^ast_n?[0-9a-f]{20}$`. |
| `operations[].outputStartSec` | number | Required | A time in the video that comes out, in seconds. At least 0. |
| `operations[].durationSec` | number | Required | A length in seconds. More than 0. |
| `operations[].fit` | string | Required | One of `cover`, `contain`. |

#### op = "update_broll"

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `operations[].op` | string | Required | Always `update_broll`. |
| `operations[].brollId` | string | Required | The b-roll id. It starts with brl_. Matches `^brl_[0-9a-f]{20}$`. |
| `operations[].outputStartSec` | number | Optional | A time in the video that comes out, in seconds. At least 0. |
| `operations[].durationSec` | number | Optional | A length in seconds. More than 0. |
| `operations[].fit` | string | Optional | One of `cover`, `contain`. |

#### op = "remove_broll"

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `operations[].op` | string | Required | Always `remove_broll`. |
| `operations[].brollId` | string | Required | The b-roll id. It starts with brl_. Matches `^brl_[0-9a-f]{20}$`. |

#### op = "set_music"

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `operations[].op` | string | Required | Always `set_music`. |
| `operations[].assetId` | string or null | Required | The music file; null removes the music. Matches `^ast_n?[0-9a-f]{20}$`. |
| `operations[].volume` | number | Optional | From 0 to 0.5. |

#### op = "set_aspect"

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `operations[].op` | string | Required | Always `set_aspect`. |
| `operations[].aspect` | string | Required | One of `source`, `9:16`, `1:1`, `16:9`. |

#### op = "set_reframe"

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `operations[].op` | string | Required | Always `set_reframe`. |
| `operations[].mode` | string | Required | One of `off`, `face`, `center`. |

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

## Request

Cut a segment in two and remove another.

```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/operations" \
  -H "Authorization: Bearer $GPT_VIDEO_API_KEY" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "expectedRevision": 1,
    "operations": [
      {
        "op": "split_segment",
        "segmentId": "seg_7279fbb508f4a22e65a4",
        "atSourceSec": 135
      },
      {
        "op": "remove_segments",
        "segmentIds": [
          "seg_a9dcee59c1fb9edb6751"
        ]
      }
    ]
  }'
```

```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/operations", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.GPT_VIDEO_API_KEY}`,
    "Idempotency-Key": idempotencyKey,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "expectedRevision": 1,
    "operations": [
      {
        "op": "split_segment",
        "segmentId": "seg_7279fbb508f4a22e65a4",
        "atSourceSec": 135
      },
      {
        "op": "remove_segments",
        "segmentIds": [
          "seg_a9dcee59c1fb9edb6751"
        ]
      }
    ]
  }),
});
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/operations",
    headers={
        "Authorization": f"Bearer {os.environ['GPT_VIDEO_API_KEY']}",
        "Idempotency-Key": idempotency_key,
    },
    json={
        "expectedRevision": 1,
        "operations": [
            {
                "op": "split_segment",
                "segmentId": "seg_7279fbb508f4a22e65a4",
                "atSourceSec": 135
            },
            {
                "op": "remove_segments",
                "segmentIds": [
                    "seg_a9dcee59c1fb9edb6751"
                ]
            }
        ]
    },
)
data = r.json()
print(data)
```

```json title="MCP (Full)"
{
  "method": "tools/call",
  "params": {
    "name": "apply_timeline_operations",
    "arguments": {
      "projectId": "prj_b9c1f9c7873dcef51a57",
      "expectedRevision": 1,
      "operations": [
        {
          "op": "split_segment",
          "segmentId": "seg_7279fbb508f4a22e65a4",
          "atSourceSec": 135
        },
        {
          "op": "remove_segments",
          "segmentIds": [
            "seg_a9dcee59c1fb9edb6751"
          ]
        }
      ]
    }
  }
}
```

## 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_. |

### Response example

```json title="HTTP 200"
{
  "timeline": {
    "id": "tml_bbe061171a8229f8daed",
    "projectId": "prj_b9c1f9c7873dcef51a57",
    "compositionId": "cmp_cfbdd85eaaff301fa997",
    "revision": 2,
    "durationSec": 30,
    "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"
      }
    ],
    "segments": [
      {
        "id": "seg_7279fbb508f4a22e65a4",
        "assetId": "ast_3f9a1c7e5b2d4086a1c4",
        "sourceStartSec": 120,
        "sourceEndSec": 135,
        "zoom": 1,
        "muted": false,
        "volume": 1,
        "outputStartSec": 0,
        "outputEndSec": 15
      },
      {
        "id": "seg_5a0e3c8f1d7b42e69c13",
        "assetId": "ast_3f9a1c7e5b2d4086a1c4",
        "sourceStartSec": 135,
        "sourceEndSec": 150,
        "zoom": 1,
        "muted": false,
        "volume": 1,
        "outputStartSec": 15,
        "outputEndSec": 30
      }
    ],
    "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": []
  },
  "compositionId": "cmp_cfbdd85eaaff301fa997",
  "warnings": []
}
```

**All response fields**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `timeline` | Timeline object | Required | Described in [Timeline object](/developers/objects.md#timeline). |
| `compositionId` | string | Required | The composition the edit compiles to, to export with create_export (`POST /v1/compositions/{compositionId}/exports`). |
| `warnings` | array of strings | Required | What the change adjusted or could not do, in one sentence each; empty when nothing. |

**More examples**

#### Change the order, the cut between two segments, the zoom and the sound

`POST /v1/projects/prj_b9c1f9c7873dcef51a57/timeline/operations`

```json title="Request body"
{
  "expectedRevision": 1,
  "operations": [
    {
      "op": "move_segment",
      "segmentId": "seg_a9dcee59c1fb9edb6751",
      "beforeSegmentId": "seg_7279fbb508f4a22e65a4"
    },
    {
      "op": "trim_segment",
      "segmentId": "seg_a9dcee59c1fb9edb6751",
      "sourceStartSec": 302
    },
    {
      "op": "set_transition",
      "segmentId": "seg_7279fbb508f4a22e65a4",
      "kind": "dip",
      "sec": 0.5
    },
    {
      "op": "set_zoom",
      "segmentIds": [
        "seg_a9dcee59c1fb9edb6751"
      ],
      "factor": 1.2,
      "mode": "push"
    },
    {
      "op": "set_segment_audio",
      "segmentIds": [
        "seg_7279fbb508f4a22e65a4"
      ],
      "volume": 0.8
    }
  ]
}
```

```json title="HTTP 200"
{
  "timeline": {
    "id": "tml_bbe061171a8229f8daed",
    "projectId": "prj_b9c1f9c7873dcef51a57",
    "compositionId": "cmp_cfbdd85eaaff301fa997",
    "revision": 2,
    "durationSec": 58,
    "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"
      }
    ],
    "segments": [
      {
        "id": "seg_a9dcee59c1fb9edb6751",
        "assetId": "ast_3f9a1c7e5b2d4086a1c4",
        "sourceStartSec": 302,
        "sourceEndSec": 330,
        "zoom": 1.2,
        "muted": false,
        "volume": 1,
        "outputStartSec": 0,
        "outputEndSec": 28
      },
      {
        "id": "seg_7279fbb508f4a22e65a4",
        "assetId": "ast_3f9a1c7e5b2d4086a1c4",
        "sourceStartSec": 120,
        "sourceEndSec": 150,
        "zoom": 1,
        "muted": false,
        "volume": 0.8,
        "outputStartSec": 28,
        "outputEndSec": 58
      }
    ],
    "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": []
  },
  "compositionId": "cmp_cfbdd85eaaff301fa997",
  "warnings": []
}
```

#### Change the texts, the b-roll and the music

`POST /v1/projects/prj_b9c1f9c7873dcef51a57/timeline/operations`

```json title="Request body"
{
  "expectedRevision": 1,
  "operations": [
    {
      "op": "update_text",
      "textId": "ovl_49f5239b9cd73496225d",
      "text": "They paid before we built it"
    },
    {
      "op": "add_text",
      "text": "Watch to the end",
      "preset": "caption-note",
      "outputStartSec": 50,
      "durationSec": 4
    },
    {
      "op": "update_broll",
      "brollId": "brl_f44d18e008ec4009bf10",
      "durationSec": 3
    },
    {
      "op": "add_broll",
      "assetId": "ast_8ac5d8b97546145cdbf6",
      "outputStartSec": 20,
      "durationSec": 3,
      "fit": "contain"
    },
    {
      "op": "set_music",
      "assetId": "ast_e8a35ed674148f27b585",
      "volume": 0.2
    },
    {
      "op": "set_video_fade",
      "edge": "start",
      "sec": 1
    }
  ]
}
```

```json title="HTTP 200"
{
  "timeline": {
    "id": "tml_bbe061171a8229f8daed",
    "projectId": "prj_b9c1f9c7873dcef51a57",
    "compositionId": "cmp_cfbdd85eaaff301fa997",
    "revision": 2,
    "durationSec": 60,
    "aspect": "9:16",
    "reframe": "off",
    "captions": {
      "enabled": true,
      "style": "bold-karaoke"
    },
    "music": {
      "assetId": "ast_e8a35ed674148f27b585",
      "volume": 0.2
    },
    "fadeInSec": 1,
    "fadeOutSec": 0,
    "clips": [
      {
        "assetId": "ast_3f9a1c7e5b2d4086a1c4",
        "title": "Interview with a founder",
        "durationSec": 1800,
        "state": "ready"
      }
    ],
    "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
      }
    ],
    "texts": [
      {
        "id": "ovl_49f5239b9cd73496225d",
        "text": "They paid before we built it",
        "preset": "headline",
        "outputStartSec": 0,
        "durationSec": 3,
        "xPercent": null,
        "yPercent": null
      },
      {
        "id": "ovl_7b2d9e04c1a6f538b0e4",
        "text": "Watch to the end",
        "preset": "caption-note",
        "outputStartSec": 50,
        "durationSec": 4,
        "xPercent": null,
        "yPercent": null
      }
    ],
    "broll": [
      {
        "id": "brl_f44d18e008ec4009bf10",
        "assetId": "ast_8ac5d8b97546145cdbf6",
        "outputStartSec": 12,
        "durationSec": 3,
        "fit": "cover"
      },
      {
        "id": "brl_1f9c4a7e2d6b08e35c91",
        "assetId": "ast_8ac5d8b97546145cdbf6",
        "outputStartSec": 20,
        "durationSec": 3,
        "fit": "contain"
      }
    ],
    "diagnostics": []
  },
  "compositionId": "cmp_cfbdd85eaaff301fa997",
  "warnings": []
}
```

#### Remove a text and a b-roll, and move a segment to the end

`POST /v1/projects/prj_b9c1f9c7873dcef51a57/timeline/operations`

```json title="Request body"
{
  "expectedRevision": 1,
  "operations": [
    {
      "op": "remove_text",
      "textId": "ovl_49f5239b9cd73496225d"
    },
    {
      "op": "remove_broll",
      "brollId": "brl_f44d18e008ec4009bf10"
    },
    {
      "op": "move_segment",
      "segmentId": "seg_7279fbb508f4a22e65a4",
      "beforeSegmentId": null
    }
  ]
}
```

```json title="HTTP 200"
{
  "timeline": {
    "id": "tml_bbe061171a8229f8daed",
    "projectId": "prj_b9c1f9c7873dcef51a57",
    "compositionId": "cmp_cfbdd85eaaff301fa997",
    "revision": 2,
    "durationSec": 60,
    "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"
      }
    ],
    "segments": [
      {
        "id": "seg_a9dcee59c1fb9edb6751",
        "assetId": "ast_3f9a1c7e5b2d4086a1c4",
        "sourceStartSec": 300,
        "sourceEndSec": 330,
        "zoom": 1,
        "muted": false,
        "volume": 1,
        "outputStartSec": 0,
        "outputEndSec": 30
      },
      {
        "id": "seg_7279fbb508f4a22e65a4",
        "assetId": "ast_3f9a1c7e5b2d4086a1c4",
        "sourceStartSec": 120,
        "sourceEndSec": 150,
        "zoom": 1,
        "muted": false,
        "volume": 1,
        "outputStartSec": 30,
        "outputEndSec": 60
      }
    ],
    "texts": [],
    "broll": [],
    "diagnostics": []
  },
  "compositionId": "cmp_cfbdd85eaaff301fa997",
  "warnings": []
}
```

#### Change the captions, a word and the shape

`POST /v1/projects/prj_b9c1f9c7873dcef51a57/timeline/operations`

```json title="Request body"
{
  "expectedRevision": 1,
  "operations": [
    {
      "op": "set_captions",
      "style": "word-pop",
      "options": {
        "uppercase": true,
        "maxWordsPerGroup": 3
      }
    },
    {
      "op": "replace_caption_word",
      "from": "customer",
      "to": "client"
    },
    {
      "op": "set_aspect",
      "aspect": "1:1"
    },
    {
      "op": "set_reframe",
      "mode": "center"
    }
  ]
}
```

```json title="HTTP 200"
{
  "timeline": {
    "id": "tml_bbe061171a8229f8daed",
    "projectId": "prj_b9c1f9c7873dcef51a57",
    "compositionId": "cmp_cfbdd85eaaff301fa997",
    "revision": 2,
    "durationSec": 60,
    "aspect": "1:1",
    "reframe": "center",
    "captions": {
      "enabled": true,
      "style": "word-pop"
    },
    "music": null,
    "fadeInSec": 0,
    "fadeOutSec": 0,
    "clips": [
      {
        "assetId": "ast_3f9a1c7e5b2d4086a1c4",
        "title": "Interview with a founder",
        "durationSec": 1800,
        "state": "ready"
      }
    ],
    "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
      }
    ],
    "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": []
  },
  "compositionId": "cmp_cfbdd85eaaff301fa997",
  "warnings": []
}
```

#### The edit changed since you read it

`POST /v1/projects/prj_b9c1f9c7873dcef51a57/timeline/operations`

```json title="Request body"
{
  "expectedRevision": 0,
  "operations": [
    {
      "op": "remove_fillers"
    }
  ]
}
```

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

#### A segment that is not in the edit

`POST /v1/projects/prj_b9c1f9c7873dcef51a57/timeline/operations`

```json title="Request body"
{
  "expectedRevision": 1,
  "operations": [
    {
      "op": "remove_segments",
      "segmentIds": [
        "seg_77e5dc932c642be5375c"
      ]
    }
  ]
}
```

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

## Errors

| Code | Status | When | What to do |
| --- | --- | --- | --- |
| [`NOT_FOUND`](/developers/errors.md#NOT_FOUND) | 404 | A segment that is not in the edit | 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. |

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. Adding a video costs credits, and has its own call: add_timeline_clip (`POST /v1/projects/{projectId}/timeline/clips`).

## Related operations

- [`get_timeline`](/developers/api/get-timeline.md) (`GET /v1/projects/{projectId}/timeline`) Get an edit
- [`add_timeline_clip`](/developers/api/add-timeline-clip.md) (`POST /v1/projects/{projectId}/timeline/clips`) Add a video to an edit
- [`list_timeline_revisions`](/developers/api/list-timeline-revisions.md) (`GET /v1/projects/{projectId}/timeline/revisions`) List the revisions of an edit
- [`restore_timeline`](/developers/api/restore-timeline.md) (`POST /v1/projects/{projectId}/timeline/restore`) Restore a revision of an edit
- [`create_export`](/developers/api/create-export.md) (`POST /v1/compositions/{compositionId}/exports`) Export a composition

## 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.

### Why is the op "add_clip" refused?

Adding a video costs credits, so it has its own call with its own Idempotency-Key: add_timeline_clip (`POST /v1/projects/{projectId}/timeline/clips`). Every operation of this call is free, and an op this call does not know is refused with VALIDATION_FAILED.

### What happens if one operation of the list fails?

Nothing is written: the operations apply all together or not at all, and the answer says which one failed. Fix it and send the list again with the same expectedRevision.
