Nodaro Docs
DocumentationNode ReferenceModelsAI Agents (MCP)DevelopersSelf-hostingResearch
TypeScript SDK

3D scenes

Generate an editable 3D scene from a prompt, edit it, render it to MP4, and run 3D Render Pro from TypeScript with client.scene3d and the 3D scene nodes.

client.scene3d makes editable 3D scenes: it authors a scene from a prompt, edits it by instruction or by exact operations, and renders a revision to an MP4. It also runs 3D Render Pro, one operation that authors a scene and renders it, and reads the files a render delivered. The methods use the Generate 3D Scene, Edit 3D Scene, Render Video and 3D Render Pro nodes. See the 3D scenes REST API for the endpoints.

Methods

MethodWhat it does
capabilities()Read which engines and Pro options this install offers
generate(params) and generateAndWait()Author a new scene from a prompt
edit(params) and editAndWait()Make a new revision of a scene
render(params) and renderAndWait()Render a revision to an MP4, without authoring
applyEdits(revisionId, params)Save exact edits without authoring or a charge
quotePro(params)Price a 3D Render Pro run
runPro(params, options?)Start a quoted 3D Render Pro run
renderProAndWait(params, options?)Quote, run and wait in one call
getDelivery(jobId)Read the files a render delivered
deliveryAssetBytes(jobId, asset, options?)Download one delivered file
retainedRecipe(jobId, options?)Read the recipe a refused Pro run kept
assetBytes(revisionId, asset, options?)Download a file of a scene revision
sourceBytes(revisionId, options?)Download a revision's native source file

From prompt to MP4

const created = await client.scene3d.generateAndWait({
  prompt: "Orbit a single box on a floor over four seconds",
  durationSeconds: 4,
  fps: 24,
  aspectRatio: "16:9",
})

const edited = await client.scene3d.editAndWait({
  scenePlan: created.scenePlan,
  expectedRevisionId: created.scenePlan.revisionId,
  operations: [{ op: "set-camera", changes: { focalLengthMm: 50 } }],
})

const clip = await client.scene3d.renderAndWait({ planType: "3d-scene", plan: edited.scenePlan })
console.log(clip.videoUrl)

The same three steps work with client.nodes.runAndWait() and the node types generate-3d-scene, edit-3d-scene and render-video, which have typed parameters. To let people rotate and adjust a scene in your page, use the 3D preview embed. It needs no copy of the renderer and no tokens in its messages.

client.scene3d

capabilities()

Returns what this install can author and render (GET /v1/3d-scene/capabilities).

capabilities(): Promise<Scene3DCapabilities>
const caps = await client.scene3d.capabilities()
if (caps.advanced) console.log(caps.advanced.engines) // for example ["blender-cloud"]
if (caps.pro) console.log("3D Render Pro is available")
  • basic is the deterministic engine: { available, sceneSchemaVersions }.
  • advanced lists optional engines, or is null when there are none. An explicit engine that is unavailable is refused before a Basic generation or its credit checks.
  • pro describes 3D Render Pro: the engines, quality profiles, styles and aspect ratios this install serves. Offer only those. When pro is absent, the node is unavailable.

generate(params) and generateAndWait(params, options?)

Authors a new scene from a prompt. generate() returns the job at once; generateAndWait() waits for it and resolves with scenePlan and an optional changeSummary.

generate(params: GenerateScene3DParams): Promise<RunNodeResult>
generateAndWait(params: GenerateScene3DParams, options?: RunAndWaitOptions): Promise<Scene3DJobOutput>

Prop

Type

const { scenePlan } = await client.scene3d.generateAndWait({
  prompt: "A paper boat drifts across a puddle as rain starts",
  references: [{ id: "look", kind: "image", role: "appearance", url: moodImageUrl }],
})

inputAssets select GLB models you are allowed to use, by their immutable ids; references still carry the appearance images and motion videos. Do not send asset URLs or hashes: the server supplies the byte records. Basic and engines that cannot import refuse inputAssets before charging. Reuse the same selectors when you submit a Pro quote.

edit(params) and editAndWait(params, options?)

