---
title: "Restore a revision of an edit | GPT-Video API reference"
description: "Brings an edit back to a revision listed by list_timeline_revisions, as a new revision."
url: "https://www.gpt-video.com/developers/api/restore-timeline"
operation: "restore_timeline"
method: "POST"
path: "/v1/projects/{projectId}/timeline/restore"
scope: "clips:write"
---

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

# Restore a revision of an edit

Operation id `restore_timeline`. Request `POST /v1/projects/{projectId}/timeline/restore`.

Brings an edit back to a revision listed by list_timeline_revisions (`GET /v1/projects/{projectId}/timeline/revisions`), as a new revision.

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 to go back to and the revision you read last as expectedRevision. The edit becomes what it was at that revision, and the change is a new revision, so it can be undone the same way. A restore never costs credits: a revision that would bring back a video that is not paid for is refused, and the video can be added again with add_timeline_clip (`POST /v1/projects/{projectId}/timeline/clips`).

## Example use

After an automatic change went wrong, a script sends the revision it found in the list and the revision it read last. The edit goes back as a new revision, so the person can undo the restore the same way.

## Endpoint

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

- 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 |
| --- | --- | --- | --- |
| `revision` | integer | Required | The revision to go back to, as list_timeline_revisions (`GET /v1/projects/{projectId}/timeline/revisions`) lists it. At least 0. |
| `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. |

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

## Request

Restore a revision 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/restore" \
  -H "Authorization: Bearer $GPT_VIDEO_API_KEY" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "revision": 2,
    "expectedRevision": 3
  }'
```

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

```json title="MCP (Full)"
{
  "method": "tools/call",
  "params": {
    "name": "restore_timeline",
    "arguments": {
      "projectId": "prj_b9c1f9c7873dcef51a57",
      "revision": 2,
      "expectedRevision": 3
    }
  }
}
```

## 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": 4,
    "durationSec": 60,
    "aspect": "9:16",
    "reframe": "off",
    "captions": {
      "enabled": false,
      "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": 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"
}
```

**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`). |

**More examples**

#### A revision that is not kept

`POST /v1/projects/prj_b9c1f9c7873dcef51a57/timeline/restore`

```json title="Request body"
{
  "revision": 0,
  "expectedRevision": 1
}
```

```json title="HTTP 404"
{
  "error": {
    "code": "NOT_FOUND",
    "message": "Revision 0 of this edit is not kept. Call list_timeline_revisions to see the ones you can restore.",
    "hint": "Check the id. An id that belongs to another account is not found either.",
    "requestId": "req_4f8c1a6e3b0d92e75a4c1f08"
  }
}
```

## Errors

| Code | Status | When | What to do |
| --- | --- | --- | --- |
| [`NOT_FOUND`](/developers/errors.md#NOT_FOUND) | 404 | A revision that is not kept | Change the request, then send it again. |
| [`CONFLICT`](/developers/errors.md#CONFLICT) | 409 | The project is not a Vibe Editing project, its edit is not prepared, it changed after expectedRevision, or the revision would bring back a video that is not paid for. The message says which one. | 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. A restore that would bring back a video that is not paid for is refused.

## Related operations

- [`list_timeline_revisions`](/developers/api/list-timeline-revisions.md) (`GET /v1/projects/{projectId}/timeline/revisions`) List the revisions of an edit
- [`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
- [`apply_timeline_operations`](/developers/api/apply-timeline-operations.md) (`POST /v1/projects/{projectId}/timeline/operations`) Change 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.

### Why is my restore refused with a video that is not paid for?

The revision plays a video whose credits were given back, or that this edit never paid for. A restore never charges credits, so it is refused: add the video again with add_timeline_clip (`POST /v1/projects/{projectId}/timeline/clips`), which says the price.
