NodaroAccount
Nodaro Docs
DocumentationNode ReferenceModelsTemplatesAI Agents (MCP)DevelopersSelf-hostingResearchCompare
REST API

Objects

Script props and products over REST: create and archive objects, generate main images, material and angle variants and motion clips, and approve the main image.

The Objects API scripts everything Object Studio does for props and products. You create an object, generate candidate main images, approve one, and add angle, material and variation variants and motion clips. Image and video nodes then reuse the object, so the same lantern, car or chair looks the same in every shot.

The routes work on every edition and take a bearer token: a personal API token (ndr_…), an OAuth app token (ndr_app_…), or your session token on Community edition. Every route is scoped to the caller. See Authentication.

Endpoints

MethodPathWhat it does
GET/v1/objectsList your objects.
GET/v1/objects/:idGet one object with its jobs in progress.
POST/v1/objectsCreate an object, or update one when the body has an id.
DELETE/v1/objects/:idArchive an object. It can be restored.
DELETE/v1/objects/:id?permanent=trueDelete an archived object and its files for good.
POST/v1/objects/:id/restoreRestore an archived object.
POST/v1/generate-objectGenerate candidate main images.
POST/v1/generate-object-assetGenerate one angle, material, variation or custom variant.
POST/v1/generate-object-motionAnimate the main image into a motion clip.
POST/v1/objects/:id/approve-main-imageApprove a candidate as the main image and write the object's description.
POST/v1/objects/:id/llm-captionWrite the description again from the current main image.

What an object holds

FieldWhat it holds
id, name, descriptionThe identifier, the display name and identity notes.
categoryfurniture, vehicle, weapon, food, clothing, electronics, nature, tool, animal or other.
stylerealistic, anime, 3d-pixar or illustration.
sourceImageUrlThe anchor main image, set when you approve a candidate.
canonicalDescriptionA visual description of about 80 to 120 words, written by Nodaro when the main image is approved. It is an empty string until then.
styleLockWhether variants are generated from the main image. true by default.
angles, materials, variations, motionClipsThe asset buckets. Each entry is { name, url }; motionClips holds videos.
referencePhotosUp to 20 mood-board photos, each { kind, url }.
pendingJobsOn GET /v1/objects/:id only: the variant jobs still running for this object.

The asset buckets

BucketWhat it showsPreset variants
anglesThe object from another viewpointfront, side, top, back, three-quarter, detail, in-context, exploded, perspective
materialsThe object in another materialwood, metal, glass, plastic, fabric, stone, ceramic, leather, paper, gold, silver, copper, marble
variationsAnother condition or styleclean, weathered, damaged, ornate, minimal, broken, antique, futuristic, holographic, dirty, polished
motionClipsLooping camera-move clipsrotate-360, hover, spin-slow, parallax, pulse, drift, dolly-around, push-in, drone-orbit

Reference photos

A mood board travels with the object. Every node that uses the object receives these photos as extra references, even without a wired image, and each photo's kind tells the model what it is for.

kindWhat it is for
frontA clean front view.
sideA side profile, useful for vehicles, furniture and weapons.
detailA close-up of a defining feature, such as an engraving or a hinge.
contextThe object in place, held or mounted, for scale.
moodBoardThe palette or the feel.
otherAnything else.

You can add up to 20 photos, and any number of each kind. For a hero prop or a signature product, add three to six photos before the first generation: the first results are then much more faithful.

List objects

GET /v1/objects returns your active objects, newest first. Add archived=true for the archive, or projectId for one project. Add q to keep only the objects whose name or description contains every word of it, ignoring case: the same search as characters. The MCP list_objects tool takes the same search as search. Without limit, the route returns the full list. With limit (at most 500), it returns one page and a nextCursor; pass it back as cursor until it is null.

curl "https://app.nodaro.ai/v1/objects?limit=100" \
  -H "Authorization: Bearer $NODARO_API_KEY"
import { createClient, StaticTokenAuth } from '@nodaro/sdk'

const client = createClient({
  baseUrl: 'https://app.nodaro.ai',
  auth: new StaticTokenAuth(process.env.NODARO_API_KEY!),
})

const page = await client.objects.list({ limit: 100 })
const next = await client.objects.list({ limit: 100, cursor: page.nextCursor! })
const { objects: archived } = await client.objects.listArchived()
nodaro objects list --json
nodaro objects list --archived

GET /v1/objects/:id returns one object with pendingJobs. An archived object returns 404 not_found, the same answer as an object that does not exist. Each row, in the list and by id, also carries its Nodaro Keepers shelf state: catalog, catalogSort and catalogCategory. They are false, 0 and null on your own rows and on every copy.

Create or update an object

