# 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.

Source: https://nodaro.ai/docs/developers/api/objects

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](https://nodaro.ai/docs/developers/api/authentication).

## Endpoints

| Method | Path | What it does |
| --- | --- | --- |
| `GET` | `/v1/objects` | List your objects. |
| `GET` | `/v1/objects/:id` | Get one object with its jobs in progress. |
| `POST` | `/v1/objects` | Create an object, or update one when the body has an `id`. |
| `DELETE` | `/v1/objects/:id` | Archive an object. It can be restored. |
| `DELETE` | `/v1/objects/:id?permanent=true` | Delete an archived object and its files for good. |
| `POST` | `/v1/objects/:id/restore` | Restore an archived object. |
| `POST` | `/v1/generate-object` | Generate candidate main images. |
| `POST` | `/v1/generate-object-asset` | Generate one angle, material, variation or custom variant. |
| `POST` | `/v1/generate-object-motion` | Animate the main image into a motion clip. |
| `POST` | `/v1/objects/:id/approve-main-image` | Approve a candidate as the main image and write the object's description. |
| `POST` | `/v1/objects/:id/llm-caption` | Write the description again from the current main image. |

## What an object holds

| Field | What it holds |
| --- | --- |
| `id`, `name`, `description` | The identifier, the display name and identity notes. |
| `category` | `furniture`, `vehicle`, `weapon`, `food`, `clothing`, `electronics`, `nature`, `tool`, `animal` or `other`. |
| `style` | `realistic`, `anime`, `3d-pixar` or `illustration`. |
| `sourceImageUrl` | The anchor main image, set when you approve a candidate. |
| `canonicalDescription` | A visual description of about 80 to 120 words, written by Nodaro when the main image is approved. It is an empty string until then. |
| `styleLock` | Whether variants are generated from the main image. `true` by default. |
| `angles`, `materials`, `variations`, `motionClips` | The asset buckets. Each entry is `{ name, url }`; `motionClips` holds videos. |
| `referencePhotos` | Up to 20 mood-board photos, each `{ kind, url }`. |
| `pendingJobs` | On `GET /v1/objects/:id` only: the variant jobs still running for this object. |

### The asset buckets

| Bucket | What it shows | Preset variants |
| --- | --- | --- |
| `angles` | The object from another viewpoint | front, side, top, back, three-quarter, detail, in-context, exploded, perspective |
| `materials` | The object in another material | wood, metal, glass, plastic, fabric, stone, ceramic, leather, paper, gold, silver, copper, marble |
| `variations` | Another condition or style | clean, weathered, damaged, ornate, minimal, broken, antique, futuristic, holographic, dirty, polished |
| `motionClips` | Looping camera-move clips | rotate-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.

| `kind` | What it is for |
| --- | --- |
| `front` | A clean front view. |
| `side` | A side profile, useful for vehicles, furniture and weapons. |
| `detail` | A close-up of a defining feature, such as an engraving or a hinge. |
| `context` | The object in place, held or mounted, for scale. |
| `moodBoard` | The palette or the feel. |
| `other` | Anything 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. 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**

```bash
curl "https://app.nodaro.ai/v1/objects?limit=100" \
  -H "Authorization: Bearer $NODARO_API_KEY"
```

**TypeScript SDK**

```ts

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()
```

**CLI**

```bash
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.

## 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**

```bash
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"
}'
```

**TypeScript SDK**

```ts
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 })
```

**CLI**

```bash
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 }`.

<TypeTable
type={{
id: { type: 'string (uuid)', description: 'The object to update. Omit it to create an object.' },
nodeId: { type: 'string', description: 'Required on create. The canvas node the object belongs to, or any label when there is none.' },
name: { type: 'string', description: 'Required on create.' },
description: { type: 'string', description: 'What makes the object distinctive, in one to three sentences.' },
category: { type: 'string', description: 'furniture, vehicle, weapon, food, clothing, electronics, nature, tool, animal or other.' },
style: { type: 'string', description: 'realistic, anime, 3d-pixar or illustration.' },
styleLock: { type: 'boolean', description: 'Generate variants from the approved main image.', default: 'true' },
referencePhotos: { type: 'array', description: 'Up to 20 { kind, url } mood-board photos.' },
canonicalDescription: { type: 'string', description: 'Replace the written description.' },
sourceImageUrl: { type: 'string', description: 'Set the main image directly.' },
projectId: { type: 'string (uuid)', description: 'The project to file the object in.' },
expectedUpdatedAt: { type: 'string (ISO 8601)', description: 'On update: refuse the write with 409 when the object changed since this timestamp.' },
}}
/>

### 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](https://nodaro.ai/docs/developers/api/jobs).

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**

```bash
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 }'
```

**TypeScript SDK**

```ts
const { jobIds } = await client.objects.generate({
name: 'Antique Lantern',
description: 'Weathered brass lantern with hand-engraved filigree',
count: 4,
})
```

**CLI**

```bash
nodaro objects generate --name "Antique Lantern" --count 1 \
  --attach-to-object-id <id> --watch
