# Objects and creatures

> Create objects and creatures, generate main images and variants, animate them, and make a creature talk from TypeScript with the Nodaro SDK.

Source: https://nodaro.ai/docs/developers/sdk/objects-and-creatures

**`client.objects`** scripts everything Object Studio does for props, products and vehicles, and **`client.creatures`** does the same for animals and creatures. Both create and edit items, generate main-image candidates, approve one, add variants and motion clips, and write a caption that keeps the item consistent in later prompts. The methods call the [Objects](https://nodaro.ai/docs/developers/api/objects) and [Creatures](https://nodaro.ai/docs/developers/api/creatures) REST APIs. See [Objects](https://nodaro.ai/docs/guides/objects) and [Creatures](https://nodaro.ai/docs/guides/creatures) for the editor view.

## Methods

The two resources have the same methods.

| Method | What it does |
| --- | --- |
| [`objects.list(params?)`](#objectslistparams), [`creatures.list(params?)`](#creatureslistparams) | List your items |
| [`objects.listArchived(params?)`](#objectslistarchivedparams) | List your archived items |
| [`objects.get(id)`](#objectsgetid) | Read one item, with its jobs in progress |
| [`objects.create(input)`](#objectscreateinput), [`creatures.create(input)`](#creaturescreateinput) | Create an item |
| [`objects.update(id, input)`](#objectsupdateid-input), [`creatures.update(id, input)`](#creaturesupdateid-input) | Change an item |
| [`objects.delete(id)` and `restore(id)`](#objectsdeleteid-and-restoreid) | Archive an item, or bring it back |
| [`objects.permanentDelete(id)`](#objectspermanentdeleteid) | Destroy an archived item and its files |
| [`objects.generate(input)`](#objectsgenerateinput), [`creatures.generate(input)`](#creaturesgenerateinput) | Generate main-image candidates |
| [`objects.generateAsset(input)`](#objectsgenerateassetinput), [`creatures.generateAsset(input)`](#creaturesgenerateassetinput) | Generate a variant |
| [`objects.generateMotion(input)`](#objectsgeneratemotioninput), [`creatures.generateMotion(input)`](#creaturesgeneratemotioninput) | Animate the main image into a clip |
| [`objects.approveMainImage(id, candidateJobId, expectedUpdatedAt?)`](#objectsapprovemainimageid-candidatejobid-expectedupdatedat) | Make a candidate the main image |
| [`objects.recaption(id)`](#objectsrecaptionid) | Write the description again |

## client.objects

An object has its main image in `sourceImageUrl`, four collections (`angles`, `materials`, `variations` and `motionClips`, each a list of `{ name, url }`), `boards`, `referencePhotos`, `canonicalDescription` and `styleLock`. `category` is one of `furniture`, `vehicle`, `weapon`, `food`, `clothing`, `electronics`, `nature`, `tool`, `animal` or `other`.

`Object` shares its name with the JavaScript global. When you need both, import it under another name: `import type { Object as NodaroObject } from "@nodaro/sdk"`.

### objects.list(params?)

Lists your objects. By default it returns active objects only. Paging is optional: without `limit` you get the whole list; with `limit`, at most 500, you get one page and a `nextCursor`.

```ts
list(params?: { archived?: boolean; projectId?: string; limit?: number; cursor?: string }): Promise<{
objects: Object[]
nextCursor?: string | null
}>
```

<TypeTable
type={{
archived: { type: 'boolean', default: 'false', description: "true lists archived objects instead." },
projectId: { type: 'string', description: "Only objects of this project." },
limit: { type: 'number', description: "The page size, at most 500. Omit it for the whole list." },
cursor: { type: 'string', description: "The nextCursor of the previous page." },
}}
/>

```ts
const { objects } = await client.objects.list()
const page = await client.objects.list({ limit: 100 })
```

### objects.listArchived(params?)

Lists your archived objects, as a shortcut for `list({ archived: true })`. `creatures.listArchived()` works the same way.

```ts
listArchived(params?: { projectId?: string; limit?: number; cursor?: string }): Promise<{ objects: Object[]; nextCursor?: string | null }>
```

<TypeTable
type={{
projectId: { type: 'string', description: "Only objects of this project." },
limit: { type: 'number', description: "The page size, at most 500." },
cursor: { type: 'string', description: "The nextCursor of the previous page." },
}}
/>

```ts
const { objects: archived } = await client.objects.listArchived()
```

### objects.get(id)

Reads one object, with `pendingJobs`, the variants still generating. An archived object is **not** returned: it throws `NotFoundError`. `creatures.get()` works the same way.

```ts
get(id: string): Promise<ObjectDetail>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "The object id." },
}}
/>

```ts
const object = await client.objects.get(objectId)
console.log(object.sourceImageUrl, object.materials)
```

### objects.create(input)

Creates an object. `name` and `nodeId` are required. A script without a canvas node can pass `"mcp-managed"` as `nodeId`.

```ts
create(input: CreateObjectInput): Promise<{ id: string }>
```

<TypeTable
type={{
nodeId: { type: 'string', required: true, description: "The canvas node the object belongs to, or mcp-managed." },
name: { type: 'string', required: true, description: "The name." },
description: { type: 'string', description: "A free-text description." },
category: { type: 'ObjectCategory', description: "furniture, vehicle, weapon, food, clothing, electronics, nature, tool, animal or other." },
style: { type: 'string', description: "The visual style, such as realistic." },
projectId: { type: 'string', description: "The project to file it under." },
workflowId: { type: 'string', description: "The workflow it comes from." },
sourceImageUrl: { type: 'string', description: "The main image URL." },
imageProvider: { type: 'string | null', description: "The image model of the main image." },
referencePhotos: { type: 'ObjectReferencePhoto[]', description: "Reference photos, each { url, kind }. kind is front, side, detail, context, moodBoard or other." },
canonicalDescription: { type: 'string', description: "The description used in prompts." },
styleLock: { type: 'boolean', description: "Keep variants in the approved style." },
}}
/>

```ts
const { id: objectId } = await client.objects.create({
nodeId: "mcp-managed",
name: "Antique Lantern",
description: "Weathered brass lantern with hand-engraved filigree",
category: "tool",
style: "realistic",
})
```

### objects.update(id, input)

Changes an object. Only the fields you send are written. The variant collections are not part of this call, because generation jobs add to them while you work.

```ts
update(id: string, input: UpdateObjectInput): Promise<{ id: string; updatedAt: string }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "The object id." },
name: { type: 'string', description: "The name." },
description: { type: 'string', description: "The description." },
category: { type: 'ObjectCategory', description: "The category." },
style: { type: 'string', description: "The visual style." },
sourceImageUrl: { type: 'string', description: "The main image URL." },
imageProvider: { type: 'string | null', description: "The image model of the main image." },
referencePhotos: { type: 'ObjectReferencePhoto[]', description: "Reference photos." },
canonicalDescription: { type: 'string', description: "The description used in prompts." },
styleLock: { type: 'boolean', description: "Keep variants in the approved style." },
boards: { type: 'Array<{ name, url, type?, sourceImages? }>', description: "Reference boards." },
selectedAssetByVariant: { type: 'Record<string, string>', description: "The chosen take of each variant." },
expectedUpdatedAt: { type: 'string', description: "The updatedAt value you read. When the object changed since, the update fails with 409 concurrent_modification." },
}}
/>

```ts
await client.objects.update(objectId, {
canonicalDescription: "A weathered brass lantern with engraved filigree and a glass chimney",
expectedUpdatedAt: object.updatedAt,
})
```

The `409 concurrent_modification` arrives as a plain `NodaroError`. Read the object again, merge, and retry.

### objects.delete(id) and restore(id)

`delete()` archives an object. Repeating it on an archived object changes nothing. `restore()` brings it back; when the name now matches an active object's name, ignoring case, the server adds `(restored)` and returns the name it used.

```ts
delete(id: string): Promise<{ success: true; archived: true }>
restore(id: string): Promise<{ id: string; name: string }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "The object id." },
}}
/>

```ts
await client.objects.delete(objectId)
const { name } = await client.objects.restore(objectId)
```

### objects.permanentDelete(id)

Destroys an archived object and every stored file it references. It works on archived objects only: an active object fails with `400 not_archived`. Archive first with `delete()`. `creatures.permanentDelete()` works the same way.

```ts
permanentDelete(id: string): Promise<{ success: true; permanent: true }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "The id of an archived object." },
}}
/>

```ts
await client.objects.delete(objectId)
await client.objects.permanentDelete(objectId)
```

Nodaro's MCP tools do not offer this operation, so an AI assistant cannot destroy your objects.

### objects.generate(input)

Generates main-image candidates (`POST /v1/generate-object`). With `count` above 1, every job is reserved before any starts, so a failure part way through rolls the batch back.

```ts
generate(input: GenerateObjectInput): Promise<{ jobIds: string[]; jobId?: string }>
```

<TypeTable
type={{
name: { type: 'string', required: true, description: "The object's name." },
description: { type: 'string', description: "What the object looks like." },
userPrompt: { type: 'string', description: "Extra instructions." },
category: { type: 'ObjectCategory', description: "The category." },
style: { type: '"realistic" | "anime" | "3d-pixar" | "illustration"', description: "The visual style." },
sourceImageUrl: { type: 'string', description: "A photo to base the image on." },
provider: { type: 'string', description: "The image model id." },
count: { type: 'number', description: "How many candidates to generate." },
seedPromptHint: { type: 'string', description: "A picker selection to add to the prompt, such as antique brass from the Material picker." },
attachToObjectId: { type: 'string', description: "The object to write a single result to." },
attachName: { type: 'string', description: "A name for the attached result." },
expectedUpdatedAt: { type: 'string', description: "The updatedAt value you read, to avoid overwriting a newer change." },
}}
/>

```ts
const { jobIds } = await client.objects.generate({ name: "Antique Lantern", count: 4 })
for (const jobId of jobIds) {
// poll each candidate with client.jobs.getStatus(jobId)
}
```

`jobIds` is always present, with one id per candidate. `jobId` is an older alias for a single candidate; use `jobIds`. With `attachToObjectId` and one candidate, the result becomes the main image when the job completes. Otherwise, pick one with `approveMainImage()`.

### objects.generateAsset(input)

Generates one variant (`POST /v1/generate-object-asset`). With `attachToObjectId`, `attachToColumn` and `attachName`, the result is added to that collection when the job completes.

```ts
generateAsset(input: GenerateObjectAssetInput): Promise<{ jobId: string }>
```

<TypeTable
type={{
assetType: { type: '"angles" | "materials" | "variations" | "motion" | "custom"', required: true, description: "The kind of variant." },
variant: { type: 'string', required: true, description: "The variant, such as gold or three-quarter." },
name: { type: 'string', required: true, description: "The object's name." },
description: { type: 'string', description: "The prompt for the variant. When omitted and the result is attached, a language model writes one from the object's description and the variant name." },
userPrompt: { type: 'string', description: "Extra instructions." },
sourceImageUrl: { type: 'string', description: "The image to start from, usually the main image." },
provider: { type: 'string', description: "The image model id." },
seedPromptHint: { type: 'string', description: "A picker selection to add to the prompt." },
attachToObjectId: { type: 'string', description: "The object to add the result to." },
attachToColumn: { type: 'string', description: "The collection: angles, materials, variations, motion_clips, sheets or detail_closeups. Required for custom." },
attachName: { type: 'string', description: "The name of the new entry." },
}}
/>

```ts
const { jobId } = await client.objects.generateAsset({
name: "Antique Lantern",
assetType: "materials",
variant: "gold",
attachToObjectId: objectId,
attachToColumn: "materials",
attachName: "gold",
})
```

For `angles`, `materials`, `variations` and `motion`, the collection follows from the asset type. A `custom` variant needs `attachToColumn`.

### objects.generateMotion(input)

Animates the object's image into a clip (`POST /v1/generate-object-motion`), with [Generate Video](https://nodaro.ai/docs/nodes/video/generate-video) in image-to-video mode. The clip always goes to `motionClips`. The defaults suit product shots: the model is `kling-turbo` and the frame is `1:1`.

```ts
generateMotion(input: GenerateObjectMotionInput): Promise<{ jobId: string }>
```

<TypeTable
type={{
motionPrompt: { type: 'string', required: true, description: "How the object moves or how the camera moves around it." },
sourceImageUrl: { type: 'string', required: true, description: "The image to animate. There is no fallback, so pass the main image." },
name: { type: 'string', required: true, description: "The object's name." },
provider: { type: 'string', default: '"kling-turbo"', description: "The video model id." },
aspectRatio: { type: '"1:1" | "3:4" | "16:9" | "9:16" | "4:3"', default: '"1:1"', description: "The clip shape. Objects add 4:3 for catalog shots." },
duration: { type: 'number', description: "The clip length in seconds." },
refineFromVideoUrl: { type: 'string', description: "An existing clip to rework with the new prompt, in video-to-video mode, instead of starting from the image." },
category: { type: 'string', description: "The category." },
style: { type: '"realistic" | "anime" | "3d-pixar" | "illustration"', description: "The visual style." },
canonicalDescription: { type: 'string', description: "The object's description." },
seedPromptHint: { type: 'string', description: "A picker selection to add to the prompt." },
attachToObjectId: { type: 'string', description: "The object to add the clip to." },
attachName: { type: 'string', description: "The name of the new clip." },
}}
/>

```ts
const { jobId } = await client.objects.generateMotion({
name: "Antique Lantern",
motionPrompt: "Slow 360-degree rotation, soft golden rim light",
sourceImageUrl: object.sourceImageUrl!,
attachToObjectId: objectId,
attachName: "rotate-360",
})
```

### objects.approveMainImage(id, candidateJobId, expectedUpdatedAt?)

Makes a completed candidate from `generate()` the object's main image. A vision model then writes the object's description, and the method returns both.

```ts
approveMainImage(id: string, candidateJobId: string, expectedUpdatedAt?: string): Promise<{
sourceImageUrl: string
canonicalDescription: string | null
}>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "The object id." },
candidateJobId: { type: 'string', required: true, description: "The job id of the completed candidate." },
expectedUpdatedAt: { type: 'string', description: "The updatedAt value you read. When the object changed since, the call fails with 409 concurrent_modification." },
}}
/>

```ts
const { sourceImageUrl, canonicalDescription } = await client.objects.approveMainImage(objectId, jobIds[0])
```

`canonicalDescription` is `null` when the description could not be written. The main image is still set; call `recaption()` to try again.

### objects.recaption(id)

Writes the object's description again from its current main image. The call is safe to repeat and takes no concurrency token.

```ts
recaption(id: string): Promise<{ canonicalDescription: string }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "The object id." },
}}
/>

```ts
const { canonicalDescription } = await client.objects.recaption(objectId)
```

Fails with `400 main_image_required` when the object has no main image, and with a 502 when the vision model fails.

## client.creatures

A creature is an animal or a fantasy being. It works like an object, with four differences:

- **`species`** is a free-text type, such as `dragon` or `wolf`, and it is the subject of the main-image prompt. `category` is free text too.
- **`poses`** replaces `materials`, so the collections are `angles`, `poses`, `variations` and `motionClips`. The variant types are `angles`, `poses`, `variations` and `custom`.
- **`boards`** holds up to 24 named Creature Boards, dense reference sheets made with the Creature Board preset of [Generate Image](https://nodaro.ai/docs/nodes/image/generate-image). You own this list: `create()` and `update()` replace it as a whole.
- **`voice`** makes the creature a talking creature. It has the same shape as a character's voice: `{ voiceId, voiceName, traits, voiceType?, previewUrl?, ttsProvider? }`. Pass `voice: null` to remove it.

`creatures.listArchived()`, `get()`, `delete()`, `restore()`, `permanentDelete()`, `approveMainImage()` and `recaption()` take the same arguments and behave like the object methods above.

### creatures.list(params?)

```ts
list(params?: { archived?: boolean; projectId?: string; limit?: number; cursor?: string }): Promise<{
creatures: Creature[]
nextCursor?: string | null
}>
```

<TypeTable
type={{
archived: { type: 'boolean', default: 'false', description: "true lists archived creatures instead." },
projectId: { type: 'string', description: "Only creatures of this project." },
limit: { type: 'number', description: "The page size, at most 500. Omit it for the whole list." },
cursor: { type: 'string', description: "The nextCursor of the previous page." },
}}
/>

```ts
const { creatures } = await client.creatures.list()
```

### creatures.create(input)

```ts
create(input: CreateCreatureInput): Promise<{ id: string }>
```

<TypeTable
type={{
nodeId: { type: 'string', required: true, description: "The canvas node the creature belongs to, or mcp-managed." },
name: { type: 'string', required: true, description: "The name." },
species: { type: 'string', description: "The kind of creature, such as dragon or wolf." },
description: { type: 'string', description: "A free-text description." },
category: { type: 'string', description: "A free-text category." },
style: { type: 'string', description: "The visual style." },
projectId: { type: 'string', description: "The project to file it under." },
sourceImageUrl: { type: 'string', description: "The main image URL." },
referencePhotos: { type: 'CreatureReferencePhoto[]', description: "Reference photos, each { url, kind }. kind is front, side, detail, context, moodBoard or other." },
canonicalDescription: { type: 'string', description: "The description used in prompts." },
styleLock: { type: 'boolean', description: "Keep variants in the approved style." },
voice: { type: 'CreatureVoice | null', description: "The creature's voice." },
}}
/>

```ts
const { id: creatureId } = await client.creatures.create({
nodeId: "mcp-managed",
name: "Biscuit",
species: "ginger cat",
style: "realistic",
})
```

### creatures.update(id, input)

Takes the fields of `create()` except `nodeId`, plus `boards`, `selectedAssetByVariant` and `expectedUpdatedAt`. Only the fields you send are written.

```ts
update(id: string, input: UpdateCreatureInput): Promise<{ id: string; updatedAt: string }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "The creature id." },
boards: { type: 'Array<{ name, url, type?, sourceImages? }>', description: "The Creature Boards, up to 24, replaced as a whole." },
voice: { type: 'CreatureVoice | null', description: "The voice. null removes it." },
expectedUpdatedAt: { type: 'string', description: "The updatedAt value you read, to avoid overwriting a newer change." },
}}
/>

```ts
await client.creatures.update(creatureId, {
voice: { voiceId: chosenVoiceId, voiceName: "Aria", traits: "smug, unhurried" },
})
```

### creatures.generate(input)

Generates main-image candidates. It takes the fields of `objects.generate()`, plus `species`, and returns `{ jobIds }`.

```ts
generate(input: GenerateCreatureInput): Promise<{ jobIds: string[]; jobId?: string }>
```

<TypeTable
type={{
name: { type: 'string', required: true, description: "The creature's name." },
species: { type: 'string', description: "The kind of creature." },
description: { type: 'string', description: "What the creature looks like." },
count: { type: 'number', description: "How many candidates to generate." },
provider: { type: 'string', description: "The image model id." },
attachToCreatureId: { type: 'string', description: "The creature to write a single result to." },
}}
/>

```ts
const { jobIds } = await client.creatures.generate({ name: "Biscuit", species: "ginger cat", count: 4 })
```

### creatures.generateAsset(input)

Generates one variant. It works like `objects.generateAsset()`, with `attachToCreatureId`.

```ts
generateAsset(input: GenerateCreatureAssetInput): Promise<{ jobId: string }>
```

<TypeTable
type={{
assetType: { type: '"angles" | "poses" | "variations" | "custom"', required: true, description: "The kind of variant." },
variant: { type: 'string', required: true, description: "The variant, such as walking or sitting." },
name: { type: 'string', required: true, description: "The creature's name." },
attachToCreatureId: { type: 'string', description: "The creature to add the result to." },
attachToColumn: { type: 'string', description: "angles, poses, variations, motion_clips, sheets or detail_closeups. Required for custom." },
attachName: { type: 'string', description: "The name of the new entry." },
}}
/>

```ts
await client.creatures.generateAsset({
name: "Biscuit",
assetType: "poses",
variant: "sitting",
attachToCreatureId: creatureId,
attachToColumn: "poses",
attachName: "sitting",
})
```

### creatures.generateMotion(input)

Animates the creature's image into a clip. It works like `objects.generateMotion()`, with the same defaults, `kling-turbo` and `1:1`, and adds the clip to `motionClips`.

```ts
generateMotion(input: GenerateCreatureMotionInput): Promise<{ jobId: string }>
```

<TypeTable
type={{
motionPrompt: { type: 'string', required: true, description: "What the creature does." },
sourceImageUrl: { type: 'string', required: true, description: "The image to animate." },
name: { type: 'string', required: true, description: "The creature's name." },
provider: { type: 'string', default: '"kling-turbo"', description: "The video model id." },
aspectRatio: { type: '"1:1" | "3:4" | "16:9" | "9:16" | "4:3"', default: '"1:1"', description: "The clip shape." },
attachToCreatureId: { type: 'string', description: "The creature to add the clip to." },
attachName: { type: 'string', description: "The name of the new clip." },
}}
/>

```ts
await client.creatures.generateMotion({
name: "Biscuit",
motionPrompt: "The cat stretches, then yawns",
sourceImageUrl: creature.sourceImageUrl!,
attachToCreatureId: creatureId,
})
```

## Make a creature talk

Speech needs no creature-specific method. Render the line with the creature's voice, then lip-sync it onto the creature's image:

```ts
const creature = await client.creatures.get(creatureId)

// 1. Speak the line in the creature's voice
const speech = await client.nodes.runAndWait("text-to-speech", {
text: "I knocked the vase off the shelf. I regret nothing.",
voice: creature.voice!.voiceId,
provider: creature.voice!.ttsProvider,
voiceType: creature.voice!.voiceType,
})

// 2. Lip-sync the audio onto the creature's main image
const clip = await client.nodes.runAndWait("lip-sync", {
imageUrl: creature.sourceImageUrl!,
audioUrl: speech.audioUrl,
provider: "kling-avatar",
})
console.log(clip.videoUrl)
```

The [Lip Sync](https://nodaro.ai/docs/nodes/video/lip-sync) node also dubs an existing video: pass `videoUrl` and a model that takes video. `volcengine-lipsync` is the lowest-priced option for dubbing and the only one that handles several speakers:

```ts
const dub = await client.nodes.runAndWait("lip-sync", {
videoUrl: "https://example.com/scene.mp4",
audioUrl: "https://example.com/new-vocal.mp3",
provider: "volcengine-lipsync",
mode: "basic",          // for complex scenes
openScenedet: true,     // several speakers: scene and speaker detection
audioDurationSec: 42,   // sets the per-second price; without it, you pay for 5 minutes
})
```

To bind a creature into a shot as a reference, build the reference with `toConnectedReference({ kind: "creature", id, name, url, description })` from `@nodaro/shared`. Generate Image then adds a line that keeps the creature's anatomy, markings and colors. See [References](https://nodaro.ai/docs/developers/sdk/nodes#references).

## Frequently asked questions

### What is the difference between an object and a creature in Nodaro?

An object is a prop, a product or a vehicle, with angle, material and variation variants. A creature is an animal or a fantasy being, with angle, pose and variation variants, a species, reference boards and an optional voice.

### How do I generate candidates for an object's main image?

Call client.objects.generate with a name and a count. It always returns jobIds, one per candidate. When they complete, approve one with client.objects.approveMainImage.

### Can I delete an object permanently with the SDK?

Yes, in two steps. Archive it with delete(), then call permanentDelete(), which removes the object and every file it references. An active object is refused with 400 not_archived.

### How do I make a creature talk?

Render speech with the Text to Speech node and the creature's voice, then run the Lip Sync node with the creature's main image and that audio.
