Nodaro Docs
DocumentationNode ReferenceModelsAI Agents (MCP)DevelopersSelf-hostingResearch
REST API

Characters

Create, update, archive and restore characters over REST, generate portrait candidates, expressions, angles and motion clips, and approve the anchor portrait.

The Characters API scripts everything Character Studio does. You create a character, generate portrait candidates, approve one as the anchor portrait, and add expressions, angles, poses, lighting variants and motion clips. Image and video nodes then reuse the character, so the same person looks the same in every shot.

The routes work on every edition. They 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: you only see and change your own characters. See Authentication.

Endpoints

MethodPathWhat it does
GET/v1/charactersList your characters, one page at a time.
GET/v1/characters/:idGet one character with its jobs in progress.
POST/v1/charactersCreate a character, or update one when the body has an id.
POST/v1/characters/:id/duplicateCopy a character to a new one named with a (copy) suffix.
DELETE/v1/characters/:idArchive a character. It can be restored.
POST/v1/characters/:id/restoreRestore an archived character.
GET/v1/characters/:id/usageCount and list the workflows that use the character.
POST/v1/generate-characterGenerate 1 to 10 portrait candidates.
POST/v1/generate-character-assetGenerate one expression, pose, angle or lighting variant.
POST/v1/generate-character-motionAnimate the character into a motion clip.
POST/v1/characters/:id/approve-portraitApprove a candidate as the portrait and write the character's description.
POST/v1/characters/:id/llm-captionWrite the description again from the current portrait.

Training a dedicated model on a character is a Nodaro Cloud feature with its own routes. See Character training.

What a character holds

A character is one saved identity. The fields below come back from GET /v1/characters/:id in camelCase.

FieldWhat it holds
id, nameThe identifier and the display name. Names are unique per account, ignoring case.
description, gender, style, baseOutfitIdentity notes that shape every generated image of the character.
seedPromptA short prompt that frames the portrait, up to 4,000 characters.
sourceImageUrlThe anchor portrait. It is set when you approve a candidate.
canonicalDescriptionA visual description of about 80 to 120 words, written by Nodaro when the portrait is approved. Prompts that reference the character include it.
expressions, poses, angles, bodyAngles, lightingVariations, motionsThe asset buckets. Each entry is { name, url }.
referencePhotosUp to 20 real photos, each tagged with its framing.
realLifeRefsByVariant, referenceVideosByVariantExtra reference photos or clips for one variant, for example the smile expression.
person, wardrobeStructured appearance and wardrobe choices, as set on the Pickers page of Character Studio.
voice, personalityThe character's voice and personality.
identityLockHow strictly generated assets keep the face: off, soft or strict. The default is strict.
deletedAtSet when the character is archived.

The asset buckets

Each bucket holds variants of the anchor portrait. You can name a variant anything; these are the preset names.

BucketWhat it showsPreset variants
expressionsHead and shoulders, a different emotionneutral, smile, angry, surprised, sad, talking, laughing, disgusted, fearful, smirk, crying
anglesHead and shoulders from another camera anglefront, 3/4 left, left profile, right profile, 3/4 right
bodyAnglesFull body from another angle, arms relaxedfront, 3/4 left, left profile, right profile, 3/4 right, back
posesFull body in another posturestanding, walking, sitting, running, crouching, pointing, fighting stance, jumping, turning
lightingVariationsThe same pose under other lightdaylight, night, dramatic
motionsVideo clips of the character movingAny label, for example walking or head turn

List characters

GET /v1/characters returns one page of your characters, newest first. Keep requesting pages until nextCursor is null: a single response is never "all characters" for an account above the page size.

Query parameterWhat it does
limitRows per page. The default is 100 and the maximum is 500.
cursorThe nextCursor of the previous page.
projectIdOnly characters of one project.
archivedtrue lists archived characters instead.
CURSOR=""
while :; do
  PAGE=$(curl -s "https://app.nodaro.ai/v1/characters?limit=100${CURSOR:+&cursor=$CURSOR}" \
    -H "Authorization: Bearer $NODARO_API_KEY")
  echo "$PAGE" | jq -r '.characters[] | "\(.id) \(.name)"'
  CURSOR=$(echo "$PAGE" | jq -r '.nextCursor // empty')
  [ -z "$CURSOR" ] && break
done
import { createClient, StaticTokenAuth } from '@nodaro/sdk'

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

