NodaroAccount
Nodaro Docs
DocumentationNode ReferenceModelsTemplatesAI Agents (MCP)DevelopersSelf-hostingResearchCompare
TypeScript SDK

Run nodes

Run any Nodaro node from TypeScript without a workflow. Discover node types, start runs, wait for results, and pass references and camera direction.

client.nodes lists the node types a Nodaro server supports and runs any one of them directly, without building a workflow. A run posts your parameters to the node's own route, POST /v1/<type> for most node types, and returns a job you can wait for. It is the same path the CLI uses for nodaro nodes run and the one Nodaro's MCP tools use. See Run a single node for the REST view.

Methods

MethodWhat it does
nodes.list()List every node type, with its models and credit cost
nodes.get(type)Read one node type
nodes.run(type, params?, options?)Start a node and return its job id at once
nodes.quote(type, params?)Price a run before you start it
nodes.runAndWait(type, params?, opts?)Start a node, poll its job, and return the output
nodes.runMany(type, paramsList, opts?)Start several runs of one node at once and wait for all of them
nodes.runBatchAndWait(type, params, opts?)Make several images in one Generate Image call and wait for every one

client.nodes

list()

Lists every node type the server supports. The answer can be cached by the server for 5 minutes. The call costs nothing and needs no scopes.

list(): Promise<{ data: NodeDescriptor[] }>
const { data: nodes } = await client.nodes.list()

const imageGenerators = nodes.filter((n) => n.category === "ai-image")
const takesReferences = nodes.filter((n) => n.capabilities?.includes("supports-reference-image"))

Each NodeDescriptor has these fields:

FieldTypeDescription
typestringThe API type, such as generate-image. Pass it to run().
labelstringThe node's name in the editor.
categorystringThe category, such as ai-image, ai-video, ai-audio, ai-text, processing or parameter.
descriptionstringA one-line description.
outputTypestringtext, image, video, audio, data or none.
creditCostnumber | stringThe credit cost a run is charged, the same figure as the node's Run button: a number when fixed, or a range such as "3-682" when it depends on the model. For Trim Video, Loop Video and Combine Videos, it is the price of one 5-second step instead, and a run costs a number of steps. Nodaro Cloud only.
estimatebooleantrue when creditCost is an estimate: a metered LLM node's typical call on its default model. Show it as ≈N. A metered LLM node whose default model has no price yet lists creditCost: "unpriced".
creditRateNodeCreditRateFor a node charged per unit of its own measure, such as Speaker Frames (creditCost "per-1000-frames"): { identifier, unit, creditsPerUnit, minUnits, rounding, note }. A run of N units is charged Math.ceil(Math.max(minUnits, N) * creditsPerUnit), rounded once.
providersstring[]The model ids the node can run, for the provider parameter.
capabilitiesstring[]Flags such as supports-reference-image or supports-end-frame.
inputSchema{ fields }The input fields you can set, each with key, type, required and options.
maxDurationSecnumberThe longest duration the node accepts, where it has one.
providerResolutionsRecord<string, string[]>The resolutions each model accepts, where they differ.

Self-hosted Community and Business installs have no credit system, so their descriptors omit creditCost.

get(type)

Reads the descriptor of one node type.

get(type: string): Promise<{ data: NodeDescriptor }>

Prop

Type

const { data: node } = await client.nodes.get("generate-video")
console.log(node.providers)  // every video model id
console.log(node.creditCost) // Nodaro Cloud only

To show prices next to the models, pass node.providers to client.credits.modelCosts().

run(type, params?, options?)

Starts one node and returns at once. The body is sent to the node's route, POST /v1/<type> for most node types. Field names match the node's input fields.

run(type: string, params?: Record<string, unknown>, options?: { idempotencyKey?: string }): Promise<RunNodeResult>

Prop

Type

const result = await client.nodes.run("generate-image", {
  prompt: "A snow leopard in the mountains",
  provider: "nano-banana-2",
})

if ("jobId" in result) {
  const { data: job } = await client.jobs.getStatus(result.jobId)
  console.log(job.status)
}

Node types with a route of their own. Some node types run on a different route, and run() posts there for you:

Node typesRoute
generate-3d-scene, edit-3d-scene/v1/3d-scene/generate, /v1/3d-scene/edit
llm-chat, after-effects, motion-graphics, lottie-overlay, 3d-title/v1/<type>/generate
image-to-text/v1/image-to-text/describe
video-composer/v1/scene-graph/generate
telegram-channel-feed/v1/telegram-channel/fetch
Every suno-* node, such as suno-generate and suno-cover/v1/suno/<name>. suno-voice uses /v1/suno/voice/generate.
instagram-post, tiktok-post, youtube-upload, linkedin-post, x-post, facebook-post, telegram-post, publish-social/v1/social/publish
ugc-creator, ugc-script, ugc-clips, ugc-clip, ugc-cards/v1/ugc/creator, /v1/ugc/script and so on. Nodaro Cloud only.
render-video with a planType/v1/render-video/plan

The publish route needs a platform. A social node for one platform, such as x-post, fills it in from its type when you leave it out. publish-social takes the platform you send.