POST /v1/objects creates an object when the body has no id, and updates it when it has one. A create needs nodeId and name; use any label for nodeId, such as "scripted", when there is no canvas node.

On an update, only the fields you send are written. The asset buckets are never written by an update, so a save cannot overwrite a variant that a job is adding. Send expectedUpdatedAt, the object's current updatedAt, to refuse the update when someone changed the object since you read it: the route then answers 409 concurrent_modification.

curl -X POST https://app.nodaro.ai/v1/objects \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "nodeId": "scripted",
    "name": "Antique Lantern",
    "description": "Weathered brass lantern with hand-engraved filigree",
    "category": "tool",
    "style": "realistic"
  }'
const { id } = await client.objects.create({
  nodeId: 'scripted',
  name: 'Antique Lantern',
  description: 'Weathered brass lantern with hand-engraved filigree',
  category: 'tool',
  style: 'realistic',
})

const object = await client.objects.get(id)
await client.objects.update(id, { styleLock: false, expectedUpdatedAt: object.updatedAt })
nodaro objects create "Antique Lantern" --node-id scripted \
  --description "Weathered brass lantern with hand-engraved filigree" \
  --category tool --style realistic

nodaro objects update <id> --style-lock false

A create returns { id }. An update returns { id, updatedAt }.

Prop

Type

What Style Lock changes

  • On (default). Every angle, material and variation is generated from the approved main image. The variant keeps the proportions, the silhouette and the defining details. Nodes that use the object also receive its canonical description. Use it for anything that must look like the same item in every shot.
  • Off. Variants are generated from text only. Nodes still receive the canonical description, but only as guidance, so the model may reinterpret the design. Use it to explore alternatives or compare looks.

Generate candidate main images

POST /v1/generate-object starts one job per candidate and returns jobIds at once, one id per candidate. A single-candidate request also returns jobId. Poll the jobs with the Jobs API.

With attachToObjectId and a count of 1, the result becomes the object's main image when the job completes. With several candidates nothing is attached; approve the one you prefer.

curl -X POST https://app.nodaro.ai/v1/generate-object \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Antique Lantern", "count": 4 }'
const { jobIds } = await client.objects.generate({
  name: 'Antique Lantern',
  description: 'Weathered brass lantern with hand-engraved filigree',
  count: 4,
})
nodaro objects generate --name "Antique Lantern" --count 1 \
  --attach-to-object-id <id> --watch
{
  "jobIds": [
    "5e2a8c1f-3b7d-4f9a-a6c2-8d1e4b7f0a3c",
    "6f3b9d2a-4c8e-4a1b-b7d3-9e2f5c8a1b4d",
    "7a4c1e3b-5d9f-4b2c-c8e4-1f3a6d9b2c5e",
    "8b5d2f4c-6e1a-4c3d-d9f5-2a4b7e1c3d6f"
  ]
}
FieldWhat it does
nameRequired. The object name, 1 to 200 characters.
descriptionIdentity notes, up to 2,000 characters.
category, styleThe object's category and visual style.
countCandidates to generate, 1 to 10. The default is 1.
providerThe image model id. Omit it for the default model.
seedPromptHintA prompt fragment to fold into the prompt, up to 2,000 characters, for example antique brass from the Material picker.
sourceImageUrlAn image to start from.
aspectRatioThe frame of the main image.
attachToObjectId, expectedUpdatedAtAttach a single candidate to this object, optionally only when the object is unchanged.

seedPromptHint is also accepted by generate-object-asset and generate-object-motion. It lets you compose a catalog choice, such as a vehicle or a material, into the prompt without wiring a picker node.

Generate a variant

POST /v1/generate-object-asset generates one variant and returns { jobId }. Send attachToObjectId, attachToColumn and attachName to append { name: attachName, url } to the bucket when the job completes.

curl -X POST https://app.nodaro.ai/v1/generate-object-asset \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Antique Lantern",
    "assetType": "materials",
    "variant": "gold",
    "attachToObjectId": "2c7e9a4b-1d3f-4e8a-9b6c-5f0d2a7e3b1c",
    "attachToColumn": "materials",
    "attachName": "gold"
  }'
await client.objects.generateAsset({
  name: 'Antique Lantern',
  assetType: 'variations',
  variant: 'weathered',
  attachToObjectId: id,
  attachToColumn: 'variations',
  attachName: 'weathered',
})
nodaro objects generate-asset --asset-type materials --variant gold \
  --attach-to-object-id <id> --attach-to-column materials --watch
