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.

Edit a composition

edit_compositionPOST/v1/compositions/{compositionId}/editBeta

Changes the title, hook, captions, start and end, or music volume of a composition.

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 only the fields to change, with the revision you read; at least one field is needed. A change that leaves everything as it was answers with changed empty and the same revision, and writes nothing. trim moves the start and the end on the source video, and the captions move with them; it works on Auto Clip and streamer compositions, and the hook on Auto Clip compositions only. Captions, title and music volume work on Auto Clip, streamer, split screen and Reddit story compositions. A composition of a Vibe Editing or fake text project is changed with the operations of its kind, and the answer says which. Some changes are refused because the video would not work: a clip shorter than 3 seconds or longer than 180, captions over the face of a clip that follows it, a caption line too wide for the video, a hook of more than 12 words. The revision also grows when a job changes the composition, such as the framing a trim computes again.

Example use

A script adds the name of a guest to the title of each clip and moves the end of a clip two seconds earlier. It sends the revision it read, so a change made by someone else in the meantime is not overwritten.

Endpoint

Code
POST https://api.gpt-video.com/v1/compositions/{compositionId}/edit
  • 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.
  • Idempotency-Key is optional.

Parameters

Path

NameTypeRequiredDescription
compositionIdstringRequiredThe id of the composition: accept_clip (POST /v1/clips/{clipId}/accept) returns it for a clip, and list_compositions (GET /v1/projects/{projectId}/compositions) or the project's compositionId give it for the other kinds of project. Matches ^cmp_[0-9a-f]{20}$.

Headers

NameTypeRequiredDescription
Idempotency-KeystringOptionalSend the same key on every retry of this request. From 1 to 255 visible characters, such as a UUID. See idempotency.

Body

NameTypeRequiredDescription
expectedRevisionintegerRequiredThe revision of the composition you read with get_composition (GET /v1/compositions/{compositionId}). If the composition changed since, the request answers CONFLICT and changes nothing. At least 0.
titlestringOptionalThe new title of the composition, from 1 to 200 characters. From 1 to 200 characters. Matches \S.
hookstring or nullOptionalAuto Clip only: the text shown over the first 2 seconds, up to 120 characters and 12 words. null removes it. From 1 to 120 characters. Matches \S.
captionsobjectOptionalThe captions to change. Captions added to a composition that has none are made from the transcript of its video.
captions.enabledbooleanOptionalfalse removes the captions. true adds them back from the transcript when the composition has none.
captions.stylestringOptionalThe caption style, as get_setting_options (GET /v1/reference/setting-options) lists them. One of bold-karaoke, word-pop, boxed, outline.
captions.optionsobjectOptionalThe caption options to change. A field you leave out stays as it is.
trimobjectOptionalAuto Clip and streamer only: the start and end on the source video. A value you leave out stays as it is.
trim.startSecnumberOptionalThe new start, in seconds of the source video. At least 0.
trim.endSecnumberOptionalThe new end, in seconds of the source video. More than 0.
musicVolumenumberOptionalThe volume of the music: 0 is muted, 1 is normal, 2 is double. From 0 to 2.
Fields of captions.options
NameTypeRequiredDescription
captions.options.fontFamilystringOptionalThe typeface of the captions. One of Montserrat, Anton, System.
captions.options.fontSizeintegerOptionalThe size of the caption text, from 20 to 200. From 20 to 200.
captions.options.yPercentnumberOptionalWhere the captions sit, from 0 at the top to 100 at the bottom. From 0 to 100.
captions.options.colorstringOptionalThe color of the words, as #RRGGBB. Matches ^#[0-9A-Fa-f]{6}$.
captions.options.highlightColorstringOptionalThe color of the word being spoken, as #RRGGBB. Matches ^#[0-9A-Fa-f]{6}$.
captions.options.maxWordsPerGroupintegerOptionalHow many words are on screen at once, at most, from 1 to 12. From 1 to 12.
captions.options.maxCharsPerGroupintegerOptionalHow many characters are on screen at once, at most, from 4 to 80. From 4 to 80.
captions.options.uppercasebooleanOptionaltrue shows the captions in capital letters.
captions.options.maxGapSecnumberOptionalThe longest pause, in seconds, inside one group of words on screen, from 0.1 to 5. From 0.1 to 5.

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

