Rostodocs Open the Console

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://prod.rosto.ai/rig/avatar-rig.js?v=913c102f"></script>
<script>
  fetch("https://prod.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://prod.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://prod.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://prod.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:

// 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://prod.rosto.ai/v1/avatars/$ID/stickers?captions=matched" \\\n  -H "x-api-key: $ROSTO_KEY" -o stickers.zip

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://prod.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

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.

a selfie becomes a living, feeling face