FieldWhat it does
nameRequired. The object name.
assetTypeRequired. angles, materials, variations or custom.
variantRequired. The variant to generate, 1 to 100 characters.
descriptionA description of this variant, up to 1,000 characters. When you attach to an object and omit it, Nodaro drafts one from the object's canonical description and the variant name. Send your own to skip the draft.
provider, sourceImageUrl, aspectRatioThe image model, the image to vary and the frame.
attachToObjectId, attachToColumn, attachNameWhere the result goes. attachToColumn is angles, materials or variations, and a custom variant must name it.

Animate the main image

POST /v1/generate-object-motion turns an image of the object into a short camera-move clip, such as a slow rotation, a hover or a drone orbit. Use the clips as B-roll or as the start of a longer video. The route returns { jobId }.

sourceImageUrl is required; there is no fallback, so pass the approved main image. With attachToObjectId and attachName, the clip is appended to motionClips when it completes; you do not send a column.

curl -X POST https://app.nodaro.ai/v1/generate-object-motion \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Antique Lantern",
    "motionPrompt": "slow 360 rotation, soft golden rim light",
    "sourceImageUrl": "https://cdn.nodaro.ai/objects/lantern-main.png",
    "provider": "kling-turbo",
    "attachToObjectId": "2c7e9a4b-1d3f-4e8a-9b6c-5f0d2a7e3b1c",
    "attachName": "rotate-360"
  }'
const lantern = await client.objects.get(id)

await client.objects.generateMotion({
  name: 'Antique Lantern',
  motionPrompt: 'slow 360 rotation, soft golden rim light',
  sourceImageUrl: lantern.sourceImageUrl!,
  provider: 'kling-turbo',
  attachToObjectId: id,
  attachName: 'rotate-360',
})
nodaro objects generate-motion --name "Antique Lantern" \
  --motion-prompt "slow 360 rotation, soft golden rim light" \
  --source-image-url "https://cdn.nodaro.ai/objects/lantern-main.png" \
  --provider kling-turbo --attach-to-object-id <id> --attach-name "rotate-360" --watch
FieldWhat it does
name, motionPromptRequired. The object name and the movement to create.
sourceImageUrlRequired. The start frame.
providerkling-turbo (the default), kling, kling-3.0, hailuo-2.3, wan-i2v or seedance-2-mini.
aspectRatio1:1 (the default, a centered product frame), 3:4, 16:9, 9:16 or 4:3.
refineFromVideoUrlAn existing clip to refine with the new prompt, instead of starting again from the image. The composition stays; use a model that supports video-to-video, such as wan-i2v.
attachToObjectId, attachNameThe object and the clip's name in motionClips.

The price of a clip is the model's image-to-video price:

ModelMakerModesCreditsDetails
Kling 2.5 Turbo ProKuaishouImage to video, Text to videofrom 121Faster Kling — good quality at lower cost. Supports end frame.
Kling 2.6KuaishouImage to video, Text to videofrom 152Kling 2.6 I2V — strong motion realism. 5s/10s, optional native audio.
Kling 3.0KuaishouImage to video, Text to videofrom 297Premium Kling 3.0 — variable 3-15s duration, native audio, 720P/1080P.
Hailuo 2.3 StandardMiniMaxImage to videofrom 83Cheaper Hailuo 2.3 tier — good baseline quality.
Wan 2.6 I2VAlibabaImage to videofrom 193Wan 2.6 image-to-video — 5/10/15s at 720p/1080p.
Seedance 2 MiniBytedanceImage to video, Text to videofrom 132Budget Seedance 2 tier — 480p/720p only, per-second pricing by resolution.

seedance-2-mini is Seedance 2 Mini. Seedance 1.5 was retired in October 2026: a request that still names seedance runs, and is charged, as seedance-2-mini.

Approve a main image

POST /v1/objects/:id/approve-main-image sets a completed candidate as the main image and, in the same call, writes canonicalDescription: the text that later prompts use to describe the object. The body is { candidateJobId, expectedUpdatedAt? }, and the candidate must be a completed job that belongs to you.

The route returns { sourceImageUrl, canonicalDescription }. When writing the description fails, the main image is still set and canonicalDescription is an empty string; the SDK returns null instead. Call POST /v1/objects/:id/llm-caption to try again. That route returns { canonicalDescription }, answers 502 when the description cannot be written, and 400 main_image_required when there is no main image yet. It does not take expectedUpdatedAt: it is always safe to repeat. The approval itself is free. The description is a Claude Sonnet 4.6 vision call, charged for the tokens it used at that model's rate. See How LLM calls are charged. An approval is never refused for credits. With too little balance for the description, the image is still approved, the description is skipped, and llm-caption can write it later. A description the provider billed is charged even when it comes back empty. One the provider did not bill is refunded.