```

```json
{
"jobIds": [
"5e2a8c1f-3b7d-4f9a-a6c2-8d1e4b7f0a3c",
"6f3b9d2a-4c8e-4a1b-b7d3-9e2f5c8a1b4d",
"7a4c1e3b-5d9f-4b2c-c8e4-1f3a6d9b2c5e",
"8b5d2f4c-6e1a-4c3d-d9f5-2a4b7e1c3d6f"
]
}
```

| Field | What it does |
| --- | --- |
| `name` | Required. The object name, 1 to 200 characters. |
| `description` | Identity notes, up to 2,000 characters. |
| `category`, `style` | The object's category and visual style. |
| `count` | Candidates to generate, 1 to 10. The default is 1. |
| `provider` | The image model id. Omit it for the default model. |
| `seedPromptHint` | A prompt fragment to fold into the prompt, up to 2,000 characters, for example `antique brass` from the [Material](https://nodaro.ai/docs/nodes/creative-controls/material) picker. |
| `sourceImageUrl` | An image to start from. |
| `aspectRatio` | The frame of the main image. |
| `attachToObjectId`, `expectedUpdatedAt` | Attach 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**

```bash
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"
}'
```

**TypeScript SDK**

```ts
await client.objects.generateAsset({
name: 'Antique Lantern',
assetType: 'variations',
variant: 'weathered',
attachToObjectId: id,
attachToColumn: 'variations',
attachName: 'weathered',
})
```

**CLI**

```bash
nodaro objects generate-asset --asset-type materials --variant gold \
  --attach-to-object-id <id> --attach-to-column materials --watch
```

| Field | What it does |
| --- | --- |
| `name` | Required. The object name. |
| `assetType` | Required. `angles`, `materials`, `variations` or `custom`. |
| `variant` | Required. The variant to generate, 1 to 100 characters. |
| `description` | A 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`, `aspectRatio` | The image model, the image to vary and the frame. |
| `attachToObjectId`, `attachToColumn`, `attachName` | Where 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**

```bash
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"
}'
```

**TypeScript SDK**

```ts
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',
})
```

**CLI**

```bash
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
```