Node types that do not run on their own. A node type with no route of its own rejects with a NodaroError whose code is node_not_runnable, without calling the API. The message names what to run instead:

  • modify-image runs through image-to-image, or edit-image for the nano-banana-edit model.
  • upscale-image and remove-background run through edit-image.
  • image-to-video runs through generate-video.
  • composite, manual-edit and paint-mask run only inside a workflow. scene and generative-pipeline run only inside a pipeline.

Node types that run only inside a workflow, such as inputs, triggers, utilities and pickers (upload-image, combine-text), reject the same way once the API answers that it has no route for them. A 404 about a missing resource, such as a character id that does not exist, stays a NotFoundError.

A model this deployment does not offer, or one an admin switched off, is refused before any credits are held. The call rejects with ModelUnavailableError, with code set to model_not_available or model_disabled. Choose another model: a retry changes nothing.

What comes back. Most node types are asynchronous: the result carries a jobId, and a worker does the generation. Poll client.jobs.getStatus(jobId) until it ends, or use runAndWait(). A few synchronous node types, such as meta-ads-scrape, return their full result at once, without a jobId. Branch on the presence of jobId.

Parameter corrections. On the image node types (generate-image, image-to-image and edit-image), the server may correct a value the chosen model does not accept. The run goes ahead with the corrected value, and the credits reserved match it. The result then carries adjustments, one entry per corrected field:

const result = await client.nodes.run("generate-image", {
  prompt: "A snow leopard",
  provider: "gpt-image-2",
  aspectRatio: "3:2",
})
if ("adjustments" in result && result.adjustments?.length) {
  for (const a of result.adjustments) {
    console.warn(`${a.field}: ${a.from} -> ${a.to ?? "(dropped)"} (${a.reason})`)
  }
}

Each adjustment has field (aspectRatio, resolution, quality or duration), from, to and reason. adjustments is absent when nothing changed.

Throws InsufficientCreditsError when the account cannot pay, StorageExceededError when storage is full, and JobBlockedError when a content policy of the deployment refuses the request. See Errors.

quote(type, params?)

Prices a run before you start it, with POST /v1/quote. Pass exactly the body you would pass to run. The platform prices it with the run route's own code: the validation, the measuring of reference clips, the credit id and the amount. Nothing is started and nothing is charged. Nodaro Cloud only.

quote(type: string, params?: Record<string, unknown>): Promise<NodeQuote>

