# 3D scenes

> Generate, edit and render editable 3D clay scenes over REST, quote and run 3D Render Pro, and read scene revisions, assets and deliveries.

Source: https://nodaro.ai/docs/developers/api/3d-scenes

The **3D scenes API** creates editable, animated clay scenes from a prompt and optional image or video references, edits them, and renders them to MP4. The result of a generation is a **scene plan**, not a video. It holds the objects, their motion, the camera and the lighting, so you can check the framing, the camera move and the blocking before you render. A render is then a layout reference for a video model.

**3D Render Pro** is a separate operation that authors and renders a finished shot in one paid job, where the deployment has an engine for it. The Basic authoring engine works on every edition; Advanced engines and 3D Render Pro depend on the deployment, so check its capabilities first. Credits apply on Nodaro Cloud only. The routes take a bearer token. See [Authentication](https://nodaro.ai/docs/developers/api/authentication).

## Endpoints

| Method | Path | What it does |
| --- | --- | --- |
| `GET` | `/v1/3d-scene/capabilities` | What this deployment supports: Basic, the Advanced engines and 3D Render Pro. |
| `POST` | `/v1/3d-scene/generate` | Author a new scene from a prompt. Returns `{ jobId }`. |
| `POST` | `/v1/3d-scene/edit` | Create a new revision of a scene, from an instruction or from operations. Returns `{ jobId }`. |
| `POST` | `/v1/render-video/plan` | Render a scene revision to MP4. |
| `POST` | `/v1/pro-3d-render/quote` | Price a 3D Render Pro run. Reserves nothing. |
| `POST` | `/v1/pro-3d-render` | Run the quoted 3D Render Pro job. |
| `POST` | `/v1/3d-scene/revisions/:revisionId/edits` | Save deterministic edits to a stored scene, without a job. |
| `GET` | `/v1/3d-scene/revisions/:revisionId` | A stored revision's scene manifest and asset descriptors. |
| `GET` | `/v1/3d-scene/revisions/:revisionId/assets/:assetId` | A revision's playback asset, such as a GLB or the camera track. |
| `GET` | `/v1/3d-scene/revisions/:revisionId/source` | The editable native source of a revision, when it was kept. |
| `GET` | `/v1/3d-scene/deliveries/:jobId` | What an export delivered: the source revision, digests and descriptors. |
| `GET` | `/v1/3d-scene/deliveries/:jobId/assets/:assetId` | The bytes of one delivered asset. |

`POST /v1/generate-3d-scene` and `POST /v1/edit-3d-scene` are aliases of the generate and edit routes, with the same checks and credits. `POST /v1/render-video` also accepts `planType` and `plan`. These aliases let the generic node runner of the SDK reach the same routes.

## Check what the deployment supports

`GET /v1/3d-scene/capabilities` reports Basic support and an `advanced` block, which is `null` when no Advanced engine is available. A `pro` block says whether 3D Render Pro is `available`, and lists the engines, quality profiles, styles, aspect ratios and repair ceiling you may offer. Build your controls from it, not from the full vocabulary.

An engine you request that the deployment cannot serve answers `503 SCENE_CAPABILITY_UNAVAILABLE` before any credit check. Nodaro never falls back to Basic authoring on its own. `GET /v1/nodes` also omits the 3D Render Pro node where it is unavailable, and advertises the `scene3d-embed-v1` capability on the generate node when the interactive [3D preview embed](https://nodaro.ai/docs/developers/embed/scene3d) is available.

## Generate a scene

`POST /v1/3d-scene/generate` authors a new scene and returns `{ jobId }`. Poll the job with the [Jobs API](https://nodaro.ai/docs/developers/api/jobs); the completed job's `output_data.scenePlan` is the editable scene.

**curl**

```bash
curl -X POST https://app.nodaro.ai/v1/3d-scene/generate \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
"prompt": "A red suitcase rolls behind a central pillar and reappears. Dolly right over four seconds.",
"durationSeconds": 4,
"fps": 24,
"aspectRatio": "16:9",
"references": [
{ "id": "suitcase-appearance", "kind": "image", "role": "appearance", "url": "https://cdn.nodaro.ai/uploads/suitcase.png" }
]
}'
```

**TypeScript SDK**

```ts

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

const scene = await client.nodes.runAndWait('generate-3d-scene', {
prompt: 'A red suitcase rolls behind a central pillar and reappears. Dolly right over four seconds.',
durationSeconds: 4,
fps: 24,
aspectRatio: '16:9',
references: [{ id: 'suitcase-appearance', kind: 'image', role: 'appearance', url: appearanceImageUrl }],
})
```

**CLI**

```bash
nodaro nodes run generate-3d-scene --params-file scene.json --watch --json
```

<TypeTable
type={{
prompt: { type: 'string', description: 'The objects, their motion, the camera placement and movement, and the timing.', required: true },
references: { type: 'array', description: 'Up to 8 references, at most 1 of them a video: { id, url, kind, role, objectId? }. kind is image or video; role is appearance, layout or motion.' },
inputAssets: { type: 'array', description: 'Up to 8 stored GLB files: { id, revisionId, assetId, label? }. Needs an Advanced engine with import support.' },
durationSeconds: { type: 'number', description: 'The scene length, 1 to 60 seconds.', default: '4' },
fps: { type: 'number', description: 'Frames per second, 15 to 60.', default: '24' },
aspectRatio: { type: "'16:9' | '9:16' | '1:1' | '4:5'", description: 'The frame.', default: '16:9' },
engine: { type: "'basic' | 'blender-cloud' | 'blender-local'", description: 'The authoring engine. The Advanced engines must be available on the deployment.', default: 'basic' },
llmModel: { type: 'string', description: 'Basic only: the language model that writes the scene. It sets the credit tier.' },
reasoningEffort: { type: 'string', description: 'Basic only: how long the model thinks. A high effort can raise the tier.' },
acceptedSceneSchemaVersions: { type: 'number[]', description: 'The scene-plan versions your client can read, for example [1, 2].' },
maxRepairPasses: { type: 'number', description: 'Advanced engines: how many correction passes the run may spend.' },
}}
/>

- **References are approximate.** An image does not reveal geometry it does not show, and a video is read as a guide for movement and layout. Check the preview before you render.
- **Whole clips only.** A video reference is analyzed in full. To use part of a clip, trim it first with [Trim Video](https://nodaro.ai/docs/nodes/video/trim-video); a partial time window is refused before any credit is spent.
- **Stored geometry.** `inputAssets` picks exact revisions of GLB files you already have. The server checks your access and the file's digest itself, so do not send URLs or hashes, and keep images and videos in `references`. Basic refuses imported geometry before charging.
- **Coordinates.** Scenes use meters, with Y pointing up, rotations in radians and frames counted from zero.

## Edit a scene

`POST /v1/3d-scene/edit` takes the `scenePlan`, its `revisionId` as `expectedRevisionId`, and either a `prompt` (an instruction such as "move the pillar back one meter") or `operations`. A successful edit produces a new revision whose `parentRevisionId` is the old one; the plan you sent stays unchanged. A revision mismatch is refused. The completed job returns `scenePlan` and `changeSummary`.

**curl**

```bash
curl -X POST https://app.nodaro.ai/v1/3d-scene/edit \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d @edit.json

# edit.json:
# { "scenePlan": { ... }, "expectedRevisionId": "rev_7c2e...",
#   "operations": [{ "op": "set-camera", "changes": { "focalLengthMm": 50 } }] }
```

**TypeScript SDK**

```ts
const edited = await client.nodes.runAndWait('edit-3d-scene', {
scenePlan: scene.scenePlan,
expectedRevisionId: scene.scenePlan.revisionId,
operations: [{ op: 'set-camera', changes: { focalLengthMm: 50 } }],
})
```

| Operation | Fields | What it changes |
| --- | --- | --- |
| `set-object` | `objectId`, `changes` | Any field of an object except its id. To change a keyed pose, include its keyframe changes. |
| `add-object` | `object` | Adds an object. |
| `remove-object` | `objectId` | Removes an object. |
| `set-camera` | `changes` | The camera, for example its focal length. |
| `set-lighting` | `changes` | The lighting. |
| `set-background` | `color` | The background color. |

Send `lockedObjectIds` to keep objects unchanged during an instruction edit. New `references` merge with the existing ones by id; the same id replaces a reference, and the total must stay within the generation limits. The whole edited scene is validated, so an edit cannot leave an orphaned parent or reference.

Operations call no language model and cost 0 credits. An instruction edit uses the same tiers as authoring. Keep the previous revision to undo or compare.

### Save edits to a stored scene

For a stored version-2 scene, `POST /v1/3d-scene/revisions/:revisionId/edits` saves deterministic edits (transforms, material colors, visibility and camera offsets) without a job and without a language-model charge. Send `newRevisionId`, the base revision's `expectedContentHash`, the `operations` and, optionally, `lockedObjectIds`. The route returns `{ scenePlan, changeSummary }`.

A stale digest or a conflicting revision id answers `409`. When a request fails in transit, retry with the same `newRevisionId` and the same body. The route needs edit access to the scene, and OAuth app tokens need `workflows:write`. The new revision is saved on its own; select it in your workflow yourself, after checking that nobody changed the active revision meanwhile.

## Render a scene to MP4

`POST /v1/render-video/plan` with `{ "planType": "3d-scene", "plan": scenePlan }` renders one exact revision. The camera, the frame size, the frame rate and the duration come from the plan; no language model is called and the prompt is not read again. The completed job returns `videoUrl`, `thumbnailUrl`, `sceneRevisionId` and `renderer: "scene3d/three"`.

**curl**

```bash
curl -X POST https://app.nodaro.ai/v1/render-video/plan \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{ \"planType\": \"3d-scene\", \"plan\": $(cat scene-plan.json) }"
```

**TypeScript SDK**

```ts
const clip = await client.nodes.runAndWait('render-video', {
planType: '3d-scene',
plan: edited.scenePlan,
})
```

A render is priced by the plan's own `width` and `height`:

| Frame | Price on Nodaro Cloud | Examples |
| --- | --- | --- |
| Longest side 1920 px or less, any shape | 50 credits | 1920x1080, 1080x1920, 1920x1920 |
| Longer than 1920 px, up to 5.12 megapixels | 75 credits | 2560x1440, 1440x2560 |
| Longer than 1920 px, above 5.12 megapixels | 125 credits | 2048x2560, 2560x2560 |

Every aspect ratio the generate route offers renders at 1920 px or less, so a scene you did not resize always costs the base price. The larger tiers apply only when you set a larger `width` and `height` on the plan yourself. The model-cost identifiers are `render-video`, `render-video:3d-large` and `render-video:3d-xlarge`.

## Use a render as a video reference

A clay render carries the layout: where the subjects are, what is in front of what, the framing, the camera move and the timing. It also carries a look, untextured grey clay, and a video model copies that look unless told not to. Two rules keep the layout and drop the clay:

1. **Always scope the reference.** Pass the MP4 in `referenceVideoUrls[N]` on [Generate Video](https://nodaro.ai/docs/nodes/video/generate-video), with a caption in `referenceVideoCaptions[N]` that says what to match and what to ignore. In a workflow, Nodaro adds this caption for you. Its wording is: "LAYOUT reference only — match its subject positions and blocking, its foreground occlusion, its framing, its camera angle, its camera motion and its timing. Ignore its untextured grey clay placeholder look, its flat placeholder colours, its materials, its lighting and its empty background; none of that is the target look. Take the look from the prompt and from the other references".
2. **Give every real-looking figure its own character reference.** A figure without one stays a clay proxy. Keep two reference slots free for a location or a style image.

Also send the original appearance images, and never use the render as the start frame: a start frame fixes the look, and no caption reaches it.

## Run 3D Render Pro

3D Render Pro authors a scene and exports it in one durable job, on a hosted build engine. One run settles with both the exact composition and the MP4. Its price is set by the deployment, so the quote is the authority.

### Quote

`POST /v1/pro-3d-render/quote` returns `{ quoteId, expiresAt, maxCredits, breakdown, pricingVersion, capabilitiesVersion, normalizedInputHash }`. It reserves nothing. `maxCredits` is a ceiling, not a charge.

### Run

`POST /v1/pro-3d-render` takes the same body plus the `quoteId`, and an `Idempotency-Key` header of 8 to 255 characters. It returns `{ jobId }`. A body changed after the quote, or an expired quote, is refused before anything is reserved. Reuse the same key when you retry a request that timed out, so one intent never becomes two paid runs.

**curl**

```bash
curl -X POST https://app.nodaro.ai/v1/pro-3d-render/quote \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d @pro.json

curl -X POST https://app.nodaro.ai/v1/pro-3d-render \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Idempotency-Key: suitcase-shot-0001" \
  -H "Content-Type: application/json" \
  -d @pro-with-quote.json
```

**TypeScript SDK**

```ts
const caps = await client.scene3d.capabilities()
if (caps.pro?.available) {
const params = {
source: {
kind: 'prompt' as const,
prompt: 'A red suitcase rolls behind a central pillar and reappears',
references: [{ id: 'look', kind: 'image' as const, role: 'appearance' as const, url: appearanceImageUrl }],
},
durationSeconds: 30,
fps: 24,
aspectRatio: '21:9',
maxRepairPasses: 2,
acceptedSceneSchemaVersions: [2],
}
const quote = await client.scene3d.quotePro(params)
console.log(quote.maxCredits, quote.breakdown)
const shot = await client.scene3d.renderProAndWait({ ...params, quoteId: quote.quoteId })
console.log(shot.videoUrl, shot.sceneRevisionId)
}
```

The body's `source` is exactly one of:

| `source` | What happens |
| --- | --- |
| `{ kind: "prompt", prompt, references?, inputAssets? }` | Authors a new scene, then renders it. |
| `{ kind: "scene", revisionId, sourceJobId }` | Renders that exact revision only, with no authoring or build charge. `sourceJobId` is required for Basic scenes kept only in job history. |
| `{ kind: "scene", revisionId, sourceJobId, editPrompt }` | Revises the scene first, then renders it. Omit `editPrompt` for a plain export: an empty string is a different request. |
| `{ kind: "local-export", exportId, connectionId }` | Uses a finished export from a paired desktop app, where that is available. |

| Field | What it does |
| --- | --- |
| `engine` | `blender-cloud` (the default), or `blender-local` where a desktop is paired. |
| `localConnectionId` | The paired desktop, for `blender-local`. |
| `quality`, `style` | A quality profile, and the style, `clay`. |
| `maxRepairPasses` | 0 to 2, default 2. Each pass is paid work. |
| `durationSeconds`, `fps`, `aspectRatio` | The timing and the frame. `aspectRatio` includes `21:9`. |
| `acceptedSceneSchemaVersions` | The scene-plan versions your client can read. |
| `workflowId`, `nodeId`, `forcePrivate` | The usual run context. |

There is no model field: the planner is fixed. For a `scene` source, omit the timing fields to keep the scene's own; sending them re-times the scene, and an incompatible value is refused. A prompt source produces a version-2 scene, so a client that does not accept version 2 is refused for free.

### What a finished run returns

| Field | What it holds |
| --- | --- |
| `videoUrl`, `scenePlan`, `sceneRevisionId` | The MP4, the exact composition it was rendered from, and that revision's id. Export the revision again later, render-only. |
| `posterAssetId`, `shotStills` | The poster, and one still per shot as `{ shotIndex, frame, assetId, url }`, ordered by shot, at no extra cost. |
| `validation` | `{ status, reportAssetId, warnings }`. |
| `renderer`, `metadata` | The renderer, and `{ width, height, fps, frames, duration }`. |
| `metadata.summary`, `repairPasses`, `admissionRetries`, `mechanicalPasses`, `restoredAssertions` | A run that authored reports what it did: a short summary, the repairs it ran, and other passes. A render-only export omits them. |
| `metadata.review` | Present only when the scene passed every mandatory check but was delivered without the visual reviewer's approval. |

Each still's `url` is an authenticated endpoint on the deployment, not a public link: fetch it with your own token. You can still pass it as an image reference to a generation, and Nodaro grants that run a short-lived read of that one file.

Test for `metadata.review` itself, then read its `verdict`. `refused` means the reviewer still objected after the repair budget was spent, and `objections` lists what it found. `unavailable` means the review gave no usable answer, so nobody judged the scene. In both cases `validation.status` is still `passed`, because the mandatory checks did pass. Warning codes are open-ended; treat a code you do not know as information. See [3D Render Pro](https://nodaro.ai/docs/nodes/video/pro-3d-render) for every field and warning code.

### When a run fails

Errors on the submission are refusals: `503 SCENE_CAPABILITY_UNAVAILABLE`, `503 price_not_configured` (the operator has not set a price; nothing was reserved) and `400 validation_error`. Failures during the run are on the job, and the error message starts with the code:

| Code | Retry? | Meaning |
| --- | --- | --- |
| `SCENE_PROVIDER_UNAVAILABLE` | Yes, after a few minutes | The planner's model was unavailable or overloaded. |
| `SCENE_PLANNING_TIMEOUT` | Yes | Planning took too long. Retry, or shorten the brief and the references. |
| `SCENE_PLANNER_OUTPUT_INVALID` | Not unchanged | The plan could not be built. Simplify the brief or use fewer references. |
| `SCENE_QUALITY_FAILED` | Read the draft first | A mandatory check failed, or the recipe was refused, after the repair budget was spent. |
| `SCENE_RESOURCE_LIMIT`, `SCENE_EXPORT_UNSUPPORTED`, `SCENE_REVISION_CONFLICT`, `SCENE_BUILD_TIMEOUT`, `SCENE_RENDER_FAILED` | Depends | The build or the render could not finish. |

A failed Advanced job can still carry what it built. When a pass built a scene, `output_data` holds the draft: `scenePlan`, `sceneRevisionId`, `deliveryId`, `posterAssetId`, and `validation` with `status: "failed"`. The draft is an ordinary revision you can edit or render. When no pass compiled, there is no draft, but `validation.sourceRetained` says whether the refused recipe was kept; read it from the delivery's `source-json` descriptor with edit access, for free. Re-running the same prompt instead pays for the same authoring twice.

In a workflow run, a failed node that kept a result also carries it in `nodeStates[nodeId].output`. Check for the field, not the status, and never read a present `output` as success.

## Scene versions and stored assets

A scene plan is a union of two versions. Read `schemaVersion` before any version-specific field.

| Version | What it stores | Produced by |
| --- | --- | --- |
| 1 | Primitive shapes, groups and sparse keyframes for objects and the camera. | The Basic engine. |
| 2 | Named entities, stored GLB geometry, a camera sampled on every frame, and contiguous shots. | Advanced engines and 3D Render Pro. |

A version-2 plan lists each asset by an opaque `assetId`, its byte length and its SHA-256 digest; it never contains a storage key or a download URL. Version 2 accepts clay geometry with rigid animation; textured, skinned and morphing assets are refused. Its limits are 100 entities, 2,000 mesh nodes, 200,000 triangles, 32 shots and 64 MiB of playback assets. See [Scene plan format](https://nodaro.ai/docs/developers/embed/scene3d-format) for the full format.

Stored revisions have their own routes. They need a bearer token, answer with `Cache-Control: no-store`, and return `404` for a deleted or unreachable revision:

- `GET /v1/3d-scene/revisions/:revisionId` returns the manifest and the asset descriptors. Playback assets need view access to the revision's workflow; the native source needs edit access. Personal revisions are owner-only.
- `GET /v1/3d-scene/deliveries/:jobId` returns what an export delivered: `sourceKind`, the exact `sceneRevisionId`, the source digests and the descriptors (the poster, the validation report and one `shot-still` per shot). `…/assets/:assetId` returns the bytes, with range support. Reads need access to both the delivery and the source workflow, and they cost nothing.

In the SDK, `client.scene3d.getDelivery`, `deliveryAssetBytes`, `retainedRecipe`, `assetBytes`, `sourceBytes` and `applyEdits` wrap these routes.

## Use it from MCP

AI assistants use `generate_3d_scene`, `edit_3d_scene` and `render_3d_scene` with the `workflows:execute` scope, and `pro_3d_render` where the deployment can serve it. See [3D scenes over MCP](https://nodaro.ai/docs/mcp/3d-scenes). To show a scene in your own page, see the [3D preview embed](https://nodaro.ai/docs/developers/embed/scene3d).

## Errors

| Status | Code | Meaning |
| --- | --- | --- |
| `400` | `validation_error` | The body is invalid, for example a reference list over its limits, an unknown engine, a missing `quoteId`, a missing or bad `Idempotency-Key`, or a schema version your client does not accept. |
| `401` | `unauthorized` | The token is missing, invalid or revoked. |
| `402` | `insufficient_credits` | Nodaro Cloud only. The account cannot cover the reservation. |
| `404` | `not_found` | The revision or delivery does not exist, or you cannot reach it. |
| `409` | — | An edit's revision or content digest no longer matches. Read the scene again. |
| `503` | `SCENE_CAPABILITY_UNAVAILABLE` | The engine, the import support or 3D Render Pro is not available on this deployment. |
| `503` | `price_not_configured` | No credit price is set for 3D Render Pro on this deployment. Nothing was reserved. |

## Frequently asked questions

### What does the 3D scene API produce?

An editable scene plan, not a video. The plan holds the objects, their motion, the camera and the lighting. Edit it as often as you like, then render it to an MP4 with POST /v1/render-video/plan.

### How much does a 3D scene cost?

On Nodaro Cloud, authoring a scene costs 10, 30 or 40 credits depending on the language model tier, plus the Video Analysis price when you give a video reference. Rendering costs 50 credits up to 1920 px on the longest side, 75 up to 5.12 megapixels, and 125 above that.

### Are deterministic edits free?

Yes. Edits sent as operations, such as moving an object or changing the lens, call no language model and cost 0 credits. Instruction edits, written as a prompt, cost the same as authoring.

### How do I use a clay render as a video reference?

Pass the MP4 in referenceVideoUrls on Generate Video with a caption in referenceVideoCaptions that says to match the layout and camera and ignore the grey clay look. Also send your appearance images and a character reference for every figure that must look real.

### What is the difference between 3D Render Pro and Generate 3D Scene?

Generate 3D Scene makes a cheap, editable clay preview, and a separate render turns it into an MP4. 3D Render Pro authors and renders a finished shot in one paid job, where the deployment has an engine for it.