Makes a new revision of a scene. The scene you pass is never changed. Give an instruction in prompt, or exact operations, never both.

edit(params: EditScene3DParams): Promise<RunNodeResult>
editAndWait(params: EditScene3DParams, options?: RunAndWaitOptions): Promise<Scene3DJobOutput>

Prop

Type

const { scenePlan: next, changeSummary } = await client.scene3d.editAndWait({
  scenePlan,
  expectedRevisionId: scenePlan.revisionId,
  prompt: "Make the rain heavier and lower the camera",
})

render(params) and renderAndWait(params, options?)

Renders the exact revision you pass to an MP4, without authoring or rebuilding anything (POST /v1/render-video/plan).

render(params: { planType: "3d-scene"; plan: Scene3DPlan; workflowId?: string; nodeId?: string }): Promise<RunNodeResult>
renderAndWait(params: RenderScene3DParams, options?: RunAndWaitOptions): Promise<NodeJobOutput>

Prop

Type

const { videoUrl } = await client.scene3d.renderAndWait({ planType: "3d-scene", plan: scenePlan })

The price follows the plan's own width and height:

FrameCreditsPrice identifier
Up to 1,920 pixels on the longest side50render-video
Larger, up to 5.12 megapixels75render-video:3d-large
Above 5.12 megapixels125render-video:3d-xlarge

Read the current prices of those identifiers with client.credits.modelCosts().

applyEdits(revisionId, params)

Saves exact edits to a kept revision without authoring and without a charge (POST /v1/3d-scene/revisions/:id/edits). It returns the new scenePlan and a changeSummary.

applyEdits(revisionId: string, params: {
  newRevisionId: string
  expectedContentHash: string
  operations: Scene3DV2EditOperation[]
  lockedObjectIds?: string[]
}): Promise<{ scenePlan: Scene3DPlanV2; changeSummary: string }>

Prop

Type

const { scenePlan: saved } = await client.scene3d.applyEdits(revisionId, {
  newRevisionId: crypto.randomUUID(),
  expectedContentHash,
  operations,
})

Adopt the returned scene only if the user is still editing the revision you started from. Geometry and camera files are reused. Posters, validation and native downloads are attached again only after they are made for the new revision.

3D Render Pro

3D Render Pro is one operation: a source goes in, and one job settles with both scenePlan, the exact composition, and videoUrl, the MP4. The result also carries the revision, a poster, shotStills, validation and renderer details. client.nodes.run("pro-3d-render") and runAndWait reach the same route with the same typed parameters.

An install without the engine answers 503 SCENE_CAPABILITY_UNAVAILABLE and never falls back to Basic. An install without a price answers 503 price_not_configured before it reserves anything. There is no model or effort setting: the planner is fixed by the server.

quotePro(params)

Prices a run without starting it. It reserves and spends nothing. The answer has quoteId, a ceiling in maxCredits, a breakdown to show, and the input hash the run is later checked against.

quotePro(params: Pro3DRenderParams): Promise<Pro3DRenderQuote>

Prop

Type

source is one of three shapes:

  • { kind: "prompt", prompt, references? } authors a new scene.
  • { kind: "scene", revisionId, sourceJobId } without editPrompt exports a kept scene with no authoring charge. Adding editPrompt revises the scene first. sourceJobId is required for Basic scenes that exist only in job history.
  • { kind: "local-export", exportId, connectionId } uses a paired desktop.
const quote = await client.scene3d.quotePro(params)
showPrice(quote.maxCredits, quote.breakdown)

runPro(params, options?)

Starts a quoted run. It needs the quoteId from quotePro(), so no run starts at a price nobody saw. It sends an Idempotency-Key: a new one per call, or yours in options.idempotencyKey. Reuse yours when you retry a call that timed out.

runPro(params: Pro3DRenderParams & { quoteId: string }, options?: { idempotencyKey?: string }): Promise<RunNodeResult>

Prop

Type

await client.scene3d.runPro({ ...params, quoteId: quote.quoteId })

renderProAndWait(params, options?)

