Skip to main content
A character is a special kind of world you talk to. Under the hood it’s a SMWorld subtype: the same stance-graph that drives the live video (each node is an emotional stance, plus one hidden Speaking overlay), paired with a brain: the persona, lore, voice, and stance palette that decide what the character says and how they feel. Because a character is a world, everything you already know carries over: tenancy, session tokens, versions, forking, and usage all work the same way. Three things are new: how you create one, the /say turn loop, and /tts voice.
The brain is a secret. The persona and lore are loaded server-side to drive each turn. They are never sent to the browser. The embed only ever receives what it needs to render: the stances, voice id, greeting, and intro.

The auth model

There are two distinct callers, exactly as with worlds:
  • Your backend (secret sk_ key) creates, lists, updates, and forks characters, and mints session tokens.
  • The browser (short-lived session token) runs the live conversation: POST /v1/characters/{id}/say and /tts. The secret key and the persona never reach the client.

Create a character

POST /v1/characters (secret key) supports three modes.

Import a complete character (examples)

The fastest way to get a playable character is to import one. The six shipped first-party characters are published as ready-to-import example files, each shaped { "world": … }:
This instantiates your own copy: you own it, can fork it, version it, and talk to it. See Example characters below.

Explicit

Provide the brain and a stance palette. The server compiles the stance-graph for you. Each stance is one emotional state with an expression (its visual prose) and a mood (the cue the brain reads to choose it).
The cover/seed frame is resolved from cover, frame_url, frame_b64, or, as above, painted from premise when you give no image.

Authored

Give a frame (or a premise alone) and authored: true, and the platform invents the persona, subject, style, and a 4–6 stance palette for you:
All three modes reserve one generation against your daily quota and run the same fail-closed validation gate before persisting.

Manage characters

Stance-node and graph edits go through the existing /v1/worlds/{id}/… endpoints. A character’s graph is an SMWorld.

Talk to a character

Mint a session token for the character (the worldId is the character’s id), then call /say from the browser with that token. The brain is loaded server-side. You send only what the player said.
The response drives the live scene: move the video to turn.stance, fire the hidden Speaking overlay, and optionally render turn.action:
  • change_location / fetch_object: scene prose (morph the place / hold up an object). Apply locally, no extra call.
  • show_image: the character shows a picture. Paint it with POST /v1/characters/{id}/image { token, prompt: action.image_prompt }{ src, generated }. With no prompt (or on failure / over quota) it returns the character’s base image, so a demo card is never empty. Metered as image.
To voice the line:
/say is metered as a say turn and /tts as a tts turn. See Pricing.

Cross-session memory (per player)

Characters remember the people they talk to (decaying affect/rapport, a consolidated summary, and a “welcome back” line), exactly like the first-party character page. You bring your own player identity: set playerIdentity to your user id when you mint the session token, and memory is scoped per (your app, character, playerIdentity). Build on top of the API with your own user list, and each of your users gets their own relationship.
  • The default identity is anon, which is never persisted (so anonymous players don’t share one relationship). Pass a real playerIdentity to opt in.
  • POST /v1/characters/{id}/say automatically loads that player’s prior affect + summary and persists the turn (no extra calls in the happy path).
  • POST /v1/characters/{id}/resume{ returning, welcome, affect, messageCount } for a memory-aware greeting when they come back.
  • POST /v1/characters/{id}/memory/end consolidates the session into the summary (call on unmount, though the embed does this for you).
  • GET /v1/characters/{id}/memory inspects it. DELETE forgets it (start over / right-to-be-forgotten).
All of these are session-token auth (the token carries the player), so they’re safe to call from the browser. The persona/lore never leave the server.

Read a player’s transcript

To display or manage the chat in your own UI, read the stored transcript from your server:
Unlike the memory calls above, GET /v1/characters/{id}/transcript is secret-key auth (it’s your backend reading its own end-user’s history, not the embed), so pass playerIdentity as a query param (the session token isn’t involved). Paginate with limit and cursor (the opaque nextCursor from the previous page).
transcript is a rolling window of the most recent turns (capped at 40), not a complete log. Older turns are folded into summary (a consolidated few-sentence recap) rather than kept verbatim, and anonymous players (playerIdentity = anon) are never persisted, so they have no transcript. If you need a permanent, uncapped archive of every message, store turns on your side as you send them.

Embed a live conversation

A character embeds exactly like a world: mint a session token and load the embed with it. The streaming video, stance morphs, and the talk loop run inside the iframe. The persona/lore stay server-side. See Embedding.

Example characters

Six hand-authored first-party characters ship as importable examples. Drop any one into POST /v1/characters (import mode) to get your own playable copy. Each file is the exact compiled character (its stance-graph, persona, lore, and voice), so the imported copy is faithful to the shipped one.