Request

Change the title and the caption style.

Request example
curl -X POST "https://api.gpt-video.com/v1/compositions/cmp_4e8c2a6f09d14b73a1e5/edit" \
  -H "Authorization: Bearer $GPT_VIDEO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "expectedRevision": 0,
    "title": "They paid before the product existed",
    "captions": {
      "style": "boxed"
    }
  }'
const res = await fetch("https://api.gpt-video.com/v1/compositions/cmp_4e8c2a6f09d14b73a1e5/edit", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.GPT_VIDEO_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "expectedRevision": 0,
    "title": "They paid before the product existed",
    "captions": {
      "style": "boxed"
    }
  }),
});
const data = await res.json();
console.log(data);
import os

import requests

r = requests.post(
    "https://api.gpt-video.com/v1/compositions/cmp_4e8c2a6f09d14b73a1e5/edit",
    headers={
        "Authorization": f"Bearer {os.environ['GPT_VIDEO_API_KEY']}",
    },
    json={
        "expectedRevision": 0,
        "title": "They paid before the product existed",
        "captions": {
            "style": "boxed"
        }
    },
)
data = r.json()
print(data)
{
  "method": "tools/call",
  "params": {
    "name": "edit_composition",
    "arguments": {
      "compositionId": "cmp_4e8c2a6f09d14b73a1e5",
      "expectedRevision": 0,
      "title": "They paid before the product existed",
      "captions": {
        "style": "boxed"
      }
    }
  }
}

Response

A successful call answers HTTP 200. Returns a Composition object in composition.

Fields to check

NameTypeDescription
composition.idstringThe composition id. It starts with cmp_.
composition.framing.statusstring
composition.framing.jobIdstring or null

Response example

HTTP 200
{
  "composition": {
    "id": "cmp_4e8c2a6f09d14b73a1e5",
    "projectId": "prj_7c2e9a4f1b3d4e8096a5",
    "clipId": "cnd_9e04a6c3d12b4f78a5b9",
    "kind": "autoclip",
    "title": "They paid before the product existed",
    "revision": 1,
    "durationSec": 30,
    "width": 1080,
    "height": 1920,
    "fps": 30,
    "source": {
      "assetId": "ast_3f9a1c7e5b2d4086a1c4",
      "startSec": 120,
      "endSec": 150
    },
    "hook": "They paid us before we had a product",
    "captions": {
      "style": "boxed",
      "wordCount": 75
    },
    "framing": {
      "status": "centered",
      "jobId": null
    },
    "createdAt": "2026-09-01T10:00:00.000Z",
    "updatedAt": "2026-09-01T11:10:00.000Z"
  },
  "changed": [
    "title",
    "captions"
  ],
  "notes": []
}
All response fields
NameTypeRequiredDescription
compositionComposition objectRequiredDescribed in Composition object.
changedarray of stringsRequiredThe fields whose value is now different. Empty when the request changed nothing: then nothing was written and the revision is the one you sent.
notesarray of stringsRequiredWhat else happened, or why a field changed nothing, one sentence each.
More examples

A change that changes nothing

POST /v1/compositions/cmp_4e8c2a6f09d14b73a1e5/edit

Request body
{
  "expectedRevision": 0,
  "captions": {
    "style": "word-pop"
  }
}
HTTP 200
{
  "composition": {
    "id": "cmp_4e8c2a6f09d14b73a1e5",
    "projectId": "prj_7c2e9a4f1b3d4e8096a5",
    "clipId": "cnd_9e04a6c3d12b4f78a5b9",
    "kind": "autoclip",
    "title": "Paid before the product",
    "revision": 0,
    "durationSec": 30,
    "width": 1080,
    "height": 1920,
    "fps": 30,
    "source": {
      "assetId": "ast_3f9a1c7e5b2d4086a1c4",
      "startSec": 120,
      "endSec": 150
    },
    "hook": "They paid us before we had a product",
    "captions": {
      "style": "word-pop",
      "wordCount": 75
    },
    "framing": {
      "status": "centered",
      "jobId": null
    },
    "createdAt": "2026-09-01T10:00:00.000Z",
    "updatedAt": "2026-09-01T10:00:00.000Z"
  },
  "changed": [],
  "notes": []
}