Quotes when params has no quoteId, runs, and waits: the whole operation in one call. It sends two requests at most and starts one paid job, admitted against a hash of exactly what was priced.

renderProAndWait(params: Pro3DRenderParams | Pro3DRenderRunParams, options?: RunAndWaitOptions & { idempotencyKey?: string }): Promise<Pro3DRenderJobOutput>

Prop

Type

const caps = await client.scene3d.capabilities()
if (caps.pro?.available) {
  const shot = await client.scene3d.renderProAndWait({
    source: {
      kind: "prompt",
      prompt: "A red suitcase rolls behind a central pillar and reappears",
      references: [{ id: "look", kind: "image", role: "appearance", url: appearanceImageUrl }],
    },
    durationSeconds: 30,
    fps: 24,
    aspectRatio: "21:9",
    maxRepairPasses: 2,
    acceptedSceneSchemaVersions: [2],
  })
  console.log(shot.videoUrl)        // the MP4
  console.log(shot.sceneRevisionId) // export it again later, with no authoring charge
}

Shot stills. shotStills is a list of { shotIndex, frame, assetId, url }, one still per shot at the frame the shot opens on, made by the same run at no extra cost. A single-shot scene has one, at frame 0. Results made before the field existed have none, so read shot.shotStills ?? [].

Each url is an authenticated endpoint of your install, not a public link. Fetch it with the same credentials you used for the run; an img tag, or a third-party service, gets a 401:

for (const still of shot.shotStills ?? []) {
  const res = await fetch(still.url, { headers: { Authorization: `Bearer ${token}` } })
  const bytes = await res.arrayBuffer() // use the bytes, or store them where your pipeline can read them
}

You can still use a still as a model reference: pass its URL as you read it, in referenceImageUrls or through the node's stills output. The platform grants that run a short read of that one file, in your name. The grant expires minutes later, so store the authenticated URL, never the grant.

What a Pro run reports about itself

A run that authored a scene reports what it assumed and did. Read every field as optional: an install without an advanced engine, or an older result, has none of them.

const summary = shot.metadata?.summary // what the planner says it built
const repairs = shot.repairPasses      // 0 means accepted the first time
const retries = shot.admissionRetries  // planner retries before a build
const mechanical = shot.mechanicalPasses
const restored = shot.restoredAssertions
const assumptions = (shot.validation?.warnings ?? [])
  .filter((w) => w.code === "SCENE_AUTHORING_ASSUMPTION")
  .map((w) => w.message)
  • SCENE_AUTHORING_ASSUMPTION warnings list what the prompt did not say, so the run decided. Warning codes can be added: treat an unknown code as information, not as an error.
  • repairPasses counts repairs, not authoring passes, so 0 means the scene was accepted the first time. admissionRetries counts something else: recipes the planner was asked again before any build.
  • mechanicalPasses counts repairs the engine applied from the compiler's own remedy, without the planner. They have their own quoted allowance of up to 2, released when unused, and each adds a REMEDY_AUTO_APPLIED warning. A run quoted before that allowance existed charged such passes as repairs; read the quote you were given to tell.
  • restoredAssertions lists required checks the engine put back after the planner changed one it was not asked to change. Each also adds an ASSERTION_RESTORED warning.
  • A render-only export authored nothing, so it has no counts and no summary. For mechanicalPasses especially, absent is not 0.

generateAndWait() results carry the same fields when an advanced engine authored the scene. The Basic engine asks no model and carries none of them.

A delivery the visual reviewer did not approve

A completed result can arrive without the visual reviewer's approval. The video is real and the credits were spent in both cases. metadata.review.verdict says which case it is:

  • "refused": the repair budget was spent, every required check passed, and the reviewer still objected. The scene was delivered with the refusal attached.
  • "unavailable": the review gave no usable verdict. reason is "provider" when it never reached its provider, and "unusable" when the answer was unusable. attempts says how many times it was asked. Nobody judged this scene.
import { scene3DReviewNote, scene3DReviewVerdictOf } from "@nodaro/shared"