The method is typed for text-to-video, generate-video, video-to-video, video-upscale (a Seedance 2.5 draft's 1080p finalize is priced from the draft) and edit-image. split-into-layers is quotable too, with the body client.media.splitIntoLayers takes. These are the types the platform can quote today. Any other type rejects with 400 quote_unsupported. A body, model or node the run would refuse rejects with the run's own 400 or 403.

const q = await client.nodes.quote("video-to-video", {
  videoUrl, prompt: "make it black and white", provider: "seedance-2-5", resolution: "480p",
})
q.reserve   // credits held when the run starts
q.expected  // what the run should actually cost, when known

A settles_to_actual warning marks a reserve that is the most the run can cost: the run is charged for what it delivers, and the rest is refunded. Without it, reserve is what the run is charged when it completes.

// Nodaro Image Upscale: the exact price of this picture at this factor.
// The platform reads the picture to measure it, and stores nothing.
const up = await client.nodes.quote("edit-image", {
  provider: "nodaro-image-upscale", imageUrl: "https://example.com/photo-1024.png", upscaleFactor: "2",
})
up.identifier // "nodaro-image-upscale:2K" for a 1024 × 1024 picture

// Split into Layers: the hold, the most the run can cost (warning settles_to_actual).
const split = await client.nodes.quote("split-into-layers", { imageUrl: "https://example.com/poster.png", maxLayers: 5 })
split.identifier // "split-into-layers:1k:6": 5 layers and the base at 1K

Where Nodaro Image Upscale is not offered, the quote rejects with the run's 403 model_not_available (ModelUnavailableError) before the picture is read. A link to another site follows the deployment's link-import setting.

NodeQuote is { identifier, reserve, expected?, adjustments, warnings }. See Run quotes for every field and code.

runAndWait(type, params?, opts?)

Runs one asynchronous node to completion. It calls run(), takes the jobId, and polls client.jobs.getStatus() until the job ends. It resolves with the job's output when the status is completed.

runAndWait(type: string, params?: Record<string, unknown>, opts?: RunAndWaitOptions): Promise<NodeJobOutput>

Prop

Type

const output = await client.nodes.runAndWait(
  "generate-video",
  { prompt: "Rain falls on a neon street at night", provider: "seedance-2-fast", duration: 4 },
  { onProgress: (s) => console.log(`${s.progress ?? 0}%`) },
)
console.log(output.videoUrl, output.thumbnailUrl)

On an image job, NodeJobOutput also carries the image's pixel size, so a client can lay out a grid before the images load:

FieldTypeDescription
imageUrlstringThe image, or the first one when the job made several.
imageUrlsreadonly string[]Every image, when the job made more than one.
width, heightnumberThe pixel size of imageUrl as stored, with EXIF orientation applied. A free-tier watermark is included.
imagesreadonly JobOutputImage[]One { url, width?, height? } per entry of imageUrls, in the same order. An entry without a size could not be measured.

The four size fields are optional. Older jobs do not have them, and the server never guesses a size it could not read.

const output = await client.nodes.runAndWait("generate-image", { prompt: "a snow leopard in the mountains" })
console.log(output.imageUrl, output.width, output.height)
for (const image of output.images ?? []) console.log(image.url, image.width ?? "?", image.height ?? "?")

The output is a NodeJobOutput: imageUrl for image nodes, videoUrl and thumbnailUrl for video nodes, audioUrl for audio nodes, plus any other fields the node writes. Audio Separation, for example, adds one URL per stem, such as vocalUrl and instrumentalUrl.

It throws these typed errors:

ErrorWhen
NodaroError with code batch_requires_run_batch_and_waitA generate-image request with count above 1 or with models makes several jobs. runAndWait() refuses it before anything is sent. Use runBatchAndWait().
InsufficientCreditsError, StorageExceededError, JobBlockedErrorThe run request was refused, before any polling.
ModelUnavailableErrorThe model is not offered by this deployment, or an admin switched it off. The run is refused before credits are held. Choose another model.
JobFailedErrorThe job ended as failed or cancelled. It carries jobId and the job's error message.
JobTimeoutErrormaxMs passed. The job is not cancelled.
JobAbortedErrorYour signal fired. The job is not cancelled.
JobHeldErrorThe job is held for human review, on deployments with a content policy.

Slow recoveries. Sometimes a model delivers its result after the worker gave up on it. The job then stays processing with recovering: true while the platform recovers it, which can take tens of minutes for slow models. If JobTimeoutError ends your wait, fetch the job later with client.jobs.get(jobId), or raise maxMs.

runMany(type, paramsList, opts?)

Starts several runs of one node type at the same time and waits for all of them, for example to generate a grid of candidates. Each entry runs through runAndWait().

runMany(type: string, paramsList: Record<string, unknown>[], opts?: RunAndWaitOptions): Promise<RunManyResult[]>

Prop

Type

const results = await client.nodes.runMany("generate-image", [
  { prompt: "A snow leopard at sunrise" },
  { prompt: "A snow leopard at golden hour" },
  { prompt: "A snow leopard at blue hour" },
])
for (const { jobId, output } of results) console.log(jobId, output.imageUrl)

It resolves when every run has finished, to one { jobId, output } per entry, in input order. It rejects as soon as any run fails, with the same errors as runAndWait(). An entry that is itself a batch, with count above 1 or with models, is refused before anything is sent. To choose the best result afterwards, pass the URLs to client.reduce.run().

runBatchAndWait(type, params, opts?)

Makes several images in one Generate Image call and waits for every one. Use count for 1 to 4 images, each its own job. Use models to send the same request to 2 to 4 image models instead of one provider. With both, count applies per model, up to 8 images in all.

runBatchAndWait(
  type: "generate-image",
  params: GenerateImageParams,
  opts?: RunAndWaitOptions,
): Promise<GenerateImageBatchOutput>

The method submits the batch once, then waits for every job the way runAndWait() waits. The batch is checked against your balance and charged as a whole or not at all. With opts.idempotencyKey, image number i is keyed <key>:<i>, so a retry returns the same jobs and charges nothing again. A presetId combines with both levers: count images of the preset's request, or the preset applied once per model.

const { batch, results } = await client.nodes.runBatchAndWait(
  "generate-image",
  { prompt: "a lighthouse at dusk", models: ["gpt-image-2", "flux-2-max"], count: 2, seed: 41 },
  { idempotencyKey: crypto.randomUUID() },
)
console.log(batch.batchId, batch.totalCredits)
for (const r of results) {
  if (r.status === "completed") console.log(r.model, r.output.imageUrl)
  else console.warn(r.model, r.jobId, r.error.message)
}

It resolves to { batch, results }:

  • batch is the submit's answer, a GenerateImageBatchResult. It has jobId (the first job), jobIds (model by model, then image by image), batchId, groups ({ model, jobIds, credits?, adjustments? } per model) and totalCredits. nodes.run("generate-image", …) returns the same object when the params set count above 1 or models.
  • results holds one BatchJobOutcome per job, in the order of batch.jobIds. Each is { jobId, model, status: "completed", output } or { jobId, model, status: "error", error }. The error is the JobFailedError, JobHeldError or JobTimeoutError that ended the wait. One failed image never discards the others.

It rejects only before any job exists, with the submit's own errors, such as InsufficientCreditsError with the batch's total as required. It also rejects with JobAbortedError when your signal fires. A request for one image is refused before anything is sent, with code not_a_batch: wait for it with runAndWait(). With a seed, image i uses seed + i. Find the jobs of a batch later with client.jobs.list({ batchId }).

Typed parameters

These node types have typed parameters, so your editor completes and checks their fields. Every other node type takes a plain object; its fields are the node's input fields, listed in inputSchema and on its page in the Node Reference. The 3D scene nodes also have typed parameters: see 3D scenes.

Node typeParameter typeNode page
generate-imageGenerateImageParamsGenerate Image
image-to-imageImageToImageParamsModify Image
edit-imageEditImageParamsUpscale Image
split-imageSplitImageParams
generate-maskGenerateMaskParamsGenerate Mask
generate-videoGenerateVideoParamsGenerate Video
text-to-videoTextToVideoParamsGenerate Video
video-to-videoVideoToVideoParamsVideo to Video
assemble-narrated-videoAssembleNarratedVideoParamsAssemble Narrated Video
video-depthVideoDepthParamsVideo Depth
video-upscaleVideoUpscaleParamsUpscale Video

Every typed parameter object also accepts other fields of the route. The server validates the full body.

GenerateImageParams

Prop

Type

strength and guidanceScale are typed on GenerateImageParams too.

Image file formats

GenerateImageParams and ImageToImageParams type outputFormat (ImageOutputFormat). It is "png" by default, or "jpeg", "webp", "gif" or "svg" on the models whose outputFormats list it in client.models.list(). Today those are GPT Image 2.6 Flare and Sunburst.

  • A GIF takes gifFrames (2 to 16) and gifFrameDurationMs (20 to 2,000 ms, in steps of 10). A GIF is square, 512×512 per frame. Its aspectRatio and resolution are ignored, and adjustments says so.
  • An SVG takes svgLevel (ImageSvgLevel): "standard", "detailed" or "max".
  • A transparent background is kept only in PNG and WebP.
  • A mask cannot be used with a GIF or an SVG. The request is refused with 400.

The finished job's output names the file's type in imageMimeType. For a GIF or an SVG, it also holds a PNG still in thumbnailUrl. A GIF is priced by its quality and by the band its number of frames falls in: 2 to 8, 9 to 12 or 13 to 16. The Generate Image node shows the price before it runs.

await client.nodes.runAndWait("generate-image", {
  prompt: "a waving flag",
  provider: "gpt-image-2-6-flare",
  outputFormat: "gif",
  gifFrames: 12,
} satisfies GenerateImageParams)

Presets on Generate Image

presetId applies a node preset exactly as the editor does: the prompt, the pre and post text, placeholder defaults, the style, the negative prompt and the settings. The job then matches the editor's Run. Use a built-in id from presets.listFactory("generate-image") or one of your own presets. Fields you pass override the preset.

A placeholder without a default needs a value in presetVariables. Without one, the call fails with 400 preset_variables_required, which names the placeholder.

await client.nodes.runAndWait("generate-image", {
  presetId: "generate-image/logo-wordmark",
  presetVariables: { BRAND: "Acme" },
})

GenerateVideoParams

The image-to-video lane: a start frame, an optional end frame, and references.

Prop

Type

TextToVideoParams

The prompt-only video lane, POST /v1/text-to-video. The prompt is required, and start and end frames belong to generate-video instead. A model without a text-to-video mode answers 400 image_required.

Prop

Type

TextToVideoParams takes contentPolicyRewrite?: boolean. When it is absent or true, a copyright refusal is retried once with a reworded prompt. With false, a copyright refusal fails the job instead.

Seedance 2.5 reference task type

GenerateVideoParams and TextToVideoParams take referenceTaskType?: "auto" | "reference" | "edit" | "extend". It tells seedance-2-5 what the reference media is for, instead of leaving it to the prompt. Every other model ignores it, and so does a run with no reference media. A start or end frame does not count as reference media.

  • "auto", the default, declares nothing.
  • "reference" keeps your aspectRatio and duration. With an Auto duration (-1), the run is reserved at the longest output the model can make, and charged for what it makes.
  • "edit" edits the reference video in its own shape and length: aspectRatio becomes "adaptive" and duration Auto. Every reference video must be 4 to 30 seconds long. Otherwise nothing is declared, and the run keeps the ratio and length you sent.
  • "extend" continues the reference video in its own shape, at a set length. An Auto or missing duration becomes the model's default. You get the continuation only, not the source joined to it. For both as one video, run extend-video.
  • edit and extend need a reference video. A draft (draft: true) declares the type exactly as the same run without draft, at 480p.

What they change comes back in result.adjustments, and nodes.quote prices the body the same way the run does. See Seedance 2.5 reference task type.

await client.nodes.run("generate-video", {
  provider: "seedance-2-5",
  prompt: "remove the sign on the wall",
  referenceVideoUrls: [clipUrl],
  referenceTaskType: "edit",
} satisfies GenerateVideoParams)

Seedance 2.5 drafts

GenerateVideoParams and TextToVideoParams take draft: true. On seedance-2-5, the run is a 480p draft at the price of a 480p run. Finalize the take you like with run("video-upscale", { provider: "seedance-2-5-hd", sourceJobId }): the same take, rendered again at 1080p, priced from the draft's length. A source that is not your own finished draft is refused with a 4xx before anything is charged. See Seedance 2.5 Draft mode.

const draft = await client.nodes.run("generate-video", { prompt, provider: "seedance-2-5", draft: true, duration: 8 })
// Wait for the draft and review it. When you like the take, finalize it at 1080p.
if ("jobId" in draft) {
  const final = await client.nodes.runAndWait("video-upscale", { provider: "seedance-2-5-hd", sourceJobId: draft.jobId })
  console.log(final.videoUrl)
}

Topaz video upscale. VideoUpscaleParams takes targetResolution ("720p", "1080p" or "4k", the frame's short side, default 1080p) and targetFps (15 to 60, default the source's own rate): run("video-upscale", { videoUrl, targetResolution: "4k", targetFps: 60 }). Topaz is metered. See Upscale Video › Credits. upscaleFactor is deprecated. It is still accepted and applied to the source's short side.

Nodaro Video Upscale. provider: "nodaro-video-upscale" takes targetResolution "1080p" (default), "2k" or "4k", the output frame. A value it does not take, such as "720p", runs at 1080p: run("video-upscale", { provider: "nodaro-video-upscale", videoUrl, targetResolution: "2k" }).

  • It takes clips of up to 60 seconds and 50 MB. It is priced per second of the clip, and the amount held is the amount charged.
  • It is offered only where models.list lists it. Elsewhere the request is refused with 403 model_not_available, a ForbiddenError.
  • Its other refusals: 400 for a clip past a limit or one that cannot be read (video_too_long, video_too_large, video_unreadable), and 403 link_imports_disabled for a link to another site where the deployment turned such links off.
  • A clip the deployment's upload policy refuses answers 422 upload_blocked. A clip that could not be fetched just now answers 429 input_busy, or 503 input_timed_out or input_failed: send the same request again.
  • Every refusal comes before anything is charged. "2k" is this provider's target only: Topaz and VEO still answer it with a 400.

VideoToVideoParams

The video edit lane, POST /v1/video-to-video. A seedance-2-5 edit is built on the server, so you send the same body as for any other edit model. See Video to Video.

Prop

Type

The type also carries audio, multiShots, videoEditDuration, audioSetting and promptExtend for the models that take them, plus sessionId and origin. Any other model refuses the three reference lists. The numbering of the references and the limits are in Video to Video.

Typed image routes

image-to-image, edit-image, split-image and generate-mask have typed overloads on run. The three that start a job also have them on runAndWait. Each params type takes the platform's own vocabularies, such as ModifyImageProvider, ImageEditProvider, ImageAspectRatio and ImageResolution, which the SDK re-exports. Any other route field still passes through.

The compile-time check applies when the body is typed as the params type: const params: EditImageParams = …, or { … } satisfies EditImageParams inline. A body written straight into the call that the typed overload rejects still compiles. It falls through to the untyped run(type: string, …) overload, and the route answers 400. Use satisfies to get the check.

  • image-to-image, edit-image and generate-mask. run answers ImageJobStarted, that is { jobId: string; adjustments? }, so result.jobId needs no cast.
  • ImageToImageParams requires imageUrl and prompt. Its maskUrl (white is the area to change) is sent to the model as its own mask. Only ideogram-edit, flux-fill, gpt-image-2-6-flare-i2i and gpt-image-2-6-sunburst-i2i use it. On GPT Image 2.6, the rest of the picture is kept exactly as it was, and the result has the source's size. The run keeps the photo's shape, and is rendered and priced at the size nearest the photo. adjustments says so. A mask costs nothing extra. It takes outputFormat and the GIF and SVG fields as GenerateImageParams does.
  • EditImageParams is one of two forms. It takes imageUrl for every model except the Grok task operations. For those, it takes taskId with provider: "grok-2-edit" | "grok-2-segment": the task id of an earlier Grok Imagine 2 generation, stored in that job's output data. A typed body that sends both, or a taskId with another model, does not compile. The model defaults to recraft-upscale. prompt is required for nano-banana-edit and grok-2-edit. maskIndexes is for grok-2-edit only.
  • upscaleFactor (UpscaleFactor: "1", "2", "4" or "8") is 1, 2 or 4 on topaz-image-upscale, and 1, 2, 4 or 8 on nodaro-image-upscale. A factor the model does not take is a 400 validation_error. nodaro-image-upscale is priced by its output's size. It refuses an output past 67,108,864 pixels, or 16,384 pixels on a side, with 400 image_too_large before anything is charged. See Nodaro Image Upscale.
  • split-image is free and answers at once. It creates no job, so runAndWait does not apply. run answers SplitImageResult, that is { images: { name, url }[] }, one PNG per tile in names order. names must hold exactly gridCols × gridRows entries, and gridCols and gridRows are each 1 to 6.
  • generate-mask is the SDK's way to start a Generate Mask job. runAndWait("generate-mask", …) resolves GenerateMaskJobOutput: maskUrl, the mask PNG where white is the subject, and imageUrl, the source passed through unchanged.
import type { EditImageParams, GenerateImageParams, GenerateMaskParams } from "@nodaro/sdk"

const { maskUrl } = await client.nodes.runAndWait("generate-mask", { imageUrl, prompt: "the red car" } satisfies GenerateMaskParams)
await client.nodes.runAndWait("generate-image", { prompt: "a blue car", baseImageUrl: imageUrl, maskUrl } satisfies GenerateImageParams)
const { jobId } = await client.nodes.run("edit-image", { imageUrl, provider: "topaz-image-upscale", upscaleFactor: "4" } satisfies EditImageParams)
await client.nodes.runAndWait("edit-image", { imageUrl, provider: "nodaro-image-upscale", upscaleFactor: "8" } satisfies EditImageParams)

VideoDepthParams

Turns a clip into a depth video, near bright and far dark (POST /v1/video-depth). The finished job has videoUrl and thumbnailUrl. See Video Depth for the prices and length limits.

Prop

Type

A setting the chosen provider does not take is ignored, so switching providers never fails a request. A value outside the lists above is a 400 validation_error. The clip is measured before any credits are reserved. A clip that is too long answers 400 video_too_long: more than 2,400 frames or 300 seconds on video-depth-anything, and more than 80 seconds on depth-anything-frames. A clip that cannot be read answers 400 video_unreadable.

const { videoUrl } = await client.nodes.runAndWait("video-depth", {
  videoUrl: "https://cdn.example.com/clip.mp4",
  colormap: "turbo",
  resolution: "720p",
})

AssembleNarratedVideoParams

Joins video blocks with narration into one video. The run is priced by the number of blocks and costs 44–143 credits: 3 units, plus one unit for every 6 blocks or part of 6. See Assemble Narrated Video for how each block is fitted to its narration.

Prop

Type

References

generate-image, generate-video and text-to-video accept references the way the editor wires them. The server turns them into numbered directives such as @image_1 in the prompt, so you do not write "Image 1 is..." yourself.

  • connectedReferences is a list of ConnectedReference entries, the editor's wired-reference shape, exported from the SDK. The server removes duplicates and keeps as many as the model accepts. referenceOrder sets their order by id.
  • Identity lock. An entry may carry identityLock: { enabled: true, text? }. The server then adds a short line that tells the model to keep that reference's identity. text replaces the built-in wording, and {ref} in it stands for the reference's name. It is off by default.
  • describedReferences takes up to 10 { name, description } entries for subjects you can name but have no picture of, such as a role in a script. Each becomes a line Name — description. in the prompt. Keep the name in your prompt text, so the model knows who it is.
  • descriptionOverride on a reference entry replaces its stored description for this run only.
  • Captions for video and audio references. referenceVideoCaptions and referenceAudioCaptions follow the order of referenceVideoUrls and referenceAudioUrls. Each caption appears in the prompt as @video_1: caption. or @audio_1: caption..
  • Mention an image reference in the prompt. On generate-image, name a reference and mention it as @<name>:<index> or @<name>:<index>:<role>, for example @town:1:background. The mention renders that reference, or its role, at that point of the prompt. ~lock and ~nolock work as on character mentions.

See Reference roles for the roles you can use and Consistent characters for identity work.

Direction

generate-image, generate-video and text-to-video accept a direction object of picker ids instead of words. Nodaro writes tested wording for each id into the prompt, so your code sends ids and the wording stays up to date.

await client.nodes.runAndWait("generate-image", {
  prompt: "A detective waits under a street lamp",
  direction: { shotSize: "wide-shot", timeOfDay: "golden-hour", mood: "suspicious" },
})
  • Keys are picker dimensions, such as shotSize, lightingStyle, style, mood, photographer or era. The video routes add motion keys, such as cameraMotion, actionFx, transition, loopSubject and the temporal keys.
  • Values are one id or an array of ids. A dimension that takes several values keeps at most its own limit and drops the rest. The request is refused only above 8 values per key or 100 characters per id.
  • Unknown keys and ids are skipped, not refused. An empty direction leaves your prompt unchanged.
  • One map serves images and video. A still-only key sent to a video run is accepted and adds nothing.
  • extend-video takes no direction, because its prompt continues an existing clip.
  • Your own palettes and moodboards work on generate-image. Set colorLook to paletteDirectionId(id) or style to moodboardDirectionId(id). See palettes and moodboards.

Read the valid ids from client.pickerCatalogs. See Picker catalogs for every dimension.

Subject

GenerateImageParams and TextToVideoParams type an optional subject, with the exported Subject type. It says who is in the shot: the person, how they are styled and the props in the frame, as a flat map of picker field to catalog id. You never write prompt text for it. Nodaro writes the wording and places it right after your prompt, before any direction.

import type { Subject } from "@nodaro/sdk"

const subject: Subject = { type: "woman", age: "age-30s", hairBase: "base-short-straight", heldProp: "smartphone" }
await client.nodes.run("generate-image", { prompt: "on the seawall at dusk", subject })
  • Field names and ids come from the picker catalogs. client.pickerCatalogs.get("person") and get("styling") list each dimension's field with its options[].id. get("held-prop"), get("material") and get("animal") write their ids to the catalog's valueField: heldProp, material and animal.
  • Values are one id or a list of ids. Ids past a dimension's own limit are dropped, not refused. customAge is the one number, in years, read only with age: "age-custom".
  • Limits. Without a preset, the route checks the map. More than 128 keys, a key over 64 characters, more than 8 ids on a key or an id over 100 characters is a 400 validation_error. Unknown keys and ids within those limits are skipped, and the job's input_data.subject keeps exactly the ids that were used.
  • With presetId, your subject replaces the preset's own and is added to the preset's prompt, as direction is. Values that are too long are dropped rather than refused, and input_data does not record the subject.

See Nodes for every key and the order in which the wording is added.

Language-model nodes

run(type, params) posts each language-model node type to its own route: /v1/<type> for generate-script, image-critic, qa-check and describe-to-picker, and a longer path for the others. You do not need client.request() for them:

Node typeEndpoint
llm-chat/v1/llm-chat/generate
after-effects/v1/after-effects/generate
motion-graphics/v1/motion-graphics/generate
lottie-overlay/v1/lottie-overlay/generate
3d-title/v1/3d-title/generate
image-to-text/v1/image-to-text/describe
video-composer/v1/scene-graph/generate
await client.nodes.run("generate-script", {
  prompt: "A 3-scene product launch script for a smart water bottle",
  reasoningEffort: "high",
})
  • reasoningEffort is "none", "low", "medium", "high", "xhigh" or "max", depending on the model. Omit it, or send a level the model does not support, for the model's default. Effort has no price of its own. Every LLM call is charged for the tokens it used, at the model's rate on the lane that served it, so a higher effort costs more only through the tokens it writes. On a Claude model, any effort runs the call on the maker's own API, at that lane's rate. See Generate Text - Any LLM for the models, and How LLM calls are charged. One exception: describe-to-picker and its text version text-to-picker run at "high" when you omit the field and llmModel is claude-opus-5.5. That run is on Anthropic's own API, at that lane's rate. Send another level to change it. Any other model keeps its default, including their default model, Gemini 3.8 Flash.
  • advancedMode: true runs a Gemini or Claude model directly on its maker's API. Only there do temperature, maxTokens and the full effort range take full effect. The call is charged at that lane's per-token rate. A model without that option answers 400 advanced_mode_unsupported. While the model's own provider cannot take requests, the call answers advanced_mode_unavailable instead of running on the default lane: 400 up front, or 502 midway, refunded unless the provider billed the attempt. Turn Advanced mode off to run the model. Describe to Picker is the exception: a Claude run there that the provider refuses falls back to Gemini 3.8 Flash, as it does with Advanced mode off.
  • Quoting a call. client.credits.modelCosts([llmPriceKey(feature, model)]) returns an estimate of a typical call, listed in estimates. Show it as ≈N. llmPriceKey comes from @nodaro/shared/llm-models. See Models and credits.
  • Streaming is not wrapped. The SDK does not read the streaming answer of the Generate Text - Any LLM node. Use fetch with a readable stream for it.

Model-specific rules

Some video models accept values the others do not. Each model page lists the full options and prices.

  • Seedance 2 accepts resolution: "4k" and aspectRatio: "adaptive" or "21:9". Seedance 2 Fast and Seedance 2 Mini render at 480p or 720p only.
  • Seedance 2.5 renders at 480p, 720p or 1080p, runs up to 30 seconds in one call, and takes 30 image, 10 video and 10 audio references. With a start frame it uses that frame's aspect and refuses an explicit aspectRatio.
  • MiniMax H3 (minimax-h3) takes 9 image, 3 video and 3 audio references at resolution: "2K" (the default) or "768P". Any other value renders and bills as 2K.
  • Wan 3.0 (wan-3 and the faster wan-3-prime) takes 10 image, 5 video and 5 audio references. The reference lists cannot be combined with imageUrl or endFrameUrl. duration is a whole number from 2 to 30, and resolution is 480p, 720p or 1080p.
  • Gemini Omni Flash takes the same request as Gemini Omni: durations of 4, 6, 8 or 10 seconds, 720p to 4K, and 16:9 or 9:16 only.
  • Gemini Omni Flash 1.2 (gemini-omni-flash-1-2) is priced per second. It takes any whole-second duration from 3 to 15, 360p, 720p, 1080p or 4k, and an aspectRatio of 16:9, 9:16 or auto. It takes up to 6 reference images, or 5 beside a source video. A start frame alone, or a start and end frame up to 10 seconds, are exact frames. With references, or past 10 seconds, the frames join the reference list and the prompt names them. A source video is edited and charged by its own length, up to 15 seconds, and videoTrimStart and videoTrimEnd pick the stretch. The model takes no characterReferences. See Edit a clip.
  • Kling 4.0 (kling-4) runs from text alone, or from reference materials. It takes referenceImageUrls (up to 10), referenceVideoUrls (up to 5, each 2 to 30 seconds), characterReferences (up to 7, as named subjects) and referenceAudioUrls (up to 3). More than 15 materials in all is a 400 before anything is reserved. A sent imageUrl or endFrameUrl joins the reference images. On kling-4 and kling-4-flash, each character reference is built from imageUrl, bodyImageUrl and up to 3 extraImageUrls, a start frame is allowed, and voice is refused. keepOriginalSound: true keeps a reference video's own sound. The first audio clip plays as the soundtrack by default. It is sent as one more reference video, so the run bills as a reference-video run, and the other clips stay named voices. audioSoundtrack: false keeps every clip a voice sample. A reference-video run bills the output plus the longer of the output and the clips, at the reference rate. Kling 4.0 Keyframes (kling-4-keyframes) takes imageUrl, up to 8 in-between frames as referenceImageUrls, in order, and endFrameUrl. The frames in all are capped by duration, from 4 at 3 seconds to 10 from 9 seconds, and more is a 400. Both take a duration of 3 to 30 (default 5) and a resolution of 720p or 1080p; anything else renders 720p. aspectRatio is auto, 16:9, 9:16, 1:1 or 21:9. Audio is always on, and there is no seed.
  • Kling 4.0 Flash (kling-4-flash) is the faster, lower-cost Kling 4.0. An imageUrl alone is the exact opening frame. Anything else runs in reference mode: a prompt alone, referenceImageUrls (up to 10), referenceVideoUrls (up to 5, 2 to 20 seconds together), characterReferences (up to 7), or a start frame beside them. Reference mode takes 15 materials in all. referenceAudioUrls clip N (up to 7) becomes the voice of characterReferences entry N. A character needs 2 to 4 images to carry a voice. Otherwise its clip is dropped, as is any clip past the last character. duration is 3 to 20 (default 5), and the output is 720p only; anything else renders 720p. There is no end frame, no seed and no audioSoundtrack.
  • PixVerse V6 Max (pixverse-v6-max) runs run("generate-video") with an imageUrl as the exact first frame, and no aspectRatio is sent. It runs run("text-to-video") with aspectRatio 16:9 (the default), 4:3, 1:1, 3:4, 9:16, 2:3, 3:2 or 21:9. duration is 1 to 15 (default 5), resolution is 360p, 540p, 720p (default) or 1080p (anything else renders 720p), and seed is 0 to 2147483647. Native audio is on unless sound: false (or, with no sound, generateAudio: false), and it changes the price. It takes no end frame and no references.
  • Grok Imagine 1.5 (grok-imagine-video-1.5) needs imageUrl, the exact first frame. endFrameUrl is the exact last frame, and the same URL as both makes a seamless loop. Up to 4 in-between frames go in referenceImageUrls, in order, spread over the clip at whole seconds. A D-second clip takes at most D − 1, and the rest are left out. duration is 1 to 15 (default 8), resolution is 480p (default), 720p or 1080p (anything else renders 480p), and there is no seed. At 1080p it takes imageUrl alone: with an end frame, a loop or in-between frames, the run is refused with 400 start_frame_only_resolution before any credits are reserved. Use 720p for those.
  • Gemini Omni and Gemini Omni Flash (gemini-omni-video and gemini-omni-flash) keep a real person's face with characterReferences. Pass up to 3 entries of { imageUrl, description, bodyImageUrl?, name?, voice? }. voice is { preset, description?, exampleLine? }, where preset is one of the GEMINI_OMNI_VOICE_PRESETS ids from @nodaro/shared. It keeps one voice for that character across clips. Only these models, kling-4 and kling-4-flash accept characterReferences. Any other model answers 400. On Gemini Omni, characterReferences cannot be combined with a start frame (imageUrl). A character uses 1 of the model's 7 input units, or 2 with a bodyImageUrl, and reference images and a source video share those units. Character references add no credits. Use them instead of referenceImageUrls when the face must stay the same.

Text to Speech. When you omit provider, Text to Speech uses ElevenLabs v4 for text up to 10,000 characters. Longer text falls back to ElevenLabs Turbo v2.5, whose limit is 40,000 characters, so it is not cut short. A provider you name is always used. Send provider: "elevenlabs-v3" to keep ElevenLabs v3, whose limit is 5,000 characters.

Continuity across clips. Text to Speech also takes previousText and nextText: the lines spoken just before and after the clip, up to 1,000 characters each. They are not spoken. ElevenLabs v4, v4 Turbo, Turbo v2.5 and Multilingual v2 use them to keep one intonation across clips made separately. ElevenLabs v3 ignores them.

Timings and the audio file. With withTimestamps: true, the finished job's output_data.transcript carries per-word timings (words[] with startMs and endMs), at no extra credits. outputFormat picks the audio file: mp3_44100_128 (the default), mp3_44100_192 for a higher MP3 bitrate, or wav_24000 for an uncompressed WAV. The credits are the same for every format. The job's audioUrl ends in .mp3 or .wav by the file that came back, and output_data.audioMimeType names it (audio/mpeg or audio/wav). Name a download from that field, not from the format you asked for. The values are TTS_OUTPUT_FORMATS in @nodaro/shared/models.

Scrapers and other input nodes

Input nodes run the same way. A scraper, such as Web Scrape, answers at once: the result carries a jobId for your history and the data itself, so you can use it without polling. The request fields are the node's input fields, listed in inputSchema.

Frequently asked questions

How do I generate an image with the Nodaro SDK?

Call client.nodes.runAndWait with the node type generate-image and a prompt. It resolves with the job output, whose imageUrl is the generated picture.

What is the difference between run and runAndWait?

run starts the node and returns a jobId at once. runAndWait starts it, polls the job every 2 seconds, and resolves with the output when the job completes.

How long does runAndWait wait?

Up to 15 minutes by default. Change it with the maxMs option. A timeout does not cancel the job, which usually still completes on the server.

Which node types can I run?

Call client.nodes.list() for every type the server supports, with its category, output type, models and credit cost. Every node in the Node Reference has an API type.

How do I generate several images in one call?

Call client.nodes.runBatchAndWait with generate-image and count (1 to 4 images) or models (2 to 4 image models). The batch is charged as a whole or not at all, and one failed image never discards the others.

How do I avoid paying twice when I retry a request?

Pass an idempotencyKey in the options of run or runAndWait, and reuse the same key when you retry. The platform returns the first run instead of starting a second one.

Last updated on

On this page