Nodes
Run any Nodaro node with POST /v1/<node-type>, discover nodes, models and picker values, and steer prompts with references and cinematic direction ids.
A node run calls one Nodaro node directly, without building a workflow: you send POST /v1/<node-type> with the node's settings as the body. Every generation node follows this shape, from generate-image and generate-video to text-to-speech, and the discovery endpoints tell you which nodes, models and settings exist. Most node runs are asynchronous: the response is a jobId that you poll until the result is ready.
Run a node
The route for a node type is POST /v1/ followed by the type, and the body is the node's settings as JSON:
POST /v1/generate-image
Authorization: Bearer ndr_…
Content-Type: application/json
{ "prompt": "a lighthouse in a storm, oil painting", "provider": "nano-banana-pro", "aspectRatio": "3:4" }What comes back depends on the node:
- Generation nodes answer
200with{ "jobId": "…" }. The work runs on a worker, and you poll the job withGET /v1/jobs/:id/statusuntil its status iscompleted. See Jobs. - Image and video generations may add
adjustments, a list of the settings the server corrected for the chosen model.generate-videoandgenerate-imagemay also addwarnings. See Parameter corrections. - Inline nodes, such as
combine-text, return their full result at once, with nojobId. - Scrapers, such as
web-scrape, also answer at once. Their response carries both ajobId, for your history, and the data itself.
A generation reserves credits when it starts. When the account cannot cover the run, the call answers 402 insufficient_credits.
Nodes with a longer path
Most text nodes that call a language model follow the same POST /v1/<node-type> rule: generate-script, image-critic, qa-check and describe-to-picker. A few others are registered under a longer path:
| Node type | Route |
|---|---|
llm-chat | POST /v1/llm-chat/generate |
after-effects | POST /v1/after-effects/generate |
motion-graphics | POST /v1/motion-graphics/generate |
lottie-overlay | POST /v1/lottie-overlay/generate |
3d-title | POST /v1/3d-title/generate |
image-to-text | POST /v1/image-to-text/describe |
video-composer | POST /v1/scene-graph/generate |
The SDK's client.nodes.run(type, params) and the CLI's nodaro nodes run post each of these node types to its own route for you. See Run nodes.
Language-model routes accept two optional fields:
reasoningEffort:none,low,medium,high,xhighormax, depending on the model. Leave it out, or choose a level the model does not support, for the model's own default. Effort has no price of its own. The call is charged for the tokens it uses, so a higher effort costs more only through the reasoning it writes. On a Claude model, any effort runs the call on Anthropic's own API, at that lane's rate.advancedMode: true: Gemini and Claude models. The request runs on the model maker's own API, the only lane wheretemperature,maxTokensand the full reasoning range take effect. The call is charged at that lane's per-token rate. A model without that lane answers400 advanced_mode_unsupported. While the model's own provider cannot take requests, the request is refused instead of running on the default lane, where its levers would not take effect. The answer is400 advanced_mode_unavailablebefore anything is charged, or502 advanced_mode_unavailablemid-call. A mid-call refusal is refunded unless the provider billed the attempt. Turn Advanced mode off to run the model on its default lane.
describe-to-picker answers with its result in the same request, and it can also stream the answer as server-sent events. See Describe to Picker. The fields of generate-script are on Generate Script.
Example: generate an image
Start the job
curl -s -X POST https://app.nodaro.ai/v1/generate-image \
-H "Authorization: Bearer $NODARO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "a knight on a hill at dawn, cinematic",
"provider": "nano-banana-pro",
"aspectRatio": "16:9",
"resolution": "2K"
}'{ "jobId": "0f1a9c2e-5b7d-4e8a-9c31-6d2f8b4a7e10" }import { createClient, StaticTokenAuth } from '@nodaro/sdk'
const client = createClient({
baseUrl: 'https://app.nodaro.ai',
auth: new StaticTokenAuth(process.env.NODARO_API_KEY!),
})
const result = await client.nodes.run('generate-image', {
prompt: 'a knight on a hill at dawn, cinematic',
provider: 'nano-banana-pro',
aspectRatio: '16:9',
resolution: '2K',
})nodaro nodes run generate-image \
--param prompt="a knight on a hill at dawn, cinematic" \
--param provider=nano-banana-pro \
--param aspectRatio=16:9 \
--param resolution=2KPoll the job
Ask for the job's status every 2 to 5 seconds until it is completed or failed:
curl -s https://app.nodaro.ai/v1/jobs/0f1a9c2e-5b7d-4e8a-9c31-6d2f8b4a7e10/status \
-H "Authorization: Bearer $NODARO_API_KEY" | jq -r .data.statusRead the result
A completed image job carries the picture's URL in output_data.imageUrl. A video job uses videoUrl, an audio job audioUrl, and many jobs also carry a thumbnailUrl.
{
"data": {
"id": "0f1a9c2e-5b7d-4e8a-9c31-6d2f8b4a7e10",
"status": "completed",
"progress": 100,
"output_data": { "imageUrl": "https://…/0f1a9c2e.png" },
"error_message": null
}
}The SDK and the CLI can do the polling for you:
const output = await client.nodes.runAndWait('generate-image', {
prompt: 'a knight on a hill at dawn, cinematic',
provider: 'nano-banana-pro',
})
console.log(output.imageUrl)
// Several candidates at once, in input order:
const results = await client.nodes.runMany('generate-image', [
{ prompt: 'a knight on a hill, sunrise' },
{ prompt: 'a knight on a hill, golden hour' },
{ prompt: 'a knight on a hill, blue hour' },
])
for (const { jobId, output } of results) console.log(jobId, output.imageUrl)nodaro nodes run generate-image \
--param prompt="a knight on a hill at dawn, cinematic" \
--param provider=nano-banana-pro \
--watch --json | jq -r '.output_data.imageUrl'runAndWait polls every 2,000 ms for up to 15 minutes by default. Change that with pollMs and maxMs, stop it with an AbortSignal in signal, and follow progress with onProgress. It throws typed errors you can catch with instanceof:
| Error | When |
|---|---|
InsufficientCreditsError, StorageExceededError, JobBlockedError | The run was refused before any job started. |
ModelUnavailableError | The model is not offered by this deployment, or an admin switched it off. Choose another model. |
JobFailedError | The job ended failed or cancelled. It carries jobId and the error message. |
JobTimeoutError | maxMs passed. The job is not cancelled and usually still completes: fetch it later with client.jobs.get(jobId). |
JobAbortedError | Your signal fired. |
JobHeldError | The job entered pending_review on a deployment that reviews results. The job is not cancelled: check back later. |
With the CLI, pass complex bodies, such as arrays or nested objects, in a file with --params-file body.json. Flag values override the file for the same key, and true, false, null and numbers are converted from text.
Generate Image settings
The fields below are the most used settings of POST /v1/generate-image. Read Generate Image for what each setting does in the editor, and the image models for what each model supports.
Prop
Type
A masked edit or a refine costs the same as a new generation on that model.
On GPT Image 2.6 Flare and Sunburst, outputFormat writes a JPEG, a WebP, a 512×512 animated GIF or an SVG drawing instead of a PNG. A GIF ignores aspectRatio and resolution, and a mask with gif or svg is refused with 400. The finished job names the file's type in output_data.imageMimeType. See Parameter corrections for how each format's settings are corrected, and Generate Image formats for the prices.
/v1/generate-image can answer warnings, a list of { code, message }, for something you asked for that was not sent. Today the only code is moodboard_images_not_sent: some of a moodboard's images did not fit the model's reference limit. The run goes ahead, and the job keeps the same warnings in input_data.warnings.
Apply a preset
POST /v1/generate-image takes presetId: a built-in id such as generate-image/cinematic-still, or the uuid of one of your own presets. List the built-in ids with GET /v1/node-presets/factory?nodeType=generate-image, which needs no token. The route applies the preset the way the editor applies it to a Generate Image node, so the job is the one the editor's Run would send:
- The preset's prompt and its pre and post text (
promptPrefixandpromptSuffix). {name || default}placeholders filled with their defaults.- Its
style, folded into the prompt. - Its negative prompt, sent natively on a model that takes one, and added to the prompt as an
Avoid:line on every other model. - Its settings, and the editor's defaults for the ones it leaves unset: model
gpt-image-2and ratio16:9. The model and the price are the same as in the editor.
Fields you send override the preset, as an edit on the applied node would. Your prompt replaces the preset's and is wrapped in its pre and post text. provider, aspectRatio, resolution, quality, negativePrompt, style and the other settings replace the preset's values. References, sessionId and origin are kept.
curl -s -X POST https://app.nodaro.ai/v1/generate-image \
-H "Authorization: Bearer $NODARO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"presetId": "generate-image/logo-wordmark", "presetVariables": {"BRAND": "Acme"}}'- A placeholder with no default, such as
{BRAND}, needs its value inpresetVariables. Without it the route answers400withcode: "preset_variables_required"andtokens: ["BRAND"]. - An unknown
presetIdanswers404 preset_not_found. - The credit check prices the request after the preset is applied. Neither
presetIdnorpresetVariablesis stored on the job. - A preset that works from photos has
minReferencesabove 0. Send that many images inreferenceImageUrls. See Presets. - A preset combines with
countandmodels. See the next section. - A preset cannot use your own palettes or moodboards. When its
directionnamespalette:<id>ormoodboard:<id>, sent by you or saved in the preset, the route answers400 validation_errorwithfieldnaming the key, before anything is created or charged.
Several images in one call
POST /v1/generate-image can make several images at once:
count(1 to 4) makes that many images of one model. Each image is its own job.models(2 to 4 distinct image model ids, the same idsprovidertakes) replacesproviderand sends the same request to each model. Each model's settings are fitted to it as on a single call, and the corrections are reported per model.- With both,
countis per model:models: ["gpt-image-2", "flux-2-max"]withcount: 2makes four images. A request makes at most 8 images.
curl -s -X POST https://app.nodaro.ai/v1/generate-image \
-H "Authorization: Bearer $NODARO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 0f8e6c1a-try-1" \
-d '{"prompt": "a lighthouse at dusk", "models": ["gpt-image-2", "flux-2-max"], "count": 2, "seed": 41}'{
"jobId": "<first job>",
"jobIds": ["<job 1>", "<job 2>", "<job 3>", "<job 4>"],
"batchId": "6b1f7e2a-0c4d-5e8f-9a1b-2c3d4e5f6a7b",
"groups": [
{ "model": "gpt-image-2", "jobIds": ["<job 1>", "<job 2>"], "credits": <credits> },
{ "model": "flux-2-max", "jobIds": ["<job 3>", "<job 4>"], "credits": <credits>,
"adjustments": [{ "field": "resolution", "from": "1K", "to": "2 MP", "reason": "..." }] }
],
"totalCredits": <credits>
}Each image is charged what a single call for it costs. Without count and models, or with count: 1, the answer is the usual { "jobId" }. jobId is always the first job, so code that reads it keeps working.
- Credits: the whole batch, or nothing. The balance is checked against the batch's total before anything is created. When it is short, the route answers
402 insufficient_creditswithrequiredset to the total, and nothing is created. When a charge fails part-way, the jobs already created are refunded and removed before the answer. - Idempotency, per image. With
Idempotency-Key: K, image i is keyedK:i. Repeating the request with the same key returns the same job ids and never charges a job twice. A batch that was only partly created is completed. A second request with the same key at the same time waits for the first, or answers409 batch_in_progressafter 15 seconds; retry it. Repeating a key with a different body answers422 idempotency_key_reused. - Seeds. With
seed, image i of each model getsseed + i, so the images differ. - With a preset,
countmakes N images of the preset's request. Withmodels, the preset is applied once per model, so each model gets the editor's request for it. - Find a batch later. Every job of the batch carries the same
input_data.batchIdand its position ininput_data.batchIndex.GET /v1/jobs?batchId=<id>lists them. - Refused before anything is created with
400 validation_error:countoutside 1 to 4, fewer than 2 or more than 4models, a repeated or unknown model,modelstogether withprovider, or more than 8 images. A model the deployment does not offer answers403 model_not_available.
Generate a mask
POST /v1/generate-mask selects a subject described in text in an image, and returns a black-and-white mask image. White is the subject. The body is { imageUrl, prompt, threshold? }:
prompttakes 1 to 500 characters, for example"the person".thresholdis from 0 to 1, with a default of 0.3. A lower value selects more.
A malformed body is a 400 validation_error. The answer is { jobId }. The finished job's output is { maskUrl, imageUrl }: maskUrl is the mask PNG, and imageUrl is the source image, passed through unchanged. Feed both to an inpainting edit. A run costs 55 credits, and GET /v1/credits/model-cost?model=generate-mask answers the same price. See Generate Mask.
Split an image into layers
POST /v1/split-into-layers turns one image into a base picture and separate layers, each element on its own transparent PNG. The body is { imageUrl, maxLayers?, size?, prompt?, sessionId?, origin? }:
imageUrlis an http(s) URL.maxLayersis 1 to 99, with a default of 9. A picture with more elements gets them merged into fewer layers, and none is dropped.sizeis"1K"(default),"1.5K","2K"or"auto": each image's pixel budget. 1K is about one megapixel, and"auto"keeps the picture's own size, between 1K and 2K.prompttakes up to 2,000 characters: what to separate. It may carry<bbox>x1 y1 x2 y2</bbox>marks, from 0 to 1000, to point at a region. Left empty, the main elements are found automatically.sessionIdandoriginwork as on every job-creating route. An optionalIdempotency-Keyheader makes a retry with the same key answer{ jobId, deduped: true }for the job it already started.
A malformed body is a 400 validation_error before any credits are held. The answer is { jobId }. The finished job's output is { imageUrl, layers, layerData, layerCount, layersNotSaved, libraryImages }:
imageUrlis the base picture, an opaque PNG, andlayerslists each layer's file, bottom to top.layerDataholds one{ name?, description?, zIndex, width?, height?, box?, boxNormalized?, url, baseUrl }per layer, in the same order.zIndexis the stacking order: 1 sits on the base, and higher is drawn above.box({ x1, y1, x2, y2 }, top-left and bottom-right) is where the layer sits on the base, in the base's pixels.boxNormalizedis the same box on a 0 to 1000 scale.widthandheightare the layer file's own pixels. A layer is cropped to its element, so scale it to itsboxto put it back in place.
A run holds the price of the most images it may return, maxLayers + 1 with the base, at its size. It is charged only for the images it returns. 1K and 1.5K are priced alike, and so are 2K and auto. A run that finds no element completes with the base only, at the one-image price. A layer that cannot be saved is left out and counted in layersNotSaved, and it is not charged. POST /v1/quote states a run's hold before it starts: see Run quotes. See Split into Layers.
Generate a video
Two routes make video. POST /v1/generate-video animates from pictures: a start frame in imageUrl, an optional last frame in endFrameUrl, or references alone on the models that accept them. POST /v1/text-to-video makes a clip from a prompt alone, so its prompt is required. When you leave out provider, the platform's default video model is used.
POST /v1/text-to-video also takes an optional contentPolicyRewrite boolean. When it is absent or true, a copyright refusal is retried once with a reworded prompt, and the job's output then carries contentPolicyRewrite: { original, rewritten }. With false, a copyright refusal fails the job instead.
{
"imageUrl": "https://…/frame.png",
"provider": "seedance-2",
"prompt": "she turns toward the window",
"duration": 8,
"resolution": "720p",
"direction": { "cameraMotion": "dolly-in", "timeOfDay": "dawn" }
}The response is { "jobId": "…" }, with warnings and adjustments when they apply. The finished job carries output_data.videoUrl.
Prop
Type
A few rules differ by model:
resolutionandaspectRatioon the Seedance 2 family are passed through: a value the model does not support is ignored, never refused. Seedance 2 is the only one with4kandadaptive. Seedance 2.5 makes up to 30 seconds in one call and, with a start frame, always renders at that frame's aspect ratio.- MiniMax H3 takes
resolution2Kor768P, written exactly like that.GET /v1/nodes/:typelists the value to send inproviderResolutionWire. - Frames and references can be combined on Seedance 2 and MiniMax H3. When you send a reference beside a start or end frame, the frames become numbered references in the prompt instead of fixed endpoints. Wan 3.0 cannot send both, so the frame is added to the end of the reference list instead, and the call still succeeds.
- Reference videos cost more. On models that bill them, a reference clip is priced by its own duration plus the output duration, so a longer source clip reserves more credits. Wan 3.0 bills output seconds only. Kling 4.0 and Kling 4.0 Flash bill the output plus the longer of the output and the clips, so a clip shorter than the output counts as the output's length.
characterReferenceskeeps a real person's face on Gemini Omni and Gemini Omni Flash. Each entry is{ imageUrl, description, bodyImageUrl?, name?, voice? }: a portrait, a description of the person up to 2,000 characters, an optional full-body picture and an optional name up to 100 characters.referenceImageUrlsis loose context for the model, while this input holds the face. Any other model answers400 character_references_unsupported. A start frame or an end frame in the same request answers400 character_references_with_start_frameor400 character_references_with_end_frame. The request must fit the model's input budget of 7 units: each picture counts 1, each video 2, and each character 1, or 2 with abodyImageUrl. A request over the budget answers400 character_references_quota. Character references add no credits.- Kling 4.0 (
kling-4) runs from text alone, or from reference materials, with at most 15 in all. It takesreferenceImageUrls(up to 10),referenceVideoUrls(up to 5, each 2 to 30 seconds),characterReferences(up to 7) andreferenceAudioUrls(up to 3). More answers400before anything is reserved:character_references_quotawhen the request carriescharacterReferences,validation_errorotherwise. AnimageUrlorendFrameUrlcounts as an image: it joins the reference images after your own, named in the prompt as the opening or closing frame. Each character reference is a named subject:imageUrl,bodyImageUrland up to 3extraImageUrlsare its 2 to 4 images, and the prompt calls it@<name>. An entry with one image is sent as a plain reference image. A start or end frame is allowed with it, and avoiceanswers400 character_voice_unsupported; send voice clips asreferenceAudioUrls. In the prompt, name materials@image_N,@video_N,@<name>and@audio_N. A mention of a material the request does not carry has its@removed, so the run still starts. A reference-video run is billed per second of the output plus the longer of the output and the reference videos, at the reference rate. - Kling 4.0 audio soundtrack (on by default). The first
referenceAudioUrlsclip plays as the result's soundtrack. It is sent as one more reference video: a still frame that carries the audio, as long as the audio, at least 2 seconds and at most the output. Its sound is kept, so the people perform to the exact track. Any further clips stay named voices, and@audio_2becomes@Voice1. The soundtrack takes one of the 5 reference-video places, and the run is billed as a reference-video run.audioSoundtrack: falsekeeps every clip a voice sample. With the soundtrack, Kling also keeps the sound of any reference videos you send. - Kling 4.0 Keyframes (
kling-4-keyframes) takes animageUrlstart frame, up to 8 in-between frames asreferenceImageUrls, in order, and anendFrameUrl. An end frame alone is allowed, and no frame at all is text-to-video. The frames in all are capped byduration: 4 at 3 seconds, one more for each second, and 10 from 9 seconds. More, or over 8 in-between frames, answers400 validation_errorbefore anything is reserved. In the prompt, the frames are@first_frame,@keyframe_Nand@last_frame, and@image_Nis rewritten to@keyframe_N.aspectRatioapplies only with no frame. It takes no reference videos, audio or characters. - Both Kling 4.0 models take a
durationof 3 to 30 whole seconds (default 5) and aresolutionof720p(default) or1080p; any other resolution renders and bills 720p.aspectRatioisauto(default: it follows the materials, and 16:9 with none),16:9,9:16,1:1or21:9. Sound is always on (soundis ignored), there is noseed, and the prompt can be up to 8,000 characters. They are priced per second and are not available on the free tier. - Kling 4.0 Flash (
kling-4-flash) chooses its mode from what you send. AnimageUrlwith noreferenceImageUrls,referenceVideoUrlsorcharacterReferencesis the exact opening frame.aspectRatiois then not used, and the prompt names the frame@first_frame. Anything else runs in reference mode: a prompt alone, references, characters, or a start frame beside them. In reference mode, animageUrljoins the reference images after your own, as onkling-4. - Kling 4.0 Flash references. Reference mode takes
referenceImageUrls(up to 10),referenceVideoUrls(up to 5, 2 to 20 seconds together) andcharacterReferences(up to 7 named subjects, as onkling-4), with at most 15 materials in all. A set of reference videos outside 2 to 20 seconds answers400before anything is reserved. There are no separate voices:referenceAudioUrlsclip N (up to 7) becomes the voice ofcharacterReferencesentry N, in order. A character needs 2 to 4 images to carry a voice. A clip whose character has one image, and any clip after the last character, is dropped, and the run still starts. A clip takes no material place, and an@audio_Nmention has its@removed.keepOriginalSoundworks as onkling-4. There is noaudioSoundtrack. - Kling 4.0 Flash settings.
durationis 3 to 20 seconds (default 5). The model renders 720p only: anyresolutionrenders and bills 720p.aspectRatiois as onkling-4, sound is always on, there is noseed, and the prompt can be up to 8,000 characters. There is no end frame: anendFrameUrlbeside animageUrlor references is not sent to the model. AnendFrameUrlalone onPOST /v1/generate-videoanswers400(imageUrl is required). It is priced per second, on one rate for both modes. A reference-video run bills the output plus the longer of the output and the reference videos, at the reference rate. Unlike the other Kling models, it runs on free credits, as a 3 or 4 second clip. - PixVerse V6 Max (
pixverse-v6-max) is one id for two modes. OnPOST /v1/generate-video, theimageUrlis the exact first frame, and the clip takes its shape. OnPOST /v1/text-to-video,aspectRatiois16:9(default),4:3,1:1,3:4,9:16,2:3,3:2or21:9; another ratio moves to the nearest of them.durationis 1 to 15 seconds (default 5).resolutionis360p,540p,720p(default) or1080p, and any other value renders and bills 720p.seedis a whole number from 0 to 2147483647, and the prompt can be up to 5,000 characters. - PixVerse V6 Max sound. Native audio is on unless
sound: false. With nosound,generateAudio: falsealso turns it off, andsoundwins when both are sent. Sound changes the price: each length and resolution has a silent price and a price with audio. There is no end frame and there are no reference images, videos or audio: they are not sent to the model. It runs on free credits at 360p, for 1 to 4 seconds. - Grok Imagine 1.5 (
grok-imagine-video-1.5) needs animageUrl, the exact first frame. AnendFrameUrlis the exact last frame, and the same URL as both makes a seamless loop. Up to 4 in-between frames go inreferenceImageUrls, in order. They land at whole seconds spread evenly over the clip, so two frames on an 8-second clip land at 3 and 5 seconds. - Grok Imagine 1.5 limits. A clip of D seconds takes at most D − 1 in-between frames, and extra frames are left out, never refused. In-between frames or an
endFrameUrlwithout animageUrlanswer400 image_required.durationis 1 to 15 seconds (default 8).resolutionis480p(default),720por1080p, and any other value renders and bills 480p. There is noseed, and no reference videos, audio or characters. It is priced per second by resolution, and the frames add nothing. - Grok Imagine 1.5 at 1080p takes the start frame alone. A 1080p run that also sends an end frame, a loop or in-between frames answers
400 start_frame_only_resolutionbefore any credits are reserved. Choose 720p, or remove the other frames. 480p and 720p take every frame. voicepins one voice to a character reference across clips:{ preset, description?, exampleLine? }, wherepresetis one of the Gemini voice ids. A request can pin up to 3 different voices, or it answers400 character_voices_over_limit. See Generate Video for the voices.
For every model's durations, resolutions and credit prices, read the video models or call GET /v1/models.
Seedance 2.5 reference task type
POST /v1/generate-video and POST /v1/text-to-video take an optional referenceTaskType. It tells Seedance 2.5 what the reference media is for (referenceImageUrls, referenceVideoUrls and referenceAudioUrls), instead of leaving the model to guess from the prompt.
| Value | What it does |
|---|---|
auto | The default, also when the field is absent. Nothing is declared, and the model decides from the prompt. |
reference | The media only guides a new clip. The request keeps the aspect ratio and length it was sent with. |
edit | The clip is an edit of the reference video, in the video's own shape and length: aspectRatio becomes adaptive and duration becomes Auto (-1). It needs at least one reference video, and every reference video must be 4 to 30 seconds long as the platform measures it. Otherwise nothing is declared, and the request keeps its ratio and length. |
extend | The clip continues the reference video, in the video's own shape (aspectRatio becomes adaptive). An Auto or missing duration becomes the model's default length, defaultDuration in GET /v1/models. You get the continuation only, not the source clip joined to it. For both as one video, use Extend Video. It needs at least one reference video. |
- Ignored, never an error, on any other model and on a request with no reference media. A start or end frame is not reference media. A value other than the four above answers
400 validation_error. The models that take the field list its values inreferenceTaskTypesinGET /v1/models. - What it changes is reported. A ratio or length that
editorextendreplaces comes back inadjustments, and on/v1/generate-videoalso inwarnings. An Auto ratio is not replaced, because with a reference video it already runs atadaptive. - The price follows the values it runs at. The run is priced, reserved and sent at those values.
POST /v1/quotewith the same body prices it the same way. Areferencerun with an Auto length is reserved at the longest output the model can make, and charged for what it makes. - A draft declares it too. A
draft: truerequest declares the type exactly as the same request withoutdraft, at 480p. Aneditdraft's 1080p finalize re-renders the same edit, at the source clip's length. - A dropped declaration is not reported. A request that asked for a type and did not get it, for example with no reference video, answers like any other. The job's
input_data.referenceTaskTypeis set only when a type was declared. POST /v1/video-to-videowithprovider: "seedance-2-5"always asks foredit, whatever the body carries. The edit is declared when every clip, the sourcevideoUrland anyreferenceVideoUrls, is 4 to 30 seconds long. Otherwise it goes out undeclared, and the model reads the prompt.
In MCP, the field is reference_task_type on generate_video.
Use references
References are pictures, and on video also clips and audio, that the model should match. You can send them as a flat list of URLs, or as structured references that the route numbers, labels and writes into the prompt for you, exactly as the editor does for wired nodes.
Structured references
connectedReferences is accepted by POST /v1/generate-image, POST /v1/generate-video, POST /v1/text-to-video and POST /v1/extend-video. Each entry describes one picture:
Prop
Type
On the video routes, the route turns these entries into numbered references:
- Every reference you do not mention is attached. Its URL joins the reference list, with duplicates removed, and gets a line such as
@image_1 (reference): <label>. Awired-characterentry becomes part of a "Use these characters:" instruction instead. - The list is capped at the model's limit before numbering, so a reference number in the prompt never points at a picture that was not sent.
{image:N:label}in the prompt becomes "the label from@image_N", numbered against the attached references.{ref:<id>}and{ref:<id>:label}address a reference by theidyou gave it. The platform replaces the token with the reference's number after it has numbered the list, so you never compute a number yourself. The list is numbered in this order: the flatreferenceImageUrlsfirst, then characters you did not mention, then the other entries in your order. A token whose reference was not attached, because it was over the cap or the model takes no references, falls back to its label, then todefaultName, then to nothing. It never reaches the model as raw text.referenceOrder, a list of reference ids, reorders the references and renumbers them to match.POST /v1/generate-imageaccepts it too.
Display copies are replaced by their originals. This applies to every image input on POST /v1/generate-video and POST /v1/text-to-video: imageUrl, endFrameUrl, referenceImageUrls and connected or character references. An input that is a resized display copy of a Nodaro CDN image, https://cdn.nodaro.ai/cdn-cgi/image/<options>/<path>, is recorded and sent as the original file, https://cdn.nodaro.ai/<path>. The same applies to the pictures of a trend run and of a Nodaro Motion run. URLs on other hosts are used as sent.
Embedded metadata is removed before a model sees an image. Some input images carry embedded data, such as Content Credentials, EXIF, XMP, IPTC or PNG text chunks. Such an image is re-encoded without that data, in its own format, before it is sent to the model. A clean image is sent as it is. Your stored file does not change.
connectedReferences feeds pictures only. referenceVideoUrls and referenceAudioUrls stay flat lists. Leave connectedReferences out and the routes behave as before: your prompt and referenceImageUrls are sent as they are.
On POST /v1/generate-image and the MCP tool generate_image, an {image:N} or {image:N:label} token in prompt points at the N-th reference picture:
- Without connected references, N counts the flat
referenceImageUrlsin order, so{image:1}is the first URL you sent. That includes a request whose only other references aredescribedReferences. - With at least one
connectedReferencesentry, N counts theconnectedReferencesentries in order. FlatreferenceImageUrlsin the same request are still attached, ahead of the connected ones, but no token points at them. To name a photo in the prompt, send it as aconnectedReferencesentry. - A token with no picture at its position becomes its label:
{image:2:person}reads asperson. A bare{image:2}is left out.
The editor and a workflow run number the tokens the same way. An image that reaches a Generate Image node through a node in between, such as a Text node, is sent and numbered too.
Which video models accept picture references
| Model family | Picture references |
|---|---|
| Seedance 2 family | Up to 9 |
| HappyHorse Ref2V | Up to 9 |
| Gemini Omni, Kling 3 Omni, Grok Imagine image-to-video | Up to 7 |
| VEO 3.1 Fast and VEO 3.1 Lite | Up to 3 |
On any other model, the {image:N} tokens are reduced to their labels and nothing is attached. VEO 3.1 Quality is not on the list: references sent with veo3 are ignored, and the run uses its frames.
Video from references alone
On POST /v1/generate-video, the start frame is optional when at least one kind of reference you send is supported by the model, for example Kling 3 Omni with referenceImageUrls alone. A references-only run on VEO 3.1 Fast or Lite switches to reference mode by itself, so you do not need generationType. A reference kind the model cannot use does not count: audio-only references on a model that takes only pictures are refused with 400.
An end frame alone is accepted on the models that fold it into their references: the Seedance 2 family, MiniMax H3 and Wan 3.0. Send endFrameUrl once, without repeating the picture in referenceImageUrls. The @nodaro/shared package exports videoProviderFoldsLoneEndFrame(provider) so your interface can use the same rule.
When a request has no start frame, no reference mode and no reference the model can use, the answer depends on the model:
- A model that cannot make video from text alone, such as Kling 3 Omni, HappyHorse Ref2V or Hailuo 2.3, answers
400 image_required. The message says whether references would work instead.GET /v1/modelsis the authority on which models these are. - Every other model answers
400 validation_errorand tells you to usePOST /v1/text-to-videofor a prompt-only clip.
Some models have one id for text-to-video and another for image-to-video: Grok Imagine (grok and grok-i2v), Wan (wan and wan-i2v), Wan 2.7 (wan-2.7-t2v and wan-2.7-i2v) and HappyHorse (happyhorse and happyhorse-i2v). POST /v1/generate-video is the image-to-video route. A request there that names the text-to-video id, with an imageUrl or reference images, runs as the image-to-video id. It is priced, recorded on the job and sent as that id, and POST /v1/quote quotes it the same way. A prompt-only request naming either id gets the 400 validation_error above. On POST /v1/text-to-video, name the text-to-video id: an image-to-video id there is a 400 validation_error. Both refusals come before anything is reserved.
Extend Video
POST /v1/extend-video accepts connectedReferences and referenceImageUrls only with provider: "seedance-2-5-extend" (Seedance 2.5) or "seedance-2-0-extend" (Seedance 2.0). An older request that names seedance-2-extend runs as Seedance 2.5. Any other extend model refuses references with 400. The limit, on both, is 8 of your pictures, because the source clip's last frame takes one reference seat, after yours, so your numbers never shift. The source's last 2 seconds travel as @video_1. Reference pictures add no credits. Extend Video has no direction and no subject, because its prompt continues a clip that already has a look.
Described references
describedReferences names a subject you can describe but have no picture for yet, such as a role in a script. POST /v1/generate-image, /v1/generate-video, /v1/text-to-video and /v1/extend-video accept up to 10 entries of { name, description }. The name can be up to 80 characters and the description up to 2,000.
- Nothing is attached. A described reference uses no reference seat, and the numbering of your other references does not change.
- It reaches the model as a line of text:
<Name> — <description>.Keep the name in your prompt, for exampleNatalie walks down the pier., and the line tells the model who Natalie is. Do not write an@mention for it: that grammar addresses an attached picture. - It works on its own. Send
describedReferenceswithout anyconnectedReferences, the usual case for a story written before any character exists. - Entries without a name or a description are dropped, and a repeated name is written once. Every extend model accepts described references, because they carry no URL.
Per-use descriptions and captions
descriptionOverrideon aconnectedReferencesentry says what the reference is for this run only, up to 2,000 characters. It fills the description the reference's instruction already has, ahead of the stored description. When the instruction has no description, it adds one line, so the model is told once, never twice.referenceVideoCaptionsandreferenceAudioCaptionsonPOST /v1/generate-videoand/v1/text-to-videodescribe the reference clips and audio, up to 500 characters each. They are index-aligned:referenceVideoCaptions[0]describesreferenceVideoUrls[0]. Each becomes a line such as@video_1: <caption>., and a blank entry skips one clip without breaking the alignment.
Mention a reference in the prompt
On POST /v1/generate-image, you can place a reference inside your sentence instead of leaving it in the list at the end. Write @<name-slug>:<index>, or @<name-slug>:<index>:<role> to say what to take from it:
{
"prompt": "a wide shot of @nessie:1 rising beside @dock:2:material",
"connectedReferences": [
{ "id": "cr-1", "defaultName": "Nessie", "source": "wired-creature", "url": "https://…/nessie.png" },
{ "id": "ob-1", "defaultName": "Dock", "source": "wired-object", "url": "https://…/dock.png" }
]
}The model receives "a wide shot of the creature from reference image A rising beside the material from reference image B".
- The slug comes from
defaultName, in lower case, with every run of other characters turned into one-:Old Townbecomesold-town. There is no slug field to set. - The index only matches the mention to a reference. It is never written into the prompt, and the platform numbers the references itself.
- Roles for pictures (
manualandwired-image) areobject,person,face,clothes,background,style,poseandtexture. Creatures takecreature,anatomy,markings,pose,colorandstyle. Objects takeobject,shape,material,color,textureandstyle. Any other single word passes through as written. Without a role, the entry'sdefaultRoleapplies. ~lockand~nolockafter a mention, as in@town:1:background~lock, turn that reference's identity lock on or off for the mention.- Names resolve in this order: character, location, image, creature, object. A name shared by a character and a picture means the character, and within one kind the first match wins.
- A name whose slug starts with a digit cannot be mentioned, for example
3D Render, which becomes3d-render. Rename the reference to mention it. A mention of a reference that was over the model's cap stays as literal text. - Mentioning a reference moves it from the list at the end to the place you typed it, and the letters of the references after it change to follow the sentence. For a creature or an object, a mention also replaces the line it would otherwise add at the end.
Reference lock
referenceLock on POST /v1/generate-image adds Nodaro's tested reference-fidelity wording ahead of the scene:
| Value | Adds | Use it for |
|---|---|---|
standard | Instructions to use only what the references show, keep likeness, and compose them together | Compositions from several references |
multi-person | The same, plus rules never to change or blend faces | Two or more faces in one shot |
You send the id, not text: the wording belongs to the platform and improves without a client update. Leave the field out and no lock is added.
Cinematic direction by id
direction describes the camera, the light and the look with picker ids instead of prose. POST /v1/generate-image, POST /v1/generate-video and POST /v1/text-to-video accept it. Nodaro writes its own tested wording for each id into the prompt, so a saved request picks up improved wording over time instead of freezing the text your client wrote.
{
"prompt": "a knight on a hill",
"provider": "nano-banana-pro",
"direction": {
"shotSize": "wide-shot",
"lens": "wide-24mm",
"lightingStyle": "rembrandt",
"style": "anime",
"mood": ["happy", "joyful"]
}
}Keys and where their ids come from
Each key is a field of a Creative Controls picker. Get the valid ids from GET /v1/picker-catalogs/<picker>, the same catalogs the editor's pickers use.
| Keys | Picker | Image | Video |
|---|---|---|---|
shotSize, angle, coverage, composition, vantage | Framing | Yes | Yes |
pose | Pose | Yes | Yes |
compositionEffect | Composition Effects | Yes | Yes |
cameraFormat | Camera / Film Stock | Yes | Yes |
lens | Lens | Yes | Yes |
aperture, shutterSpeed, isoValue | Exposure Settings | Yes | No |
timeOfDay, lightingStyle, lightingDirection, lightingRatio, colorTemperature | Lighting | Yes | Yes |
colorLook | Color / Look | Yes | Yes |
atmosphere | Atmosphere | Yes | Yes |
postProcess | Post-Process Effects | Yes | No |
style | Style | Yes | Yes |
mood | Mood | Yes | Yes |
aesthetic | Aesthetic | Yes | Yes |
photoGenre | Photo Genre | Yes | No |
photographer | Photographer | Yes | No |
renderQuality | Render Quality | Yes | No |
setting | Setting | Yes | Yes |
era | Era | Yes | Yes |
backdrop | Backdrop | Yes | Yes |
cameraMotion | Camera Motion | No | Yes |
actionFx | Action FX | No | Yes |
temporalSpeed, temporalFreeze, temporalDirection, temporalShutter | Temporal | No | Yes |
transition | Transition | No | Yes |
loopSubject | Loop Subject | No | Yes |
A key that does not apply to a route is accepted and simply adds nothing, so one map of look ids can be sent to both the image and the video routes unchanged.
Values and limits
- One id or a list. The multi-pick keys,
mood,aesthetic,photographer,atmosphere,postProcess,compositionandlightingStyle, take up to their own limit, and extra ids are dropped. A single-pick key given a list uses the first entry. - Two limits are refused with
400 validation_error: more than 8 entries for one key, and an id longer than 100 characters. - Absent is not empty. A missing key means no hint, never a default. An empty string or an empty list adds nothing.
- Your own palettes and moodboards are never skipped.
colorLookalso takes"palette:<id>"andstyletakes"moodboard:<id>"on/v1/generate-image. See Your own palettes and moodboards. - Unknown keys and unknown ids are skipped, not refused. A newer client on an older server gets fewer hints instead of an error, so update the server before a client that sends new keys.
- Custom catalog packs. On a deployment that registers its own catalog packs,
GET /v1/picker-catalogslists the ids a pack adds. Those ids are accepted but add no wording todirection.
Where the words go
The clauses are added after your prompt, in a [style] section. The film line carries cameraFormat, colorLook, style and era, and the scene line carries the other look keys:
a knight on a hill
[style]:
<film line>
<scene line>- The order inside a line is the platform's fixed order, not the order of your keys. A clause repeated by two keys is written once.
- A line with nothing on it is left out. When no key adds anything, there is no section at all, and your
promptreaches the model unchanged. - On the video routes, motion keys work differently. They add a short professional term, such as
cross-dissolve, and stay in the body, after your prose, because motion is part of the shot.cameraMotioncomes first. Only look keys go into the[style]section. - On the video routes,
styleadds the style's video wording instead of its image wording, in the same[style]section. The video wording is written for moving shots: it names the medium first, then its techniques, and a photographic style names a live-action medium, so it stays photographed. The same"style": "anime"therefore reads differently on a video route and on generate-image.jobs.input_data.promptrecords the text the model received. The catalog endpoints return only the image wording. On the canvas it works the same way: see Styles for images and for video.
When the prompt is too long
Each model accepts a prompt up to its own length. A full direction can exceed a small limit on its own, for example 3,000 characters on Seedream on the image side and 1,000 characters on Kling on the video side. When that happens, Nodaro removes direction clauses one at a time, starting from the end of its fixed order, until the prompt fits. Nothing else is removed before them:
- Subject clauses are removed only after every direction clause.
- Your prose, your references and the phrases that bind them,
@mentions, and theStyle:andAvoid:lines always stay. - Only when the prompt still does not fit with no hints left is the end of the text cut, with
...added.
On the video routes, the budget also counts the reference instructions the route adds, which are never removed. For a model with no negative-prompt setting, your negativePrompt is added as an Avoid: line whose room is reserved first, so a long negative prompt costs you hint clauses, not prose. The optional injectCharacterContext text is added after this step and is not part of the budget.
The job records what happened. input_data.prompt is what the model received, input_data.userPrompt is the text you sent (an empty string when you sent only direction), and input_data.direction is your ids as sent.
Direction stored on a node
A Generate Image node in a saved workflow can carry the same direction object in its data, written by the API, MCP or an app that creates workflows. The editor honors it on every run and in the final-prompt preview. Stored ids add to any wired Framing, Lighting or Style picker: the wired hint comes first, then the stored ids. Presets and workflow exports keep the ids with the rest of the node.
Your own palettes and moodboards
A palette is your own set of 2 to 12 colours, and a moodboard is your own set of 1 to 12 reference images with a description. Save them once, then steer any POST /v1/generate-image run with one, the same way you pick a built-in colour look or style. They work on every edition. For what they do in the editor, read Palettes and moodboards.
| A palette | A moodboard | |
|---|---|---|
| What it holds | A name of 1 to 80 characters, 2 to 12 colours as #RRGGBB, and an optional note of up to 500 characters | A name, an optional description of up to 1,000 characters, and 1 to 12 images { url, caption? } |
| How you use it | direction.colorLook: "palette:<id>" | direction.style: "moodboard:<id>" |
| What the run gets | Every colour in the prompt, by an everyday name and its hex code | The description in the prompt, and the images as style references |
{
"prompt": "a lighthouse on a cliff at dusk",
"provider": "nano-banana-pro",
"referenceImageUrls": ["https://example.com/my-lighthouse.png"],
"direction": {
"colorLook": "palette:0f9e8d7c-6b5a-4c3d-9e2f-1a2b3c4d5e6f",
"style": "moodboard:1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"lightingStyle": "rembrandt"
}
}Manage palettes and moodboards
| Method | Path | Body or query | What it does |
|---|---|---|---|
GET | /v1/palettes | limit (1 to 100, default 50), cursor | Your palettes, newest first: { data, nextCursor }. |
POST | /v1/palettes | { name, colors, description? } | Create a palette. Answers 201. Colours are stored upper case. |
GET | /v1/palettes/:id | None | One palette. |
PATCH | /v1/palettes/:id | Any of name, colors, description | Only the fields you send are written. description: null clears it. |
DELETE | /v1/palettes/:id | None | Returns { success: true }. |
GET | /v1/moodboards | limit, cursor | Your moodboards, newest first: { data, nextCursor }. |
POST | /v1/moodboards | { name, description?, images } | Create a moodboard. Answers 201. |
GET | /v1/moodboards/:id | None | One moodboard. |
PATCH | /v1/moodboards/:id | Any of name, description, images | Only the fields you send are written. images replaces all of them. |
DELETE | /v1/moodboards/:id | None | Returns { success: true }. The linked images are not touched. |
- Moodboard images are links, never copies. Each
urlis anhttporhttpsaddress that is notlocalhostor a private network, up to 2,048 characters. A caption is up to 200 characters. An image you delete, or one your plan's media retention removes, is gone from the moodboard too. nextCursoris an opaque token. Pass it back ascursorfor the next page. It isnullon the last page.- They are yours only. Another person's palette or moodboard answers
404, like one that does not exist. - Scopes: an OAuth app token needs
assets:readfor the reads andassets:writefor the writes. Writes are limited to 30 a minute. - The same routes are in the SDK (
client.palettes,client.moodboards), the CLI (nodaro palettes,nodaro moodboards) and the MCP toolslist_palettes,save_palette,list_moodboardsandsave_moodboard. MCPgenerate_imagetakespalette_idandmoodboard_id. There is no delete over MCP.
What a run gets
- A palette adds
color palette: <name> <hex>, …to the film line of the[style]section, where the built-in colour look would go. Each colour gets its nearest everyday name, because a model follows a named colour more reliably, and its exact hex code. The palette's note is not added to the prompt. - A moodboard adds its description, on one line, where the built-in style would go. With no description, only its images go.
- Like every direction clause, these are removed first when the prompt is over the model's limit.
- The moodboard's images go to the model as reference images, each with a short line that tells the model to take the colours, light, texture and mood from it, not its subject or layout. A caption is added when the image has one.
- Your own references come first. Images you send yourself keep their places and their numbering. The moodboard's images fill only the reference places left after them.
- Never more than the model takes. Images that do not fit are not sent, in the moodboard's order, and the answer carries a
moodboard_images_not_sentwarning. A model that takes no reference images gets none of them, and the description still goes in. - Price. The images count as references, so a run is priced like any run with that many references. The price checked before the run and the price reserved count the same images: the ones actually sent.
aspectRatio: "auto"keeps the shape of the first image a run sends. When you send no image of your own, that is the moodboard's first image.- The job stores the ids, not copies. Running it again uses the palette or moodboard as it is then.
Errors and limits
| Status | Code | When |
|---|---|---|
400 | validation_error | palette: or moodboard: without a uuid after it, a palette on a key other than colorLook, a moodboard on a key other than style, one combined with another value on the same key, or a request that also sends presetId. field names the key. |
404 | direction_not_found | The palette or moodboard is not yours, or no longer exists. field is direction.colorLook or direction.style. |
Both answers come before anything is created or charged. Palettes and moodboards work on generate-image only: POST /v1/generate-video and POST /v1/text-to-video answer 400 validation_error for one. In a saved workflow, a Generate Image node whose direction names one is resolved each time the workflow runs.
Describe the subject by id
subject is the same idea for who is in the shot: the person, how they are styled, and the props in the frame. POST /v1/generate-image, POST /v1/generate-video, POST /v1/text-to-video, POST /v1/generate-character and POST /v1/generate-creature accept it:
{
"prompt": "on the seawall at dusk",
"provider": "nano-banana-pro",
"subject": {
"type": "woman",
"age": "age-30s",
"ethnicity": "east-asian",
"hairBase": "base-short-straight",
"makeup": "makeup-smoky",
"outerwear": "outerwear-trench",
"heldProp": "smartphone"
}
}- The keys are the fields of the Person and Styling pickers, such as
type,age,ethnicity,faceShape,hairColor,skinTone,makeup,outfit,outerwearandfootwear, plus three prop keys:heldProp(Held Prop),material(Material) andanimal(Animal).GET /v1/picker-catalogs/personand/stylinglist every field and id. subjectanddirectionnever overlap. Their keys are separate, so one choice never adds two clauses.posebelongs todirection.customAgeis the one number. Send"age": "age-custom"with"customAge": 34for an exact age in years. It is rounded and kept between 0 and 120.- Lists have limits per key.
jewelry,wardrobeStateanddistinctiveFeaturetake 3 ids.ethnicity,regionalAesthetic,hairColor,eyeColor,lipState,eyeState,skinTexture,hairState,heldPropandmaterialtake 2. Every other key,animalincluded, takes 1. Extra ids are dropped. - Refused limits: more than 8 entries for one key, an id longer than 100 characters, more than 128 keys, or a key longer than 64 characters answer
400 validation_error. - Unknown keys are removed and unknown ids are skipped.
input_data.subjecton the job records exactly the ids that were used. - Subject clauses are part of your prose, ahead of the direction clauses and never in the
[style]section. Person becomes one clause and Styling another, and overlapping choices are written once. On the image route each choice adds its full clause; the video routes add the short term, because the start frame already shows who the subject is. - On
POST /v1/generate-characterandPOST /v1/generate-creature,subjectdescribes who the portrait shows. The wording goes right afterseedPromptordescriptionand the row's own person and wardrobe picks, and a pick both carry with the same wording is written once. These two routes use the still-image wording and never shorten the clauses. See Characters.
Generation sessions
A creation tool built on the API, such as an image, video or music maker, can keep its work in sessions inside your projects: a project holds sessions, and each session holds items. The same sessions show on every device, and your app stores nothing of its own.
# 1. Open a session, optionally inside one of your projects.
SESSION=$(curl -s -X POST "https://app.nodaro.ai/v1/sessions" \
-H "Authorization: Bearer $NODARO_API_KEY" -H "Content-Type: application/json" \
-d '{"projectId": "<project-id>", "tool": "create-image", "title": "Beach"}' | jq -r .data.id)
# 2. Generate inside it: send sessionId beside the usual settings.
curl -s -X POST "https://app.nodaro.ai/v1/generate-image" \
-H "Authorization: Bearer $NODARO_API_KEY" -H "Content-Type: application/json" \
-d "{\"prompt\": \"a red car\", \"sessionId\": \"$SESSION\", \"origin\": \"create-image\"}"
# 3. Read the session's feed, newest first.
curl -s "https://app.nodaro.ai/v1/sessions/$SESSION/items" -H "Authorization: Bearer $NODARO_API_KEY"| Method | Path | What it does |
|---|---|---|
POST | /v1/sessions | Create a session. Body: tool (required: lowercase letters, digits and dashes, up to 40 characters), projectId (optional, one of your projects) and title (optional, up to 200 characters). |
GET | /v1/sessions | Your sessions, newest first. Query: projectId (none for the sessions with no project), tool, includeArchived=true, limit (1 to 100, default 50) and cursor. Each session carries itemCount (its live items), removedCount (its items in the Trash), coverJobId and up to four previewUrls: the cover's image first when one is set, then the images of its newest completed items. |
GET | /v1/sessions/:id | One session. |
PATCH | /v1/sessions/:id | Change title (null means untitled), archived and coverJobId: one of the session's items to lead its previews (404 when it is not an item of this session), or null to go back to automatic. A session's project is fixed when it is created. |
DELETE | /v1/sessions/:id | Delete the session and its links. Its jobs are kept, and stay in every other session that holds them. |
GET | /v1/sessions/:id/items | The feed: every live item, made in the session or imported, newest first. With removed=true, the items removed from it instead, which are its part of the Trash, newest removal first. Each item has jobId, addedAt, removedAt (null when live), imported, importedFrom { sessionId, title }, and job in the shape GET /v1/jobs/:id returns. An item whose job you deleted is in neither list. Page with limit and cursor. |
POST | /v1/sessions/:id/items | Import { "jobIds": [...] }, 1 to 100 jobs, as links, never copies. When any id is not one of your jobs, or is deleted, nothing is imported (404). An item removed from the session comes back where it was. Answers { imported, requested }. |
DELETE | /v1/sessions/:id/items/:jobId | Remove one item from this session. It goes to the Trash and stays there until you restore it or delete it for good. Nothing is cleaned up automatically. Only the link moves: the job, and every other session that holds it, stay. ?forever=true deletes an item that is already removed for good, and the job still stays. On a live item it answers 409 not_in_trash. |
POST | /v1/sessions/:id/items/:jobId/restore | Bring a removed item back, where it was. |
POST | /v1/sessions/:id/items/move | Move { "jobIds": [...], "fromSessionId": "…" }, 1 to 100 items, into this session in one transaction. This session gets them, and the source session keeps them as removed items, so you undo the move from the source's Trash. When any id is not a live item of fromSessionId, nothing moves (404). Answers { moved }. |
- Every route that creates a job takes
sessionId, such asgenerate-image,generate-video,text-to-video,text-to-speechandgenerate-music. It must be one of your sessions, on the same side of the workspace line as the request. Otherwise the request answers404, and nothing is created or charged. Runs of a workflow are never filed in a session. - The job's
session_idis set once, when the job is created. It shows onGET /v1/jobs/:idandGET /v1/jobs. - Every route that creates a job also takes
origin, your app's slug. The job stores it ininput_data.origin, andGET /v1/jobs?origin=filters on it. A value that is not a slug answers400 validation_error, and nothing is created or charged. - To move work to another project, move the items into a session of that project with
POST /v1/sessions/:id/items/move. A session's project is fixed, so this is how an item changes project. To keep an item in both projects, import it instead. - Where a job is now.
GET /v1/jobstakesprojectId=<id>, the jobs a session of that project holds now, andprojectId=none, the jobs a session with no project holds. It also takesinSession=false, the jobs no session holds now (General), andinSession=true. Each job is listed once, however many sessions hold it.withSessions=trueaddssessions: [{ id, title, projectId }]to each job, for an "everything in this project" view.projectIdcannot be combined withinSession=false(400). These filters combine withtype,statusand the other filters. - The Trash. Removed items and deleted jobs both go to the Trash. See The Trash.
- Scopes: with an OAuth app token, reading sessions needs
jobs:read. Creating, editing, importing and removing needassets:write, and so does sendingsessionIdwith a generation. A personal API token limited to some workflows cannot reach these routes.
Run quotes
POST /v1/quote returns what a node run will reserve, before you start it. Send the node type and exactly the body you would send to its run route. The server prices it with the run route's own code: the same body validation, the same measuring of reference clips, the same credit id and the same arithmetic. The quote is therefore what the run reserves. Nothing is started and nothing is charged.
- Where: Nodaro Cloud only. The route needs authentication and allows 30 requests a minute.
- Which types:
text-to-video,generate-video,video-to-video,video-upscale,edit-image(every edit model) andsplit-into-layers. A Seedance 2.5 draft's 1080p finalize onvideo-upscaleis priced from the draft and the clips it was made with. Any other type is a400 quote_unsupportedthat lists them. - Seedance edits: a Seedance edit on
video-to-videois priced with every clip it is sent,videoUrlandreferenceVideoUrls. See Video to Video for the reference lists. - SDK:
client.nodes.quote(type, params).
{
"type": "text-to-video",
"params": {
"prompt": "edit {video:1} as follows: make it black and white",
"provider": "seedance-2-5",
"duration": -1,
"aspectRatio": "adaptive",
"referenceVideoUrls": ["https://…/clip.mp4"]
}
}The answer is { "data": { identifier, reserve, expected?, adjustments, warnings } }:
| Field | Meaning |
|---|---|
identifier | The credit id the run is charged under. |
reserve | The credits the run holds when it starts. |
expected | The credits the run should actually cost, when the measured inputs allow it. A Seedance reference-video run is reserved at its worst case: the input seconds plus the longer of the requested length and the longest clip. With Auto duration and every clip measured, it is the input seconds plus the longest clip's length, rounded up to a whole second, plus one second. A run that declares referenceTaskType: "reference" is reserved at the model's longest output instead. expected is the input plus the length it will most likely render. That is the source clip's length for an edit with Auto duration, and the requested seconds otherwise. It is never above reserve. |
adjustments | A list of { code, message, credits? }, why the price is what it is: ref_video_length (priced by the clips' lengths), extra_reference_images (MiniMax H3 images past the first 5), auto_duration, loop_trim_addon and voiced_audio_addon (with their credits), params_adjusted (a resolution or ratio snapped to one the model offers), output_size (an image upscale priced by the size of the picture it makes) and max_images (Split into Layers, priced as the most images the run can return). |
warnings | A list of { code, message }: settles_to_actual (reserve is the most the run can cost: it is charged for what it delivers, and the rest is refunded), ref_video_unmeasured (a clip could not be measured, so it is priced as the longest clip the model accepts) and image_size_unmeasured (a picture's size could not be read before the run). |
Without a settles_to_actual warning, reserve is what the run is charged when it completes.
- An invalid body gets the run route's own
400. A clip the run would refuse gets its400 video_too_long. A model this deployment does not offer gets the run's403 model_not_available, and a node it does not offer you gets the run's403 node_not_available. - A clip stored on Nodaro, such as an upload or a generated result, is remembered for 15 minutes once measured. Quoting and then running the same body measures it once.
- A quote checks the body and the price, not your balance or your plan. The run can still answer
402. A few checks only the run makes can still refuse a quoted body, for example a model that needs a start frame.
Nodaro Image Upscale (edit-image with provider: "nodaro-image-upscale") is priced by the size of the picture it makes. Its quote reads the picture you send, checks and measures it as the run does, stores nothing, and answers that size's exact price. The credit id names the output step, for example nodaro-image-upscale:2K for a 1024 × 1024 picture at upscaleFactor: "2". A picture whose size cannot be read is quoted at the hold, with the warnings image_size_unmeasured and settles_to_actual.
- A link to another site follows this deployment's link-import setting, and answers
403 link_imports_disabledwhen it is off. - A picture the run would refuse, such as one too large or unreadable, gets the run's own
400. - An outside picture has 15 seconds to arrive. Past that, the quote answers
503 input_timed_out: try again, or upload the picture. - The other edit models are quoted at their own price. The upscaler priced by its factor can cost more for a very large output, and an
output_sizeadjustment says so.
Split into Layers is quoted at its hold: the most images the run can return (maxLayers + 1, with the base) at the size's price. The run is charged only for the images it returns. So the quote carries the warning settles_to_actual, and a max_images adjustment that says how many images it holds.
{ "type": "split-into-layers", "params": { "imageUrl": "https://…/poster.png", "maxLayers": 5, "size": "1K" } }This quote's identifier is split-into-layers:1k:6: 5 layers and the base, at the price that 1K and 1.5K share.
Discover nodes
GET /v1/nodes lists every node type the server knows, and GET /v1/nodes/:type returns one. Both are public, need no token, and are cached for 5 minutes. An unknown type answers 404 not_found.
curl -s https://app.nodaro.ai/v1/nodes/generate-image | jq .dataconst { data: nodes } = await client.nodes.list()
const imageNodes = nodes.filter((n) => n.category === 'ai-image')
const { data: generateImage } = await client.nodes.get('generate-image')
console.log(generateImage.providers)nodaro nodes list --category ai-image
nodaro nodes get generate-image{
"data": {
"type": "generate-image",
"label": "Generate Image",
"category": "ai-image",
"description": "Generate an image from a text prompt using an AI provider.",
"outputType": "image",
"creditCost": "3-682",
"providers": ["nano-banana-pro", "gpt-image-2", "gpt-image-2-5-flare", "seedream-5-pro", "z-image"],
"capabilities": ["supports-reference-image", "supports-aspect-ratio"],
"inputSchema": {
"fields": [
{ "key": "prompt", "type": "text", "required": true },
{ "key": "provider", "type": "select", "options": ["nano-banana-pro", "gpt-image-2"] },
{ "key": "aspectRatio", "type": "select" },
{ "key": "promptPrefix", "type": "text" },
{ "key": "promptSuffix", "type": "text" }
]
}
}
}| Field | Meaning |
|---|---|
type | The node type. For most node types it is also the route: POST /v1/<type>. |
label, category, description | How the editor names and groups the node. |
outputType | text, image, video, audio, data or none. |
creditCost | The node's credit cost, or its range. It is the price a run is charged, the same figure as the node's Run button. 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: editions without credits leave it out. |
estimate | true when creditCost is an estimate: a metered LLM node's typical call on its default model. The run is charged its actual usage, so show the figure as ≈N. Choose Best lists a "0-N" range marked estimate: its free strategies up to its AI judge's typical call. Absent for a fixed price. A metered LLM node whose default model has no price yet lists creditCost: "unpriced", and a run on that model is refused. See Errors. |
creditRate | For a node charged per unit of its own measure, whose creditCost is a word rather than a number. Today that is Speaker Frames, with creditCost "per-1000-frames". The object is { identifier, unit, creditsPerUnit, minUnits, rounding, note }. creditsPerUnit is the exact price of one unit, not rounded. With rounding: "ceil-run-total", a run of N units (at least minUnits) is charged ceil(N × creditsPerUnit), rounded up once. Do not multiply the one-unit price from /v1/credits/model-cost. |
providers | The model ids the node accepts in provider. |
capabilities | Feature flags, such as supports-reference-image. |
inputSchema.fields | The node's settings, with their type, whether they are required, and their options. |
Every node that takes a prompt also lists promptPrefix and promptSuffix: text added before and after the prompt. See Prompt pre and post text. Nodes with per-model limits carry extra fields:
| Field | Meaning |
|---|---|
maxDurationSec | The longest duration the node accepts. |
sparseProviders | Models with only a few segment durations. A value between them snaps to the nearest one. |
providerResolutions | Each model's resolutions, for example { "minimax-h3": ["2K", "768P"] }. |
providerResolutionWire | The exact value to send for each resolution, for example 768P, not 768p, for MiniMax H3's cheaper tier. |
soundtrack | On Generate Video Pro: the server accepts the original-audio soundtrack input. |
The descriptor grows over time, so ignore fields you do not know. The same data drives the node reference.
Discover models
GET /v1/models returns the model catalog, grouped by kind and by maker. It is public and cached for 5 minutes, and it is the same data the MCP list_models tool returns.
A model this deployment does not offer is not listed. This includes a model an admin switched off. The same switch refuses a run of that model with 403 model_not_available. A client that keeps the list for the 5 minutes may still name such a model.
curl -s "https://app.nodaro.ai/v1/models?kind=video&mode=i2v" | jq '.totalModels'const catalog = await client.models.list({ kind: 'video', mode: 'i2v' })
for (const section of catalog.sections)
for (const family of section.families)
for (const m of family.models) console.log(m.id)nodaro models list --kind video --mode i2vThe response is { sections, recommendations, totalModels }. Every model names its maker and carries the maker's mark, except a model that the instance offers under its own label. Such a model names no maker: maker is absent and logoUrl is null.
| Field | Type | Meaning |
|---|---|---|
maker | { id, name }, absent on a model offered under the instance's own label | The company that made the model. id is stable, one per company, written in lowercase with hyphens, such as google, black-forest-labs or bytedance. Key your own assets on it, and handle an id you do not know, because a newer server can add makers. It is not always the family the model is grouped under. |
logoUrl | string or null | The maker's official mark as an SVG on this server's media CDN, or null when the server does not publish one. Draw a fallback then, such as the maker's name or initial. Use the address as given: it carries the file's version, so a replaced mark arrives under a new address. |
GET /v1/model-makers returns only the makers, for an app that draws marks and does not need the catalog. The answer is { makers: [{ id, name, logoUrl }] }, with every maker the platform knows. The fields are the same as above, so the two routes never disagree. The route is public and cached for 5 minutes, like GET /v1/models. It lists a maker even when none of its models is offered.
The MCP list_models tool carries maker but not logoUrl.
Each model also carries its capabilities: modes, features, aspectRatios, resolutions, qualities, backgrounds, durations, and the size rules and output formats below. It also carries its credit pricing per variant on Nodaro Cloud, short promptTips, and doctrineCovered, which is true only when a sourced prompting guide exists for the model's family. The pricing credits are the price a run is charged, the same figure as the Run button in the editor. A video model that takes a reference task type lists its values in referenceTaskTypes: ["reference", "edit", "extend"] on seedance-2-5, and absent on every other model.
A speech model, such as a Text to Speech or dialogue model, lists a second pricing row, <model>:per-100-chars, where speech is priced by length. That row is the price of each started 100 characters of the text sent, with a minimum of 8 units per run. A request is priced as a whole and rounded once, so N × the row's credits, with N = max(8, ceil(characters / 100)), is only an upper bound. The exact price is the identifier <model>:per-100-chars:<N>u, which POST /v1/credits/model-costs and GET /v1/credits/model-cost answer. Quote that figure rather than a product. Where the row is absent, every :<N>u identifier is absent too: quote the model's flat row. See Exact prices of runs priced per unit.
A few image models render fewer sizes at some aspect ratios, or a transparent background at one size only. Their entries say so with these fields, which are absent on every other model and on an older server:
| Field | Type | Meaning |
|---|---|---|
resolutionsByAspectRatio | { [ratio]: string[] } | The aspect ratios at which the model renders fewer sizes than resolutions, each with the sizes it renders there. A ratio not listed renders every entry of resolutions. A larger size runs, and is charged, at the largest size the ratio renders at or below it, and adjustments says so. |
transparentResolution | string | The one size a transparent background renders at. A transparent request at another size runs, and is charged, at this size. |
backgroundOnlyAtTransparentResolution | true | The model takes a background setting only at transparentResolution. At any other size, opaque renders as auto, reported in adjustments. |
GPT Image 2.5 (gpt-image-2-5-flare, gpt-image-2-5-sunburst and their -i2i edit models) renders 27:16, 16:27, 9:8 and 8:9 at 1K only, and a transparent background at 1K:
{
"id": "gpt-image-2-5-flare",
"aspectRatios": ["auto", "1:1", "16:9", "9:16", "27:16", "16:27", "9:8", "8:9"],
"resolutions": ["1K", "2K", "4K"],
"resolutionsByAspectRatio": { "27:16": ["1K"], "16:27": ["1K"], "9:8": ["1K"], "8:9": ["1K"] },
"backgrounds": ["auto", "opaque", "transparent"],
"transparentResolution": "1K",
"backgroundOnlyAtTransparentResolution": true
}The example shows only these fields, and not every ratio. A size menu offers resolutionsByAspectRatio[ratio] when the chosen ratio is listed there, and resolutions otherwise. GPT Image 2 (gpt-image-2 and gpt-image-2-i2i) lists no resolutionsByAspectRatio: it renders every ratio it offers, auto and 1:1 included, at 1K, 2K and 4K. GPT Image 2.6 Flare and Sunburst list transparentResolution: "1K" and no resolutionsByAspectRatio.
A model that writes more than one kind of file lists its formats. These fields are absent on every other model and on an older server:
| Field | Type | Meaning |
|---|---|---|
outputFormats | string[] | The files the model writes (png, jpeg, webp, gif, svg), its default first. These are the values outputFormat takes. |
gifFrameSize | number | With gif: the pixel size of every square frame. A GIF ignores aspectRatio and resolution. |
gifFrames | { min, max, default, values? } | With gif: the frames gifFrames takes, a whole number from min to max, or one of values where the model takes only some. |
gifFrameDurationMs | { min, max, step, default } | With gif: how long each frame shows, in milliseconds. |
svgLevels | string[] | With svg: the levels of detail svgLevel takes, the default first. |
{
"id": "gpt-image-2-6-flare",
"outputFormats": ["png", "jpeg", "webp", "gif", "svg"],
"gifFrameSize": 512,
"gifFrames": { "min": 2, "max": 16, "default": 9 },
"gifFrameDurationMs": { "min": 20, "max": 2000, "step": 10, "default": 200 },
"svgLevels": ["standard", "detailed", "max"]
}| Query | Values | Filters to |
|---|---|---|
kind | image, video or audio | One kind of media |
mode | For example t2i, i2v, t2v, tts, video-analysis | One operation |
family | A maker, for example Google or Bytedance | One maker |
featuredOnly | true | Featured models |
The model pages show the same catalog.
What this deployment offers you
GET /v1/surface/availability says what an app should show the person who is signed in: the node types, models, Web Scrape sources and worlds this deployment offers them. Unlike GET /v1/nodes and GET /v1/models, the answer depends on who asks, so it is sent with Cache-Control: private, no-store. A token is optional, and signed out the answer is a user's. No scope is needed.
{
"nodes": { "denied": ["social-publish"], "hiddenFromUsers": [] },
"models": { "denied": ["flux-flex"], "restricted": true },
"webScrapeSources": { "denied": [], "hiddenFromUsers": [] },
"worlds": [
{ "id": "image", "enabled": true },
{ "id": "video", "enabled": false }
]
}| Field | Meaning |
|---|---|
nodes.denied | Node types you are not offered. Saving or running one is refused with 403 node_not_available. |
nodes.hiddenFromUsers | Empty for anyone but an administrator: the node types an administrator can use and the deployment's users cannot. |
models.denied | Model ids nobody on this deployment is offered. A run of one answers 403 model_not_available. |
models.restricted | true when the deployment restricts which models it offers. A model an administrator only switched off is listed in models.denied without making this true. Where it is true, the workflow editor starts a new node on a model its menu offers. A server that predates the field leaves it out: read that as false. |
webScrapeSources.denied, webScrapeSources.hiddenFromUsers | Web Scrape sources withdrawn with the node that governs them, in the same two views. The Instagram source follows the Instagram node. |
worlds | Every world of the shared site header, in the deployment's display order: explore, image, video, music, trends, motion, voice, app (the workflow editor) and studio. A newer server may add a world, so handle an id you do not know. |
worlds[].enabled | true when the deployment lets the world through and every node type the world runs is available to you. image runs generate-image, video runs generate-video and text-to-video, music runs suno-generate, trends and motion run cast-swap, and voice runs voice-changer-pro. explore, app and studio run no node of their own. |
worlds reports the deployment's availability settings only. Whether the edition serves a node at all is decided by the server. The table of which world runs which node is published as WORLD_REQUIRED_NODE_TYPES in @nodaro/shared/runtime-config. In the SDK it is client.surface.availability().
Models with a content filter forced on
GET /v1/provider-safety/locks answers { "forcedOn": ["seedance-2"] }: the models this deployment forces the content filter on for. A client that offers a filter choice for one of these models should show it on and locked. The list is empty when no filter is forced.
Discover picker values
The Creative Controls pickers have public catalogs of valid ids, the values direction and subject take:
| Method | Path | What it returns |
|---|---|---|
GET | /v1/picker-catalogs | Every picker: nodeType, label, kind, its field or fields, optionCount and imageCount. imageCount is 0 for every picker on a deployment that turned pictures off. |
GET | /v1/picker-catalogs/:nodeType | One picker's options. ?detail=full adds each option's description and promptHint (for a style option, its image wording), ?category= filters a single-field picker, and ?field= returns one field of a multi-field picker. |
GET | /v1/catalogs | Every catalog in one call, as the deployment curated it. data is present when the deployment registered catalog packs, or shows its own picker pictures. |
POST | /v1/text-to-picker | "AI Fill": picks ids for many pickers from a free-text scene description. Without llmModel, it runs Gemini 3.8 Flash. It is charged for the tokens its calls use, at the model's rate. See Picker catalogs. |
Every option carries id, label and term, the short phrase to use in a prompt, plus imageUrl when the option has a picture. A deployment can turn the pictures off with catalogs.images in its surface profile. Its replies then carry no imageUrl or imageSrcSet on any option. A deployment that shows its own pictures may add imageLargeUrl, an enlarged picture for where a picker shows the option big. See Pictures. The catalogs also ship as data in the @nodaro/prompts npm package. Read Picker catalogs for the full shapes. From the terminal: nodaro pickers list, nodaro pickers get mood --full and nodaro pickers analyze "<text>".
Structured LLM output
POST /v1/llm/structured runs one language-model call whose answer is forced into a JSON Schema you supply, validated, and returned as an object. The call is charged for the tokens it actually used, at the model's rate on the lane that served it. The tokens include your system prompt, input and schema, every validation re-ask and lane fallback, and the answer. See How LLM calls are charged.
- Before the call, a hold is reserved: the first attempt at the model's output cap. A balance below the hold is refused with
402("Needs up to N credits"). - After the call, the call is charged what it used. This is usually less than the hold.
- The quote for a typical call comes from
POST /v1/credits/model-costswith the keyllm-structured:<model>. It is an estimate, so show it as≈N. See Credits. - Failures: a failure the provider billed is charged for that usage, and the error says how much. Examples are an answer that never validated after its retries, a refusal, or an answer cut off at the cap. A failure the provider did not bill is refunded.
Claude Fable 5 is offered on this route.
{
"system": "You write production plans.",
"input": "A rainy chase through Rome.",
"jsonSchema": {
"type": "object",
"properties": { "title": { "type": "string" } },
"required": ["title"]
}
}The answer is { jobId, output, usage: { inputTokens, outputTokens } }, where output has your schema's shape.
- Fields:
system,input,jsonSchema, and optionallyschemaName(up to 64 characters),llmModel,reasoningEffort,maxRetries,origin,advancedMode,temperatureandmaxTokens.systemtakes up to 200,000 characters, so it has room for a vocabulary your schema writes in.inputtakes 1 to 100,000 characters. - Model: without
llmModel, the call runs on Gemini 3.6 Flash. - Schema: the root must be a plain object schema, up to 64 KB and 20 levels deep. It may use
properties,required,additionalProperties,items, the basic types,enum,const, the numeric and length bounds,multipleOf,exclusiveMinimumanddescription.anyOfandoneOfare accepted below the root only.not,if,then,else, thedependentkeywords, external$ref, and combinators at the root answer400. AnanyOfofrequiredbranches below the root is accepted but not enforced, so check rules across fields yourself. - Retries:
maxRetries, from 0 to 3 with a default of 2, is how many times an invalid answer goes back to the model with its validation error. - Sampling:
maxTokensapplies on every call and may not exceed the model's own limit.temperatureis ignored unless you also sendadvancedMode: true, which runs the call on the model maker's own API, at that lane's rate. - Duration: the call is synchronous and may run several minutes. Each attempt may take up to 240 seconds on each of two lanes, so the worst case is 24 minutes at the default
maxRetriesand 32 minutes at the maximum. Raise your HTTP client's timeout, or use the job form below. The SDK's defaulttimeoutMsof 60 seconds is too short. - Errors:
400 validation_error,400 advanced_mode_unavailable,401,402,500 internal_error,502 llm_erroronce the retries are spent,502 advanced_mode_unavailable, and503 provider_unavailable. SeeadvancedModeabove for theadvanced_mode_unavailablerefusal. Two more codes concern the hold:409 job_cancelledand503 job_start_failed. See Errors.
In the SDK, call client.llm.structured(body).
As a job
POST /v1/llm/structured/jobs takes the same body and answers { jobId } at once. Poll GET /v1/jobs/:id/status: on completed, output_data is { output, inputTokens, outputTokens }, and on failed, error_message says why. Find your drafts again with GET /v1/jobs?type=llm-structured&origin=<your app>. Three extra fields are accepted:
label: a display name for the job, up to 120 characters.videoUrl: draft from a video. The video is analyzed first, as a separate job you also own, at the video analysis price, and the analysis is added to yourinput. While it runs,output_data.stageisanalyzing, thendrafting.videoAnalysis:{ llmModel?, selectionMode? }for that analysis.
The draft's hold is reserved when the job is created, under llm-structured:<model> (with :direct on the provider's own API). The job is then charged what the draft used. Cancelling the draft with POST /v1/jobs/:id/cancel also cancels a running analysis. A refused analysis releases every credit reserved for the draft. On an install that sends its language-model calls to nodaro.ai, the job form answers 503 provider_unavailable: use the synchronous call there. In the SDK, call client.llm.structuredJob(body).
Frequently asked questions
How do I generate an image with the Nodaro API?
Send POST /v1/generate-image with a prompt and a provider. The response carries a jobId. Poll GET /v1/jobs/:id/status until the status is completed, then read output_data.imageUrl. In the SDK, client.nodes.runAndWait does both steps.
Which nodes can I run from the API?
Every node type that GET /v1/nodes lists. The route is POST /v1/ followed by the node type, with the node's settings as the JSON body. A few text nodes use a longer path, such as /v1/llm-chat/generate.
What is the direction object?
A flat map of picker ids, such as { "shotSize": "wide-shot", "timeOfDay": "golden-hour" }, that you send with an image or video generation. Nodaro writes its own tested wording for each id into the prompt. Valid ids come from GET /v1/picker-catalogs.
How do I keep a character consistent through the API?
Send the character's picture in connectedReferences with source wired-character and the character's name as defaultName. The route attaches the picture and writes the identity instructions into the prompt, the same way the editor does.
Do the discovery endpoints need a token?
No. GET /v1/nodes, GET /v1/models, GET /v1/picker-catalogs and GET /v1/catalogs are public and cached for 5 minutes. They expose only catalog data, never anything about your account.
What happens when my prompt plus the direction ids is too long for the model?
Nodaro drops direction clauses first, one at a time from the end, until the prompt fits. Subject clauses go last, and your own prose and your references are never dropped to make room for them.
Related
Jobs
Workflows
Picker catalogs
Generate Image
AI models in Nodaro
Last updated on
Workflows
Run saved Nodaro workflows from the API with new input values, wait for the result or poll for it, and list, create, update, export and move workflows.
Jobs
Poll Nodaro jobs for status and results, filter your job list, rate and favorite results, read image sizes, check 100 jobs in one call and cancel jobs.