const all = []
let cursor: string | undefined
do {
  const page = await client.characters.list({ limit: 100, cursor })
  all.push(...page.characters)
  cursor = page.nextCursor ?? undefined
} while (cursor)
nodaro characters list --limit 100 --json
nodaro characters list --archived
{
  "characters": [
    {
      "id": "3f6c2a9e-8d41-4b7a-9c35-1e2f7a6b0d94",
      "name": "Kira",
      "description": "young protagonist with auburn hair",
      "sourceImageUrl": "https://cdn.nodaro.ai/characters/kira-portrait.png",
      "expressions": [{ "name": "smile", "url": "https://cdn.nodaro.ai/characters/kira-smile.png" }]
    }
  ],
  "nextCursor": "eyJjcmVhdGVkQXQiOiIyMDI2LTA5LTIwVDEwOjAwOjAxWiJ9"
}

The cursor is opaque. Pass back only a nextCursor the server gave you, and never store one across releases. A malformed cursor is a 400 validation_error, not a silent return to the first page. Characters created while you page are not included; start again without a cursor to see them.

Create or update a character

POST /v1/characters creates a character when the body has no id, and updates the character when it has one. A create needs nodeId and name. nodeId links the character to a canvas node; use any label, such as "scripted", when you create it from code.

On an update, only the fields you send are written. Omitted fields keep their values, so a save never overwrites asset buckets that a running job is filling. Send an asset bucket only when you mean to replace it.

curl -X POST https://app.nodaro.ai/v1/characters \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "nodeId": "scripted",
    "name": "Kira",
    "description": "young protagonist with auburn hair",
    "style": "realistic",
    "seedPrompt": "kira portrait, warm natural lighting"
  }'
const { id } = await client.characters.create({
  nodeId: 'scripted',
  name: 'Kira',
  description: 'young protagonist with auburn hair',
  style: 'realistic',
  seedPrompt: 'kira portrait, warm natural lighting',
})

await client.characters.update(id, { gender: 'female', identityLock: 'soft' })
nodaro characters create --name "Kira" \
  --description "young protagonist with auburn hair" \
  --style realistic --seed-prompt "kira portrait, warm natural lighting"

nodaro characters update <id> --gender female
{ "id": "3f6c2a9e-8d41-4b7a-9c35-1e2f7a6b0d94", "name": "Kira" }

Prop

Type

person and wardrobe shape only the character's own portrait and asset generations. They use the same catalogs as the Person picker; see Picker catalogs.

When a workflow runs, the character's voice fills in any Text to Speech node connected after the character: the voice, the voice type and the recommended model. A value set on the Text to Speech node itself wins.

Generate portrait candidates

POST /v1/generate-character starts one job per candidate and returns their ids at once. Poll each job with the Jobs API until it is completed.

With attachToCharacterId, the first candidate to finish becomes the character's portrait. For a single candidate that is all you need. For several candidates, approve the one you prefer; approval replaces the portrait.

curl -X POST https://app.nodaro.ai/v1/generate-character \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Kira",
    "seedPrompt": "kira portrait, warm natural lighting",
    "count": 4,
    "attachToCharacterId": "3f6c2a9e-8d41-4b7a-9c35-1e2f7a6b0d94"
  }'
const { jobIds } = await client.characters.generate({
  name: 'Kira',
  seedPrompt: 'kira portrait, warm natural lighting',
  count: 4,
  attachToCharacterId: id,
})
nodaro characters generate <id> --count 4 \
  --seed-prompt "kira portrait, warm natural lighting" --watch
{
  "jobId": "a1c4e8f2-5b3d-4e6a-8f7c-2d9b1e0a3c5f",
  "jobIds": [
    "a1c4e8f2-5b3d-4e6a-8f7c-2d9b1e0a3c5f",
    "b7d2f9a1-6c4e-4a8b-9d3f-3e0c2f1b4d6a",
    "c3e5a7b9-8d1f-4c2e-a6b8-4f1d3a2c5e7b",
    "d9f1b3c5-2e4a-4d6f-b8c1-5a2e4b3d6f8c"
  ]
}

Prop

Type

quality and resolution price the job exactly like Generate Image: a 4K or high-quality run costs more than the same model at its base tier. A value the model does not support is changed to the nearest one it does, never refused, and the credits follow the changed value. These routes do not return an adjustments list; read the value that ran in the job's input_data from GET /v1/jobs/:id.

Generate an expression, angle, pose or lighting variant

POST /v1/generate-character-asset generates one variant of the anchor portrait and returns { jobId }. To add the result to the character, send all three attach fields: attachToCharacterId, attachToColumn and attachName. The worker appends { name: attachName, url } to that bucket when the job completes.

curl -X POST https://app.nodaro.ai/v1/generate-character-asset \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Kira",
    "assetType": "expressions",
    "variant": "smile",
    "attachToCharacterId": "3f6c2a9e-8d41-4b7a-9c35-1e2f7a6b0d94",
    "attachToColumn": "expressions",
    "attachName": "smile"
  }'
