Docs
Living avatars,
in a few lines.
One consenting selfie becomes an avatar that breathes, speaks and feels, animated live in any browser, with no AI at runtime.
Embed an avatar
Already have a public token? This is the whole integration: drop it anywhere. The token is a capability, so rotating it revokes every embed at once.
<div id="rosto"></div>
<script src="https://studio.rosto.ai/rig/avatar-rig.js?v=913c102f"></script>
<script>
fetch("https://studio.rosto.ai/pub/PUBLIC_TOKEN/manifest.json").then(r => r.json()).then(m => {
const rig = AvatarRig.mount(document.getElementById("rosto"), m, {size: 320});
rig.direct("await"); // present and waiting
// one call per line, and the word is what the line IS:
// rig.direct({ intent: "greet", say: "Hi! How can I help?" });
});
</script>
Create one via the API
Authenticate with x-api-key (get a key from the Console by
signing in, or redeeming an invite code). Consent is required. Your photo is kept encrypted only until you accept the avatar, then deleted.
# 1 — create an avatar (get a key from the Console: sign in, or redeem an invite)
curl -sX POST https://studio.rosto.ai/v1/avatars \
-H "x-api-key: $ROSTO_KEY" -H "content-type: application/json" \
-d '{"image_base64": "'"$(base64 -i selfie.jpg)"'", "mime_type": "image/jpeg", "consent_attested": true}'
# -> { "avatar": { "id": "...", "public_token": "TOKEN" } }
# 2 — build the expression set
curl -sX POST https://studio.rosto.ai/v1/avatars/$AVATAR_ID/frames -H "x-api-key: $ROSTO_KEY"
# 3 — the runtime loads the public manifest (no key; the token is the capability)
curl -s https://studio.rosto.ai/pub/$TOKEN/manifest.json
Drive the rig
One call per line, and the word is what the line is. Your app already knows whether it is
greeting, apologising or correcting; rig.direct() takes that and performs it — mood, state,
expression, head language and lip-sync as one coherent posture. There is nothing per-word or
per-expression to drive, and no state to reset between turns.
rig.direct({ intent: "empathize", say: "I completely understand. We will fix this." });\n\n// with your TTS audio, the voice locks to the lips (starts on its play event):\nrig.direct({ intent: "reassure", say: "All fixed.", audio: new Audio(url) });\n\n// between turns, still one call:\nrig.direct("attend"); // the user is speaking or typing\nrig.direct("ponder"); // you are working — a tool call, a lookup\n\n// AvatarRig.INTENTS lists all 14: greet · await · attend · ponder · empathize ·\n// apologize · reassure · laugh · celebrate · agree · disagree · surprised · curious · farewell
An intent is a complete performance. The same intent gives the same face whatever preceded it, so you never compose one out of smaller commands and never clear state first. If an intent reads wrong for your content, fix the preset once — do not compensate at the call site.
Where the intent comes from is the one design decision that matters, and the common answer is the
wrong one: send a dialogue act, not a sentiment score. “My package arrived smashed” is
strongly negative sentiment, but the right face is empathize — concerned, attending to you —
not sad, which reads as the avatar being upset about itself. If an LLM writes your replies,
have it return the intent in the same structured output; it costs one enum field and no extra latency.
The integration standard →
The lower-level vocabulary is still there when you need it — setState, setMood,
express, react, speak, and a JSON command stream — but
reach for it only for something an intent cannot say.
rig.setState("listening"); // idle · listening · thinking · speaking · acknowledging\nrig.setMood("warm");\nrig.express("genuine-joy"); // the manifest lists what this avatar supports\nrig.command({ state: "speaking", emotion: "curious", say: "Tell me more?" });
Full vocabulary and dynamics: the control protocol.
Adding voice
Rosto animates the face; it does not include a voice. You bring the audio, which keeps you free to use any provider (or none). Two ways to run it:
- Silent — skip
speak; the face reacts while your user reads (states and emotions). - Voiced — call
rig.speak(text)the moment your audio starts, so the lips move with it.
// THE RULE: lips start WITH the audio, never with the request.
// TTS generation takes seconds — call rig.speak too early and the voice
// lands on a face that already finished talking.
// Free, client-side — perfect for a demo:
const say = (t) => {
const u = new SpeechSynthesisUtterance(t);
u.onstart = () => rig.speak(t); // lips start exactly when the voice does
speechSynthesis.speak(u);
};
// Production — fetch your TTS first, then let speakWith handle the sync:
async function speak(text) {
const url = await myTTS(text); // your provider returns an audio URL
rig.speakWith(text, new Audio(url)); // starts playback, lips on its 'play'
} // event; still talks if audio is blocked
// speakWith gives you four sync layers automatically:
// 1. START lock — lips begin on the audio's 'play' event, never earlier
// 2. LENGTH lock — the viseme timeline is paced to the real audio duration
// 3. LIVE follow — an analyser tracks the actual sound: pauses close the
// mouth, stressed syllables open it (fails open — cadence — if the
// browser blocks the audio tap)
// 4. ALIGNMENT — the letter clock pauses with the voice and resumes on the
// next word, re-anchoring the mouth-shape sequence at every pause; when
// the audio ends, so do the lips
// Perfect per-phoneme sync (providers with character timestamps, e.g.
// ElevenLabs): drive rig.setViseme(...) from the timestamp stream instead.
For production, any text-to-speech works (ElevenLabs, OpenAI, Google, Azure). Start rig.speak(text)
together with playback for the tightest sync; it is provider-agnostic.
Reaction stickers
Every built avatar also renders as eight looping reactions — greeting, laughing, delighted, empathizing, agreeing, thinking, surprised, winking — in both animated GIF and the 512px WebP that WhatsApp and Telegram sticker trays want. Rendered server-side from the frames you already paid for, at no extra model cost.
curl -sL "https://studio.rosto.ai/v1/avatars/$ID/stickers?captions=matched" \\\n -H "x-api-key: $ROSTO_KEY" -o stickers.zip
captions=matched— each reaction gets its own line ("hey!", "lmao", "100%"…). One phrase across eight different emotions cannot be coherent, so there is no single-caption default.caption=…— the same text on all eight, for a name or a handle where repetition is the point.clips=greet,laugh— a subset, if you only want some.
The ZIP carries a README with GIPHY-ready tags. Every GIF is provenance-marked as AI-generated, on every tier. See them moving →
Connect an agent (MCP)
Rosto ships a remote MCP server so an agent can go from selfie to embedded avatar on its own.
// Point any MCP client at the Rosto server (JSON-RPC 2.0, streamable HTTP):
{ "mcpServers": { "rosto": { "url": "https://mcp.rosto.ai/mcp" } } }
// Then the agent walks:
// start_free() -> show the user_code + approve_url to the person you're
// helping; they sign in and press Approve (one tap)
// start_free(user_code) -> api_key
// -> create_avatar(api_key, image_base64, mime_type, consent)
// -> build_frames(api_key, avatar_id) -> get_embed_guide(public_token)
Reference
- Avatar Package spec (avatar-package/1.0)→
- OpenAPI schema→
- Interactive API explorer→
- llms.txt (for agents)→
- Console — bring a face to life→
Every avatar is consented; the photo is kept encrypted only until it's accepted, then permanently deleted; generated media is provenance-stamped; tokens are revocable to the CDN edge.