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.
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.
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 is available.
Generate a scene
POST /v1/3d-scene/generate authors a new scene and returns { jobId }. Poll the job with the Jobs API; the completed job's output_data.scenePlan is the editable scene.
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" }
]
}'import { createClient, StaticTokenAuth } from '@nodaro/sdk'
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 }],
})nodaro nodes run generate-3d-scene --params-file scene.json --watch --jsonProp
Type
- 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; a partial time window is refused before any credit is spent.
- Stored geometry.
inputAssetspicks 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 inreferences. 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 -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 } }] }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 -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) }"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:
- Always scope the reference. Pass the MP4 in
referenceVideoUrls[N]on Generate Video, with a caption inreferenceVideoCaptions[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". - 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 -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.jsonconst 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 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 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/:revisionIdreturns 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/:jobIdreturns what an export delivered:sourceKind, the exactsceneRevisionId, the source digests and the descriptors (the poster, the validation report and oneshot-stillper shot).…/assets/:assetIdreturns 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. To show a scene in your own page, see the 3D preview embed.
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
Related
Generate 3D Scene
3D Render Pro
Embed the 3D scene viewport
3D scenes
Generate Video
Last updated on
Character training
Train a high-fidelity model on a character over REST, poll the training, remove the model, and learn when Generate Image uses the trained model.
Workspaces and organizations
Act in an organization's workspace from the Nodaro API with X-Nodaro-Workspace, bind tokens to a workspace, and manage members, invitations, sharing and usage.