await client.characters.generateAsset({
  name: 'Kira',
  assetType: 'bodyAngles',
  variant: 'front',
  attachToCharacterId: id,
  attachToColumn: 'body_angles',
  attachName: 'front',
})
nodaro characters generate-asset <id> --asset-type expressions --variant smile --watch
FieldWhat it does
nameRequired. The character name.
assetTypeRequired. expressions, poses, lighting, headAngles, angles (same as headAngles), bodyAngles or custom.
variantRequired. The variant to generate, 1 to 100 characters, for example smile or 3/4 left.
descriptionA one-sentence description of this variant, up to 1,000 characters. When you attach to a character and omit it, Nodaro drafts one from the character's canonical description.
sourceImageUrlThe image to vary, usually the approved portrait.
provider, quality, resolutionThe image model and its output tier, priced as on Generate Image.
aspectRatio1:1, 3:4, 16:9 or 9:16. The default depends on the type: 1:1 for expressions, 9:16 for poses and body angles, 3:4 for the others.
attachToCharacterId, attachToColumn, attachNameWhere the result goes. attachToColumn is expressions, poses, angles, body_angles or lighting_variations. A custom variant must name its column.

When you attach the variant to a character, the real-life photos saved under the variant's key in realLifeRefsByVariant are sent with the request automatically.

Animate the character

POST /v1/generate-character-motion turns a still of the character into a video clip and returns { jobId }. With attachToCharacterId and attachName, the clip is appended to the motions bucket when it completes.

When you attach to a character and omit sourceImageUrl, Nodaro picks the start frame in this order:

  1. The sourceImageUrl you send, which always wins.
  2. The front entry of bodyAngles. A full-body frame animates much better than a head-and-shoulders portrait.
  3. Any other entry of bodyAngles, the most recent first.
  4. The anchor portrait.

For the best clips, generate a front body angle before the first motion.

curl -X POST https://app.nodaro.ai/v1/generate-character-motion \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Kira",
    "motionPrompt": "slow head turn left, soft smile",
    "provider": "kling",
    "attachToCharacterId": "3f6c2a9e-8d41-4b7a-9c35-1e2f7a6b0d94",
    "attachName": "head turn"
  }'
await client.characters.generateMotion({
  name: 'Kira',
  motionPrompt: 'slow head turn left, soft smile',
  provider: 'kling',
  attachToCharacterId: id,
  attachName: 'head turn',
})
nodaro characters generate-motion <id> \
  --motion-prompt "slow head turn left, soft smile" --attach-name "head turn" --watch
FieldWhat it does
nameRequired. The character name.
motionPromptRequired. What moves and how, 1 to 2,000 characters.
providerThe video model: kling (the default), kling-turbo, kling-3.0, wan-i2v or wan-2.7-i2v.
sourceImageUrlThe start frame. Required when you do not attach to a character.
description, motionDescriptionA visual description (up to 1,000 characters) and a description of the movement (up to 500). Nodaro drafts both when you attach and omit them.
aspectRatio1:1, 3:4, 16:9 or 9:16. The default is 9:16, a full-body vertical clip.
attachToCharacterId, attachNameThe character and the clip's name in motions.

These models can animate a character. The price is the model's image-to-video price:

ModelMakerModesCreditsDetails
Kling 2.6KuaishouImage to video, Text to videofrom 138Kling 2.6 I2V — strong motion realism. 5s/10s, optional native audio.
Kling 2.5 Turbo ProKuaishouImage to video, Text to videofrom 110Faster Kling — good quality at lower cost. Supports end frame.
Kling 3.0KuaishouImage to video, Text to videofrom 270Premium Kling 3.0 — variable 3-15s duration, native audio, 720P/1080P.
Wan 2.6 I2VAlibabaImage to videofrom 175Wan 2.6 image-to-video — 5/10/15s at 720p/1080p.
Wan 2.7 I2VAlibabaImage to video188Wan 2.7 image-to-video — 2–15s at 720p/1080p, supports start+end frame.

Approve a portrait

POST /v1/characters/:id/approve-portrait sets a completed candidate as the anchor portrait. In the same call, Nodaro looks at the portrait and writes canonicalDescription, the text that later prompts use to describe the character. Without that description, a character drifts much more between scenes.

The candidate must be a completed job that belongs to you. If writing the description fails, the portrait is still set and canonicalDescription is null; call POST /v1/characters/:id/llm-caption to try again. Both routes are safe to repeat and cost no credits.

