Skip to content
Create a key

Paste a key to use it in the examples on these pages. The key stays in this browser tab. These pages never send it anywhere.

Add a video to an edit

add_timeline_clipPOST/v1/projects/{projectId}/timeline/clipsBeta

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

Code
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.
  • Idempotency-Key is required.

Parameters

Path

NameTypeRequiredDescription
projectIdstringRequiredThe project id. It starts with prj_. Matches ^prj_[0-9a-f]{20}$.

Headers

NameTypeRequiredDescription
Idempotency-KeystringRequiredThis 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.

Body

NameTypeRequiredDescription
expectedRevisionintegerRequiredThe 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.
assetIdstringRequiredThe video to add: the asset id that complete_upload (POST /v1/uploads/{uploadId}/complete) returned. Matches ^ast_n?[0-9a-f]{20}$.
beforeSegmentIdstring or nullOptionalThe 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.

Request example
# 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"
  }'
// 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);
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)
{
  "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 in timeline.

Fields to check

NameTypeDescription
timeline.idstringThe edit id. It starts with tml_.
timeline.segments[].idstringThe segment id. It starts with seg_.
timeline.texts[].idstringThe text id. It starts with ovl_.
timeline.broll[].idstringThe b-roll id. It starts with brl_.
creditsChargedintegerThe credits this request was charged: 0 when the edit already had the video, or had paid for it.
paidWithstring or nullWho 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

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
NameTypeRequiredDescription
timelineTimeline objectRequiredDescribed in Timeline object.
creditsChargedintegerRequiredThe credits this request was charged: 0 when the edit already had the video, or had paid for it. At least 0.
paidWithstring or nullRequiredWho 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.
clipobjectRequired
clip.assetIdstringRequiredThe video that was added.
clip.statestringRequiredready 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

Request body
{
  "expectedRevision": 1,
  "assetId": "ast_8e21b0d94c6f4a17b358",
  "beforeSegmentId": "seg_a9dcee59c1fb9edb6751"
}
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

Request body
{
  "expectedRevision": 0,
  "assetId": "ast_8e21b0d94c6f4a17b358"
}
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

Request body
{
  "expectedRevision": 1,
  "assetId": "ast_0b1e5c9d3a7f40628e2d"
}
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

CodeStatusWhenWhat to do
INSUFFICIENT_CREDITS402See the error reference.Stop and tell the user.
PLAN_REQUIRED402See the error reference.Stop and tell the user.
NOT_FOUND404The video does not existChange the request, then send it again.
CONFLICT409The edit changed since you read itRead the resource again, then decide. Do not send the same request again.
NOT_READY409See the error reference.Wait for Retry-After, then send the same request again.
DAILY_LIMIT_REACHED429See the error reference.Stop until the limit resets at 00:00 UTC.

Every call can also answer these errors. VALIDATION_FAILED, UNKNOWN_PARAMETER, IDEMPOTENCY_KEY_REQUIRED, UNAUTHORIZED, SCOPE_MISSING, IDEMPOTENCY_IN_PROGRESS, PAYLOAD_TOO_LARGE, IDEMPOTENCY_KEY_REUSED, RATE_LIMITED, INTERNAL, 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 for who pays first.
  • get_timeline (GET /v1/projects/{projectId}/timeline) Get an edit
  • apply_timeline_operations (POST /v1/projects/{projectId}/timeline/operations) Change an edit
  • estimate_analysis (POST /v1/projects/{projectId}/analyze/estimate) Estimate the credits of an analysis or a build
  • get_credits (GET /v1/credits) Get the credit balance
  • restore_timeline (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.

View as Markdown