---
title: "Build a split screen video | GPT-Video API reference"
description: "Starts the build of the video of a split screen project, and returns the job to follow."
url: "https://www.gpt-video.com/developers/api/build-split-screen-video"
operation: "build_split_screen_video"
method: "POST"
path: "/v1/projects/{projectId}/split-screen/build"
scope: "spend"
---

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

# Build a split screen video

Operation id `build_split_screen_video`. Request `POST /v1/projects/{projectId}/split-screen/build`.

Starts the build of the video of a split screen project, and returns the job to follow.

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 when the project has its settings; estimate_analysis (`POST /v1/projects/{projectId}/analyze/estimate`) gives the price first. The answer is 202 with the job of the build: follow it with get_job (`GET /v1/jobs/{jobId}`), and when it is done export the composition named by get_project (`GET /v1/projects/{projectId}`) with create_export (`POST /v1/compositions/{compositionId}/exports`). A build makes the video again from the settings, replacing the changes made to the composition in the GPT-Video editor. Retrying with the same Idempotency-Key never pays twice; a new key is a new build, and building again pays again for the minutes of both videos, the background included. If a build of the project is already running, you get that job back and nothing is charged.

## Example use

After checking the price with estimate_analysis (`POST /v1/projects/{projectId}/analyze/estimate`), a script builds the video of a split screen project, follows the job with get_job (`GET /v1/jobs/{jobId}`), and then exports the composition whose id get_project (`GET /v1/projects/{projectId}`) gives.

## Endpoint

```text
POST https://api.gpt-video.com/v1/projects/{projectId}/split-screen/build
```

- 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

Build a split screen video.

```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_9cf6ba9cd17fd89cf513/split-screen/build" \
  -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_9cf6ba9cd17fd89cf513/split-screen/build", {
  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_9cf6ba9cd17fd89cf513/split-screen/build",
    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": "build_split_screen_video",
    "arguments": {
      "projectId": "prj_9cf6ba9cd17fd89cf513",
      "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_3e9c5a17d0b84f26e1a7",
    "kind": "build",
    "status": "queued",
    "progress": 0,
    "errorMessage": null,
    "projectId": "prj_9cf6ba9cd17fd89cf513",
    "compositionId": null,
    "exportId": null,
    "assetId": null,
    "toolRunId": null,
    "turnId": null,
    "createdAt": "2026-09-01T11:10:00.000Z",
    "startedAt": null,
    "finishedAt": null
  },
  "creditsCharged": 31,
  "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**

#### A video of the project no longer exists

`POST /v1/projects/prj_9cf6ba9cd17fd89cf513/split-screen/build`

```json title="HTTP 422"
{
  "error": {
    "code": "SOURCE_MISSING",
    "message": "A video this project uses no longer exists or cannot be used through the API. Create a new project with videos you uploaded.",
    "hint": "A project's video cannot be changed through the API. Upload the video, then create a new project with its asset id as sourceAssetId.",
    "requestId": "req_2e7b0c5a8d3f41e96b0c3a58"
  }
}
```

#### Not enough credits

`POST /v1/projects/prj_9cf6ba9cd17fd89cf513/split-screen/build`

```json title="HTTP 402"
{
  "error": {
    "code": "INSUFFICIENT_CREDITS",
    "message": "Out of credits: 10 of 800 left this month and this video needs 31. Top up or upgrade your plan.",
    "hint": "Buy credits or change plan at https://www.gpt-video.com/app/billing.",
    "requestId": "req_8c3f6a1d9b2e40c75e8a1f36"
  }
}
```

#### A project of another kind

`POST /v1/projects/prj_7c2e9a4f1b3d4e8096a5/split-screen/build`

```json title="HTTP 409"
{
  "error": {
    "code": "CONFLICT",
    "message": "This project is not a split screen project. Use the operations of its kind, or analyze_project for an Auto Clip project.",
    "requestId": "req_5f1a8c3e0b7d42a96c3e8b02"
  }
}
```

## Errors

| Code | Status | When | What to do |
| --- | --- | --- | --- |
| [`INSUFFICIENT_CREDITS`](/developers/errors.md#INSUFFICIENT_CREDITS) | 402 | Not enough credits | 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 | A project of another kind | Read the resource again, then decide. Do not send the same request again. |
| [`SOURCE_MISSING`](/developers/errors.md#SOURCE_MISSING) | 422 | A video of the project no longer exists | 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 main video and of the background video together, charged when the build starts and not returned if it fails. Building again is a new build and is 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

- [`estimate_analysis`](/developers/api/estimate-analysis.md) (`POST /v1/projects/{projectId}/analyze/estimate`) Estimate the credits of an analysis or a build
- [`update_split_screen_project`](/developers/api/update-split-screen-project.md) (`PATCH /v1/projects/{projectId}/split-screen`) Update a split screen project
- [`get_job`](/developers/api/get-job.md) (`GET /v1/jobs/{jobId}`) Get a job
- [`get_project`](/developers/api/get-project.md) (`GET /v1/projects/{projectId}`) Get a project
- [`create_export`](/developers/api/create-export.md) (`POST /v1/compositions/{compositionId}/exports`) Export a composition
- [`create_split_screen_project`](/developers/api/create-split-screen-project.md) (`POST /v1/projects/split-screen`) Create a split screen project

## Questions

### What happens if my build 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 is a new build, and it is charged again.

### What happens to the changes I made in the GPT-Video editor?

A build makes the video of the project again from its settings, so it replaces the changes made to its composition in the editor. Change the settings with the update operation of the project, then build.

### Why does building again cost the background minutes again?

Each build puts both videos through the engine again, the background included, so it is priced on both. estimate_analysis (`POST /v1/projects/{projectId}/analyze/estimate`) tells you the price before each build.