curl -X POST https://app.nodaro.ai/v1/characters/3f6c2a9e-8d41-4b7a-9c35-1e2f7a6b0d94/approve-portrait \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "candidateJobId": "b7d2f9a1-6c4e-4a8b-9d3f-3e0c2f1b4d6a" }'
const { portraitUrl, canonicalDescription } =
  await client.characters.approvePortrait(id, jobIds[1])

if (canonicalDescription === null) {
  await client.characters.recaption(id)
}
nodaro characters approve-portrait <id> --job <jobId>
nodaro characters recaption <id>
{
  "portraitUrl": "https://cdn.nodaro.ai/characters/kira-portrait-2.png",
  "canonicalDescription": "Kira is a woman in her mid-twenties with shoulder-length auburn hair, green eyes and light freckles..."
}

llm-caption returns { canonicalDescription }. It answers 400 no_portrait when the character has no portrait yet, and 502 when the description could not be written.

Archive, restore and copy a character

  • Archive. DELETE /v1/characters/:id archives the character and returns { success: true, archived: true }. The character leaves the default list, but GET /v1/characters/:id still returns it, so workflows that use it keep working. On Nodaro Cloud, archiving also cancels a training in progress, refunds its credits and removes the trained model.
  • Restore. POST /v1/characters/:id/restore returns { id, name }. When an active character now has the same name, Nodaro adds a (restored) suffix and returns the new name.
  • Delete for good. No API route deletes a character permanently. Use the archive view in the editor's library.
  • Copy. POST /v1/characters/:id/duplicate returns { id, name } for a new character with a (copy) suffix. The copy shares the original's asset URLs until you regenerate them.
  • Usage. GET /v1/characters/:id/usage returns { workflowCount, workflows: [{ id, name }] }. Check it before you archive a character.
ActioncurlTypeScript SDKCLI
ArchiveDELETE /v1/characters/:idclient.characters.delete(id)nodaro characters delete <id>
RestorePOST /v1/characters/:id/restoreclient.characters.restore(id)nodaro characters restore <id>
CopyPOST /v1/characters/:id/duplicateclient.characters.duplicate(id)nodaro characters duplicate <id>
UsageGET /v1/characters/:id/usageclient.characters.usage(id)nodaro characters usage <id>

A complete run: create, generate, approve, add variants

Create the character

POST /v1/characters with nodeId, name and a seedPrompt. Keep the returned id.

Generate portrait candidates

POST /v1/generate-character with count: 4 and attachToCharacterId. Poll each job id until it is completed or failed.

Approve your favorite

POST /v1/characters/:id/approve-portrait with that candidate's job id. The portrait and the canonical description are set.

Add variants and motion

Generate a front body angle, then expressions and poses with POST /v1/generate-character-asset, and clips with POST /v1/generate-character-motion.

Use the character in other generations

After the assets exist, pass their URLs as reference images to Generate Image or Generate Video. For example, read the smile entry of expressions and send its URL as a reference with your prompt. Explicit URLs are the simplest choice for code, because they do not depend on how a workflow is wired. See Run a single node for the request fields.

In a workflow, wire the Character Asset node into the image or video node, or mention the character in the prompt with @, for example @kira:1:smile. Reference roles explains the mention grammar, and Consistent characters the full method.

Use it from MCP

AI assistants use the same routes through these tools. Archive and restore are deliberately not available over MCP.

ToolWhat it does
list_characters, get_characterFind a character and read its asset URLs.
create_character, update_characterCreate a character or change its identity fields.
generate_characterGenerate a portrait (kind: "main") or a variant (kind: "asset").
generate_character_motionAnimate the character into a clip.
approve_portrait, recaption_characterApprove a portrait, or write its description again.

See the MCP tools reference.

Credits

On Nodaro Cloud, character generation uses the same credit prices as the matching nodes.

RoutePrice
POST /v1/generate-characterThe image model's price times count, reserved for every candidate before the first job starts.
POST /v1/generate-character-assetThe image model's price, per variant.
POST /v1/generate-character-motionThe video model's image-to-video price, per clip.
approve-portrait, llm-captionFree.

Each model's page lists its exact price. See Credits.

Errors

Errors use the standard envelope, { "error": { "code", "message" } }. See Errors.

StatusCodeMeaning
400validation_errorA field is missing or invalid, a cursor is malformed, or a generate request has no seedPrompt, description or referencePhotos.
400no_portraitllm-caption was called before the character has a portrait.
401unauthorizedThe token is missing, invalid or revoked.
402insufficient_creditsNodaro Cloud only. The account cannot cover the reservation.
404not_foundNo character or job with that id belongs to you.
409name_takenAnother active character already has that name.
502—The canonical description could not be written. The portrait is unchanged. Try again.

Frequently asked questions

Last updated on

On this page