| Field | What it does |
| --- | --- |
| `name`, `motionPrompt` | Required. The object name and the movement to create. |
| `sourceImageUrl` | Required. The start frame. |
| `provider` | `kling-turbo` (the default), `kling`, `kling-3.0`, `minimax`, `hailuo-2.3`, `wan-i2v`, `seedance` or `bytedance-lite`. |
| `aspectRatio` | `1:1` (the default, a centered product frame), `3:4`, `16:9`, `9:16` or `4:3`. |
| `refineFromVideoUrl` | An 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`, `attachName` | The object and the clip's name in `motionClips`. |

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

| Model | Maker | Modes | Credits | Details |
| --- | --- | --- | --- | --- |
| [Kling 2.5 Turbo Pro](https://nodaro.ai/docs/models/video/kling-2-5-turbo-pro) | Kuaishou | Image to video, Text to video | from 110 | Faster Kling — good quality at lower cost. Supports end frame. |
| [Kling 2.6](https://nodaro.ai/docs/models/video/kling-2-6) | Kuaishou | Image to video, Text to video | from 138 | Kling 2.6 I2V — strong motion realism. 5s/10s, optional native audio. |
| [Kling 3.0](https://nodaro.ai/docs/models/video/kling-3-0) | Kuaishou | Image to video, Text to video | from 270 | Premium Kling 3.0 — variable 3-15s duration, native audio, 720P/1080P. |
| [Hailuo 02 I2V Pro](https://nodaro.ai/docs/models/video/hailuo-02-i2v-pro) | MiniMax | Image to video, Text to video | 143 | Hailuo 02 Pro — strong photoreal motion, fixed 5-second clips. Supports end frame. |
| [Hailuo 2.3 Standard](https://nodaro.ai/docs/models/video/hailuo-2-3-standard) | MiniMax | Image to video | from 75 | Cheaper Hailuo 2.3 tier — good baseline quality. |
| [Wan 2.6 I2V](https://nodaro.ai/docs/models/video/wan-2-6-i2v) | Alibaba | Image to video | from 175 | Wan 2.6 image-to-video — 5/10/15s at 720p/1080p. |
| [Bytedance Lite I2V](https://nodaro.ai/docs/models/video/bytedance-lite-i2v) | Bytedance | Image to video, Text to video | 57 | Cheapest Bytedance video tier with end-frame support. |

## 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.

**curl**

```bash
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" }'
```

**TypeScript SDK**

```ts
const approved = await client.objects.approveMainImage(id, jobIds[1])
if (approved.canonicalDescription === null) {
await client.objects.recaption(id)
}
```

**CLI**

```bash
nodaro objects approve-main-image <id> --candidate-job-id <jobId>
nodaro objects recaption <id>
```

## Archive, restore and delete an object

| Action | curl | TypeScript SDK | CLI |
| --- | --- | --- | --- |
| Archive | `DELETE /v1/objects/:id` | `client.objects.delete(id)` | `nodaro objects delete <id>` |
| Restore | `POST /v1/objects/:id/restore` | `client.objects.restore(id)` | `nodaro objects restore <id>` |
| Delete for good | `DELETE /v1/objects/:id?permanent=true` | `client.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: the main image, the variants, the clips and the reference photos. It 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](https://nodaro.ai/docs/nodes/assets/object) 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](https://nodaro.ai/docs/developers/api/workflows) for running apps.

## Use the object in other generations

Pass the object's asset URLs as reference images to [Generate Image](https://nodaro.ai/docs/nodes/image/generate-image) or [Generate Video](https://nodaro.ai/docs/nodes/video/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](https://nodaro.ai/docs/guides/objects).

## Use it from MCP

| Tool | What it does |
| --- | --- |
| `list_objects`, `get_object` | Find an object and read its variant URLs. |
| `generate_object` | Generate a main image or a variant. |
| `approve_object_main_image`, `recaption_object` | Approve a main image, or write its description again. |
| `generate_object_motion` | Animate the main image. |

There are no MCP tools to create, update, archive, restore or delete objects: `generate_object` creates them, and the other changes go through REST, the SDK or the CLI. See the [MCP tools reference](https://nodaro.ai/docs/mcp/tools).

## Credits

| Route | Price on Nodaro Cloud |
| --- | --- |
| `POST /v1/generate-object` | The image model's price times `count`, reserved for every candidate before the first job starts. |
| `POST /v1/generate-object-asset` | The image model's price, per variant. |
| `POST /v1/generate-object-motion` | The video model's image-to-video price, per clip. |
| `approve-main-image`, `llm-caption` | Free. |

## Errors

| Status | Code | Meaning |
| --- | --- | --- |
| `400` | `validation_error` | A field is missing or invalid. |
| `400` | `not_archived` | A permanent delete was sent for an object that is not archived. |
| `400` | `main_image_required` | `llm-caption` was called before the object has a main image. |
| `401` | `unauthorized` | The token is missing, invalid or revoked. |
| `402` | `insufficient_credits` | Nodaro Cloud only. The account cannot cover the reservation. |
| `404` | `not_found` | No active object with that id belongs to you. |
| `409` | `concurrent_modification` | `expectedUpdatedAt` 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?

Eight video models, by provider id kling-turbo (the default, Kling 2.5 Turbo Pro), kling, kling-3.0, minimax, hailuo-2.3, wan-i2v, seedance and bytedance-lite. 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.
