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 four shipped first-party characters are published as ready-to-import example files (conjure-service/examples/characters/*.json), each shaped { "world": … }:
curl -X POST https://api.alakazam.gg/v1/characters \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  --data @examples/characters/nero.json
# → { "id": "…", "slug": "…", "kind": "character", "cover": "https://…" }
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).
curl -X POST https://api.alakazam.gg/v1/characters \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Aria",
    "premise": "a young astronomer in a cluttered observatory at night",
    "subject": "a young woman with ink-stained fingers and tired, bright eyes",
    "style": "warm lamplight, 35mm film grain",
    "persona": "Curious, dry-witted, a little lonely. Lights up about the stars.",
    "greeting": "Oh — you'\''re up late too?",
    "voiceId": "<elevenlabs-voice-id>",
    "stances": [
      { "id": "neutral",   "label": "Neutral",   "mood": "her default",       "expression": "a calm, attentive look" },
      { "id": "delighted", "label": "Delighted", "mood": "when praised",      "expression": "eyes lighting up, a quick grin" }
    ]
  }'
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 just a premise) and authored: true, and the platform invents the persona, subject, style, and a 4–6 stance palette for you:
curl -X POST https://api.alakazam.gg/v1/characters \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "authored": true, "premise": "a grizzled lighthouse keeper who has seen things" }'
All three modes reserve one generation against your daily quota and run the same fail-closed validation gate before persisting.

Manage characters

MethodPathWhat
GET/v1/charactersList your characters (cursor-paginated).
GET/v1/characters/{id}Read one — metadata and the brain (you own it).
PATCH/v1/characters/{id}Update metadata + brain fields (persona, greeting, lore, voiceId, stances, intro, free).
DELETE/v1/characters/{id}Soft-delete.
POST/v1/characters/{id}/forksClone (world + brain) into a new character.
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.
const r = await fetch(`https://api.alakazam.gg/v1/characters/${id}/say`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    token: SESSION_TOKEN,
    text: "what are you working on?",
    currentStance: "neutral",
    history: [{ role: "user", text: "hi" }, { role: "character", text: "Oh — hello." }],
  }),
});
const turn = await r.json();
// → { reply, stance, emotion, affect, action }
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:
const audio = await fetch(`https://api.alakazam.gg/v1/characters/${id}/tts`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ token: SESSION_TOKEN, text: turn.reply }),
});
new Audio(URL.createObjectURL(await audio.blob())).play();
/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.
# mint a token FOR a specific player
curl -X POST https://api.alakazam.gg/v1/sessions/token \
  -H "Authorization: Bearer sk_live_…" -H "Content-Type: application/json" \
  -d '{"worldId":"<characterId>","playerIdentity":"your-user-id-123"}'
  • 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 — 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.

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.
SlugNameVoiceNotes
cafeMinaA quiet afternoon; inline persona (no bible).
neroNeroeleven_v3; rich character bible.
hugoHugoeleven_v3; the public free-demo character.
gojoGojoFull character bible.
makimaMakimaFull character bible.
astolfoAstolfoFull character bible.
Each file is the exact compiled character — its stance-graph, persona, lore, and voice — so the imported copy is faithful to the shipped one.