# Cast Swap

> Replace the people or the location in a 4 to 30 second video with your own reference images over REST, the SDK or MCP, keeping the motion, framing and sound.

Source: https://nodaro.ai/docs/developers/api/cast-swap

The **Cast Swap API** replaces the people, the location, or both in a short video with your own reference images. The video keeps its motion, framing, timing and sound. You quote a run for free, start it, and follow its job like any other.

Cast Swap runs on Nodaro Cloud only. On a self-hosted install, both routes answer `404`. The routes take a bearer token. See [Authentication](https://nodaro.ai/docs/developers/api/authentication).

## Endpoints

| Method | Path | What it does | Cost |
| --- | --- | --- | --- |
| `POST` | `/v1/cast-swap/estimate` | Quote a run. Nothing is created or reserved. | Free |
| `POST` | `/v1/cast-swap` | Start a run. Answers `202 { jobId }`. Follow the job with `GET /v1/jobs/:id`. | The quoted run |

## The request body

Both routes take the same body.

| Field | What it does |
| --- | --- |
| `videoUrl` | Required. The source clip, from 4 to 30 seconds. |
| `roles` | Required. Up to 6 person roles and 1 location role. See below. |
| `postPrompt` | Optional. Your own words for the model, up to 4,000 characters. Mention a role as `{role:N}`, where N counts from 1 over `roles`, empty roles included. |
| `preview` | Optional. `true` renders 4 seconds of the clip, from `previewStart`. |
| `previewStart` | Optional. Where the preview starts, in seconds. The default is 0. |
| `quality` | Optional. `"draft"`, the only value today. |
| `sessionId`, `origin` | Optional. The session to file the result in, and your app's origin label. |

A role names someone, or the setting, in the video, and takes 0 to 3 images of one person or one place:

- **A person:** `{ kind: "person", name, images, body?, outfit?, hair? }`. `name` says who in the video, for example `"the woman dancing"`. Each of `body`, `outfit` and `hair` is `"new"`, the default, which takes it from your images, or `"original"`, which keeps it from the video.
- **The location:** `{ kind: "location", name?, images }`. The default name is `"the setting"`.

A role with no images, empty or `null`, keeps that person or place as in the video. At least one role with images, or a `postPrompt`, is required.

```json
{
"videoUrl": "https://example.com/clip.mp4",
"roles": [
{ "kind": "person", "name": "the woman dancing", "images": ["https://example.com/me.jpg"], "outfit": "original" }
],
"preview": true
}
```

## The quote

`POST /v1/cast-swap/estimate` answers `{ totalCredits, breakdown: { castSwap, seedance }, settlesLower }`.

- `totalCredits` is `breakdown.castSwap`, the run itself, plus `breakdown.seedance`, the video model it calls.
- `settlesLower: true` means the length of the result is left to the model, so the final charge is expected to be lower than the quote.

## Repeats

Send the same `Idempotency-Key` header, 8 to 200 characters, when you retry a run. A repeat within about 10 seconds answers `200 { jobId, deduped: true }` with an `X-Dedup-Hit: 1` header. A later repeat answers `202 { jobId }` with the first run's job. Either way, nothing new is started or charged. Without a key, every request is a new run.

## Errors

Every refusal comes before anything is charged. The body is `{ error: { code, message } }`.

| Status | `code` | When |
| --- | --- | --- |
| `400` | `validation_error` | The body is not valid: no role with images and no `postPrompt`, too many roles, more than 3 images on a role, a `postPrompt` over 4,000 characters, or an invalid `origin`. |
| `400` | `raw_reference_token` | A role's `name` or the `postPrompt` contains a raw reference token: `{image:`, `{video:` or `{audio:`, in any case. |
| `400` | `invalid_role_mention` | `postPrompt` mentions `{role:N}` with N outside 1 to the number of roles. |
| `400` | `unsafe_url` | `videoUrl` or a role image is not a public http or https address. |
| `400` | `video_unreadable` | The length of the clip could not be read. It is not a playable video. |
| `400` | `clip_out_of_range` | The clip is shorter than 4 seconds or longer than 30 seconds. |
| `400` | `preview_out_of_range` | A preview runs past the end of the clip. `previewStart` plus 4 must be at most the clip's length. |
| `401` | `unauthorized` | No authenticated user. |
| `402` | `insufficient_credits` | Your credits cannot cover the run. `POST /v1/cast-swap` only. In a workspace, the limit that refused it can answer instead: `budget_exceeded`, `member_cap_exceeded` or `user_allowance_exceeded`. |
| `404` | `not_found` | `sessionId` names a session you cannot use. `POST /v1/cast-swap` only. |
| `422` | `job_blocked` | The request was blocked by the deployment's job policy. `POST /v1/cast-swap` only. |
| `503` | `host_too_old` | This platform version cannot run Cast Swap. |
| `503` | `estimate_unavailable` | The video model's price cannot be quoted right now. Try again shortly. `/estimate` only. |

## From the SDK

`client.castSwap` wraps both routes. It is also importable as `@nodaro/sdk/cast-swap`.

```ts
run(params: CastSwapParams, options?: { idempotencyKey?: string }): Promise<{ jobId: string; deduped?: boolean }>
estimate(params: CastSwapParams): Promise<CastSwapEstimate>
```

`CastSwapParams` is the request body above. `CastSwapEstimate` is the quote. `run()` sends `idempotencyKey` unchanged as the `Idempotency-Key` header. Without it, no header is sent.

```ts
const params: CastSwapParams = {
videoUrl,
roles: [{ kind: "person", name: "the woman dancing", images: [myPhotoUrl], outfit: "original" }],
preview: true,
}
const quote = await client.castSwap.estimate(params)
const { jobId } = await client.castSwap.run(params, { idempotencyKey: crypto.randomUUID() })
```

Follow the job with `client.jobs.get(jobId)`. A preview's job keeps every input in its `input_data`, so you can run it again with `preview: false` to render the full clip with the same choices.

## From MCP

The MCP tool is [`cast_swap`](https://nodaro.ai/docs/mcp/tools/video#cast_swap). Without `confirm`, it returns the quote and charges nothing. With `confirm: true`, after the user accepts the price, it starts the run and returns a job id.

## Frequently asked questions

### What does Cast Swap do?

Cast Swap replaces the people, the location, or both in a short video with your own reference images. The motion, the framing, the timing and the sound of the video stay as they are.

### How long can the source video be?

From 4 to 30 seconds. A shorter or longer clip is refused with clip_out_of_range before anything is charged.

### How do I know what a Cast Swap run costs?

Call POST /v1/cast-swap/estimate with the same body you will run. It is free and returns totalCredits, with a breakdown of the run itself and the video model it calls.

### Can I try the look before I render the whole clip?

Yes. Send preview set to true to render 4 seconds of the clip, from previewStart. A preview costs less, and its job keeps every input, so you can run it again with preview set to false.

### Does Cast Swap work on a self-hosted install?

No. Cast Swap runs on Nodaro Cloud only. On a self-hosted install, both routes answer 404.
