---
title: "Frame a composition again | GPT-Video API reference"
description: "Computes the vertical framing of a composition again, following the face or keeping the center."
url: "https://www.gpt-video.com/developers/api/reframe-composition"
operation: "reframe_composition"
method: "POST"
path: "/v1/compositions/{compositionId}/reframe"
scope: "clips:write"
---

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

# Frame a composition again

Operation id `reframe_composition`. Request `POST /v1/compositions/{compositionId}/reframe`.

Computes the vertical framing of a composition again, following the face or keeping the center.

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

It starts a framing job and answers 202 with it; follow it with get_job (`GET /v1/jobs/{jobId}`), or read framing in get_composition (`GET /v1/compositions/{compositionId}`). face follows the face of the speaker, center keeps the middle of the video. When a framing job is already running on the composition, the answer is that job with reused true, and nothing new starts. Works on Auto Clip and streamer compositions; a Vibe Editing project changes its framing with the set_reframe operation of apply_timeline_operations (`POST /v1/projects/{projectId}/timeline/operations`). An export started while the framing runs answers NOT_READY.

## Example use

A clip was framed on the center, and the speaker sits at the edge of the picture. A script asks for the face framing, follows the job with get_job (`GET /v1/jobs/{jobId}`), and exports the clip again when the job is done.

## Endpoint

```text
POST https://api.gpt-video.com/v1/compositions/{compositionId}/reframe
```

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

## 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](/developers/pagination-and-idempotency.md). |

### Body

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `mode` | string | Required | face follows the face of the speaker, center keeps the middle of the video. One of `face`, `center`. |

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

## Request

Follow the face.

```bash title="curl"
curl -X POST "https://api.gpt-video.com/v1/compositions/cmp_4e8c2a6f09d14b73a1e5/reframe" \
  -H "Authorization: Bearer $GPT_VIDEO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "mode": "face"
  }'
```

```ts title="TypeScript"
const res = await fetch("https://api.gpt-video.com/v1/compositions/cmp_4e8c2a6f09d14b73a1e5/reframe", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.GPT_VIDEO_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "mode": "face"
  }),
});
const data = await res.json();
console.log(data);
```

```python title="Python"
import os

import requests

r = requests.post(
    "https://api.gpt-video.com/v1/compositions/cmp_4e8c2a6f09d14b73a1e5/reframe",
    headers={
        "Authorization": f"Bearer {os.environ['GPT_VIDEO_API_KEY']}",
    },
    json={
        "mode": "face"
    },
)
data = r.json()
print(data)
```

```json title="MCP (Full)"
{
  "method": "tools/call",
  "params": {
    "name": "reframe_composition",
    "arguments": {
      "compositionId": "cmp_4e8c2a6f09d14b73a1e5",
      "mode": "face"
    }
  }
}
```

## 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. |
| `reused` | boolean | true when a framing job was already running on the composition and this is that job. |

### Response example

- `Location: https://api.gpt-video.com/v1/jobs/job_4e8a2c6f0b3d49a7e1c5`

```json title="HTTP 202"
{
  "job": {
    "id": "job_4e8a2c6f0b3d49a7e1c5",
    "kind": "framing",
    "status": "queued",
    "progress": 0,
    "errorMessage": null,
    "projectId": "prj_7c2e9a4f1b3d4e8096a5",
    "compositionId": "cmp_4e8c2a6f09d14b73a1e5",
    "exportId": null,
    "assetId": null,
    "toolRunId": null,
    "turnId": null,
    "createdAt": "2026-09-01T11:10:00.000Z",
    "startedAt": null,
    "finishedAt": null
  },
  "reused": false
}
```

**All response fields**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `job` | Job object | Required | Described in [Job object](/developers/objects.md#job). |
| `reused` | boolean | Required | true when a framing job was already running on the composition and this is that job. |

**More examples**

#### A framing that is already running

`POST /v1/compositions/cmp_4e8c2a6f09d14b73a1e5/reframe`

```json title="Request body"
{
  "mode": "center"
}
```

- `Location: https://api.gpt-video.com/v1/jobs/job_4e8a2c6f0b3d49a7e1c5`

```json title="HTTP 202"
{
  "job": {
    "id": "job_4e8a2c6f0b3d49a7e1c5",
    "kind": "framing",
    "status": "queued",
    "progress": 0,
    "errorMessage": null,
    "projectId": "prj_7c2e9a4f1b3d4e8096a5",
    "compositionId": "cmp_4e8c2a6f09d14b73a1e5",
    "exportId": null,
    "assetId": null,
    "toolRunId": null,
    "turnId": null,
    "createdAt": "2026-09-01T11:10:00.000Z",
    "startedAt": null,
    "finishedAt": null
  },
  "reused": true
}
```

#### A composition of a Vibe Editing project

`POST /v1/compositions/cmp_cfbdd85eaaff301fa997/reframe`

```json title="Request body"
{
  "mode": "face"
}
```

```json title="HTTP 409"
{
  "error": {
    "code": "CONFLICT",
    "message": "Only Auto Clip and streamer compositions follow a face. For a Vibe Editing project use the set_reframe operation.",
    "requestId": "req_2b7d5f9a1c3e46b0d8a2c4e6"
  }
}
```

#### Frame a composition that does not exist

`POST /v1/compositions/cmp_7a1f5d3c9e0b42d8f6a2/reframe`

```json title="Request body"
{
  "mode": "face"
}
```

```json title="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

| Code | Status | When | What to do |
| --- | --- | --- | --- |
| [`NOT_FOUND`](/developers/errors.md#NOT_FOUND) | 404 | Frame a composition that does not exist | Change the request, then send it again. |
| [`CONFLICT`](/developers/errors.md#CONFLICT) | 409 | A composition of a Vibe Editing project | Read the resource again, then decide. Do not 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.

## Related operations

- [`get_job`](/developers/api/get-job.md) (`GET /v1/jobs/{jobId}`) Get a job
- [`get_composition`](/developers/api/get-composition.md) (`GET /v1/compositions/{compositionId}`) Get a composition
- [`create_export`](/developers/api/create-export.md) (`POST /v1/compositions/{compositionId}/exports`) Export a composition
- [`edit_composition`](/developers/api/edit-composition.md) (`POST /v1/compositions/{compositionId}/edit`) Edit a composition

## Questions

### Does a trim keep the framing?

No. A trim made with edit_composition (`POST /v1/compositions/{compositionId}/edit`) clears the face framing, because it was computed for the old start; when the project follows the face, a framing job starts again by itself, and get_composition (`GET /v1/compositions/{compositionId}`) shows it as tracking.

### Why did I get a job that was started earlier?

A framing job was already running on the composition, so the answer is that job with reused true. Wait for it with get_job (`GET /v1/jobs/{jobId}`), then ask again if you want the other mode.
