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
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
writecall. See rate limits. Idempotency-Keyis optional.
Parameters
Path
| Name | Type | Required | Description |
|---|---|---|---|
compositionId | string | Required | The 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
| 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. |
Body
| Name | Type | Required | Description |
|---|---|---|---|
expectedRevision | integer | Required | The 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. |
title | string | Optional | The new title of the composition, from 1 to 200 characters. From 1 to 200 characters. Matches \S. |
hook | string or null | Optional | Auto 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. |
captions | object | Optional | The captions to change. Captions added to a composition that has none are made from the transcript of its video. |
captions.enabled | boolean | Optional | false removes the captions. true adds them back from the transcript when the composition has none. |
captions.style | string | Optional | The caption style, as get_setting_options (GET /v1/reference/setting-options) lists them. One of bold-karaoke, word-pop, boxed, outline. |
captions.options | object | Optional | The caption options to change. A field you leave out stays as it is. |
trim | object | Optional | Auto Clip and streamer only: the start and end on the source video. A value you leave out stays as it is. |
trim.startSec | number | Optional | The new start, in seconds of the source video. At least 0. |
trim.endSec | number | Optional | The new end, in seconds of the source video. More than 0. |
musicVolume | number | Optional | The volume of the music: 0 is muted, 1 is normal, 2 is double. From 0 to 2. |
Fields of captions.options
| Name | Type | Required | Description |
|---|---|---|---|
captions.options.fontFamily | string | Optional | The typeface of the captions. One of Montserrat, Anton, System. |
captions.options.fontSize | integer | Optional | The size of the caption text, from 20 to 200. From 20 to 200. |
captions.options.yPercent | number | Optional | Where the captions sit, from 0 at the top to 100 at the bottom. From 0 to 100. |
captions.options.color | string | Optional | The color of the words, as #RRGGBB. Matches ^#[0-9A-Fa-f]{6}$. |
captions.options.highlightColor | string | Optional | The color of the word being spoken, as #RRGGBB. Matches ^#[0-9A-Fa-f]{6}$. |
captions.options.maxWordsPerGroup | integer | Optional | How many words are on screen at once, at most, from 1 to 12. From 1 to 12. |
captions.options.maxCharsPerGroup | integer | Optional | How many characters are on screen at once, at most, from 4 to 80. From 4 to 80. |
captions.options.uppercase | boolean | Optional | true shows the captions in capital letters. |
captions.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. |
Send only the fields listed here. Any other field is refused with UNKNOWN_PARAMETER.
Request
Change the title and the caption style.
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
| Name | Type | Description |
|---|---|---|
composition.id | string | The composition id. It starts with cmp_. |
composition.framing.status | string | |
composition.framing.jobId | string or null |
Response example
{
"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
| Name | Type | Required | Description |
|---|---|---|---|
composition | Composition object | Required | Described in Composition object. |
changed | array of strings | Required | The fields whose value is now different. Empty when the request changed nothing: then nothing was written and the revision is the one you sent. |
notes | array of strings | Required | What else happened, or why a field changed nothing, one sentence each. |
More examples
A change that changes nothing
POST /v1/compositions/cmp_4e8c2a6f09d14b73a1e5/edit
{
"expectedRevision": 0,
"captions": {
"style": "word-pop"
}
}{
"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
{
"expectedRevision": 0,
"title": "They paid before the product existed"
}{
"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
{
"expectedRevision": 0,
"hook": "Watch the product do the work"
}{
"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
{
"expectedRevision": 0,
"title": "Launch video"
}{
"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
{
"expectedRevision": 0,
"title": "Launch video"
}{
"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
| Code | Status | When | What to do |
|---|---|---|---|
NOT_FOUND | 404 | Edit a composition that does not exist | Change the request, then send it again. |
CONFLICT | 409 | A revision that is not the current one | Read 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.
Related operations
get_composition(GET /v1/compositions/{compositionId}) Get a compositionreplace_caption_words(POST /v1/compositions/{compositionId}/captions/replace) Correct words in the captionsreframe_composition(POST /v1/compositions/{compositionId}/reframe) Frame a composition againcreate_export(POST /v1/compositions/{compositionId}/exports) Export a compositionlist_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.