# Recast

> Recast an analyzed video with your own cast over REST: quote and buy a run, answer interactive gates, remix its audio, or import an authored script.

Source: https://nodaro.ai/docs/developers/api/recast

The **Recast API** regenerates an analyzed video with your own cast. You quote the run, buy its plan, render it scene by scene, and, on an interactive run, pick the cast, the scene stills and the music along the way. You can also write a movie as a JSON script and import it, so a recast needs no source video at all. It is the engine behind [recast.nodaro.ai](https://recast.nodaro.ai).

Recast runs on Nodaro Cloud only; on self-hosted installs the routes answer `404`. On a self-hosted install, break a video down with the [Video Analysis](https://nodaro.ai/docs/nodes/video/video-analysis) node and regenerate its scenes with [Generate Video](https://nodaro.ai/docs/nodes/video/generate-video) in a workflow. The routes take a bearer token. See [Authentication](https://nodaro.ai/docs/developers/api/authentication).

## Endpoints

| Method | Path | What it does | Cost |
| --- | --- | --- | --- |
| `POST` | `/v1/recast/estimate` | Quote a run. | Free |
| `POST` | `/v1/recast` | Create a run. This buys the plan. | The quoted plan |
| `GET` | `/v1/recast/:id` | Poll a run and read the pending gate. | Free |
| `POST` | `/v1/recast/:id/start` | Start rendering a `planned` run. | Covered by the plan |
| `POST` | `/v1/recast/:id/select` | Answer a pending gate. | Free |
| `POST` | `/v1/recast/:id/estimate-rescore` | Quote a new soundtrack or a new mix. | Free |
| `POST` | `/v1/recast/:id/rescore` | Apply the quoted audio change. | The quoted price |
| `GET` | `/v1/video-analysis/authoring-skill` | Get the guide for writing a script. | Free |
| `POST` | `/v1/video-analysis/import/validate` | Validate a script. | Free |
| `POST` | `/v1/video-analysis/import` | Import a script as a completed analysis. | Free |

## Quote and create a run

A run starts from an analysis job: either a video analyzed by the [Video Analysis](https://nodaro.ai/docs/nodes/video/video-analysis) node, or an [imported script](#import-a-script-as-a-movie). Quote it first. `POST /v1/recast/estimate` takes the settings you will create the run with and returns `{ totalCredits, breakdown }`.

`POST /v1/recast` then creates the run and buys its plan. It returns `{ recastId }`. The body needs `workflowId`, the id of a workflow you own that the run attaches to. Without it, the route answers `400 workflow_id_required`; with an unknown or foreign id, `404 workflow_not_found`.

**curl**

```bash
curl -X POST https://app.nodaro.ai/v1/recast/estimate \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "analysisJobId": "6c1e8a3f-9b2d-4f5a-8e7c-3d1b9a5f2e6c", "resolution": "720p", "interactive": true }'

curl -X POST https://app.nodaro.ai/v1/recast \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
"workflowId": "8d3f5b7a-1c9e-4a2d-b6f8-4e2a7c9d1b3f",
"analysisJobId": "6c1e8a3f-9b2d-4f5a-8e7c-3d1b9a5f2e6c",
"resolution": "720p",
"interactive": true,
"clientCapabilities": ["sheet-gate"]
}'
```

**TypeScript SDK**

```ts

const client = createClient({
baseUrl: 'https://app.nodaro.ai',
auth: new StaticTokenAuth(process.env.NODARO_API_KEY!),
})

const quote = await client.recast.estimate({ analysisJobId, resolution: '720p', interactive: true })
console.log(quote.totalCredits, quote.breakdown)

const { recastId } = await client.recast.create({
workflowId,
analysisJobId,
resolution: '720p',
interactive: true,
clientCapabilities: ['sheet-gate'],
})
```

**CLI**

```bash
nodaro recast estimate --analysis-job <jobId> --resolution 720p --json
nodaro recast create --workflow <workflowId> --analysis-job <jobId> --resolution 720p --json
```

<TypeTable
type={{
workflowId: { type: 'string (uuid)', description: 'Create only. A workflow you own that the run attaches to.', required: true },
analysisJobId: { type: 'string (uuid)', description: 'The analysis to recast: from Video Analysis or from a script import.', required: true },
fidelity: { type: 'string', description: 'How closely the run follows the analysis. An imported script uses faithful: exactly as written.' },
rightsAttested: { type: 'boolean', description: 'Create only. Required as true for a faithful render of an imported script.' },
resolution: { type: 'string', description: 'The render resolution, for example 480p, 720p or 1080p.' },
segmentSec: { type: 'number', description: 'How the video is cut into generated segments.' },
renderMethod: { type: 'string', description: 'How segments are rendered, for example extend or keyframes.' },
provider: { type: 'string', description: 'The video model.' },
interactive: { type: 'boolean', description: 'Stop at gates so you can pick the cast, the stills and the music. Adds a priced surcharge that the quote includes.' },
clientCapabilities: { type: 'string[]', description: 'Create only. The gate kinds your client can answer, for example sheet-gate.' },
}}
/>

To reuse a set of render settings, save them as a `recast-render` preset. See [Presets](https://nodaro.ai/docs/developers/api/presets#recast-render-presets).

## Follow a run

`GET /v1/recast/:id` returns `{ status, interactive?, capabilities?, audio? }`. The status goes through `planning`, `planned`, `generating` and then `completed` or `failed`. A `planned` run waits for `POST /v1/recast/:id/start`, which starts rendering and returns `{ gvpJobId? }`. The start route is idempotent and costs nothing more, because the plan's quote already covered the render.

**curl**

```bash
curl https://app.nodaro.ai/v1/recast/2e9b4d6f-8a1c-4e3b-9f5d-7c2a4e6b8d1f \
  -H "Authorization: Bearer $NODARO_API_KEY"

curl -X POST https://app.nodaro.ai/v1/recast/2e9b4d6f-8a1c-4e3b-9f5d-7c2a4e6b8d1f/start \
  -H "Authorization: Bearer $NODARO_API_KEY"
```

**TypeScript SDK**

```ts
const run = await client.recast.get(recastId)
if (run.status === 'planned') await client.recast.start(recastId)
```

**CLI**

```bash
nodaro recast status <recastId> --json
nodaro recast start <recastId>
```

## Answer interactive gates

An interactive run is driven by the server: Nodaro advances every step that needs no choice, and you only poll and answer gates. When a gate is waiting, `interactive.next` in the status names it. Gates open in this order:

| `gate` | What you choose |
| --- | --- |
| `cast` | One portrait for each cast member. |
| `sheet` | For a person only, when the run offers it: one of 3 identity sheets that share the chosen face, so you choose the body and the wardrobe. |
| `anchors` | The stills of a scene segment. |
| `music` | The music for a section of the film. |

A gate opens only for the kinds your create declared in `clientCapabilities`, for example `sheet-gate`. Any other gate is decided automatically, so a client never sees a question it cannot answer.

Answer with `POST /v1/recast/:id/select`. The pick is free.

| Field | What it does |
| --- | --- |
| `gate` | `cast`, `sheet`, `anchors` or `music`. |
| `picks` | For `cast` and `sheet`: your choices, in the shape the pending gate shows. |
| `segment`, `anchorPicks` | For `anchors`: the segment, and `{ start?, end? }`, the chosen stills. |
| `section`, `musicPick` | For `music`: the section, and the chosen track. |
| `finishAuto` | `true` hands this gate and every remaining one to the automatic reviewer. |

```ts
await client.recast.resolveGate(recastId, { gate: 'cast', picks })
await client.recast.resolveGate(recastId, { gate: 'music', section: 0, musicPick: 1, finishAuto: true })
```

An abandoned interactive run is safe: it waits, then resolves itself when its deadline passes.

## Change the soundtrack or the mix

After a take completes, you can replace its music or rebalance it without rendering the video again. This works only when the status carries `capabilities.audioLayers: 1` and the take has an `audio` manifest:

```ts
interface RecastAudioManifestV1 {
version: 1
revision: string
mode: 'bed' | 'replace'
present: { music?: true; video?: true }
layers: { music?: { url: string }; video?: { url: string } }
bakedEffectiveGain: { music?: number; video?: number }
pendingRescore?: {
jobId: string
requestId: string
state: 'pending' | 'running'
expectedAudioRevision: string
requestedEffectiveGain: { music?: number; video?: number }
}
}
```

- `present` lists the audio lanes the take has: `music` and, in `bed` mode, the original `video` sound.
- `layers` lists only the lanes with a preview file your browser can play. A lane missing from `layers` can still be in the download.
- `bakedEffectiveGain` is the level of each lane in the current file, in percent.
- `resultUrl` in the status is the only video URL you receive.

### Quote, then apply

Quote and apply take the same operation. Send at most one music replacement, either `audioUrl` or one or more `sections` with a `brief`, plus the complete `mix` you want. A mix alone is also valid.

```json
{
"expectedAudioRevision": "server-revision",
"sections": [{ "index": 0, "brief": "Sparse analogue pulse" }],
"mix": {
"music": { "gain": 60, "muted": false },
"video": { "gain": 85, "muted": false }
}
}
```

1. **Quote.** `POST /v1/recast/:id/estimate-rescore` is free and returns `{ credits, audioRevision, noOp }`. It returns the price even when your balance is too low.
2. **Apply.** `POST /v1/recast/:id/rescore` takes the same body plus a `requestId` (a UUID) and the same `expectedAudioRevision`. It returns `{ recastId, jobId }`, or `{ recastId, noOp: true, audioRevision }` when nothing changes. A no-op reserves no credits and creates no job.
3. **Follow.** Poll the status. `audio.pendingRescore` shows the operation, survives a reload, and disappears when the new revision is published or the operation fails. Read the status again before the next operation.

Gains are percentages from 0 to 200; a muted lane counts as 0. Address only the lanes in `present`, or music that this request adds. A `replace`-mode take has no `video` lane, and the result may not leave every lane silent. Reuse a `requestId` only to retry the identical request.

Send the complete `mix` with a music replacement. Omitting it works only when the result matches the fixed standard levels: music 35 and video 100 in `bed` mode, or music 100 in `replace` mode. Any other current level answers `409 legacy_mix_mismatch`.

**curl**

```bash
curl -X POST https://app.nodaro.ai/v1/recast/2e9b4d6f-8a1c-4e3b-9f5d-7c2a4e6b8d1f/rescore \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
"requestId": "4f6a8c1e-3b5d-4e7f-9a2c-6d8b1f3e5a7c",
"expectedAudioRevision": "server-revision",
"mix": { "music": { "gain": 60, "muted": false }, "video": { "gain": 85, "muted": false } }
}'
```

**TypeScript SDK**

```ts
const status = await client.recast.get(recastId)
const revision = status.audio?.revision
if (status.capabilities?.audioLayers === 1 && revision) {
const operation = {
expectedAudioRevision: revision,
mix: { music: { gain: 60, muted: false }, video: { gain: 85, muted: false } },
}
const quote = await client.recast.estimateRescore(recastId, operation)
if (!quote.noOp) {
await client.recast.rescore(recastId, { ...operation, requestId: crypto.randomUUID() })
}
}
```

## Import a script as a movie

You can write a movie as a JSON document, often with the help of a language model, and recast it with no source video. All three routes are free.

### Read the authoring guide

`GET /v1/video-analysis/authoring-skill` returns the guide as Markdown: the document's fields, the allowed values, the limits, the audio rules and a validated example. Give it to the model that writes your script.

### Validate until the script is valid

`POST /v1/video-analysis/import/validate` with `{ "script": { … } }` returns `{ valid, errors, warnings }`. Each error has a `path`, a `message` and usually a `hint` written for a repair loop. Fix each path and validate again until `valid` is `true`.

### Import it

`POST /v1/video-analysis/import` with `{ "script": { … }, "rightsAttested": true }` stores the script as a completed analysis and returns `{ jobId, created, warnings, json }`. `json` is your document with the fields the server derives; keep it as the document of record. Importing the same script again returns the same `jobId` with `created: false`.

### Recast it

Create a run with that `jobId` as `analysisJobId`, `fidelity: "faithful"` and `rightsAttested: true`.

`rightsAttested: true` is required: an authored recast renders exactly as written, brand names included, so it confirms the script is your own work. Without it, the import answers `403 rights_attestation_required`.

The document has these parts:

| Part | What it holds |
| --- | --- |
| `meta` | `durationSec`, `width`, `height`, `aspectRatio` (`16:9` or `9:16`, matching the width and height) and a required `title`, which names the project. |
| `look` | Optional. The film's overall look. |
| `slots` | The cast and the settings, each with a `role`: `person`, `object` or `background`. |
| `scenes` | The scenes, numbered from 0 without gaps, each 8 seconds or less. The total runs from 4 seconds up to the platform's run limit. |

A document over the run limit is refused, never cut short. Do not write `sceneNumber`, `slotRefs` or `visualResolved`: the server derives them and ignores your values. That is also why an analysis you copied from the editor with **Copy JSON** imports as it is.

**curl**

```bash
curl https://app.nodaro.ai/v1/video-analysis/authoring-skill \
  -H "Authorization: Bearer $NODARO_API_KEY" > recast-authoring.md

curl -X POST https://app.nodaro.ai/v1/video-analysis/import \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{ \"script\": $(cat script.json), \"rightsAttested\": true }"
```

**TypeScript SDK**

```ts
const guide = await client.recast.authoringSkill()
const check = await client.recast.validateScript(script)
if (check.valid) {
const { jobId } = await client.recast.importScript(script, { rightsAttested: true })
}
```

**CLI**

```bash
nodaro recast skill > recast-authoring.md
nodaro recast validate --file script.json
nodaro recast import --file script.json --rights-attested --json
```

## Use it from MCP

AI assistants run the same loop with `get_recast_authoring_skill`, `validate_recast_script`, `import_recast_script`, `start_recast`, `get_recast_status` and `resolve_recast_gate`. `start_recast` shows the price first and spends only when called again to confirm. See [Recast over MCP](https://nodaro.ai/docs/mcp/recast).

## Errors

| Status | Code | Meaning |
| --- | --- | --- |
| `400` | `workflow_id_required` | `POST /v1/recast` was sent without `workflowId`. |
| `400` | `validation_error`, `duplicate_section`, `unknown_section`, `all_audio_silent` | The request or the audio operation is invalid. |
| `402` | `insufficient_credits` | The account cannot cover the plan or the audio change. |
| `403` | `rights_attestation_required` | A script import came without `rightsAttested: true`. |
| `404` | `workflow_not_found` | The workflow does not exist or is not yours. |
| `404` | `not_found` | The run does not exist, or the instance is self-hosted. |
| `409` | `audio_layers_unavailable`, `audio_layer_unavailable`, `audio_preview_unavailable` | The take has no revisioned audio, or the lane you named is missing or has no usable preview. |
| `409` | `rescore_sections_unavailable`, `legacy_mix_mismatch` | Music sections cannot be replaced on this take, or a replacement without a `mix` does not match the current levels. |
| `409` | `stale_audio_revision`, `rescore_in_progress`, `idempotency_conflict` | The audio changed, another change is running, or a `requestId` was reused for a different request. Read the status and retry. |

## Frequently asked questions

### What is a recast?

A recast regenerates an analyzed video, scene by scene, with your own cast. The source can be a real video that Nodaro analyzed, or a screenplay you wrote as JSON and imported. It is the engine behind recast.nodaro.ai.

### How do I know what a recast will cost before I pay?

Call POST /v1/recast/estimate with the same settings you will create the run with. It returns the total in credits and a breakdown, and it is free. POST /v1/recast then buys the plan.

### Can I make a movie without a source video?

Yes. Write the movie as a JSON script, validate it for free, and import it. The import creates an analysis you recast with fidelity faithful, so every scene renders exactly as written.

### Why does my interactive run never stop at the sheet gate?

A gate opens only when the create request declared that your client can answer it, in clientCapabilities. Undeclared gate kinds are decided automatically.

### Does Recast work on a self-hosted install?

No. The Recast routes run on Nodaro Cloud only and answer 404 on self-hosted installs.