const review = scene3DReviewVerdictOf(shot)
if (review) {
  console.log(scene3DReviewNote(review)) // one user-safe sentence for either verdict
  if (review.verdict === "unavailable") console.log(`unreviewed (${review.reason}) after ${review.attempts} attempts`)
  for (const objection of review.objections) console.log(objection.category, objection.what, objection.correction)
}

Use the two helpers from @nodaro/shared instead of reading the fields yourself, because three readings look right and are not:

  • validation.status is still "passed". The required checks did pass, which is why the scene was delivered.
  • objections can be empty. A refusal that named nothing specific is still a refusal, so counting SCENE_REVIEW_REFUSED warnings misses it.
  • Objections under "unavailable" are not the verdict. They come from review batches that answered before one failed. An empty list there means silence, not approval.

Each objection is { category, what, correction?, frames }. On the "unavailable" verdict, a SCENE_REVIEW_UNAVAILABLE warning leads validation.warnings. A visual refusal alone no longer fails the job. SCENE_QUALITY_FAILED means a required check failed or the compiler refused the recipe. See 3D Render Pro for what such a failed result keeps.

Deliveries and files

These methods read files a run already delivered. They never start a render. Every read checks your access to both the delivery and its source again, even after the source revision was deleted.

getDelivery(jobId)

Reads a delivery's record (GET /v1/3d-scene/deliveries/:jobId): its sourceKind, its exact source revision, and the files it pinned.

getDelivery(jobId: string): Promise<Scene3DDelivery>

Prop

Type

const delivery = await client.scene3d.getDelivery(jobId)
const stills = delivery.assets.filter((a) => a.kind === "shot-still")

Four kinds of files appear: poster and validation-report on every delivery, shot-still once per shot, each with its shotIndex, frame, width and height, and source-json on a refused-authoring delivery only. sourceKind is retained-revision, job-output or refused-authoring. On a refused delivery, sceneRevisionId and sourcePlanSha256 are null and there is no poster, because nothing compiled.

deliveryAssetBytes(jobId, asset, options?)

Downloads one file the delivery lists, with fresh authentication and a size limit.

deliveryAssetBytes(jobId: string, asset: Scene3DDeliveryAsset, options?: { signal?: AbortSignal }): Promise<ArrayBuffer>

Prop

Type

const bytes = await client.scene3d.deliveryAssetBytes(jobId, stills[0])

retainedRecipe(jobId, options?)

Reads the recipe a refused 3D Render Pro run kept, parsed, or null when there is none. A refused run's recipe never compiled, so there is no scene revision, no poster and no source file; the recipe is what it leaves behind.

retainedRecipe(jobId: string, options?: { signal?: AbortSignal }): Promise<unknown | null>

Prop

Type

const recipe = await client.scene3d.retainedRecipe(jobId)
  • It needs edit access to the job's workflow. A reader with less does not see the recipe at all, so the answer is null, not an error.
  • The failed job says whether to ask. Its validation.sourceRetained is true when a recipe was kept.
  • It is evidence, not an input. A refused run published no revision, so you cannot run it again from the recipe. Read it to see what was tried and to improve the next prompt. Reading costs no credits.

assetBytes(revisionId, asset, options?)

Downloads a file of a kept scene revision: a GLB model, a camera track, the poster or the validation report. Pass the exact descriptor from that revision. The SDK limits the download to the declared size, and the scene renderer also checks the SHA-256 digest.

assetBytes(revisionId: string, asset: Scene3DAssetRef, options?: { signal?: AbortSignal }): Promise<ArrayBuffer>

Prop

Type

const glb = await client.scene3d.assetBytes(scenePlan.revisionId, glbAsset)

sourceBytes(revisionId, options?)

Downloads a revision's native source file, such as a .blend file, through its own authorization. It needs the same edit access as a retained recipe. A native file is available only when it represents exactly that accepted revision.

sourceBytes(revisionId: string, options?: { signal?: AbortSignal }): Promise<ArrayBuffer>

Prop

Type

const blend = await client.scene3d.sourceBytes(revisionId)

Both byte methods use fresh credentials, respect cancellation, and throw the usual typed errors.

Frequently asked questions

Last updated on

On this page