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
| Method | What 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")basicis the deterministic engine:{ available, sceneSchemaVersions }.advancedlists optional engines, or isnullwhen there are none. An explicit engine that is unavailable is refused before a Basic generation or its credit checks.prodescribes 3D Render Pro: the engines, quality profiles, styles and aspect ratios this install serves. Offer only those. Whenprois 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:
| Frame | Credits | Price identifier |
|---|---|---|
| Up to 1,920 pixels on the longest side | 50 | render-video |
| Larger, up to 5.12 megapixels | 75 | render-video:3d-large |
| Above 5.12 megapixels | 125 | render-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 }withouteditPromptexports a kept scene with no authoring charge. AddingeditPromptrevises the scene first.sourceJobIdis 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_ASSUMPTIONwarnings 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.repairPassescounts repairs, not authoring passes, so0means the scene was accepted the first time.admissionRetriescounts something else: recipes the planner was asked again before any build.mechanicalPassescounts 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 aREMEDY_AUTO_APPLIEDwarning. A run quoted before that allowance existed charged such passes as repairs; read the quote you were given to tell.restoredAssertionslists required checks the engine put back after the planner changed one it was not asked to change. Each also adds anASSERTION_RESTOREDwarning.- A render-only export authored nothing, so it has no counts and no summary. For
mechanicalPassesespecially, absent is not0.
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.reasonis"provider"when it never reached its provider, and"unusable"when the answer was unusable.attemptssays 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.statusis still"passed". The required checks did pass, which is why the scene was delivered.objectionscan be empty. A refusal that named nothing specific is still a refusal, so countingSCENE_REVIEW_REFUSEDwarnings 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.sourceRetainedistruewhen 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
Related
Generate 3D Scene
3D Render Pro
Embed the 3D scene viewport
3D scenes
3D scenes
Last updated on
Editing
Edit podcasts and long videos from TypeScript. Detect silence, sync several recordings, plan cuts from a transcript, and render an edit decision list.
Characters
Create characters, generate portrait candidates, approve one, and add expressions, poses and motion clips from TypeScript with client.characters.