A revision that is not the current one

POST /v1/compositions/cmp_4e8c2a6f09d14b73a1e5/edit

Request body
{
  "expectedRevision": 0,
  "title": "They paid before the product existed"
}
HTTP 409
{
  "error": {
    "code": "CONFLICT",
    "message": "This composition changed since revision 0; its revision is now 1.",
    "hint": "Read the composition again with get_composition, then send the change with its current revision.",
    "requestId": "req_5a9c2e7f1b3d48a6c0e4f2b8"
  }
}

A hook on a streamer composition

POST /v1/compositions/cmp_372b3304ae7cd6327fed/edit

Request body
{
  "expectedRevision": 0,
  "hook": "Watch the product do the work"
}
HTTP 409
{
  "error": {
    "code": "CONFLICT",
    "message": "Only Auto Clip compositions have a hook. A streamer composition shows its headline: change it with update_streamer_project.",
    "requestId": "req_1f6d8b3a0e5c47d9a2b6e4c8"
  }
}

A composition of a Vibe Editing project

POST /v1/compositions/cmp_cfbdd85eaaff301fa997/edit

Request body
{
  "expectedRevision": 0,
  "title": "Launch video"
}
HTTP 409
{
  "error": {
    "code": "CONFLICT",
    "message": "This composition belongs to a Vibe Editing project. Change it with apply_timeline_operations.",
    "requestId": "req_7c2e9a4f6b1d40e8c3a5d9f1"
  }
}

Edit a composition that does not exist

POST /v1/compositions/cmp_7a1f5d3c9e0b42d8f6a2/edit

Request body
{
  "expectedRevision": 0,
  "title": "Launch video"
}
HTTP 404
{
  "error": {
    "code": "NOT_FOUND",
    "message": "Composition cmp_7a1f5d3c9e0b42d8f6a2 not found",
    "hint": "Check the id. An id that belongs to another account is not found either.",
    "requestId": "req_6b2d9f0e4a7c41b3d8e5a2f9"
  }
}

Errors

CodeStatusWhenWhat to do
NOT_FOUND404Edit a composition that does not existChange the request, then send it again.
CONFLICT409A revision that is not the current oneRead the resource again, then decide. Do not send the same request again.

Every call can also answer these errors. VALIDATION_FAILED, UNKNOWN_PARAMETER, UNAUTHORIZED, SCOPE_MISSING, IDEMPOTENCY_IN_PROGRESS, PAYLOAD_TOO_LARGE, IDEMPOTENCY_KEY_REUSED, RATE_LIMITED, INTERNAL, UNAVAILABLE.

Cost

No credits are used.

  • get_composition (GET /v1/compositions/{compositionId}) Get a composition
  • replace_caption_words (POST /v1/compositions/{compositionId}/captions/replace) Correct words in the captions
  • reframe_composition (POST /v1/compositions/{compositionId}/reframe) Frame a composition again
  • create_export (POST /v1/compositions/{compositionId}/exports) Export a composition
  • list_compositions (GET /v1/projects/{projectId}/compositions) List the compositions of a project

Questions

What do I send as expectedRevision?

The revision of the composition as you last read it with get_composition (GET /v1/compositions/{compositionId}), or as the previous change returned it. If someone or something changed it since, the request answers CONFLICT: read it again and send the change once more.

Why did my change answer with changed empty?

The values you sent are the ones the composition already has, so nothing was written and the revision did not move. notes says why when a field could not apply, such as a music volume on a composition with no music.

How do I change the headline of a streamer video?

The headline is a setting of the streamer project: change it with update_streamer_project (PATCH /v1/projects/{projectId}/streamer), then build the video again.

View as Markdown