---
title: "Prepare an edit | GPT-Video API reference"
description: "Starts the preparation of a Vibe Editing project: the video is transcribed and analyzed once, and the edit is built from it."
url: "https://www.gpt-video.com/developers/api/prepare-timeline"
operation: "prepare_timeline"
method: "POST"
path: "/v1/projects/{projectId}/timeline/prepare"
scope: "spend"
---

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

# Prepare an edit

Operation id `prepare_timeline`. Request `POST /v1/projects/{projectId}/timeline/prepare`.

Starts the preparation of a Vibe Editing project: the video is transcribed and analyzed once, and the edit is built from it.

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 on a new Vibe Editing project; estimate_analysis (`POST /v1/projects/{projectId}/analyze/estimate`) gives the price first. The answer is 202 with the job of the preparation: follow it with get_job (`GET /v1/jobs/{jobId}`), then read the edit with get_timeline (`GET /v1/projects/{projectId}/timeline`). A prepared edit is never prepared or charged again: calling it again answers CONFLICT. Retrying with the same Idempotency-Key never pays twice, and if the preparation is already running you get its job back and nothing is charged.

## Example use

An agency that edits the weekly vlog of a client checks the price with estimate_analysis (`POST /v1/projects/{projectId}/analyze/estimate`), then calls prepare_timeline (`POST /v1/projects/{projectId}/timeline/prepare`) with an Idempotency-Key. It follows the job with get_job (`GET /v1/jobs/{jobId}`) and reads the finished edit with get_timeline (`GET /v1/projects/{projectId}/timeline`).

## Endpoint

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

- 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.
- It starts work that runs on its own. The answer comes back before the work is done.

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

## Request

Prepare 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_37573e451e0ff7f863c8/timeline/prepare" \
  -H "Authorization: Bearer $GPT_VIDEO_API_KEY" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY"
```

```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_37573e451e0ff7f863c8/timeline/prepare", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.GPT_VIDEO_API_KEY}`,
    "Idempotency-Key": idempotencyKey,
  },
});
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_37573e451e0ff7f863c8/timeline/prepare",
    headers={
        "Authorization": f"Bearer {os.environ['GPT_VIDEO_API_KEY']}",
        "Idempotency-Key": idempotency_key,
    },
)
data = r.json()
print(data)
```

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

## Response

A successful call answers HTTP 202. Returns a [Job object](/developers/objects.md#job) in `job`.

### Fields to check

| Name | Type | Description |
| --- | --- | --- |
| `job.id` | string | The job id. It starts with job_. |
| `job.status` | string | queued and running are still working; done, failed and cancelled are final. |
| `creditsCharged` | integer | The credits this request was charged: the price of the work it started, 0 when the same work was already running and its job came back. |
| `reused` | boolean | true when the same work was already running on the project and this is its job, charged to nobody again. |
| `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 202"
{
  "job": {
    "id": "job_d41c7e0a95b3f26c8e17",
    "kind": "analysis",
    "status": "queued",
    "progress": 0,
    "errorMessage": null,
    "projectId": "prj_37573e451e0ff7f863c8",
    "compositionId": null,
    "exportId": null,
    "assetId": null,
    "toolRunId": null,
    "turnId": null,
    "createdAt": "2026-09-01T11:10:00.000Z",
    "startedAt": null,
    "finishedAt": null
  },
  "creditsCharged": 2,
  "reused": false,
  "paidWith": "plan"
}
```

**All response fields**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `job` | Job object | Required | Described in [Job object](/developers/objects.md#job). |
| `creditsCharged` | integer | Required | The credits this request was charged: the price of the work it started, 0 when the same work was already running and its job came back. At least 0. |
| `reused` | boolean | Required | true when the same work was already running on the project and this is its job, charged to nobody again. |
| `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`. |

**More examples**

#### The edit is already prepared

`POST /v1/projects/prj_b9c1f9c7873dcef51a57/timeline/prepare`

```json title="HTTP 409"
{
  "error": {
    "code": "CONFLICT",
    "message": "This edit is already prepared, and it is never prepared or charged again. Read it with get_timeline.",
    "requestId": "req_9c2f6a0e4b8d13a75f0c2e61"
  }
}
```

#### A project of another kind

`POST /v1/projects/prj_7c2e9a4f1b3d4e8096a5/timeline/prepare`

```json title="HTTP 409"
{
  "error": {
    "code": "CONFLICT",
    "message": "This project is not a Vibe Editing project. Timeline operations work on Vibe Editing projects only.",
    "requestId": "req_1d5a8e2c6f0b47c93e1a5d82"
  }
}
```

## 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 | See the error reference. | Change the request, then send it again. |
| [`CONFLICT`](/developers/errors.md#CONFLICT) | 409 | The edit is already prepared | Read the resource again, then decide. Do not send the same request again. |
| [`SOURCE_MISSING`](/developers/errors.md#SOURCE_MISSING) | 422 | See the error reference. | Change the request, then send it 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 source video, charged when the preparation starts and not returned if it fails. A prepared edit is never prepared or charged again.

- Paid by the API bonus, it uses the same number of bonus credits.
- When the API bonus pays, 1 of its jobs runs at a time. The next one waits with status `queued`.
- See [API pricing](/developers/pricing.md) for who pays first.

## Related operations

- [`create_vibe_edit_project`](/developers/api/create-vibe-edit-project.md) (`POST /v1/projects/vibe-edit`) Create a Vibe Editing project
- [`estimate_analysis`](/developers/api/estimate-analysis.md) (`POST /v1/projects/{projectId}/analyze/estimate`) Estimate the credits of an analysis or a build
- [`get_job`](/developers/api/get-job.md) (`GET /v1/jobs/{jobId}`) Get a job
- [`get_timeline`](/developers/api/get-timeline.md) (`GET /v1/projects/{projectId}/timeline`) Get an edit

## Questions

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

Send it again with the same Idempotency-Key: the retry never pays twice, and it answers what the first try paid and who paid it. A new key on a prepared edit answers CONFLICT, and nothing is charged.

### Can I prepare the edit again to start over?

No. An edit is prepared once. To go back to an earlier state, restore a revision with restore_timeline (`POST /v1/projects/{projectId}/timeline/restore`); to start from scratch, create another Vibe Editing project.