curl -X POST https://app.nodaro.ai/v1/objects/2c7e9a4b-1d3f-4e8a-9b6c-5f0d2a7e3b1c/approve-main-image \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "candidateJobId": "6f3b9d2a-4c8e-4a1b-b7d3-9e2f5c8a1b4d" }'
const approved = await client.objects.approveMainImage(id, jobIds[1])
if (approved.canonicalDescription === null) {
  await client.objects.recaption(id)
}
nodaro objects approve-main-image <id> --candidate-job-id <jobId>
nodaro objects recaption <id>

approve-main-image and llm-caption both change the object. With an OAuth app token, they need the assets:write scope. A token without it gets 403 insufficient_scope. Your session token and personal API tokens need no scope.

Archive, restore and delete an object

ActioncurlTypeScript SDKCLI
ArchiveDELETE /v1/objects/:idclient.objects.delete(id)nodaro objects delete <id>
RestorePOST /v1/objects/:id/restoreclient.objects.restore(id)nodaro objects restore <id>
Delete for goodDELETE /v1/objects/:id?permanent=trueclient.objects.permanentDelete(id)nodaro objects delete <id> --permanent
  • Archive returns { success: true, archived: true }. Archiving an archived object changes nothing.
  • Restore returns { id, name }. When an active object has the same name, ignoring case, Nodaro adds a (restored) suffix and returns the new name.
  • Delete for good returns { success: true, permanent: true } and removes the object and every file it references that is yours: the main image, the variants, the clips and the reference photos. A file that another user's generation made, or that another user's upload folder or library holds, stays in storage. That file never counted against your storage quota, so nothing is credited back for it. Delete for good works only on an archived object; an active one returns 400 not_archived. Archive first, then delete.

Pick a variant when you run an app

When a workflow with an Object/Props Asset node is published as an app, the object becomes one of the app's inputs. Pass "<bucket>/<variant>" to use a variant as the object's main image for that run, for example "materials/gold". Write the variant name in lowercase with hyphens for spaces: polished-brass matches a variant named Polished Brass. An unknown bucket or variant falls back to the main image. See Workflows for running apps.

Use the object in other generations

Pass the object's asset URLs as reference images to Generate Image or Generate Video; explicit URLs are the simplest choice for code. In a workflow, wire the object node into the image node, or mention a variant in the prompt, for example @lantern:1:materials/gold for an object named Lantern. When you wire an object without a mention, Nodaro also looks for variant names in your prompt: "gold finish" selects materials/gold. See Objects.

Use it from MCP

ToolWhat it does
list_objects, get_objectFind an object and read its variant URLs.
generate_objectGenerate a main image or a variant.
approve_object_main_image, recaption_objectApprove a main image, or write its description again.
generate_object_motionAnimate the main image.

There are no MCP tools to create, update, archive, restore or delete objects: these changes go through REST, the SDK or the CLI. See the MCP tools reference.

Credits

RoutePrice on Nodaro Cloud
POST /v1/generate-objectThe image model's price times count, reserved for every candidate before the first job starts.
POST /v1/generate-object-assetThe image model's price, per variant.
POST /v1/generate-object-motionThe video model's image-to-video price, per clip.
approve-main-imageThe approval is free. Its description is a vision call, charged for the tokens it used.
llm-captionThe same vision call, charged the same way.

Errors

StatusCodeMeaning
400validation_errorA field is missing or invalid.
400not_archivedA permanent delete was sent for an object that is not archived.
400main_image_requiredllm-caption was called before the object has a main image.
401unauthorizedThe token is missing, invalid or revoked.
402insufficient_creditsNodaro Cloud only. The account cannot cover the reservation.
404not_foundNo active object with that id belongs to you.
409concurrent_modificationexpectedUpdatedAt no longer matches. Read the object again, merge and retry.
502—The canonical description could not be written. Try again.

Frequently asked questions

What is an object in the Nodaro API?

An object is a saved prop, product, vehicle or piece of furniture with an approved main image, a written description and variant images. Image and video nodes reuse it so the item looks the same in every shot.

What does Style Lock do?

With Style Lock on, the default, every variant is generated from the approved main image, so proportions and details stay the same. Turn it off to let variants reinterpret the design, for example to explore alternatives.

Can I permanently delete an object through the API?

Yes. Archive it first with DELETE /v1/objects/:id, then call DELETE /v1/objects/:id?permanent=true. The permanent delete removes the object and its files, and it refuses an object that is not archived.

Which models can animate an object?

Six video models, by provider id kling-turbo (the default, Kling 2.5 Turbo Pro), kling, kling-3.0, hailuo-2.3, wan-i2v and seedance-2-mini. Each clip costs that model's image-to-video price.

Why does GET /v1/objects/:id return 404 for an object I archived?

Archived objects are hidden from reads by id. List them with GET /v1/objects?archived=true and restore one with POST /v1/objects/:id/restore.

Last updated on

On this page