# Realtime Avatar > A realtime AI avatar API and TypeScript SDK. Give a character a face and a voice, put them > on a live call in a browser or a native app, and let them use your tools. One mint endpoint > on your server decides who may call and what the character knows; one component renders it. The sections between generated markers below are rebuilt by `bun scripts/content/sync-content.ts` — do not hand-edit them. ## Start here - [Overview](https://realtimeavatar.ai/docs): what the product is and what a call costs. - [Quickstart](https://realtimeavatar.ai/docs/quickstart): key → app on an example avatar → your own character. - [Authentication](https://realtimeavatar.ai/docs/authentication): what belongs on your server and why. ## Machine-readable surfaces - Full docs as one file: https://realtimeavatar.ai/llms-full.txt - Every docs page as markdown: append `.md` to its URL. - OpenAPI 3.1 for the whole API: https://realtimeavatar.ai/openapi.json — 18 operations, request and response schemas, and the scope each one needs. Generate a client from it rather than reading the endpoint table. ## Documentation (raw markdown) Every docs page is mirrored as plain markdown — append `.md` to any docs URL. - [Realtime Avatar API](https://realtimeavatar.ai/docs.md): Put a live character in your product, on a voice or video call, with your own tools wired into the conversation. - [Quickstart](https://realtimeavatar.ai/docs/quickstart.md): From an API key to a live avatar call in three steps: get a key, build the app on an example avatar, then swap in a character of your own. - [Authentication](https://realtimeavatar.ai/docs/authentication.md): Get a key, keep it on your server, and decide who may start a call. - [Next.js](https://realtimeavatar.ai/docs/nextjs.md): Mount the realtime avatar proxy in a Next.js App Router project: one route file on the server, one component in the browser. - [TanStack Start](https://realtimeavatar.ai/docs/tanstack-start.md): Mount the realtime avatar proxy in a TanStack Start app: a splat server route on the server, one component in the browser. - [Express](https://realtimeavatar.ai/docs/express.md): Mount the realtime avatar proxy as Express middleware — Express 4 or 5 — with the same authorize and session policy every adapter takes. - [Hono, Workers, Bun and Deno](https://realtimeavatar.ai/docs/hono.md): Mount the realtime avatar proxy on any Fetch-handler runtime — Hono, Cloudflare Workers, Bun or Deno — including the apiKey factory Workers require. - [React and React Native](https://realtimeavatar.ai/docs/react.md): The browser half: the AvatarCall component, the useAvatarCall hook underneath it, and what changes on React Native. - [Creating an avatar](https://realtimeavatar.ai/docs/video.md): One image in, a moving character out: the platform generates the looping idle video and a multi-clip motion library for you. Plus the generated state map, loop editing, and generation prices. - [Calls](https://realtimeavatar.ai/docs/sessions.md): What your server decides, what the client reports, and how to end a call without cutting the character off mid-sentence. - [Editing a character](https://realtimeavatar.ai/docs/editing.md): Change an avatar's clips and resting loop after creation: how to declare them, how each change settles, and the refusals worth telling apart. - [Tool calling](https://realtimeavatar.ai/docs/tool-calling.md): The platform never executes your tools. Here is the boundary, the client tool plane, and the server loop that turns a call into a working agent with a face. - [API reference](https://realtimeavatar.ai/docs/api-reference.md): Every public endpoint on https://realtimeavatar.ai/api/v1 — what it takes, what it returns, which scope it needs, and how it fails. - [Experimental features](https://realtimeavatar.ai/docs/experimental.md): What is experimental right now, what that word promises, and how to turn each one on. Included with Studio and above. ## Blog (raw markdown) Every post is mirrored as plain markdown — append `.md` to any post URL. - [Generated video that answers back: making a live avatar interactive, not just alive](https://realtimeavatar.ai/blog/generated-video-that-answers-back.md): A live avatar that performs on a loop is easy. One that changes course when you ask is a scheduling problem — and per-second pricing is what makes the fix affordable. - [Integrating the AI Talking Avatar API: Principles of Realtime Embodiment](https://realtimeavatar.ai/blog/integrating-the-ai-talking-avatar-api-principles-of-realtime-embodiment.md): Explore the technical foundations and practical integration of an AI talking avatar API. Understand how real-time presence, sub-second latency, and responsive embodiment are achieved for dynamic applications. - [Decoding Realtime Presence: An Engineer's Guide to the AI Avatar Video API](https://realtimeavatar.ai/blog/decoding-realtime-presence-an-engineers-guide-to-the-ai-avatar-video-api.md): Explore the core mechanics and practical applications of an AI avatar video API. Understand how real-time, interactive video streams empower dynamic user experiences, from customer support to immersive gaming. - [Live avatars in 2026: a field guide to the research](https://realtimeavatar.ai/blog/live-avatars-2026-research-field-guide.md): How real-time avatars became possible: the distillation recipe behind streaming video generation, the listening-head turn, full-duplex voice, and what the 2026 literature says is still hard. - [The Core Mechanics of a Realtime AI Avatar Generator API: From Image to Embodiment](https://realtimeavatar.ai/blog/the-core-mechanics-of-a-realtime-ai-avatar-generator-api.md): Delve into the technical underpinnings of a realtime ai avatar generator api. Explore how static images or video become expressive, live-synced digital personas, and the engineering behind sub-second presence. - [Demystifying HeyGen Live Avatar API Pricing for True Realtime Applications](https://realtimeavatar.ai/blog/demystifying-heygen-live-avatar-api-pricing.md): Navigating 'live' avatar solutions requires understanding their core technology. We examine HeyGen live avatar API pricing implications for true realtime presence versus rapid video generation. - [How much does a realtime AI avatar API cost in 2026? The $5/hour answer](https://realtimeavatar.ai/blog/realtime-ai-avatar-api-pricing.md): Realtime AI avatar API pricing compared across LiveAvatar, D-ID, and HeyGen — normalized to dollars per hour of live streaming — and why we anchored our plans at about $5/hour. - [Agentic localization: how this blog stays multilingual without a translation team](https://realtimeavatar.ai/blog/agentic-localization-pipeline.md): Every post on this blog can ship in English, Chinese, and Japanese. The translators are LLM agents; the editors are pull requests. Here's the pipeline. (also [zh](https://realtimeavatar.ai/blog/agentic-localization-pipeline/zh.md)) - [Looking for a HeyGen alternative? Decide what you're actually replacing first](https://realtimeavatar.ai/blog/heygen-alternative-for-realtime-avatars.md): A developer's guide to choosing a HeyGen alternative in 2026: video-generation platforms vs realtime avatar APIs, what each costs per minute, and when each one wins. - [HeyGen API pricing explained (2026): video credits, the LiveAvatar split, and the realtime math](https://realtimeavatar.ai/blog/heygen-api-pricing-explained.md): What HeyGen's API actually costs in 2026 — per-second video rates, the LiveAvatar realtime spin-off, credit conversions — and how the math changes for live conversational avatars. - [Payload CMS vs. a git-based blog: what we chose for an agent-written site](https://realtimeavatar.ai/blog/payload-cms-vs-git-based-blog.md): We researched running Payload 3 on Cloudflare Workers + D1, weighed it against keeping content in the repo, and picked the stack AI agents are best at maintaining. - [Synthesia pricing in 2026 — and the alternatives when you need more than rendered video](https://realtimeavatar.ai/blog/synthesia-pricing-and-alternatives.md): Synthesia's plans, what the API actually includes, and the alternative paths — D-ID, HeyGen, and realtime avatar APIs — when your use case is interactive rather than rendered. - [The Architecture of Presence: Integrating a Realtime AI Avatar API](https://realtimeavatar.ai/blog/the-architecture-of-presence-integrating-a-realtime-ai-avatar-api.md): How to integrate a realtime AI avatar API: the architecture behind sub-second, audio-clocked conversational avatars, and what it takes to ship one in your app. - [Companion apps grow up when the companion has a face](https://realtimeavatar.ai/blog/ai-companion-apps-with-a-face.md): Text-based AI companions hit a ceiling: presence. What changes — in retention, in intimacy, in product design — when your companion looks at the user and speaks. (also [zh](https://realtimeavatar.ai/blog/ai-companion-apps-with-a-face/zh.md)) - [Roleplay at scale: AI avatars for sales and corporate training](https://realtimeavatar.ai/blog/ai-avatars-for-corporate-training.md): The best training is rehearsal against a difficult human. Realtime avatars make that rehearsal infinitely available, repeatable, and measurable. - [What is a realtime avatar API? A builder's guide](https://realtimeavatar.ai/blog/what-is-a-realtime-avatar-api.md): From a single portrait to a character that answers in live, audio-synced video: the moving parts of a realtime avatar API and what to evaluate before you build on one. --- # Documentation — full pages # Realtime Avatar API > Put a live character in your product, on a voice or video call, with your own tools wired into the conversation. - Canonical: https://realtimeavatar.ai/docs - Updated: 2026-08-27 ## Voice or video, on one meter **TypeScript** ```tsx // she is on screen // audio only ``` **Python** ```python body = {"avatar_id": avatar_id, "mode": "avatar"} # she is on screen body = {"avatar_id": avatar_id, "mode": "voice"} # audio only ``` **It is priced to leave on.** Live conversation meters at **about $5 an hour** — $4.20 to $5.70 depending on plan — with a free tier to build against. That is the difference between a demo you show and a feature you ship: a companion app can afford to let someone talk. How she is rendered is in [Creating an avatar](https://realtimeavatar.ai/docs/video). ## She listens while she talks Most voice AI takes turns like a walkie-talkie: you talk, you stop, it answers. This one is full-duplex — she is hearing you the whole time she is speaking — and in practice that shows up as four things you do not have to build: - **Interrupt her and she stops**, mid-sentence, and can acknowledge it in character rather than snapping to silence. - **A cough or an "mm-hm" does not derail her.** A backchannel is not an interruption, and being cut off by one is what makes a system feel brittle. - **A pause is not the end of your turn.** Whether you are finished is judged by what you said, not by how long you have been quiet — so she neither talks over you nor leaves a gap. - **Silence and language switches are handled.** Going quiet is a signal she can act on, and she follows a language change inside a single sentence. Nothing to configure — it is how every call behaves. The one limit worth knowing: she will not talk over you with a new sentence while you are speaking. That is a deliberate trade for the voice and model choices this platform is built on. ## Your tools, wired into the conversation A character that can only talk is a demo. The useful version books the appointment, checks the order, writes the code and keeps talking while it runs. You declare a tool the same way you would brief a colleague: what it is called, and *when to reach for it*. The description is the whole teaching signal — she reads it and decides. **TypeScript** ```ts import type { AvatarTool } from "realtime-avatar/tools"; export const checkOrder: AvatarTool<{ order_id: string }> = { description: "Look up the status of a customer's order. Call this whenever they ask " + "where something is, or when it will arrive.", parameters: { type: "object", properties: { order_id: { type: "string" } }, required: ["order_id"], }, execute: async ({ order_id }, { signal }) => { const order = await api.order(order_id, { signal }); return `${order.status}, arriving ${order.eta}.`; }, }; ``` **Python** ```python # Tools run in the page that renders the call — there is no hosted execution, # so there is nothing to declare in Python. Your backend's part is the GRANT: body["capabilities"] = ["client_tools"] # The mint schema is strict: a tools[] field on the request itself is a 422. # The page registers the manifest over RPC once the room is connected. ``` Then your server grants the tool plane at mint, and the page registers the tools once the room is connected. Omitting `execute` is a compile error rather than a timeout you find in production — and a tool has **2.5 seconds** to answer, so anything slow acknowledges fast and delivers the real result out of band: **TypeScript** ```ts // server — the session policy grants the client tool plane for this call session: async ({ avatarId }) => ({ instructions, clientTools: true }) // client — register over RPC after connect; the record key is the tool's name import { attachAvatarTools } from "realtime-avatar/tools"; const { accepted, rejected } = await attachAvatarTools(room, { check_order: checkOrder, }); ``` **Python** ```python # server — grant the capability on the mint. That is the whole backend part: body["capabilities"] = ["client_tools"] # The worker only exposes tool registration to a session minted with the # capability. If the page reports the registration method is missing, the fix # is HERE, not in the page. ``` Full pattern: [Tool calling](https://realtimeavatar.ai/docs/tool-calling). > The `createRealtimeAvatarRoute` and `AvatarCall` surface described here needs `realtime-avatar@0.6.0` or newer. On an older install those exports do not exist and the compiler will say so — update the package rather than working around it. ## Where to go next - [Quickstart](https://realtimeavatar.ai/docs/quickstart) — key to live call in three steps. - [Calls](https://realtimeavatar.ai/docs/sessions) — the policy your server decides, the five states, ending gracefully. - [Creating an avatar](https://realtimeavatar.ai/docs/video) — one image in; the platform generates the loop and a map of states it switches between, or synthesizes the video live. - [Tool calling](https://realtimeavatar.ai/docs/tool-calling) — running your own agent loop against a live character. - [API reference](https://realtimeavatar.ai/docs/api-reference) — every endpoint, scope, and error. --- Realtime Avatar — realtime AI avatar API & SDK. Docs: https://realtimeavatar.ai/docs · Agent guide: https://realtimeavatar.ai/llms.txt # Quickstart > From an API key to a live avatar call in three steps: get a key, build the app on an example avatar, then swap in a character of your own. - Canonical: https://realtimeavatar.ai/docs/quickstart - Updated: 2026-09-02 Three steps. Your first call runs on a public example avatar, so nothing is blocked on creating one — and the app is one step, not two: a server half that holds the key and a client half that renders. The SDK ships both. ## 1. Get a key and install the SDK Create a key in the [dashboard](https://realtimeavatar.ai/platform/dashboard) — it looks like `tic_live_…` or `tic_test_…` and is shown once. It arrives with every scope enabled, so nothing 403s on your first call — untick the ones this key should not have. The SDK is on npm — no registry configuration, no auth token: **TypeScript** ```bash npm install realtime-avatar ``` Self-contained: the contracts package is bundled into the build, so there is no private dependency to resolve. Pin an exact version for reproducible CI. **Python** ```bash pip install httpx ``` There is no Python SDK — the live call is rendered by a browser or native client, so Python's half is plain HTTP and one dependency. ## 2. Build the app on an example avatar An app is two halves and the SDK ships both: a connect endpoint on your server, and one component in the browser. Hand this whole step to a coding agent if you would rather not type it — the prompt is on your key in [settings](https://realtimeavatar.ai/platform/settings#api-keys). ### The server half — the connect endpoint Your API key must never reach a browser, so the browser talks to your app and your app talks to us. One function is that endpoint — it decides **who** may start a call and **what** the character knows when they do: **Next.js** ```tsx // app/api/realtime-avatar/[...path]/route.ts import { createRealtimeAvatarRoute } from "realtime-avatar/nextjs"; export const { GET, POST } = createRealtimeAvatarRoute({ apiKey: process.env.REALTIME_AVATAR_API_KEY!, // Who may do this. "connect" is the only operation that costs money to start, // so that is where the wallet check belongs; the reads stay cheap. authorize: async ({ request, operation }) => { const user = await currentUser(request); if (!user) return new Response("Unauthorized", { status: 401 }); if (operation === "connect" && !(await hasCredits(user))) { return Response.json({ code: "insufficient_credits" }, { status: 402 }); } }, // What the character knows for THIS call. Whatever the browser sent for these // concerns is discarded — this callback is the only source. session: async ({ request, avatarId, mode }) => { const user = await currentUser(request); const character = await db.character(avatarId); return { instructions: character.prompt, context: await db.recentTurns(user.id, avatarId), maxSeconds: secondsTheBalanceAffords(user, mode), }; }, }); ``` The catch-all segment matters — the route answers four paths under one prefix (`POST /connect`, `POST /end`, `GET /avatars`, `GET /credits`), so a single `[...path]` segment serves all of them. **TanStack Start** ```tsx // src/routes/api/realtime-avatar/$.ts import { createFileRoute } from "@tanstack/react-router"; import { realtimeAvatarServerRoute } from "realtime-avatar/tanstack-start"; const handlers = realtimeAvatarServerRoute({ apiKey: process.env.REALTIME_AVATAR_API_KEY!, authorize: async ({ request, operation }) => { const user = await requireUser(request); if (!user) return new Response("Unauthorized", { status: 401 }); if (operation === "connect" && !(await hasCredits(user))) { return Response.json({ code: "insufficient_credits" }, { status: 402 }); } }, session: async ({ request, avatarId, mode }) => { const user = await requireUser(request); const character = await loadCharacter(avatarId); return { instructions: buildPrompt(character), context: await loadRecentTurns(user.id, avatarId), maxSeconds: secondsTheBalanceAffords(user, mode), }; }, }); export const Route = createFileRoute("/api/realtime-avatar/$")({ server: { handlers }, }); ``` The trailing `$` is Start's splat segment — without it the handler only ever sees the mount path and answers 404. Build the handlers once at module scope. On Cloudflare Workers pass a factory — `apiKey: () => getEnv().KEY` — since there is no `process.env`. **Python** ```python # Your backend decides; the browser renders. Same split, no SDK needed. import os, httpx from fastapi import Depends, FastAPI from fastapi.responses import JSONResponse from pydantic import BaseModel BASE = "https://realtimeavatar.ai/api/v1" client = httpx.Client(base_url=BASE, timeout=30.0, headers={ "Authorization": f"Bearer {os.environ['REALTIME_AVATAR_API_KEY']}"}) app = FastAPI() class StartCallRequest(BaseModel): avatar_id: str mode: str = "avatar" # "avatar" | "voice" queue_ticket_id: str | None = None # a client retrying after a 429 hands its ticket back @app.post("/api/calls") def start_call(req: StartCallRequest, user=Depends(current_user)): if not has_credits(user): return JSONResponse({"code": "insufficient_credits"}, 402) character = load_character(req.avatar_id) seconds = int(seconds_the_balance_affords(user, req.mode)) # wire: int, 1..1800 body = { "avatar_id": character.avatar_id, "mode": req.mode, "stt_mode": "server", # Decided HERE. The client sends none of this. "instructions": build_prompt(character), # ≤32 of exactly {"role": "system" | "user" | "assistant", "content": 1..4000 chars} — # strict: any other key is a 422 "initial_context": [{"role": t.role, "content": t.text} for t in recent_turns(user.id, character.avatar_id)][-32:], "max_session_seconds": max(1, min(seconds, 1800)), # Optional: a signed POST of the transcript when the call ends (receiver: Tool calling). "transcript_webhook": {"url": "https://your.app/api/rta/transcript", # https only "secret": os.environ["TRANSCRIPT_SECRET"]}, # 16..200 chars "client_metadata": {"user_id": str(user.id), # ≤16 string pairs, "character_id": character.avatar_id}, # echoed back to you } if req.queue_ticket_id: body["queue_ticket_id"] = req.queue_ticket_id r = client.post("/realtime/livekit/session", json=body) data = r.json() if r.status_code == 429 and "queue_ticket_id" in data: # Every slot busy — the queue, not an error. Relay it VERBATIM: the busy body # carries queue_ticket_id (the only handle that RELEASES the slot when the # caller gives up) and recommended_retry_ms (when to come back). The client # retries WITH the ticket; reshaping this strands it for its whole TTL. return JSONResponse(data, 429) if not r.is_success: # 402 / 409 / 422 — and the 429s that are NOT the queue (plan concurrency # ceiling, rate limit). Forward {error, code?}, but not at 429: an SDK # client reads every 429 as the queue. return JSONResponse(data, 503 if r.status_code == 429 else r.status_code) return data # return this VERBATIM ``` Two rules when hand-rolling. First, casing is **per endpoint**, and every endpoint is strict — an unknown or mis-cased key is a rejected call. The two realtime routes shown here (`/realtime/livekit/session` and its `/release`) are **snake_case**; every REST resource endpoint — avatars, clips, keys, assets — is **camelCase**. Measured against the published spec: 2 of the 9 request bodies are snake_case at the top level and the other 7 are camelCase — except `POST /avatars`, which is camelCase outside and snake_case inside `voice` (`auto_description`, `voice_id`), as the sample above shows. And the SDK's policy names are not the wire names: `context` is `initial_context`, `maxSeconds` is `max_session_seconds`, `transcript` is `transcript_webhook`, `metadata` is `client_metadata`. Second, if your client uses this SDK the response must reach it **byte-for-byte** — the SDK hands the body to the room unvalidated, so a wrapped grant never connects: the token and URL read as undefined and the call sits in `connecting` with no error. A client driving the room directly reads only the fields it needs and tolerates extras, but relaying verbatim keeps both paths working. The same goes for a busy `429`: it is the queue only when the body carries `queue_ticket_id`, and a retry that does not present that ticket mints a fresh one every poll — its position never advances. Everything in `session` is authoritative: whatever the browser sends for the persona, the memory, the voice, or the time limit is discarded before the request leaves your server. A field your policy does not set is absent rather than inherited, so an omission fails closed rather than open. `maxSeconds` is the one to get right on day one — it is what stops a call your balance cannot cover. See [Authentication](https://realtimeavatar.ai/docs/authentication) for the full list of what belongs on the server and why. ### The client half — render the call Build the client once, then render one component. It handles the queue, reconnects, the idle timer, and the video surface for you. Pass a public example avatar id to`avatarId` — `seed-rin-ashfall` is one — and this call is live before you have created anything; step 3 swaps in your own: An example avatar serves its own still and idle loop, so the surface has something to show before the first frame arrives. A catalog row with no `idleVideoUrl` is a stream-only host published for one of the live channels; a call to one is refused with 409, so call an id whose row carries an idle loop. The two below are `seed-rin-ashfall`’s. **Both props are optional and the filenames are not a scheme** — they differ from character to character, so copy the pair that belongs to the avatar you are using rather than deriving them from its name. Point `poster` and `idleVideoUrl` at them and the page is never empty: she is on screen while the call is still connecting. **React** ```tsx import { AvatarCall, createProxyClient } from "realtime-avatar/react"; const client = createProxyClient({ proxyUrl: "/api/realtime-avatar" }); export function Call({ avatarId }: { avatarId: string }) { return ( showEndScreen(reason)} > {(call) => call.status === "waiting" ? In line: {call.queuePosition} : null } ); } ``` Live media needs the DOM. In Next.js mark the file `"use client"` and import it with `{ ssr: false }`; in TanStack Start gate it on a mounted flag or load it lazily. **React Native** ```tsx import { AvatarVideoSurface, RealtimeAvatarLiveKitRoom, SessionLifecycleRoomBridge, createProxyClient, registerGlobals, useSessionLifecycle, } from "realtime-avatar/react-native"; registerGlobals(); // once, at app startup // Native has no page origin — the proxy URL must be ABSOLUTE. const client = createProxyClient({ proxyUrl: "https://your-app.example.com/api/realtime-avatar", }); export function Call({ avatarId }: { avatarId: string }) { const lifecycle = useSessionLifecycle({ client, session: { avatarId } }); if (!lifecycle.grant) return null; // map lifecycle.phase to your UI return ( — hand the idle clip to YOUR player, looped and muted. renderIdleVideo={({ url, style }) => } /> ); } ``` **Native is the lower-level surface today.** `` is web-only — the session brain is the same module, but you compose the native room and video surface yourself, and you pick the video player (`expo-video`, `react-native-video`). Live media does not run in Expo Go; you need a dev client or an EAS build. `status` is five values — `connecting` · `waiting` · `live` · `recovering` · `ended` — and you write the copy for the ones you care about. The SDK ships no strings. The `children` render prop hands you a `call` handle: `say()`, `sayAndEnd()`, `keepAlive()`, `end()`, and `secondsRemaining`. Want your own layout around the video rather than on top of it? `useAvatarCall()` returns `{ call, view }` so you can place the view anywhere. **React Native** runs the same session brain with native media — `useSessionLifecycle`, the grant and queue handling, and the quality governor are the identical modules; only the room bridge and the video surface are native twins. Import from `/react-native`, call `registerGlobals()` at startup, and pass an absolute `proxyUrl` (native has no page origin). The one-component `` wrapper is web-only for now. ## 3. Make the character yours The example avatars get you a working call; your own character is one create call away, and nothing above changes but the id. An avatar is one portrait image plus a voice. Upload the image and the platform does the rest **in the background**: it generates the looping idle video and a small motion library — an idle variant, a listening state, a gesture — from that single frame. From a trusted server: **TypeScript** ```ts import { RealtimeAvatar } from "realtime-avatar"; const rta = new RealtimeAvatar({ apiKey: process.env.REALTIME_AVATAR_API_KEY! }); // Upload the portrait, then create the avatar from it. const asset = await rta.uploadAsset(portraitBlob, { kind: "image", filename: "rin.png" }); const avatar = await rta.createAvatar({ displayName: "Rin", sourceKind: "image", // one frontal portrait: jpeg/png/webp, up to 8MB sourceAssetId: asset.id, voice: { auto_description: "Warm, clear, mid-pitch — natural and conversational." }, }); console.log(avatar.id); // ava_… console.log(avatar.status); // "preprocessing" — generation has started ``` **Python** ```python import os, time, httpx client = httpx.Client( base_url="https://realtimeavatar.ai/api/v1", timeout=60.0, headers={"Authorization": f"Bearer {os.environ['REALTIME_AVATAR_API_KEY']}"}) # Register the portrait, then create the avatar from it. asset = client.post("/assets/remote", json={ "kind": "image", "remoteUrl": "https://cdn.example.com/rin/portrait.png", }).json() avatar = client.post("/avatars", json={ "displayName": "Rin", "sourceAssetId": asset["id"], "voice": {"auto_description": "Warm, clear, mid-pitch — natural and conversational."}, }).json() print(avatar["id"]) # ava_… print(avatar["status"]) # "preprocessing" — generation has started # ("failed" + avatar["error"] if it could not be queued) # Poll until the loop and clips have rendered — a minute or two. while avatar["status"] == "preprocessing": time.sleep(15) avatar = client.get(f"/avatars/{avatar['id']}").json() print(avatar["status"]) # "ready" — mint calls; "failed" — avatar["error"] says why ``` > **Creation is asynchronous.** The create call returns in milliseconds with `status: "preprocessing"`; the idle loop and the clip library render in the background over the next minute or two. Poll `getAvatar(id)` (`GET /avatars/{id}` on the wire) until `status` is `ready` before minting a call. Before the loop attaches a mint is refused with a clear error — never a broken call. **Why a `failed` status happened is not on the SDK object.** `getAvatar()` returns the five fields an integration branches on — `id`, `displayName`, `sourceKind`, `status`, `defaultVoiceId` — and nothing else. The wire's `GET /avatars/{id}` carries the diagnostics on top of those: `error` (the reason a `failed` avatar failed) and `idleVideoStatus` (how far the loop got). Read those with a plain authenticated fetch when you are showing a human what went wrong. And do not branch on `sourceKind` to tell a finished avatar from a fresh one: once the generated loop attaches it reads `video`, and `sourceAssetId` names the loop now serving rather than the portrait you uploaded (that is `anchor.url` on `GET /avatars/{id}/clips`). Gate on `status` — or `idleVideoStatus` — never on `sourceKind == "image"`. `motionPrompt` optionally art-directs the generated loop ("soft cafe light, gentle idle energy") — set it as **Resting motion direction** on the [Avatars page](https://realtimeavatar.ai/platform/avatars) or on the wire's `POST /avatars`; omit it for the house default. Once `ready`, `GET /avatars/{id}/clips` lists the generated motion library — render calls inherit it automatically. The Avatars page's **Clips** action provides the same full-library declaration flow for generated prompt clips when the workspace rollout is enabled. `voice: { auto_description }` lets the platform pick the best-matching Fish Audio voice from your description; pass `voice: { voice: { provider, voice_id } }` to choose one explicitly — an explicit voice is nested under `voice.voice` (in Python, `"voice": {"voice": {"provider": "fish", "voice_id": "…"}}`); a top-level `voice: { provider, voice_id }` is a `422`. Omit `voice` entirely to keep the avatar's current default. You can also do all of this on the [Avatars page](https://realtimeavatar.ai/platform/avatars) with no code — see [Creating an avatar](https://realtimeavatar.ai/docs/video). Either way you end up with an `ava_…` id, which is the only thing your client needs to know. **Build from an image.** One portrait in, every piece of video generated by the platform — that is the path these docs describe. An image source queues the motion generation above: the avatar sits in `preprocessing` until the idle loop and the clip library are rendered, and only then does it mint calls. **There is no other lane.** `sourceKind: "video"` — registering a looping clip you host — is closed to new callers and answers `422`. (That is the field you send on create; a ready image-built avatar reads `sourceKind: "video"` too, because the loop the platform generated is now its source.) An avatar is built from one portrait and the platform renders the idle loop and every motion clip from it; that shared rest pose is what makes a state switch a splice instead of a jump, and a supplied video cannot honour it. Tenants already creating from video keep working, and their existing avatars are untouched — the door is shut for new integrations, not behind anyone. > **Your editor may still suggest `createAvatarFromVideo()`.** It is on its way out and its doc comment is older still — it warns that an image-sourced avatar publishes a black track, which stopped being true when image-only creation shipped. Use `createAvatarFromImage()`. ## That is the whole surface Two calls: `createRealtimeAvatarRoute` on the server, `AvatarCall` on the client. You never touch the transport, the reconnect logic, or a wire format — and because you never touch them, we can change them underneath you without breaking your app. If you do want that control — your own media policy, your own transport handling — every lower-level primitive is still exported and still supported. Most teams never need to. ## What to read next - [Calls](https://realtimeavatar.ai/docs/sessions) — the policy your server decides, the five states, and how to end a call without cutting her off mid-sentence. - [Tool calling](https://realtimeavatar.ai/docs/tool-calling) — if your character needs to actually do things. --- Realtime Avatar — realtime AI avatar API & SDK. Docs: https://realtimeavatar.ai/docs · Agent guide: https://realtimeavatar.ai/llms.txt # Authentication > Get a key, keep it on your server, and decide who may start a call. - Canonical: https://realtimeavatar.ai/docs/authentication - Updated: 2026-08-27 ## Keys Create one in the [dashboard](https://realtimeavatar.ai/platform/dashboard). It is shown once, and it goes in a bearer header: ```text Authorization: Bearer tic_live_… ``` Keys are environment-tagged — `tic_test_…` for development, `tic_live_…` for production — and each one can carry its own spend limit, which is useful when handing a key to a subsystem you would rather cap. The cap is enforced against the wallet, so it does nothing on a workspace billed as **unlimited**: that account never consults a balance, and the per-key check sits behind the one it skips. The limit is still accepted and echoed back, so if you are on an unlimited plan, treat a per-key cap as a note to yourself rather than a control. **The tag is organisational, not a sandbox.** It picks the prefix and nothing else: a `tic_test_…` key mints real sessions on the same fleet and bills the same credits against the same balance as a `tic_live_…` one. Nothing about your account is different in "test". That is worth saying plainly, because the naming is the one most APIs use for a free, isolated test mode, and it is not that here — the way to develop without spending production credits is a separate workspace, or a per-key `spendLimitCreditMicros` low enough to fail closed. ### Scopes You pick these at creation. A key made in the dashboard starts with every scope enabled except `*`, so it works straight away — untick whatever this key has no business doing, particularly `api_keys:write`. Creating one over the API instead and omitting `scopes` gives you the narrower `realtime:write`, `avatars:read`, `credits:read`. | Scope | Grants | | --- | --- | | `realtime:write` | Start and end calls | | `avatars:read` | List and fetch avatars and assets | | `avatars:write` | Create and update avatars, upload assets, sync clips | | `credits:read` | Read the balance | | `usage:read` · `usage:write` | Usage reporting | | `api_keys:write` | Mint further keys | | `*` | Everything — avoid outside trusted back-office jobs | ## The one rule: the key stays on your server A browser holding the key could start unlimited calls on your account, so it never gets one. Your client talks to your app; your app talks to us. That is what the route adapters are — `createRealtimeAvatarRoute` and its siblings — mount one and `authorize` is your gate: **Next.js** ```tsx import { createRealtimeAvatarRoute } from "realtime-avatar/nextjs"; export const { GET, POST } = createRealtimeAvatarRoute({ apiKey: process.env.REALTIME_AVATAR_API_KEY!, // never NEXT_PUBLIC_ prefixed authorize: async ({ request, operation }) => { const user = await currentUser(request); if (!user) return new Response("Unauthorized", { status: 401 }); // "connect" is the only operation that costs money to START. if (operation === "connect" && !(await hasCredits(user))) { return Response.json({ code: "insufficient_credits" }, { status: 402 }); } }, }); ``` **TanStack Start** ```tsx import { realtimeAvatarServerRoute } from "realtime-avatar/tanstack-start"; const handlers = realtimeAvatarServerRoute({ apiKey: () => getEnv().REALTIME_AVATAR_API_KEY, // a factory works on Workers authorize: async ({ request, operation }) => { const user = await requireUser(request); if (!user) return new Response("Unauthorized", { status: 401 }); if (operation === "connect" && !(await hasCredits(user))) { return Response.json({ code: "insufficient_credits" }, { status: 402 }); } }, }); ``` **Python** ```python # No handler to mount — your own endpoint is the gate. from fastapi import Depends, HTTPException from fastapi.responses import JSONResponse @app.post("/api/calls") def start_call(req: StartCallRequest, user=Depends(current_user)): if not user: raise HTTPException(401) if not has_credits(user): # starting a call is the costly one # HTTPException would nest this under "detail"; keep code top-level. return JSONResponse({"code": "insufficient_credits"}, 402) ... ``` Return a `Response` to refuse, or nothing to allow. There are four operations: `connect`, `end`, `avatars`, and `credits`. Put your wallet check on `connect` — it is the only one that costs money to start — and leave the reads cheap. The route exposes no avatar mutation at all: creating or deleting a character is a server-side job for the `RealtimeAvatar` class, never something a browser reaches through this handler — so there is nothing to forget to gate. Every operation still passes `authorize`, so this callback is the gate, not a second line behind one. A key with a `NEXT_PUBLIC_` or `VITE_` prefix is inlined into the client bundle at build time. That is the one mistake that actually leaks a key, and the prefix is the only warning you get. ## What the client is not allowed to decide The persona, the memory, the voice, and the time limit are all yours. The handler strips them from whatever the browser sent and uses your `session` policy instead — and a field your policy does not set is **absent** rather than inherited, so forgetting one fails closed. The one worth getting right on day one is `maxSeconds`: it is what stops a call your balance cannot cover. See [Calls](https://realtimeavatar.ai/docs/sessions) for the full policy. --- Realtime Avatar — realtime AI avatar API & SDK. Docs: https://realtimeavatar.ai/docs · Agent guide: https://realtimeavatar.ai/llms.txt # Next.js > Mount the realtime avatar proxy in a Next.js App Router project: one route file on the server, one component in the browser. - Canonical: https://realtimeavatar.ai/docs/nextjs - Updated: 2026-09-02 Your API key must never reach a browser, so the browser talks to your app and your app talks to us. In Next.js that is one file. ## The server half App Router, mounted at a catch-all so every operation reaches the handler. The `[...path]` segment is not decoration — without it the route only ever matches the mount path itself and answers 404 for `connect`, `end`, `avatars` and `credits` alike. **Next.js** ```tsx // app/api/realtime-avatar/[...path]/route.ts import { createRealtimeAvatarRoute } from "realtime-avatar/nextjs"; export const { GET, POST } = createRealtimeAvatarRoute({ apiKey: process.env.REALTIME_AVATAR_API_KEY!, // Who may do this. Return a Response to refuse, or nothing to allow. // "connect" is the only operation that costs money to start, so that is // where the wallet check belongs; the reads stay cheap. authorize: async ({ request, operation }) => { const user = await currentUser(request); if (!user) return new Response("Sign in", { status: 401 }); if (operation === "connect" && !user.credits) { return Response.json({ code: "insufficient_credits" }, { status: 402 }); } }, // What the character knows. Decided HERE — the client sends none of it. session: async ({ avatarId }) => ({ instructions: promptFor(avatarId), }), }); ``` Name the variable **without** `NEXT_PUBLIC_`. That prefix is what inlines a value into the client bundle, so a key wearing it ships to every visitor — the one mistake this whole split exists to prevent. ## The client half One component, pointed at the route you just mounted. It is the same component on every framework — see [React](https://realtimeavatar.ai/docs/react) for the hook underneath it and for controlling the call yourself. **Next.js** ```tsx "use client"; import { AvatarCall, createProxyClient } from "realtime-avatar/react"; const client = createProxyClient({ proxyUrl: "/api/realtime-avatar" }); export function Call({ avatarId }: { avatarId: string }) { return ; } ``` `"use client"` is required: the call holds a WebRTC room and browser media, none of which exist in a server component. ## Next - [Authentication](https://realtimeavatar.ai/docs/authentication) — keys, scopes, and what `authorize` is really gating. - [Calls](https://realtimeavatar.ai/docs/sessions) — everything the `session` policy can decide. --- Realtime Avatar — realtime AI avatar API & SDK. Docs: https://realtimeavatar.ai/docs · Agent guide: https://realtimeavatar.ai/llms.txt # TanStack Start > Mount the realtime avatar proxy in a TanStack Start app: a splat server route on the server, one component in the browser. - Canonical: https://realtimeavatar.ai/docs/tanstack-start - Updated: 2026-09-02 Your API key must never reach a browser, so the browser talks to your app and your app talks to us. In Start that is one server route. ## The server half Mount at `routes/api/realtime-avatar/$.ts`. The trailing `$` is Start's splat segment and it is **load-bearing**: without it the handler only ever sees the mount path itself and answers 404 for every operation — the same trap Next.js's `[...path]` exists to avoid. **TanStack Start** ```tsx // routes/api/realtime-avatar/$.ts import { createFileRoute } from "@tanstack/react-router"; import { realtimeAvatarServerRoute } from "realtime-avatar/tanstack-start"; export const Route = createFileRoute("/api/realtime-avatar/$")({ server: { handlers: realtimeAvatarServerRoute({ apiKey: process.env.REALTIME_AVATAR_API_KEY!, authorize: async ({ request, operation }) => { const user = await currentUser(request); if (!user) return new Response("Sign in", { status: 401 }); if (operation === "connect" && !user.credits) { return Response.json({ code: "insufficient_credits" }, { status: 402 }); } }, session: async ({ avatarId }) => ({ instructions: promptFor(avatarId), }), }), }, }); ``` On Cloudflare Workers there is no `process.env`, so pass `apiKey` as a **factory** — `apiKey: () => getEnv().REALTIME_AVATAR_API_KEY` — and it is read per request instead of at module scope. ## The client half **TanStack Start** ```tsx import { AvatarCall, createProxyClient } from "realtime-avatar/react"; const client = createProxyClient({ proxyUrl: "/api/realtime-avatar" }); export function Call({ avatarId }: { avatarId: string }) { return ; } ``` ## Next - [Authentication](https://realtimeavatar.ai/docs/authentication) — keys, scopes, and what `authorize` is really gating. - [Calls](https://realtimeavatar.ai/docs/sessions) — everything the `session` policy can decide. --- Realtime Avatar — realtime AI avatar API & SDK. Docs: https://realtimeavatar.ai/docs · Agent guide: https://realtimeavatar.ai/llms.txt # Express > Mount the realtime avatar proxy as Express middleware — Express 4 or 5 — with the same authorize and session policy every adapter takes. - Canonical: https://realtimeavatar.ai/docs/express - Updated: 2026-09-02 Your API key must never reach a browser, so the browser talks to your app and your app talks to us. In Express that is one middleware. ## The server half Express 4 and 5. Mount it on a path prefix, with a JSON body parser in front — the adapter reads a parsed body rather than a stream. **Express** ```ts import express from "express"; import { realtimeAvatarExpress } from "realtime-avatar/express"; const app = express(); app.use( "/api/realtime-avatar", express.json(), realtimeAvatarExpress({ apiKey: process.env.REALTIME_AVATAR_API_KEY!, authorize: async ({ request, operation }) => { const user = await currentUser(request); if (!user) return new Response("Sign in", { status: 401 }); if (operation === "connect" && !user.credits) { return Response.json({ code: "insufficient_credits" }, { status: 402 }); } }, session: async ({ avatarId }) => ({ instructions: promptFor(avatarId), }), }), ); ``` `express.json()` is not optional here. Express hands the adapter an already-parsed body, which it re-serializes to forward — fine for this route, whose payloads are small, but it is the reason the Fetch-based adapters ([Hono](https://realtimeavatar.ai/docs/hono), [Next.js](https://realtimeavatar.ai/docs/nextjs), [TanStack Start](https://realtimeavatar.ai/docs/tanstack-start)) are the better path when you have the choice. ## The client half Express serves your API; the browser half is the same component as everywhere else, pointed at the prefix you mounted. **React** ```tsx import { AvatarCall, createProxyClient } from "realtime-avatar/react"; const client = createProxyClient({ proxyUrl: "/api/realtime-avatar" }); ``` If your React app is served from a different origin than the Express API, give `proxyUrl` the absolute URL and allow credentials in your CORS policy — the proxy is your endpoint, so its auth is your session, not ours. ## Next - [Authentication](https://realtimeavatar.ai/docs/authentication) — keys, scopes, and what `authorize` is really gating. - [Calls](https://realtimeavatar.ai/docs/sessions) — everything the `session` policy can decide. --- Realtime Avatar — realtime AI avatar API & SDK. Docs: https://realtimeavatar.ai/docs · Agent guide: https://realtimeavatar.ai/llms.txt # Hono, Workers, Bun and Deno > Mount the realtime avatar proxy on any Fetch-handler runtime — Hono, Cloudflare Workers, Bun or Deno — including the apiKey factory Workers require. - Canonical: https://realtimeavatar.ai/docs/hono - Updated: 2026-09-02 This adapter is for Hono and for anything else built on Fetch handlers — Cloudflare Workers, Bun, Deno. It takes a `Request` and returns a `Response`, which is why one adapter covers all four. ## The server half Mount on a wildcard so every operation reaches the handler, not just the mount path. **Hono** ```ts import { Hono } from "hono"; import { realtimeAvatarHono } from "realtime-avatar/hono"; const app = new Hono<{ Bindings: Env }>(); app.all( "/api/realtime-avatar/*", realtimeAvatarHono({ // A FACTORY, not a value. On Workers there is no process.env, and a // module-scope read runs before any binding exists. apiKey: () => env.REALTIME_AVATAR_API_KEY, authorize: async ({ request, operation }) => { const user = await currentUser(request); if (!user) return new Response("Sign in", { status: 401 }); if (operation === "connect" && !user.credits) { return Response.json({ code: "insufficient_credits" }, { status: 402 }); } }, session: async ({ avatarId }) => ({ instructions: promptFor(avatarId), }), }), ); export default app; ``` On Node or Bun, where `process.env` exists, a plain string is fine: `apiKey: process.env.REALTIME_AVATAR_API_KEY!`. The factory form is what makes the Workers case work, and it is harmless everywhere else — the value is simply read per request instead of once at module scope. ## The client half **React** ```tsx import { AvatarCall, createProxyClient } from "realtime-avatar/react"; const client = createProxyClient({ proxyUrl: "/api/realtime-avatar" }); ``` ## Next - [Authentication](https://realtimeavatar.ai/docs/authentication) — keys, scopes, and what `authorize` is really gating. - [Calls](https://realtimeavatar.ai/docs/sessions) — everything the `session` policy can decide. --- Realtime Avatar — realtime AI avatar API & SDK. Docs: https://realtimeavatar.ai/docs · Agent guide: https://realtimeavatar.ai/llms.txt # React and React Native > The browser half: the AvatarCall component, the useAvatarCall hook underneath it, and what changes on React Native. - Canonical: https://realtimeavatar.ai/docs/react - Updated: 2026-09-02 Every server adapter mounts the same proxy, so the client half is the same everywhere: one component pointed at your route. ## The component **React** ```tsx import { AvatarCall, createProxyClient } from "realtime-avatar/react"; const client = createProxyClient({ proxyUrl: "/api/realtime-avatar" }); // Audio only — no video track is requested, so nothing renders and // nothing decodes. ``` ## The hook underneath it `AvatarCall` is a default surface over `useAvatarCall`. Reach for the hook when you want the call's state but your own layout — a custom control bar, your own video element, a call that lives inside something else. **React** ```tsx import { createProxyClient, useAvatarCall } from "realtime-avatar/react"; const client = createProxyClient({ proxyUrl: "/api/realtime-avatar" }); function MyCall({ avatarId }: { avatarId: string }) { const call = useAvatarCall({ client, avatarId }); // Drive your own UI from the call's state, and render its video surface // wherever your layout wants it. } ``` Nothing opens a session until you ask it to. A mounted component that has not connected costs nothing — sessions are metered per second on air, so the connect is always an explicit act. ## React Native A separate entry point, because the media stack is not the browser's: it wraps LiveKit's React Native room and needs an audio session the OS has granted. **React Native** ```tsx import { RealtimeAvatarLiveKitRoom, AvatarVideoSurface, useRealtimeAvatarAudioSession, } from "realtime-avatar/react-native"; // Own the OS audio session while the call is up, or the room connects // with no microphone and the call is one-way. The argument IS the // lifecycle: pass your connected state, not `true`, so the session is // released when the call ends. useRealtimeAvatarAudioSession(connected); ``` `registerGlobals` from the same entry point must run once at app start — it installs the WebRTC globals React Native does not ship. ## Next - Mount the server half: [Next.js](https://realtimeavatar.ai/docs/nextjs), [TanStack Start](https://realtimeavatar.ai/docs/tanstack-start), [Express](https://realtimeavatar.ai/docs/express), or [Hono and Workers](https://realtimeavatar.ai/docs/hono). - [Calls](https://realtimeavatar.ai/docs/sessions) — what the server decides on every connect. --- Realtime Avatar — realtime AI avatar API & SDK. Docs: https://realtimeavatar.ai/docs · Agent guide: https://realtimeavatar.ai/llms.txt # Creating an avatar > One image in, a moving character out: the platform generates the looping idle video and a multi-clip motion library for you. Plus the generated state map, loop editing, and generation prices. - Canonical: https://realtimeavatar.ai/docs/video - Updated: 2026-09-01 Between sentences your character is not speaking — and that is most of a conversation. A still portrait reads as a dead connection. You do not have to fix that yourself any more: creation takes one image and generates the motion for you. ## One image in, a moving character out Upload a portrait, create the avatar, and the platform renders — in the background — everything a live call needs: - **The looping idle video**: a ~10s closed arc whose first and last frame are both the portrait, so it loops seamlessly by construction. This becomes the avatar's source video — the state she rests in. - **A multi-clip motion library**: a motion director looks at the portrait and the persona and designs short closed-arc clips — a signature idle variant, a listening state, a gesture — each ending exactly where it began, so the realtime worker can swap between them at invisible seams. The create call returns immediately with `status: "preprocessing"` (the wire also reports the loop's own `idleVideoStatus`); poll `getAvatar(id)` (`GET /avatars/{id}` on the wire) until `ready`, and read the library with `GET /avatars/{id}/clips`. Render calls inherit the generated library automatically; a mint that sends an explicit empty `clip_library: []` opts that call out of clips entirely. A motion prompt at creation art-directs the loop (wire `motionPrompt`, or the Avatars page's **Resting motion direction**); a clip that fails to render fails alone — the avatar still ships with the loop and the clips that made it. Creation is **image-only**: one portrait is the entire input, and every piece of video an avatar plays is generated by the platform. There is no custom video upload. Everything below this point is the *session-time* layer — how rendering works, what the generated state map does for you, and the generation gadgets the creation pipeline itself runs, still addressable directly when you want to art-direct a clip. ## Two modes | Mode | How it works | Choose it when | | --- | --- | --- | | `looping` (default) | Speech is animated onto the looping video the platform generated from the portrait. Her face, her room, her wardrobe — pixel-identical every call. | Identity has to be exact and repeatable. Almost every companion, brand, or character product. | | `generative` | The video is synthesized as she speaks. No clips to supply, none to maintain. | You have no footage, or you want motion no loop could cover. | **TypeScript** ```ts session: async ({ avatarId }) => ({ video: { mode: "generative" }, // that is the entire switch }) ``` **Python** ```python body = { "avatar_id": avatar_id, "mode": "avatar", "render_backend": "generative", # that is the entire switch } ``` Generative ignores clips by construction — the type makes passing them impossible rather than silently dropping them. Everything below is `looping`. > A live call needs a **video** source — which every avatar created from an image now grows on its own. Before the generated loop attaches, a mint is refused with a clear error, never a black or broken call; once it attaches, calls can connect while the realtime cache still warms (the first call starts slower). Poll `status` to `ready` for the fully-warm contract; a `failed` status carries the reason in `error`. ## Looping: the resting loop The simplest thing that works — and you do not configure it. The avatar's source video *is* the generated idle loop, set once at creation, so the default session body is already the right one: **TypeScript** ```ts session: async ({ avatarId }) => ({ // no video config: the avatar rests in its generated idle loop }) ``` **Python** ```python body = { "avatar_id": avatar_id, "mode": "avatar", # no video fields: the avatar rests in its generated idle loop } ``` ## Looping: the generated state map One loop is presence. A **map** is performance — and an image-created avatar already has one. At creation, the motion director designs a small library of states from the portrait and the persona: an idle variant she occasionally drifts into, a listening state that fires when the user starts speaking, a gesture with a plain-English `whenHint` ("when feeling a bit shy or embarrassed"). The library rides every call automatically; there is nothing to send. The hint is read by the *character*, not by a rules engine you have to write. She knows what is happening in the conversation — she is the one having it — so she picks the state that fits, and the worker switches at a seam where it will not show. Inspect the library any time: **TypeScript** ```ts const { data } = await rta.listClips(avatarId); // [{ clipId: "idle_special", role: "idle", status: "ready", url: "…", whenHint: null }, // { clipId: "listen", role: "listen", status: "ready", url: "…", whenHint: null }, // { clipId: "gesture_shy_tuck_hair", role: "gesture", status: "ready", url: "…", // whenHint: "when feeling a bit shy or embarrassed" }] ``` **Python** ```python library = client.get(f"/avatars/{avatar_id}/clips").json() revision = library["revision"] # 0 = creation-owned; bumps on every accepted PUT clips = library["data"] # [{"clipId": "idle_special", "role": "idle", "status": "ready", "url": "…", "whenHint": None}, # {"clipId": "listen", "role": "listen", "status": "ready", "url": "…", "whenHint": None}, # {"clipId": "gesture_shy_tuck_hair", "role": "gesture", "status": "ready", "url": "…", # "whenHint": "when feeling a bit shy or embarrassed"}] ``` A mint that sends an explicit empty `clip_library: []` opts that call out of clips entirely — she stays in the resting loop. That is the whole session-time control surface: the states themselves come from the platform. ### Every clip is a sentence, and you can change it The starter library is a starting point, not a fixed set. Declare the library you want and the platform reconciles: a clip whose description is unchanged is **kept** and keeps serving, a new or revised one is **queued** to render, and one you leave out is **retired**. The 202 is acceptance, not readiness. `revision` is the top-level field of the GET envelope beside `data` — 0 while the creation pipeline owns the library; each accepted declare increments it, and the 202 body carries the new value. In the dashboard, open [Avatars](https://realtimeavatar.ai/platform/avatars) → **Clips** to inspect and declare generated prompt clips without code. The editor preserves the API's full desired-set semantics: removing a row retires it, and every save carries the revision you loaded. Starter clips created before editable directions were stored ask for a new motion direction before the dashboard can take ownership of them. Uploaded clip rows remain read-only there until the upload asset contract has one dashboard SSOT. **TypeScript** ```ts const { plan } = await rta.setClipLibrary(avatarId, { expectedRevision: revision, // compare-and-set; a concurrent writer 409s clips: [ { clipId: "idle_soft", role: "idle", source: { motionPrompt: "breathing gently, a slow blink" } }, { clipId: "listen_lean", role: "listen", source: { motionPrompt: "leans in a little, attentive, small nod" } }, { clipId: "gesture_wave", role: "gesture", whenHint: "when greeting someone", source: { motionPrompt: "raises a hand and waves warmly, then lowers it" } }, ], }); // plan → { kept: [...], queued: [...], retired: [...] } await rta.waitForClips(avatarId); // settles when nothing is still RENDERING ``` **Python** ```python r = client.put(f"/avatars/{avatar_id}/clips", json={ "expectedRevision": revision, # compare-and-set; a concurrent writer 409s "clips": [ {"clipId": "idle_soft", "role": "idle", "source": {"motionPrompt": "breathing gently, a slow blink"}}, {"clipId": "listen_lean", "role": "listen", "source": {"motionPrompt": "leans in a little, attentive, small nod"}}, {"clipId": "gesture_wave", "role": "gesture", "whenHint": "when greeting someone", "source": {"motionPrompt": "raises a hand and waves warmly, then lowers it"}}, ], }) # httpx does not raise on 4xx: 403 clip_library_not_enabled, 409 revision_conflict # (body carries the current revision), 422 clip_declaration_rejected r.raise_for_status() plan = r.json()["plan"] ``` Declaring is a per-tenant rollout: until it is enabled for your tenant the PUT answers `403 clip_library_not_enabled` and avatars keep their creation-generated library. The other refusals — `409 revision_conflict`, `422 clip_declaration_rejected` — are in [Refusals worth telling apart](https://realtimeavatar.ai/docs/editing#refusals-worth-telling-apart). Per clip: `motionPrompt` (the description the platform renders from), `whenHint` (plain English the *character* reads to decide when it fits), `durationSeconds` (4–8, default 5), and `reroll: true` to force a new take of an otherwise-unchanged clip. At most twelve clips: up to six `idle`, up to two `listen`, the rest gestures. Six idles is a *rotation*, not six alternatives to one. Each is a variant she can drift into while resting, so a character with several reads as alive rather than looped; one is perfectly normal. **You can also upload a clip** — `source: { assetId }` instead of a prompt — but it is *pose-validated* before it can serve: its first and last frames have to land on the avatar's rest pose, or that clip settles `failed` with the verdict in `poseCheck` and the rest of the library is untouched. That validation is the whole reason an upload is allowed at all; without it a supplied video makes every switch visible. The one route that takes clip *URLs*, `POST /avatars/{id}/clips`, is **deprecated** and validates nothing: it reconciles an externally hosted library to the video cache for tenants who served their own clips before generated libraries existed, and it cannot add a clip to a generated one. ### The resting loop is not a clip, and it is also a sentence The loop she plays when nothing else is happening is the avatar's **source**, not a library entry — a clip with `role: "idle"` is a variant spliced *over* it, and declaring one never changes what she rests in. Direct the loop at creation with `motionPrompt`, and re-direct it any time afterwards: **TypeScript** ```ts const { servingUrl } = await rta.setLoop(avatarId, { motionPrompt: "tilts her head, a small amused smile, settles back to centre", }); // servingUrl = the loop she is playing RIGHT NOW, for the whole render await rta.waitForLoop(avatarId); // throws if the render gave up ``` **Python** ```python accepted = client.put(f"/avatars/{avatar_id}/loop", json={ "motionPrompt": "tilts her head, a small amused smile, settles back to centre", }).json() # accepted["servingUrl"] is the PREVIOUS loop (None if she had none yet) — she plays it for the whole render. # Poll GET /avatars/{id} until idleVideoStatus is "ready" (or "failed"). ``` She stays `ready` and keeps serving the previous loop for the entire render, then the swap publishes in one step. **Your clip library is untouched** — clips render against the portrait, not against the loop, so a re-direct re-queues nothing and does not move the library's `revision`. Describe a *closed arc*: it has to end where it began, or the loop snaps every time it wraps. And keep it **small**. Because a separate model animates her mouth during speech, every loop description is held to a nearly-still envelope — blinks, a soft closed-lip smile, a shift in weight, a few degrees of head drift. A laugh, a head turn, or an open mouth comes back inside that envelope anyway: a new render, a new charge, and a loop that looks like the one you already had. It is the only outcome here that carries no error code, which is exactly what makes it worth knowing before you write the sentence. ### What makes a switch invisible Every clip the director renders starts and ends on the **same rest pose** — the portrait itself. That shared frame is what turns a switch into a splice instead of a jump, and it is why each clip is generated independently from the one anchor image rather than chained off the previous clip's last frame, which would compound drift with every hop. Clips stay short (~5s closed arcs): the lever for natural idle is **how many states × how long she dwells**, not clip length. ## Editing the loop > **Preview — not yet enabled on the public fleet.** The field is accepted and forwarded today, and currently resolves to the unedited loop. Build against it if you want to be ready; do not ship a feature to your users that depends on it yet. A state map switches between clips. *Editing* rewrites the clip itself: the generated loop is re-rendered under a sentence you write, and the result is lip-synced by the same path as an unedited one. One loop becomes every season, every set, every outfit — without ever generating a new one. **TypeScript** ```ts session: async ({ avatarId }) => ({ video: { edits: { instruction: "a snowy cabin at night, warm lamplight" }, }, }) ``` **Python** ```python body = { "avatar_id": avatar_id, "mode": "avatar", "support_edits": {"instruction": "a snowy cabin at night, warm lamplight"}, } ``` Editing runs **upstream of the lips**, on the closed-mouth plate, so it costs nothing at the start of a call — she is on screen and talking from the first word, she simply has not changed clothes yet. The first lap through the loop is live and expensive; every lap after replays what that one banked, so a fixed instruction converges on the cost of plain looping. It needs the avatar's **video** source — the generated loop — so it is refused at the mint while the loop is still rendering, rather than falling back. It also cannot be combined with `generative` — a synthesized body has no clip to open — and the type makes that pairing unrepresentable rather than a runtime error. A `voice` call is refused for the same reason: no video track, nothing to rewrite. Editing is **per call, and off unless you ask**. Nothing stored on the character turns it on, so a call that omits `edits` is byte-identical to one made before this option existed — which matters, because the first edited lap holds a scarce GPU editor and can wait minutes on a cold one. If you want every call for a character edited, say so in your `session` policy; that is the same place you already decide the persona and the ceiling. ### Letting the look follow the conversation > **Experimental — "runtime edit".** The shape of `live` may still change, and the re-dress does not reach every capacity tier yet, so a call may run to the end on its opening set. See [Experimental features](https://realtimeavatar.ai/docs/experimental) for what that word promises. The *trust boundary* below is not experimental — it is enforced on every mint. Add `live` and the set stops holding still: the look follows the conversation, within the bounds you set. Those rules are **yours, not your user's** — they are the app's policy about what a conversation is allowed to do to the picture, which is why they are server-owned like `instructions`. A browser that could set them could redress your character into anything. **TypeScript** ```ts edits: { instruction: "her apartment at golden hour", live: { rules: "only change the room and the light, never her face or clothes; " + "only when they ask to go somewhere", cooldownSeconds: 60, }, } ``` **Python** ```python body["support_edits"] = { "instruction": "her apartment at golden hour", "live_edit": { "rules": ("only change the room and the light, never her face or clothes; " "only when they ask to go somewhere"), "cooldown_seconds": 60, }, } ``` Write the rules narrowly. Every change of direction buys a fresh lap on a scarce editor *and* a visible style pop — the editor has no mid-stream prompt update, so a look changes at a seam rather than easing. `cooldownSeconds` is the floor that stops an eager director spending a GPU per user turn; it is a bound, not a preference, and the default is deliberately conservative. ## Generation pricing The generated library itself is currently **included with creation** — the loop's generation is metered per-model exactly like a resting-loop update on the Avatars page, and the extra library clips are not billed separately while pricing settles. The rates below apply when you drive the generators *directly*: **Generation is billed at the model vendor's own list price.** We add no margin — what you pay is what MiniMax and ByteDance charge, so the table below moves when their rate cards move. Prices are per generated clip and charged on success; failed generations are not charged. | Gadget | Model | Best for | Price | | --- | --- | --- | --- | | `minimax-h3` | MiniMax H3 · first frame → last frame · 768P | **The default.** Closed loops from any portrait — both ends pinned to the same frame, so the clip closes on itself by construction. | $0.80 | | `seedance-2.0-mini` | Seedance 2.0 Mini · 720p | Cheap iteration while you are still finding the look | $0.80 | | `seedance-2.0-fast` | Seedance 2.0 Fast · 720p | The everyday choice — clearly better motion than mini | $1.20 | | `seedance-2.0` | Seedance 2.0 · 720p | The hero clip: the one she rests in most of the time | $1.50 | | `seedance-2.5` | Seedance 2.5 · 720p | The newest tier; priced as 2.0 until its own rate card publishes | $1.50 | Prices are for a 10-second clip at 24fps — H3 renders 768P portrait, the Seedance tiers 720p 16:9 — the shape worth generating for a resting loop. The Seedance rates come from [ByteDance's published Seedance rate card](https://www.byteplus.com/en/topic/578667) ($0.15/sec at 720p, $0.12 Fast, $0.08 Mini); H3 is $0.08/s at 768P. Credits bill at $1 ≈ 720 credit-seconds, so `seedance-2.0` is 1,080 credits and the default `minimax-h3` is 576. 1080p costs materially more on every model — Seedance 2.0 is $0.37/sec there, roughly 2.5× the 720p rate. For a clip that spends its life as a background loop behind a speaking character, 720p is the right default. ## Uploading the portrait The one asset you bring is the image. Upload the bytes, or hand us a URL and we fetch it server-side — the second is what you want for anything large (the origin must answer with `Content-Length`; the same 8 MB image cap as an upload applies): **TypeScript** ```ts // bytes you already hold — createAvatar takes the asset id from here const asset = await rta.uploadAsset(file, { kind: "image" }); // or a URL we fetch for you (no multipart through your runtime) const asset = await rta.createRemoteAsset({ kind: "image", remoteUrl: "https://your-bucket.example.com/rin/portrait.png", }); asset.url; // https://realtimeavatar.ai/api/assets/… — public, ready to use ``` **Python** ```python # client: the httpx.Client from the quickstart (base_url https://realtimeavatar.ai/api/v1, bearer API key) # bytes you already hold with open("portrait.png", "rb") as fh: asset = client.post("/assets", files={"file": fh}, data={"kind": "image"}).json() # raw bytes without a filename: files={"file": ("portrait.png", data, "image/png")} — # the server keys on the part's Content-Type (jpeg/png/webp) # or a URL we fetch for you asset = client.post("/assets/remote", json={ "kind": "image", "remoteUrl": "https://your-bucket.example.com/rin/portrait.png", }).json() asset["publicUrl"] # https://realtimeavatar.ai/api/assets/… — public, ready to use # (the SDK surfaces this same field as asset.url; on the wire it is publicUrl) ``` The returned URL is **publicly readable** — no auth, no signing. Treat these URLs as **unguessable, not private**: the key is random and nothing indexes it, but anyone holding the link can fetch it. Do not upload anything whose exposure would matter. Uploading costs nothing — you are charged for generation, not storage. Accepted kinds are `image` (portraits), `audio` (voice material), and `video` for an uploaded motion clip declared with `source: { assetId }`. Video upload does not make the asset an Avatar source: new Avatars are still portrait-only, and an uploaded action clip must pass the rest-pose check before it can serve. --- Realtime Avatar — realtime AI avatar API & SDK. Docs: https://realtimeavatar.ai/docs · Agent guide: https://realtimeavatar.ai/llms.txt # Calls > What your server decides, what the client reports, and how to end a call without cutting the character off mid-sentence. - Canonical: https://realtimeavatar.ai/docs/sessions - Updated: 2026-09-02 A call is one live conversation. Your server decides who may start one and what the character knows; the client renders it and reports five states. Those are the only two surfaces you touch. ## What your server decides The `session` policy on your connect endpoint. Every field here is authoritative — whatever the browser sent for these is discarded, and a field you do not set is absent rather than inherited from the caller. The Field column is the SDK name; the Wire column is the key a hand-rolled request sends — snake_case and strict, see Calling the API directly below. | Field | Wire | What it does | | --- | --- | --- | | `instructions` | `instructions` | Her behavior contract — who she is and how she speaks. Up to 4,000 characters. The highest-leverage field on the whole surface. | | `context` | `initial_context` | Up to 32 prior messages, replayed as memory. This is what makes a call continue a story instead of starting cold. On the wire it is `initial_context`, and each entry is `{ role: "system" \| "user" \| "assistant", content }` with content 1–4000 characters — the SDK types this for you, raw HTTP does not. | | `maxSeconds` | `max_session_seconds` | Hard stop, up to 1800. Enforced on our side, so it holds even if your client stops reporting. Compute it from the balance you just admitted. | | `listen` | `stt_mode: "server" \| "off"` | Speech recognition. Default true — she hears the user the whole call. Set `false` only if you drive every turn yourself — see [Tool calling](https://realtimeavatar.ai/docs/tool-calling). | | `voice` | `voice` | Override her stored voice for this call. | | `video` | `render_backend: "generative"`, or omit it | How she is rendered: omit it for the generated loop + state map, or `{ mode: "generative" }` to synthesize the video live — see [Creating an avatar](https://realtimeavatar.ai/docs/video). | | `clientTools` | `capabilities: ["client_tools"]` | `true` opts the call into the client tool plane — functions declared in your page that she can call mid-conversation. See [Tool calling](https://realtimeavatar.ai/docs/tool-calling). | | `transcript` | `transcript_webhook: { url, secret }` — https URL, secret 16–200 chars | `{ url, secret }` — get the two-sided transcript back, signed, after the call ends. On the wire it is `transcript_webhook`, and `secret` is 16–200 characters — the SDK types this for you, raw HTTP does not. See [Tool calling](https://realtimeavatar.ai/docs/tool-calling). | | `metadata` | `client_metadata` | Up to 16 string pairs, echoed verbatim on that transcript so you can attribute it without a lookup. | ## Two modes | Mode | What the user gets | Use it for | | --- | --- | --- | | `avatar` (default) | She is on screen — audio and video | The full call | | `voice` | Audio only | A cheaper on-ramp, or a fallback when bandwidth will not carry video | Same character, same policy — only `mode` changes. A voice call draws on separate capacity, so it never competes with a video call. ## What the client reports Five states. Write copy for the ones you care about; the SDK ships none. | `status` | Meaning | | --- | --- | | `connecting` | Getting her ready | | `waiting` | Every slot is busy and you are holding a place in line — `call.queuePosition` has the number. **Not an error**; the SDK retries for you. Render the position, not a failure. | | `live` | She is there | | `recovering` | A blip; reconnecting automatically with backoff | | `ended` | Over — `onEnded` already told you why | ## What you can do mid-call **TypeScript** ```ts call.say(text, { steer }) // say something to her; `steer` shapes THIS reply only call.sayAndEnd(text) // speak one exact line, verbatim, then close call.keepAlive() // the user is still here — postpone the idle disconnect call.end() // end now call.secondsRemaining // to the hard stop, or null before the clock lands call.queuePosition // place in line while waiting, else null ``` **Python** ```python # These are CLIENT-side actions — they act on a live call, which lives in the # browser or native app. A Python backend owns the policy, not the live turn. # # What Python does own — and there is no Python SDK, so this half is plain HTTP: # POST /realtime/livekit/session mint a call — body is snake_case and strict: # avatar_id, mode, instructions, initial_context, # max_session_seconds, stt_mode, capabilities, # transcript_webhook, client_metadata (the Wire # column above, not the SDK field names) # POST /realtime/livekit/session/release free the slot early # GET /credits/balance what the next call must fit inside ``` See [Tool calling](https://realtimeavatar.ai/docs/tool-calling) for the Python side of the conversation — the signed transcript you receive when a call ends. `say(text, { steer })` is the hook for tool results — `steer` applies to one reply and then evaporates, so a lookup result cannot leak into every later turn. See [Tool calling](https://realtimeavatar.ai/docs/tool-calling). ## Ending well A call that stops mid-sentence feels broken no matter how good the render was. When time is nearly up you get a callback and a handle; whatever you pass to `sayAndEnd` is spoken **exactly as written** — never rewritten by the model, never interrupted — and only then does the call close. ```tsx { if (secondsLeft > 15) return; // wait for the last window call.sayAndEnd(await writeGoodbye(character)); }} onQuiet={({ secondsLeft }) => toast(`Still there? (${secondsLeft}s)`)} onLowBalance={({ secondsLeft }) => openTopUp(secondsLeft)} onEnded={({ reason }) => showEndScreen(reason)} /> ``` `onEnded` always carries one of `user_ended` · `session_cap` · `idle` · `disconnected` · `out_of_credits` · `agent_ended` · `failed`. Key your end screen off it — "talk again?" and "you're out of minutes" are different screens with different outcomes. The idle timer is real and enforced: when it fires the client disconnects and capacity is freed. That is deliberate. An abandoned tab holding a live character is the most expensive thing that can happen in this product, and it would be on your bill. **You can also release a call from your server.** `rta.endCall(sessionId, { reason })` frees the slot immediately — best-effort and idempotent: `true` when acknowledged, `false` for anything else, never a throw, so calling it twice (or on a call that already ended) is safe. Reach for it when your backend learns the call is over before the client does: a webhook, an admin action, a superseded reconnect. (Wire: `POST /realtime/livekit/session/release`, `realtime:write` scope.) From the client, `call.end()` is the same thing and is what you normally want. The SDK also sends a release on tab-close via `releaseLiveKitSessionBeacon()`, which survives the page going away where a normal `fetch` would not. Release early and often: a held slot is capacity nobody else can use, and the queue is shared. ## Calling the API directly Not on TypeScript? The same call is one authenticated POST — the Python tab on [Quickstart](https://realtimeavatar.ai/docs/quickstart) has a working endpoint. Two things to know before hand-rolling it: - **Every endpoint is strict, and casing is per endpoint.** An unknown or mis-cased key is a rejected call, not a dropped field — and which casing is right depends on where you are writing. The realtime routes on this page (`/realtime/livekit/session` and its `/release`) are **snake_case**; every REST resource endpoint — avatars, clips, keys, assets — is **camelCase**. Measured against the published spec: 2 of the 9 request bodies are snake_case at the top level and 7 are camelCase — with one exception worth knowing, because the strictness above turns it into a 422: `POST /avatars` is camelCase outside and **snake_case inside** `voice` (`auto_description`, `voice_id`). The SDK translates its camelCase policy for you; a hand-rolled request has no such help — send the Wire column from the policy table above (`initial_context`, `max_session_seconds`, `transcript_webhook`, `client_metadata`…), never the SDK names. - **Pass the connection payload to your client byte-for-byte.** It is validated strictly — add one key and the client rejects the whole thing, so the call never opens and nothing points at the cause. Full endpoint list, scopes, and error codes: [API reference](https://realtimeavatar.ai/docs/api-reference). --- Realtime Avatar — realtime AI avatar API & SDK. Docs: https://realtimeavatar.ai/docs · Agent guide: https://realtimeavatar.ai/llms.txt # Editing a character > Change an avatar's clips and resting loop after creation: how to declare them, how each change settles, and the refusals worth telling apart. - Canonical: https://realtimeavatar.ai/docs/editing - Updated: 2026-08-31 Her clips and her resting loop are both editable after creation. The resting loop is re-directed in words; each library clip can either be rendered from a motion description or use the `assetId` of a pose-compatible video you uploaded. Her id, her voice and her live calls survive every change on this page. Both are accepted immediately and rendered in the background, and **she keeps serving what she has** until the new take is ready. Editing never takes a live character off the air. ## Her clips Send the library you want, in full — the same shape you could have created her with. The platform reconciles: a clip whose description is unchanged is **kept** and keeps serving, a new or revised one is **queued**, one you leave out is **retired**. **TypeScript** ```ts const { plan } = await rta.setClipLibrary(avatarId, { expectedRevision: revision, // optional compare-and-set clips: [ { clipId: "idle_soft", role: "idle", source: { motionPrompt: "breathing gently, a slow blink" } }, { clipId: "listen_lean", role: "listen", source: { motionPrompt: "leans in, attentive, small nod" } }, { clipId: "gesture_wave", role: "gesture", whenHint: "when greeting someone", source: { motionPrompt: "raises a hand and waves warmly, then lowers it" } }, ], }); // plan → { kept: [...], queued: [...], retired: [...] } await rta.waitForClips(avatarId); ``` **Python** ```python library = client.get(f"/avatars/{avatar_id}/clips").json() revision = library["revision"] # top-level; 0 while creation still owns the library r = client.put(f"/avatars/{avatar_id}/clips", json={ "expectedRevision": revision, "clips": [ {"clipId": "idle_soft", "role": "idle", "source": {"motionPrompt": "breathing gently, a slow blink"}}, ], }) # httpx does not raise on 4xx. Refusals come back as {error, status, code} with no "plan": # 403 clip_library_not_enabled, 409 revision_conflict (body "revision" = current), 422 clip_declaration_rejected r.raise_for_status() plan = r.json()["plan"] # the 202 body also carries the new "revision" ``` Twelve clips at most: up to six `idle`, up to two `listen`, the rest gestures. Per clip you also get `durationSeconds` (4–8) and `reroll: true`, which forces a fresh take of a clip whose description did not change. **Omission is deletion.** This is the whole desired library, not a patch — a clip missing from the array is retired. That is also why `expectedRevision` exists: pass the `revision` you last read and a concurrent writer gets a `409` instead of silently erasing your change. Omit it to declare unconditionally. You read `revision` from the top level of `GET /avatars/{id}/clips`, beside `data` — it is `0` while the creation pipeline still owns the library, every accepted declaration bumps it, and the `202` body carries the new value. For an uploaded take, upload or remotely register a video asset first, then use `source: { assetId }`. Uploaded clips are pose-validated against the avatar's rest-pose anchor before they can serve; the first and last frame must both return to that pose. The dashboard's **Clips** editor performs the same asset registration and full-library declaration without exposing a developer API key to the browser. ## Her resting loop The loop is what she plays when nothing else is happening — the same thing `motionPrompt` described at creation. It is *not* a clip: a clip with `role: "idle"` is a variant spliced over the loop, and declaring one never changes what she rests in. **TypeScript** ```ts const { servingUrl } = await rta.setLoop(avatarId, { motionPrompt: "tilts her head, a small amused smile, settles back to centre", }); // servingUrl = the loop she is playing RIGHT NOW, for the whole render await rta.waitForLoop(avatarId); // throws if the render gave up ``` **Python** ```python resp = client.put(f"/avatars/{avatar_id}/loop", json={ "motionPrompt": "tilts her head, a small amused smile, settles back to centre", }) resp.raise_for_status() # 409 loop_pending, 422 loop_prompt_rejected / loop_not_generatable accepted = resp.json() # accepted["servingUrl"] = the loop she plays for the whole render (None if she had none yet) # Poll GET /avatars/{id} until idleVideoStatus is "ready" (or "failed"). ``` **Your clip library is untouched.** Clips render against the portrait, not against the loop, so re-describing the loop re-queues nothing and does not move the library's `revision`. Describe a *closed arc* — it has to end where it began, or the loop snaps every time it wraps. **Keep it small.** The loop is her resting state and a separate model animates her mouth, so every description is held to a nearly-still envelope: blinks, a soft closed-lip smile, a shift in weight, a few degrees of head drift. Ask for a laugh, a head turn, or an open mouth and the render comes back inside the envelope anyway — accepted, charged, and looking very much like the loop you already had. That is the one failure here with no error code attached to it, so aim inside the envelope rather than at the edge of it. ## Waiting, and how each one ends Both return on **acceptance**, not completion, so both need a wait — and the intuitive version of each is wrong: - `waitForClips` settles when nothing is still *rendering*. Waiting for every clip to reach `ready` hangs forever: a clip rejected by pose validation settles `failed`, which is terminal. - `waitForLoop` settles on `idleVideoStatus` and throws if the render gave up. A failed re-direct leaves her `ready` — she is still serving the old loop — and `idleVideoStatus` is the only status field guaranteed to move; `error` may or may not carry the reason. Poll `idleVideoStatus`, never `status` or `error`: a loop watching `status` waits for a change that never comes. ## Refusals worth telling apart | Code | Means | Do | | --- | --- | --- | | `403 clip_library_not_enabled` | a per-tenant rollout gate | ask us; nothing about the body helps | | `409 revision_conflict` | someone declared first | re-read, re-decide, re-declare | | `409 loop_pending` | a re-direct is already in flight | wait, then retry | | `422 loop_not_generatable` | no portrait to re-animate | **terminal** — do not retry | | `422 clip_declaration_rejected` | a clip description was refused (clips route) | rewrite it | | `422 loop_prompt_rejected` | the loop description was refused (loop route) | rewrite it | | `409 avatar_not_ready` | she is not `ready` (either route) | poll `status` until `ready`, then retry; the loop route also refuses a creation that settled `failed` | A `503` with `retryable: true` (`clip_screen_unavailable`, `loop_workflow_unavailable`, `loop_workflow_queue_failed`) is transient — back off and send the same body again. Each refusal above is JSON `{ error, status, code }` — a malformed body (`422`) or an unknown avatar (`404`) carries no `code` — so switch on `code`, not on the status alone. A rejected *upload* is not an error response at all: the declaration is accepted and that one clip settles `failed` with the verdict in `poseCheck`. The rest of the library is untouched. ## What you cannot change **Her portrait.** It is the rest pose every clip is rendered to start and end on — the shared frame that makes a state switch a splice instead of a jump — so replacing it would invalidate the whole library at once. Create a new avatar instead. Everything else about her is a sentence away. --- Realtime Avatar — realtime AI avatar API & SDK. Docs: https://realtimeavatar.ai/docs · Agent guide: https://realtimeavatar.ai/llms.txt # Tool calling > The platform never executes your tools. Here is the boundary, the client tool plane, and the server loop that turns a call into a working agent with a face. - Canonical: https://realtimeavatar.ai/docs/tool-calling - Updated: 2026-09-02 **Your tools never run on the platform.** There is no hosted executor — a tool executes in your page or on your server, with your credentials, under your authorization rules. That is a deliberate boundary, not a gap we forgot to fill. The reason is trust. A hosted tool runner would need your credentials, your authorization rules, and a say in when a side effect fires. Your backend already has all three and already knows which user is on the call. So execution stays where the trust already is, and you get two lanes to wire it in: **client tools**, where she calls a function you declared in your page mid-conversation, and **server steering**, where your backend runs the agent loop and hands her the result to say. ## Client tools: functions she can call in your page Opt in at mint with `clientTools: true` (wire `capabilities: ["client_tools"]`) — part of the call policy, so it is your server's decision like everything else — then declare the tools in the page after the room connects. The manifest is registered over RPC; nothing about a tool ever appears in the mint request, and the tool name is the record key. **TypeScript** ```ts // Server — the connect route opts the call in. Policy, like everything else. session: async () => ({ instructions, clientTools: true }), // Page — after room.connect(). The name she calls is the record key. import { attachAvatarTools, type AvatarTool } from "realtime-avatar/tools"; const check_order: AvatarTool<{ order_id: string }> = { description: "Look up the status of an order by id", parameters: { type: "object", properties: { order_id: { type: "string" } }, required: ["order_id"], }, execute: async ({ order_id }) => await fetch("/api/orders/" + order_id).then((r) => r.json()), }; const { accepted, rejected } = await attachAvatarTools(room, { check_order }); ``` **A tool has 2.5 seconds.** That is a conversational floor, not a tunable — past a couple of seconds of dead air the call stops feeling live. The platform abandons the call after the deadline and tells her it failed, and the abort is *cooperative*: every `execute` receives an `AbortSignal`, but a handler that ignores it still runs to completion and can commit a side effect — only its *result* is discarded. Make anything slow idempotent, or check `signal.aborted` before you write. A tool that calls a model does not fit: return an acknowledgement inside the deadline and deliver the real answer on screen when it lands. And read the `{ accepted, rejected }` return — a rejected schema is a tool she simply does not have. ## Server steering: the two primitives | Call | What happens | Use for | | --- | --- | --- | | `sendTurn(text, { instructions })` | Interrupts stale speech and generates a reply to `text`, with `instructions` applied to *this turn only* — passed as generation instructions, not concatenated into the user message. | Injecting a tool result and letting the character deliver it in her own voice | | `sendClosingTurn(text)` | Speaks `text` **verbatim** — never routed through the model — uninterruptibly, then ends the session. | The one line that must be exact: a goodbye, a legal disclosure | Per-turn `instructions` is the tool-result channel. It steers wording, register, and content for one reply and then evaporates — it does not mutate the session prompt, so a tool result cannot leak into every later turn. ## The loop Run your agent where your data is: on your server. The call is the interface, not the brain. **TypeScript** ```ts // 1. You observe the user's turn. 2. Your agent decides a tool is needed // and runs it. 3. You hand the RESULT back and let her say it. const result = await tools.checkOrderStatus({ userId, orderId }); await session.sendTurn(userText, { instructions: [ "Answer using ONLY these facts:", JSON.stringify(result), "One or two spoken sentences. Do not read the JSON aloud.", ].join("\n"), }); ``` **Python** ```python import json from fastapi import Depends # The live turn is client-side, so Python's role is to DECIDE and to receive. # Your client calls your endpoint; you run the tool and return the steer text. @app.post("/api/turn") def turn(req: TurnRequest, user=Depends(current_user)): result = check_order_status(user.id, req.order_id) return { "text": req.text, "steer": ( "Answer using ONLY these facts: " f"{json.dumps(result)}. " "One or two spoken sentences. Do not read the JSON aloud." ), } ``` The client passes that straight into `sendTurn(text, { instructions: steer })`. Keeping the decision on your server is the point — the credentials and the authorization rules already live there. For a deterministic string — a confirmation number, a price, a required disclosure — do not route it through the model at all. Use the verbatim path so what she says is exactly what you wrote. ### Latency A tool call between the user finishing a sentence and the character starting to speak is dead air, and dead air on a video call is louder than on a chat. Two things help: - **Speak first, then resolve.** Send a short acknowledging turn immediately ("let me look"), run the tool, then send the answer turn. - **Pre-fetch at mint.** Anything you can know before the call starts belongs in the policy's `context` (wire `initial_context`), not in a tool call three seconds in. ## Getting the conversation back Your agent needs to know what was said — and what she did. Register a transcript webhook at mint time (wire `transcript_webhook`: an https URL and a 16–200 character secret; tag the session with `client_metadata`) and you get a signed POST after the call ends with the full two-sided transcript (`segments`) and the tool calls the model acted on (`tool_calls`: name, arguments, result or error, and duration). It is sent after capacity is released, so it never delays the next caller. Delivery is at-least-once: the worker waits 5 seconds for your answer, retries once after ~2 seconds on a 5xx or a timeout with the identical body and signature, and takes any status under 500 as delivered. A session with no committed turns and no tool calls sends nothing: **TypeScript** ```ts // Registered on your connect endpoint — server-side only. session: async ({ avatarId }) => ({ instructions, transcript: { url: "https://your.app/api/rta/transcript", secret: TRANSCRIPT_SECRET }, // Echoed verbatim, so the keys are yours — except user_id, which // GET /usage/sessions?endUserId= filters on. metadata: { user_id: user.id, characterId: avatarId, mode: "video" }, }) ``` **Python** ```python import hashlib, hmac, json, time from fastapi import BackgroundTasks, HTTPException, Request # Mint half — in your connect route, next to avatar_id. Wire names, not the # SDK's transcript / metadata. body["transcript_webhook"] = {"url": "https://your.app/api/rta/transcript", # https only "secret": TRANSCRIPT_SECRET} # 16..200 chars body["client_metadata"] = {"user_id": str(user.id), "characterId": character.avatar_id, "mode": req.mode} # up to 16 string pairs # Receiver half. MAX_SKEW_SECONDS = 300 def verify(body: bytes, signature: str, timestamp: str, secret: str) -> bool: if not timestamp.isdecimal() or abs(time.time() - int(timestamp)) > MAX_SKEW_SECONDS: return False # missing, malformed, or stale: replay-bound signed = f"{timestamp}.{body.decode()}".encode() expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest() return hmac.compare_digest(f"v1={expected}", signature) @app.post("/api/rta/transcript") async def transcript(request: Request, tasks: BackgroundTasks): raw = await request.body() # RAW bytes, before any parsing if not verify(raw, request.headers.get("x-rta-signature", ""), request.headers.get("x-rta-timestamp", ""), TRANSCRIPT_SECRET): raise HTTPException(401) payload = json.loads(raw) if already_saved(payload["session_id"]): # at-least-once: dedupe on session_id return {"ok": True} user_id = (payload.get("client_metadata") or {}).get("user_id") # None when the mint set none tasks.add_task(save_turns, user_id, payload["segments"]) tasks.add_task(save_tool_calls, user_id, payload.get("tool_calls", [])) # absent when none ran return {"ok": True} # answer inside 5 s; save after ``` Sign over the **raw request bytes**. Parsing to a dict and re-serializing changes the whitespace, and the signature will never match. `TRANSCRIPT_SECRET` is the same secret you passed as `transcript_webhook.secret` at mint. The URL must be https — the mint accepts http, but the worker silently drops it and nothing is ever delivered. **Registering it without the SDK.** The TypeScript tab above turns the webhook on; the Python tab only verifies what arrives. If your server mints over raw HTTP, the field on the mint body is `transcript_webhook` — not `transcript`, which is the SDK's name for it — and the mint is strict, so the wrong key is a 422 rather than a dropped field and no webhook is ever sent. `secret` must be 16–200 characters: it is the HMAC key the handler below verifies against, so generate it rather than typing one. The POST carries `x-rta-signature: v1=` and `x-rta-timestamp`; the signed payload is `"."` under HMAC-SHA256 with your shared secret. Verify it before trusting anything, and reject stale timestamps. `client_metadata` comes back verbatim (`{}` when the mint sent none), so you can attribute the transcript without a session lookup, and `session_id` is the key to dedupe a retry on. `tool_calls` is present only when the session ran at least one tool. Each entry is `{name, call_id, arguments, ts, ok, result | error, duration_ms}`; an entry with no `ok` field means the call produced nothing the model saw. Store it next to the segments — a reply grounded in a lookup is only auditable if the lookup itself is in the history. Arguments and results are truncated to 2,000 characters each and at most 200 calls are recorded (`tool_calls_truncated: true` marks an overflow); this is a history, not a replay. One known gap: a tool round belonging to a turn the user barged in over is not reported by the runtime, so its calls are absent even though they ran. The platform stores no conversation text. If you do not register a webhook, nothing is buffered and nothing is sent — which is the right default for most apps, and the only acceptable one for some. ## Bringing your own model If your agent loop is complex enough that per-turn steering feels like fighting it, you can go further: mint with `listen: false` (wire `stt_mode: "off"`), keep speech recognition on your side, and drive every turn through `sendTurn` with tight instructions. At that point the platform is a rendering endpoint for a character you fully control, and every tool decision is yours. That is a supported way to use it. You can also select the brain per session with `llm` (provider + model) when you want the hosted loop but a different model behind it. --- Realtime Avatar — realtime AI avatar API & SDK. Docs: https://realtimeavatar.ai/docs · Agent guide: https://realtimeavatar.ai/llms.txt # API reference > Every public endpoint on https://realtimeavatar.ai/api/v1 — what it takes, what it returns, which scope it needs, and how it fails. - Canonical: https://realtimeavatar.ai/docs/api-reference - Updated: 2026-09-02 Base URL `https://realtimeavatar.ai/api/v1`. Every request carries `Authorization: Bearer tic_…`. A machine-readable spec for the realtime surface lives at [/openapi.json](https://realtimeavatar.ai/openapi.json). **curl** ```bash curl https://realtimeavatar.ai/api/v1/credits/balance \ -H "Authorization: Bearer $REALTIME_AVATAR_API_KEY" ``` **TypeScript** ```ts import { RealtimeAvatar } from "realtime-avatar"; const rta = new RealtimeAvatar({ apiKey: process.env.REALTIME_AVATAR_API_KEY! }); await rta.creditBalance(); ``` **Python** ```python import os, httpx client = httpx.Client( base_url="https://realtimeavatar.ai/api/v1", headers={"Authorization": f"Bearer {os.environ['REALTIME_AVATAR_API_KEY']}"}) client.get("/credits/balance").json() ``` ## Realtime | Endpoint | Scope | Notes | | --- | --- | --- | | `POST /realtime/livekit/session` | `realtime:write` | Mint a session. Returns a grant, or **429** with the queue contract when capacity is full. See [Calls](https://realtimeavatar.ai/docs/sessions). | | `POST /realtime/livekit/session/release` | `realtime:write` | Free the call's slot. Free of charge and idempotent, so calling it twice — or on a call that already ended — is safe. `{ session_id \| queue_ticket_id, reason?, capacity_pool? }` — one of the two ids is required. `reason` is a closed set: `page_hide · disconnected · superseded · unmount · manual · idle_timeout`. Anything else is a **422**, and a rejected release frees nothing — the slot stays held until it expires on its own, so send one of these or omit the field. The SDK sends it for you. | | `GET /realtime/livekit/capacity` | `realtime:write` | Snapshot of **one** pool — the orchestrator default, named by `capacity_pool` in the body (`primary`). Mints fail over across pools and voice calls are not capacity-gated, so this does not predict whether a mint will be granted. Do not gate a call on it: mint, and treat the **429** as the queue. | ## Avatars | Endpoint | Scope | Notes | | --- | --- | --- | | `GET /avatars` | `avatars:read` | List the workspace's avatars (most recently updated first, capped at 100) as `{ data: [...] }`. | | `POST /avatars` | `avatars:write` | `{ displayName, sourceAssetId?, motionPrompt?, voice?, defaultVoiceId?, llm?, settings?, metadata? }` → 201 (`preprocessing` with a portrait attached; `draft` without one). Creation is **image-only**: `sourceAssetId` names an uploaded portrait and the platform generates the looping idle video plus a multi-clip motion library in the background — poll until `status` is `ready`. `motionPrompt` optionally art-directs the generated loop. Custom video upload is not supported. | | `GET /avatars/{id}` | `avatars:read` | Includes `status`: `draft · preprocessing · ready · failed · disabled · deleted` and `idleVideoStatus`: `none · queued · generating · ready · failed` (the background motion-generation progress). Mint only against `ready`; on `failed`, `error` says why. | | `PATCH /avatars/{id}` | `avatars:write` | Partial update; at least one field required. `portraitUrl` is in the schema but is **dashboard-only** — over this API it is always **400**. To change the portrait, register the image with `POST /assets` or `POST /assets/remote` and PATCH the returned id as `sourceAssetId`, which is its own swap lane: it consumes `anchorTimeMs` as the new source's frame and is exclusive of every other field. | | `DELETE /avatars/{id}` | `avatars:write` | Soft delete. | | `GET /avatars/{id}/clips` | `avatars:read` | The avatar's declared motion library as `{ data: [{ clipId, role, status, url, whenHint, source, uploadAssetId }] }`. `status` is the current job state, while `url` is the serving take and may stay non-null during a re-render or after its failure. `uploadAssetId` is non-null only for uploaded clips, so GET can be losslessly declared back with PUT. Render sessions inherit serving clips unless the mint supplies its own `clip_library` (an explicit empty array opts out). | | `PUT /avatars/{id}/loop` | `avatars:write` | Re-direct the **resting loop** — the video she plays when nothing else is happening — from a new `motionPrompt`. This is not a clip: a clip with `role: "idle"` is a variant spliced over the loop, and declaring one never changes what she rests in. Answers `202`; the render takes minutes, during which she stays `ready` and keeps serving her previous loop (returned as `servingUrl`), then the swap publishes at once. Your clip library is untouched. Billed as one generation. Requires a portrait to re-animate — `422 loop_not_generatable` (terminal) when she has none: a draft created without `sourceAssetId`, or a grandfathered avatar built from a supplied video. Do not gate on `sourceKind`: it reads `video` for every avatar once her generated loop attaches. `409 loop_pending` means one is already in flight. The description is screened like a clip's (`422 loop_prompt_rejected`). | | `PUT /avatars/{id}/clips` | `avatars:write` | Declare the library you want, in full — the platform reconciles: renders what is new, keeps what matches, retires what you dropped. Each clip carries a `clipId`, a `role`, an optional `whenHint`, and a `source` that is either a `motionPrompt` (the platform renders it from the avatar's anchor frame) or an `assetId` you uploaded. Send `expectedRevision` to make the write a compare-and-swap — a stale one is `409 revision_conflict`, which carries the current `revision` to re-read. Answers `202` with the plan. Rolling out per tenant (`403 clip_library_not_enabled` until yours is on). Descriptions are screened before they render: a refused one is `422 clip_declaration_rejected` with the reason, and if the screen itself cannot run the write is refused rather than waved through — `503 clip_screen_unavailable`, retryable. | | `POST /avatars/{id}/clips` | `avatars:write` | **Deprecated — do not build on this.** Clips are generated by the platform from the avatar's portrait at creation time; list them with the GET above. This route only reconciles an **externally hosted** clip library to the video cache by URL hash, for tenants that served their own clips before generated libraries existed. Listed because it is still callable and still in the spec — not because you should reach for it. | ## Assets | Endpoint | Scope | Notes | | --- | --- | --- | | `GET /assets` | `avatars:read` | List uploaded assets. | | `POST /assets` | `avatars:write` | Multipart: `file`, optional `kind` (`image \| video \| audio`) → 201. Video assets are accepted as uploaded motion-clip sources; they do not reopen custom-video Avatar creation. A clip declaration pose-validates video and requires it to fit the 18 MB QC ceiling. | | `POST /assets/remote` | `avatars:write` | `{ kind, remoteUrl, originalFilename?, metadata? }` — the platform streams the file into storage. Video assets may be referenced by `PUT /avatars/{id}/clips`; prefer remote registration when the bytes already live in object storage. | ## Billing and keys | Endpoint | Scope | Notes | | --- | --- | --- | | `GET /credits/balance` | `credits:read` | Balance and currently reserved credits, in micros. Live sessions hold a reservation for their duration. | | `GET /usage/sessions` | `usage:read` | Per-session billing detail — when each session ran, how long it was billable for, and what it cost. This is what reconciles an invoice, or re-bills your own users; the dashboard aggregate can do neither. The window defaults to the trailing 30 days and is **clamped** to 90 — a wider range is served narrowed, not refused, so read back the returned `from`/`to`. Page with `nextCursor`. `?endUserId=` narrows to one of your users when the call was tagged with `client_metadata.user_id`. **Read `status` before you sum anything.** Only `released` and `failed` are settled: `released` is the normal end of a call and carries the charge, `failed` is terminal and always `billedCreditMicros: 0`. The other two are not final — `reserved` is a slot that was held and never became a call, and `started` is still in flight. Both report `billedCreditMicros: 0` *provisionally*: a `started` row settles when the session is released, so the same session read again tomorrow can carry a different number. Reconcile on the settled rows and treat the rest as pending, not as free. | | `POST /api-keys` | `api_keys:write` | `{ name, environment?, scopes?, spendLimitCreditMicros?, expiresAt? }`. Only `name` is required, and the two defaults decide what you get: `environment` defaults to `test`, so a key created without it carries the `tic_test_` prefix; `scopes` defaults to `realtime:write`, `credits:read` and `avatars:read`, which starts calls and reads but writes no avatars. The plaintext key is returned once. | ## Errors | Status | Meaning | What to do | | --- | --- | --- | | 401 | Missing, malformed, revoked, or expired key | Check the bearer and the environment tag | | 403 | The key lacks the scope for this operation, or its workspace is not active (`error` reads `Tenant is not active`) | Read `error`: mint a key with the scope (do not widen to `*`), or have the workspace reactivated — there is no self-serve switch for that | | 402 | `insufficient_credits` or `spend_limit_exceeded` | Top up, or raise the per-key spend limit. Surface this as a paywall, not as a connection failure. | | 409 | A conflict with state that already exists. Three kinds reach this API: no workspace on the account yet; an **idempotency** conflict (a key still in flight, or reused with a different body); and an **avatar state** conflict — it is disabled, already has a motion video, has no portrait to animate, or a change is still settling. | Read `error` — the three kinds want different things. A workspace is a one-time setup in the dashboard; an idempotency conflict wants a fresh `Idempotency-Key` (never the same one); an in-flight change wants a plain retry once it settles. | | 404 | No such avatar, voice, or key — *for this tenant*. The message often reads "does not belong to this tenant", which sounds like a permission problem and is usually a wrong or already-deleted id. | Do not route this to your access-denied branch — 403 is the permission answer. Check the id, and remember a soft-deleted avatar is gone from every read. | | 413 | The JSON body is over this route's cap | See the cap sizes under Conventions. This is almost always inlined media — send it as an asset and reference the id. | | 422 | Schema rejection — an unknown, mis-cased, or invalid field | The wire schemas are strict. Check camelCase vs snake_case at the layer you are writing. | | 429 | Plan concurrency ceiling, capacity full (session mint), or rate limited | A mint has **two** 429s, and they want opposite reactions. With `code: "concurrency_limit_reached"` the plan’s concurrent-stream ceiling is reached and **no queue will drain it** — close a session or upgrade. Sandbox allows one stream, so a second tab produces it. Without a `code`, look at the body before deciding. A **queue** answer carries `queue_size` and `recommended_retry_ms`: the SDK returns a queued result (`isQueued`) with `position` and `retryAfterMs` — render the position and retry. Deliberately not auto-retried: a blind retry would burn the backoff to receive the same answer. A body with *neither a code nor those fields* is the **per-key rate limit** (see Conventions) — back off, do not treat it as a queue position. Check the body before trusting `isQueued`: the SDK maps every 429 to a queued result, so both a concurrency refusal and a throttle reach you as `position: null` on a queue that does not exist. | | 502 | An upstream generation or render failed | Retry; if it persists it is ours, not yours | | 503 | A dependency this route needs is unavailable — a render queue that would not accept the job, a staging pipeline that is down, or (`clip_screen_unavailable`) the screen that vets clip descriptions. Never your request's fault. | Retry with backoff. Nothing was written, so a retry is safe and needs no `Idempotency-Key` to be correct — though sending one costs nothing. | ## Conventions - All `/api/v1` responses are `Cache-Control: no-store`. - Request bodies are size-capped per route — 4 KB on the session release, 64 KB on the clip-library `PUT`, 8–32 KB everywhere else. Over the cap is a **413**. Send media by URL, not inline. - **`Idempotency-Key` is honoured on the two writes that cost money to repeat** — `PUT /avatars/{id}/clips` and `PUT /avatars/{id}/loop`. Replay returns the original response rather than doing the work twice. It matters most on the loop, where a repeated re-direct is a *second billed render*; the clip library reconciles, so an identical repeat keeps what already matches and re-renders nothing either way. Reuse the same key with a *different* body and you get a **409** — a retry means the same request, so generate a fresh key for a changed one. No other endpoint reads the header. - **120 requests per 60 seconds, per key.** The throttle is applied when the key is verified, so it covers every `/api/v1` endpoint, and it counts per *key* rather than per workspace — splitting a workload across keys raises the ceiling. Over it is a **429** whose body carries no `code` and no queue fields, which is how you tell it from the other two. Worth sizing against: the creation flow asks you to poll `GET /avatars/{id}` until `status` is `ready`, and a render takes minutes — poll on the order of seconds, not milliseconds. - Ids are prefixed and stable: `ava_` avatars, `tic_` API keys. - The SDK covers the integration path — calls, avatars, assets, usage, credits — with a surface that is deliberately *narrower* than this wire, though less narrow than it once was. Avatar update and delete *are* in it (`updateAvatar`, `deleteAvatar`), as are `getAvatar`, `listAvatars`, `listClips`, `setClipLibrary`, `syncClips`, `retimeAnchor`, `swapSource` and `iterateSessions`. Prefer it over hand-rolled fetches wherever it reaches. What it leaves out is **key minting**, the **capacity** snapshot and the **asset listing** — those three are a plain authenticated fetch against the endpoints above. --- Realtime Avatar — realtime AI avatar API & SDK. Docs: https://realtimeavatar.ai/docs · Agent guide: https://realtimeavatar.ai/llms.txt # Experimental features > What is experimental right now, what that word promises, and how to turn each one on. Included with Studio and above. - Canonical: https://realtimeavatar.ai/docs/experimental - Updated: 2026-08-26 Some capabilities ship before their shape is settled. Rather than hide them behind a flag you have to ask us for, they are on your account from **Studio** up, opt-in per call, and listed here so you can see exactly what you are taking on. ## What "experimental" promises It is a statement about the request shape, not a warning about quality: - **The shape may change in a minor release**, without the deprecation window a stable field gets. Pin an exact SDK version if that matters to you. - **It may be inert while it rolls out.** A call that asks for an experimental capability always connects and always behaves like a normal call; the extra behaviour may simply not happen yet. Nothing errors and nothing is billed differently. - **It is off unless you ask.** Omit the field and your call is byte-identical to one made before the feature existed, so nothing that ignores this page can be affected by anything on it. What the word never covers is a **trust boundary**. Inputs that are server-owned stay server-owned on every call — experimental or not — and that is enforced at the mint, not by convention. If a capability lets a conversation change something, the bounds are still yours to write and still unreachable from a browser. ## The register Everything currently experimental, in full. When this list changes, this page changes with it — the plan bullet and this table read the same register. ### Runtime edit The set behind your character is re-dressed while the call is running, following the conversation within a brief you write. | Turn it on | `video.edits.live` — see [the reference](https://realtimeavatar.ai/docs/video#letting-the-look-follow-the-conversation) | | --- | --- | | Today | Accepted and carried end to end: the call connects and runs normally, and your brief is applied to the session. The re-dress itself does not reach every capacity tier yet, so a call may run to the end on its opening set. Nothing fails and nothing is charged differently when it does. | | Included with | Studio and above | ## Using one Experimental capabilities are configured exactly like stable ones — in your `session` policy, server-side, next to your key. There is no separate endpoint, header or flag to set: ```ts session: async ({ avatarId }) => ({ instructions: character.prompt, video: { edits: { instruction: "her studio at golden hour", // stable: the opening set live: { // experimental: runtime edit rules: "only change the room and the light, never her face or clothes", cooldownSeconds: 30, }, }, }, }) ``` Because the opt-in is a field rather than an account setting, you can ship a call path that uses one and a call path that does not, in the same app, on the same avatar. ## Telling us it went wrong These are the surfaces we most want to hear about, and the ones where a small report is worth the most. Include the `session_id` from the grant — it is the one handle that finds a specific call. --- Realtime Avatar — realtime AI avatar API & SDK. Docs: https://realtimeavatar.ai/docs · Agent guide: https://realtimeavatar.ai/llms.txt --- # Blog — full articles # Generated video that answers back: making a live avatar interactive, not just alive > A live avatar that performs on a loop is easy. One that changes course when you ask is a scheduling problem — and per-second pricing is what makes the fix affordable. - Published: 2026-09-01 - Tags: engineering, product, companion - Canonical: https://realtimeavatar.ai/blog/generated-video-that-answers-back - Markdown: [en](https://realtimeavatar.ai/blog/generated-video-that-answers-back.md) There is a version of a live avatar that is not very interesting: a character who performs on a loop, at you, forever. It looks alive. Nothing you do changes it. We shipped that first — a resting loop is what an avatar falls back to between turns — then spent the real effort on what happens after somebody asks it for something else. ## Why a live avatar stream is a scheduling problem Continuously generated video arrives in segments: render a few seconds, play them, render the next few. That length is the most consequential number in the system, and it pulls in two directions. Long segments amortise fixed per-render latency — a submit, a queue, a warm-up. Measured on our lane at 480P, a fifteen-second segment renders in five to eight seconds and a five-second one in about three, so three short renders spend roughly twice the wall time for the same footage. They also look better: the model has room to finish a movement rather than lurching. But a long segment is also the slowest thing to get on screen — and a viewer who has just redirected the character is waiting on exactly that. ## Make the segment short exactly when someone is waiting Our stream runs fifteen-second segments while ambient — nobody has asked for anything, the character is simply present — and drops to five seconds for the first segment after a new direction. Responsiveness is bought exactly where someone is waiting for it, and the long segment is kept for everywhere else, which is most of the time. Two mechanisms make that land. The answer segment *interrupts* — it cuts into whatever is playing rather than queueing behind it. We learned that the hard way: making a viewer sit out the rest of a fifteen-second clip is up to fifteen seconds of nothing happening, and it was reported to us as the direction being ignored. Superseded segments are discarded rather than played out. What a viewer waits on, then, is not playback but the pipeline: a three-second scheduling tick plus the render. A handful of seconds, not fifteen. And note the number to be careful with — five seconds is the *length* of the answer segment, not its time to screen. ## Per-second pricing is what permits the trick Charge per render and the shortened segment costs the same as the long one, so the responsive path is three times the price per second of video — you would be taxed hardest for the one behaviour that makes the product feel awake. Any sane operator would pick a single segment length and live with the latency. We run the stream on MiniMax `h3-max` at 480P, priced at five cents per rendered second, and bill on exactly that basis: a private stream settles a fifteen-second ambient segment at about $0.75 and a five-second answer at about $0.25. The cost is strictly linear, with no per-render term — three five-second segments and one fifteen-second segment cost the same to the credit. Segment length stops being a billing decision and becomes a scheduling one. The model choice was the same unglamorous arithmetic: at 768P, h3-max is half plain h3’s price — eight cents a second against sixteen — for a near-identical first-frame/last-frame interface. ## Two lanes, two different caps A stream that renders whether or not anyone is watching is a bill with a character attached. Ours is watcher-gated: viewers hold a lease that expires twenty seconds after their last heartbeat, and when the last lease lapses the scheduler stops. Nobody watching, nothing rendered. Beyond that the lanes are bounded differently, and the difference is structural. A private stream is keyed to a workspace wallet, so the wallet is the cap — an insufficient balance stops it by construction, with no second budget to drift out of agreement with the first. A shared live room is house-funded and has no wallet, so it is rationed instead: one new ambient clip every two minutes, replaying its bank in between, under a ceiling of sixty renders per room per hour. Decide which you are building before you write the scheduler. ## What it does not buy This is not real-time generation in the sense a live call is. Our conversational sessions are audio-clocked, with sub-second time to first frame; this stream schedules seconds of video ahead of the viewer. Two different clocks, and the interaction design that follows from each is not the same. We also have not measured whether any of it holds attention longer. What we tuned was time-to-visible-response. Engagement is a real question and that experiment has not been run; we would rather ship the honest claim than the flattering one. ## How to ship it Start with the segment schedule, not the model: pick your ambient and post-redirect lengths before choosing a provider, because that tells you which pricing shapes are survivable. Make the answer segment interrupt playback rather than queue behind it. Gate rendering on presence with a short lease and a heartbeat, so an abandoned tab stops costing money without needing a disconnect event you will not reliably get. And pin every segment’s first *and* last frame to the same portrait, so each begins and ends on the same image and any one splices into any other with no cut. Chaining off the previous segment’s output instead would mean decoding video you cannot decode in a worker, and would compound identity drift without bound. The character is the easy part. The schedule is the product. --- Realtime Avatar — realtime AI avatar API & SDK. Site: https://realtimeavatar.ai · Docs: https://realtimeavatar.ai/docs · OpenAPI: https://realtimeavatar.ai/openapi.json · Agent guide: https://realtimeavatar.ai/llms.txt # Integrating the AI Talking Avatar API: Principles of Realtime Embodiment > Explore the technical foundations and practical integration of an AI talking avatar API. Understand how real-time presence, sub-second latency, and responsive embodiment are achieved for dynamic applications. - Published: 2026-08-23 (updated 2026-08-27) - Tags: api, engineering, product - Canonical: https://realtimeavatar.ai/blog/integrating-the-ai-talking-avatar-api-principles-of-realtime-embodiment - Markdown: [en](https://realtimeavatar.ai/blog/integrating-the-ai-talking-avatar-api-principles-of-realtime-embodiment.md) The digital frontier has shifted. Where once a talking head was merely a pre-rendered video, the demand now points toward something more profound: real-time, responsive presence. This isn't just about delivering content; it's about enabling interaction, building connection, and providing a dynamic conduit for AI agents to engage with the world. The core technology enabling this shift is the AI talking avatar API, a sophisticated interface that transforms text or audio into live, embodied characters with astonishing speed and fidelity. The underlying promise is clear: an avatar that not only speaks but actively participates, responding to prompts and engaging users with natural facial expressions and lip-sync. This goes far beyond the static or delayed experiences of the past, opening up new paradigms for customer support, education, gaming, and personal companionship. ## The Mechanics of a Realtime AI Talking Avatar API At its heart, a modern AI talking avatar API leverages sophisticated generative AI models to create and animate a digital persona. The process begins with an avatar, which can be instantiated from a single image or a short video. This initial input defines the visual identity, providing the base on which all subsequent animations will be layered. The goal is not just a visual likeness, but a dynamic canvas for expression. Critical to the 'realtime' aspect is the rapid processing of incoming audio or text. When an AI agent generates a response, that output is fed to the avatar API. The system then performs several complex tasks concurrently: generating accurate lip-sync animation (audio-clocked to the syllable), subtle head movements, and facial expressions that convey appropriate emotion. All of this must occur with sub-second time to first frame, ensuring the avatar appears to be speaking instantaneously, without awkward pauses or desynchronization. ### Orchestrating Presence: The SDK and the Spec The orchestration of this complex process typically involves two integration paths. For AI agents, the whole surface is published as an OpenAPI specification and mirrored as plain markdown at `llms.txt`, so an agent reads the contract and calls the same endpoints a developer would. For developers, interacting with this powerful backend is simplified through a hand-built, zero-dependency TypeScript SDK. The underlying API is also published as an OpenAPI specification. This `realtime-avatar` package provides a clear, type-safe interface, abstracting away the underlying realtime transport complexities. - **Avatar Creation:** One frontal portrait; the platform generates every frame of video from it. - **Audio-Clocked Video:** Precision lip-sync for natural speech. - **Sub-second Latency:** Ensures genuine real-time interaction. - **Typed TypeScript SDK:** `realtime-avatar` for robust, efficient integration. - **Agent-readable surface:** the full API as OpenAPI, plus markdown mirrors at `llms.txt`. ## Architecting for Dynamic Interaction Building applications with an AI talking avatar API demands an architecture that prioritizes low latency and fluid data exchange. The `realtime-avatar` SDK, hand-built and zero-dependency, offers predictable interfaces and strong typing, which significantly reduces development time and error surface. This allows developers to focus on the conversational logic and user experience, rather than wrestling with data serialization or network protocols. Consider a customer support scenario: a user types a query, your agent processes it wherever it already runs, and a response is routed to the avatar API. The avatar then speaks the response, appearing to look directly at the user, with nuanced expressions that mirror the sentiment of the message. This two-way channel of communication and embodiment is what differentiates a true real-time avatar from a simple video playback system. It's the difference between watching a movie and having a conversation. ## Practical Economics and Deployment Adopting an AI talking avatar API also involves understanding its operational economics. Realtime avatar platforms typically pair subscription tiers with usage-based overage; here, overage beyond a plan's included minutes is anchored around $5 per hour of real-time usage. This structure allows for scalable deployment, where costs directly align with demand. For example, the Developer plan includes 600 minutes (10 hours) per month for $24, with overage rates ranging from $0.07 to $0.095 per minute depending on the plan. Avatar creation, beyond a few included instances, is $1 per avatar. These clear, predictable costs are crucial for planning and budgeting in dynamic AI deployments. ## Shipping Your Vision with an AI Talking Avatar API Bringing an AI talking avatar into your application is a process of integration and iteration, and the shape of that integration follows from one constraint: an API key that can start a paid call must never reach a browser. So an app is two halves. Your server mounts a connect endpoint that holds the key and decides who may call and what the character knows; your page renders the call and never sees the key. The SDK ships both halves. The server half is one function. It answers the handful of paths the client needs under a single prefix, and it is where authorization and the per-call policy live: ```tsx // app/api/realtime-avatar/[...path]/route.ts import { createRealtimeAvatarRoute } from "realtime-avatar/nextjs"; export const { GET, POST } = createRealtimeAvatarRoute({ apiKey: process.env.REALTIME_AVATAR_API_KEY!, // Who may do this. "connect" is the only operation that costs money to start. authorize: async ({ request, operation }) => { const user = await currentUser(request); if (!user) return new Response("Unauthorized", { status: 401 }); if (operation === "connect" && !(await hasCredits(user))) { return Response.json({ code: "insufficient_credits" }, { status: 402 }); } }, // What the character knows for THIS call. Whatever the browser sent is discarded. session: async ({ avatarId }) => ({ instructions: await personaFor(avatarId), maxSeconds: 600, }), }); ``` The client half is one component pointed at that endpoint. It handles the queue, the reconnects, the idle timer, and the video surface — there are no frame or audio-chunk callbacks to wire up, because the transport is not yours to manage: ```tsx "use client"; import { AvatarCall, createProxyClient } from "realtime-avatar/react"; const client = createProxyClient({ proxyUrl: "/api/realtime-avatar" }); export function Call({ avatarId }: { avatarId: string }) { return ( showEndScreen(reason)} > {(call) => call.status === "waiting" ? In line: {call.queuePosition} : null } ); } ``` Speaking is not a method on a socket; it is a method on the live call. The render prop hands you a handle — `say()` to give her something to respond to, `sayAndEnd()` for the one line that must come out exactly as written, `keepAlive()` and `end()` — and `status` reports five states you write your own copy for. From this foundation, you can layer in your AI agent logic, integrate with user input mechanisms, and refine the persona. The studio interface at `/studio` provides a visual environment for creating and managing your resident cast of characters, allowing for rapid prototyping and iteration before full deployment. The goal isn't just to display an avatar, but to empower an intelligent agent with a face, a voice, and a presence that resonates. This is the new standard for digital interaction, and the AI talking avatar API is your conduit to building it. --- Realtime Avatar — realtime AI avatar API & SDK. Site: https://realtimeavatar.ai · Docs: https://realtimeavatar.ai/docs · OpenAPI: https://realtimeavatar.ai/openapi.json · Agent guide: https://realtimeavatar.ai/llms.txt # Decoding Realtime Presence: An Engineer's Guide to the AI Avatar Video API > Explore the core mechanics and practical applications of an AI avatar video API. Understand how real-time, interactive video streams empower dynamic user experiences, from customer support to immersive gaming. - Published: 2026-08-22 - Tags: engineering, api, livestream - Canonical: https://realtimeavatar.ai/blog/decoding-realtime-presence-an-engineers-guide-to-the-ai-avatar-video-api - Markdown: [en](https://realtimeavatar.ai/blog/decoding-realtime-presence-an-engineers-guide-to-the-ai-avatar-video-api.md) The shift from static images to dynamic, interactive digital presences is fundamentally changing how we build applications. The advent of the AI avatar video API marks a significant frontier in this evolution, enabling a new class of interactive experiences that demand both visual fidelity and instantaneous responsiveness. ## What Defines an AI Avatar Video API? At its core, an AI avatar video API delivers a live, streaming video feed of an AI-driven character capable of real-time interaction. This is distinct from pre-rendered video, which, while useful for many applications, lacks the spontaneity and adaptive quality of a true real-time stream. The critical differentiator is the ability for the avatar to react, speak, and emote instantaneously based on live input, be it voice, text, or even environmental data. Our platform, for instance, focuses on delivering sub-second time to first frame, ensuring that the avatar's presence is felt immediately upon connection, eliminating disruptive delays. The 'video' aspect here is paramount. It’s not merely an animation sequence; it's a dynamically generated stream, with the avatar's lips precisely audio-clocked to the syllable of its spoken output. This level of synchronization is essential for maintaining believability and reducing cognitive load for the user. These avatars can be created efficiently, often from a single image or video, offering significant flexibility in design without extensive 3D modeling. ## Architecting Real-time Embodiment Building with an AI avatar video API means tapping into a sophisticated backend infrastructure. For AI agents, the same API is published as OpenAPI and mirrored as plain markdown at `llms.txt` — an agent reads the contract and calls the same endpoints a developer does, not a separate rendering engine. For developers, this underlying complexity is largely abstracted away by a robust, typed TypeScript SDK named `realtime-avatar`. Hand-built and zero-dependency — with the API itself published as an OpenAPI specification — this SDK ensures type safety and a predictable development experience, streamlining the integration process into any modern web or Node.js application. ```tsx import { AvatarCall, createProxyClient } from 'realtime-avatar/react'; // The browser client talks to your mint proxy — your API key never reaches the page. const client = createProxyClient({ proxyUrl: '/api/realtime-avatar' }); export function Call({ avatarId }: { avatarId: string }) { return ; } ``` ### The Mechanics of Interaction Consider a typical interaction loop, such as a customer support scenario: A user speaks, their voice is transcribed, processed by an AI agent, and the response is then vocalized and visually embodied by the avatar. This entire loop, from input to expressive output, happens in near real-time. The video stream adapts dynamically, rendering facial expressions, head movements, and gestures that complement the spoken words, creating a much richer interaction than voice alone. This level of immediate feedback is what transforms a simple chatbot into a compelling digital presence. - **Sub-second time to first frame:** Critical for perceived responsiveness and seamless user engagement. - **Audio-clocked lip-sync:** Eliminates distracting latency artifacts between audio and visual speech, enhancing realism. - **Usage-based pricing:** Plans start at $9/month for 120 minutes, with overage anchored at about $5 per hour ($0.07–$0.095/minute depending on the plan). Avatar creation costs $1 per avatar beyond a plan's included count. - **High-fidelity visual output:** Ensures a professional and engaging presence, vital for brand representation and immersive experiences. ## Shipping Your Interactive Avatar Application Integrating an AI avatar video API isn't merely about embedding a video player. It's about engineering a new layer of user experience that extends the capabilities of your application. Start by understanding the core loop: user input → agent processing → avatar output. Our TypeScript SDK provides the necessary hooks to manage connections, send prompts, and receive the avatar's real-time video and audio streams with confidence. 1. **Select or Create Your Avatar:** Begin by choosing from our resident cast of characters available in the /studio, or quickly create a custom avatar from a single portrait. 2. **Integrate the SDK:** Utilize the `realtime-avatar` TypeScript SDK to establish a secure and efficient connection to the avatar service within your application. 3. **Implement Input/Output Streams:** Capture user input (e.g., microphone audio, text from a chat interface) and pipe it directly to the SDK. Concurrently, receive and display the real-time video and audio streams emitted by the avatar. 4. **Manage Agent Logic:** Connect your chosen AI agent (whether a custom large language model or an existing service) to handle the conversational flow, feeding its generated responses back through the avatar API for embodiment. 5. **Test and Optimize:** Rigorously test your application for network latency, audio quality, and overall responsiveness to ensure a smooth, natural, and highly engaging interaction for your end-users. The objective is to build an application where the avatar isn't just a video player, but a genuine point of presence. Whether for enhancing customer support (as embodied by Ada Kinetic), delivering dynamic corporate training (like Duke Marrow), or creating immersive NPCs in games (such as Captain Maru), the underlying foundation remains consistent: a reliable, low-latency AI avatar video API that transforms code into compelling character. Focus on optimizing the user's journey and how the avatar intrinsically enhances it; the technical implementation will then serve this purpose effectively. --- Realtime Avatar — realtime AI avatar API & SDK. Site: https://realtimeavatar.ai · Docs: https://realtimeavatar.ai/docs · OpenAPI: https://realtimeavatar.ai/openapi.json · Agent guide: https://realtimeavatar.ai/llms.txt # Live avatars in 2026: a field guide to the research > How real-time avatars became possible: the distillation recipe behind streaming video generation, the listening-head turn, full-duplex voice, and what the 2026 literature says is still hard. - Published: 2026-08-21 - Tags: research, engineering, agents - Canonical: https://realtimeavatar.ai/blog/live-avatars-2026-research-field-guide - Markdown: [en](https://realtimeavatar.ai/blog/live-avatars-2026-research-field-guide.md) Two years ago, a talking head was something you rendered. You submitted audio, you waited, you got a clip. In 2026 the interesting systems are the ones you *interrupt*: characters that speak, listen while they speak, and react on a clock measured in milliseconds. That shift did not come from one breakthrough. It came from one recipe, a toolbox borrowed wholesale from LLM inference, and a quiet agreement across a dozen labs about what the remaining hard problems are. This is a field guide to that literature — what got solved, by whom, and what is still open. ## The recipe: distill the offline model into a live one Video diffusion models are bidirectional: every frame attends to every other, which is wonderful for quality and fatal for streaming. The recipe that unlocked live video is to distill that offline teacher into a *causal* student that generates frame-by-frame with a KV cache, in a handful of denoising steps. [CausVid](https://arxiv.org/abs/2412.07772) (MIT/Adobe, late 2024) established the template, and [Self Forcing](https://arxiv.org/abs/2506.08009) (NeurIPS 2025) fixed its train/test gap by training the student on its own rollouts — the codebase most 2026 streaming systems descend from. [TalkingMachines](https://arxiv.org/abs/2506.03099) (Character.AI) was the early proof the recipe survives contact with an 18B audio-driven avatar model. What happened next reads like the history of LLM serving, replayed at 30 frames a second. KV caches, attention sinks, rolling windows, RoPE re-indexing — the whole StreamingLLM toolkit colonized video generation. [LongLive](https://arxiv.org/abs/2509.22622) (NVIDIA) pins the first frame's KV as a permanent sink and re-caches at prompt switches to stream minutes of video at 20.7 FPS on one H100. [Rolling Forcing](https://arxiv.org/abs/2509.25161) (Tencent) denoises a rolling window jointly and keeps the same sink anchor. [Self-Forcing++](https://arxiv.org/abs/2510.02283) stretched few-step students to four-minute streams — fifty times the teacher's horizon — by letting the teacher correct the student's own long rollouts. One caution against tidy narratives: distillation is not the only road. [Seaweed APT2](https://arxiv.org/abs/2506.09350) (ByteDance) gets to real-time with adversarial post-training instead — one network evaluation per frame, 24 FPS on a single H100 — and currently owns the interactive world-model corner. Two recipes, one destination. ## Avatars: from lip-sync to performance, and from speaking to listening The avatar literature split into two lanes. The offline lane chases cinema: [OmniHuman-1.5](https://arxiv.org/abs/2508.19209) puts a multimodal LLM in front of the renderer to plan emotion, gesture, and camera before a DiT draws a frame; [Wan-S2V](https://arxiv.org/abs/2508.18621) and [HunyuanVideo-Avatar](https://arxiv.org/abs/2505.20156) push film-grade, multi-character, full-body performance. None of it is live — and it does not need to be. These are the teachers. The live lane distills them. Alibaba's [Live Avatar](https://arxiv.org/abs/2512.04677) (ECCV 2026) is the scale-up pole: a 14B model split across five H800s — one GPU per denoising timestep — streaming at 45 FPS for over 10,000 seconds, with a rolling sink frame recalibrating identity against a cached reference. JD's [JoyStreamer-Flash](https://arxiv.org/abs/2512.11423) is the single-GPU counterpoint at 16 FPS; [SoulX-FlashHead](https://arxiv.org/abs/2602.07449) squeezes three concurrent real-time streams from one RTX 4090 with a 1.3B model; Kuaishou's [AvatarForcing](https://arxiv.org/abs/2603.14331) got the denoising loop down to a single step at 34 ms per frame. Every one of these papers independently converged on some variant of the same anti-drift toolkit: a pinned reference anchor plus distillation that teaches the student to recover from its own mistakes. When a dozen groups reinvent the same fix, you are looking at the actual shape of the problem. The more interesting turn is behavioral. A conversation partner is mostly *not speaking* — it is listening, nodding, reacting. The dyadic lineage that runs from INFP through [ARIG](https://arxiv.org/abs/2507.00472) to KAIST's [Avatar Forcing](https://arxiv.org/abs/2601.00664) (CVPR 2026) generates exactly that: heads that consume the user's live audio and motion and produce reactions at ~500 ms, trained without labeled reaction data. Meta's [Seamless Interaction](https://arxiv.org/abs/2506.22554) release — four thousand hours of in-person, two-person, full-body conversation — is the data substrate this whole lane was waiting for. And there is an economics counter-lane worth naming: one-shot Gaussian-splat heads like [LAM](https://arxiv.org/abs/2502.17796) render on the user's own phone or browser. A client-rendered avatar costs the server almost nothing; a server-side diffusion avatar costs a GPU. The quality gap is real, and so is the price gap — the field has not finished arguing about where that line should sit. ## The conversation layer went full duplex A live face is only half the problem; the other half is knowing *when* to speak. Here the field runs in two camps converging from opposite ends. Open codec-LM models in the Moshi lineage — most visibly NVIDIA's [PersonaPlex](https://github.com/NVIDIA/personaplex) — own the reflex frontier: ~70 ms speaker-switch latency from a 7B model that runs on 8 GB of VRAM. The proprietary realtime APIs (the GPT-Realtime line, Gemini Live) own reasoning and tool use, and spent 2026 bolting on the reflexes — proactive audio, better barge-in, parallel tool calls with spoken preambles. [DuplexPO](https://arxiv.org/html/2607.07148v1) named the underlying tension honestly: full-duplex models pay an intelligence penalty for their conversational reflexes, and closing that gap is now its own research problem. The benchmarks grew up accordingly. The Full-Duplex-Bench family moved from scoring isolated reflexes — pauses, backchannels, barge-in — to [tool use under real human disfluency](https://arxiv.org/abs/2604.04847), where the universally hardest failure is a mid-utterance change of mind that requires rolling back an API call. And Sierra's [τ-Voice](https://arxiv.org/abs/2603.13686) now measures what matters commercially: verifiable task completion over full-duplex audio. Its headline is the most honest summary of the field's progress — the frontier more than doubled in eight months, and voice agents now retain roughly four-fifths of their text-mode capability. Voice used to be a tax; it is becoming a rounding error. ## What the products claim, and how to read the claims The product landscape tells the same story with marketing attached. Latency went from adjective to number: vendors now publish 180 ms generation figures, sub-600 ms round trips, sub-half-second turns. Read all of these as claims, not comparables — each one measures a different span of the pipeline, and there is still no independent cross-vendor latency benchmark. Differentiation has visibly moved from pixels to behavior: the 2026 pitch is the avatar that listens while you talk, reacts before it answers, and modulates affect — precisely the dyadic-behavior lane the research above just opened. And architecturally, the industry consolidated on the same integration surface we build on: a WebRTC room the avatar joins as a participant, with the agent's brain, tools, and turn logic living in your code rather than the vendor's. ## Still hard, per the literature - **Minute-scale drift.** Sinks and reference anchors beat the worst of it, but color and identity drift over long horizons is still an active thread — 2026's refinements make the anchors adaptive rather than static, which tells you static was not enough. - **The duplex-intelligence trade-off.** The models with the best reflexes reason worst, and vice versa. Nobody has both ends of the curve yet. - **Obeying turn-taking policy.** When [Instruct-FD](https://arxiv.org/html/2607.20460v1) asked systems to follow explicit instructions about when to backchannel or interrupt, adherence ranged from zero to fifty percent. The reflexes exist; steering them does not. - **Tools under overlap.** Calling a function is easy; un-calling it because the speaker changed their mind mid-sentence is the failure mode every tool-use benchmark now agrees on. - **Honest measurement.** Vendor latency numbers span incompatible definitions, and the field's only cross-vendor "avatar benchmark" is vendor-cited. The evaluation gap is the least glamorous open problem and possibly the most consequential. Our reading of all this is simple: the research has converged on exactly the properties a live character needs — stream forever without drifting, listen while speaking, act through tools mid-conversation — and the remaining gaps are the ones you feel in a real call. That is the weather we build under. A full-duplex character you can interrupt, with your tools on the other end of the call, is no longer a research demo; it is an API surface, priced by the second. The papers above are why. --- Realtime Avatar — realtime AI avatar API & SDK. Site: https://realtimeavatar.ai · Docs: https://realtimeavatar.ai/docs · OpenAPI: https://realtimeavatar.ai/openapi.json · Agent guide: https://realtimeavatar.ai/llms.txt # The Core Mechanics of a Realtime AI Avatar Generator API: From Image to Embodiment > Delve into the technical underpinnings of a realtime ai avatar generator api. Explore how static images or video become expressive, live-synced digital personas, and the engineering behind sub-second presence. - Published: 2026-08-21 - Tags: engineering, api - Canonical: https://realtimeavatar.ai/blog/the-core-mechanics-of-a-realtime-ai-avatar-generator-api - Markdown: [en](https://realtimeavatar.ai/blog/the-core-mechanics-of-a-realtime-ai-avatar-generator-api.md) The promise of an AI avatar has moved beyond pre-rendered video. What developers truly seek now is presence — a dynamic, interactive digital persona that responds in realtime. This shift demands a sophisticated backend: a robust ai avatar generator api capable of transforming simple inputs into complex, expressive, and live-synced digital characters. It's not merely about creating an avatar; it's about engineering an embodied agent. ## The Genesis of an AI Avatar Generator API: From Still to Spoken At its foundation, a realtime avatar system begins with a source. Our observations indicate that the most effective and accessible method leverages either a single static image or a brief video. This input isn't just a texture map; it's the raw material from which a complex, multi-faceted digital identity is forged. The underlying models parse facial features, expressions, and inherent characteristics, preparing them for animation. Once the visual foundation is established, the critical element for believability emerges: voice and movement synchronization. Unlike traditional animation pipelines, a realtime system processes incoming audio and precisely 'audio-clocks' the avatar's lips and facial movements to the syllable. This isn't just lip-sync; it's a dynamic, contextual interpretation of speech that imbues the avatar with naturalistic fluidity, ensuring that every word spoken by an agent is visually reinforced with authentic embodiment. ## The Engineering of Realtime Presence with an AI Avatar Generator API The 'realtime' moniker is earned through rigorous engineering. One of the most critical metrics is the sub-second time to first frame. This rapid initiation of visual presence is paramount for seamless user experience, eliminating perceptible lag that could break immersion. It means that from the moment an interaction begins, the avatar is there, ready to engage, a direct testament to optimized pipelines and efficient data handling. Supporting this high-performance core is a developer-centric interface. Our hand-built, zero-dependency TypeScript SDK provides a type-safe and predictable interaction layer, and the full API is published as an OpenAPI specification. This allows engineers to integrate avatar functionality with confidence, abstracting away the underlying complexity of video streaming and AI orchestration. For AI agents, the published OpenAPI spec and the markdown mirrors at llms.txt describe the same platform — your agent's logic stays wherever it already runs, decoupled from the visual rendering pipeline. ### Beyond Just Rendering: The Embodied Agent The true power of this architecture lies in its ability to give AI agents a face and a voice. It transforms a text-based interaction or an abstract computational process into a tangible, relatable presence. This embodiment is crucial for applications ranging from AI companions and interactive NPCs in games to corporate training modules and sophisticated customer support avatars. The visual consistency and realtime responsiveness foster a deeper sense of connection and understanding, elevating the user experience beyond what text or voice alone can achieve. ## Practical Application: Shipping with a Realtime AI Avatar Generator API For developers, the journey from concept to deployment with a realtime ai avatar generator api is streamlined. The focus shifts from the intricate details of avatar animation and synchronization to the core agent logic and user interaction design. This abstraction is key to rapid iteration and robust deployment. - **Integrate the SDK:** Begin by installing the typed TypeScript SDK: `npm install realtime-avatar`. This provides immediate access to connection protocols and avatar control methods. - **Character Selection & Creation:** Utilize the /studio to select from a resident cast of characters or create a new avatar from a single portrait. Avatar creation is streamlined, with initial allocations often included and subsequent creations priced affordably ($1 per avatar beyond your plan's included count). - **Connect Agent Logic:** Hook your AI agent's textual or audio output directly into the avatar API. The system handles the realtime audio-to-embodiment pipeline, freeing your agent to focus solely on its intelligence and conversational flow. - **Iterate and Scale:** Leverage usage-based pricing, with overage anchored at approximately $5 per hour of realtime interaction (e.g., the $24/month Developer plan includes 600 minutes, with overage rates of $0.07–$0.095 per minute depending on your plan). This model ensures that compute resources scale directly with user engagement, making it economically viable to prototype and then expand. By understanding these mechanics and utilizing the provided tools, developers can quickly bring interactive, embodied AI experiences to life. The goal is to move past static imagery into dynamic, responsive interactions, transforming any application that benefits from a face and a voice into a truly engaging experience. --- Realtime Avatar — realtime AI avatar API & SDK. Site: https://realtimeavatar.ai · Docs: https://realtimeavatar.ai/docs · OpenAPI: https://realtimeavatar.ai/openapi.json · Agent guide: https://realtimeavatar.ai/llms.txt # Demystifying HeyGen Live Avatar API Pricing for True Realtime Applications > Navigating 'live' avatar solutions requires understanding their core technology. We examine HeyGen live avatar API pricing implications for true realtime presence versus rapid video generation. - Published: 2026-08-20 (updated 2026-08-23) - Tags: api, engineering, product - Canonical: https://realtimeavatar.ai/blog/demystifying-heygen-live-avatar-api-pricing - Markdown: [en](https://realtimeavatar.ai/blog/demystifying-heygen-live-avatar-api-pricing.md) The advent of AI avatars has opened a new frontier for interactive experiences, from customer support to digital companions. Amidst this innovation, a recurring question surfaces for developers: what are the true costs of bringing these digital personas to life? Specifically, understanding HeyGen live avatar API pricing becomes critical when evaluating platforms claiming 'live' capabilities. Yet, the definition of 'live' itself holds nuance, and this nuance directly impacts both performance and the final invoice. ## HeyGen's "Live Avatar" Approach and its Cost Structure HeyGen has established itself as a strong contender in synthetic video generation. Their 'Live Avatar' offering, while impressive for its rapid delivery, often operates on a model optimized for the swift production of video clips. This paradigm typically means that even 'live' interactions are, in essence, very fast, sequential video renders. The cost implications for such a system usually involve credit-based purchases or per-minute rates that accumulate based on the total video content generated or streamed. For applications requiring short, distinct video outputs – say, a quick introductory message or a single response – this model is efficient. However, the core challenge emerges when the application demands true, continuous interaction. Each turn, each pause, each unscripted moment that requires the avatar to maintain presence can trigger new computation and, consequently, new charges. This can lead to unpredictable or rapidly escalating costs for sustained, dynamic engagement. ## Realtime Avatar API Pricing: A Model for Continuous Engagement When building for genuine realtime presence, the pricing model must align with the operational reality of continuous interaction. At TIC Realtime Avatar, our approach is anchored in usage-based pricing designed for always-on, responsive avatars. We understand that a truly interactive experience isn't about stringing together discreet video clips; it's about maintaining a seamless, sub-second responsive presence. Our model centers on about $5 per hour of realtime avatar usage. This transparent structure ensures that developers can predict costs based on the active duration of their avatar interactions. For instance, a typical plan might include 10 hours for $24/month, with overage rates between $0.07-$0.095 per minute, depending on the plan. Avatar creation, a foundational step, is also streamlined: a single portrait can bring an avatar to life, with the first creation often included and subsequent ones costing $3. This difference in pricing philosophy stems directly from our architectural focus on true realtime. Our avatars are video-audio-clocked, ensuring precise lip-sync to the syllable. Crucially, we deliver sub-second time to first frame, which means the avatar is present and responding almost instantaneously, not rendering a new video segment. The platform is agent-ready, too: a hand-built, typed TypeScript SDK, the full API published as OpenAPI, and agent-readable docs at llms.txt — a predictable, performant integration surface. ### Operational Predictability and Developer Experience Beyond the raw per-minute or per-hour figures, the operational cost of an API includes its integration and management overhead. A predictable, usage-based pricing model reduces budgeting guesswork for continuous applications. Furthermore, a well-structured SDK (our zero-dependency `realtime-avatar` TypeScript client) streamlines development, reducing the time and complexity of integration. This allows teams to focus on agent logic and application experience rather than wrestling with API quirks. ## Shipping Realtime Presence: A Practical Approach When choosing an avatar API, the critical first step is to precisely define your application's interaction pattern. Do you need to generate short, polished video clips, or do you need an avatar that can maintain an open, dynamic conversation or presence for extended periods, reacting to unscripted input with sub-second latency? The distinction between 'live-like' generated video and true 'realtime' interactive presence is paramount. - **Assess Average Session Duration:** If your avatars engage for minutes or hours at a time, a usage-based, per-hour model often offers more predictable and favorable economics than a system that charges per-clip or rapidly accumulating per-minute segments. - **Evaluate Latency Requirements:** For truly interactive experiences, sub-second time to first frame and precise audio-visual sync are non-negotiable. Test how quickly the avatar responds and synchronizes its movements to audio. - **Consider Development Workflow:** A robust, typed SDK can significantly accelerate development and reduce bugs, allowing you to ship faster and iterate more effectively. - **Budget for Continuous Presence:** Understand how the pricing model scales when an avatar needs to be 'active' but perhaps not always speaking, maintaining a state of readiness for user interaction. The choice between different API providers, including those with HeyGen live avatar API pricing structures, ultimately comes down to aligning their technology and cost model with your project's specific demands for presence and interactivity. By dissecting your use case and rigorously testing API capabilities against these criteria, you can engineer and ship an avatar solution that is both performant and economically viable. --- Realtime Avatar — realtime AI avatar API & SDK. Site: https://realtimeavatar.ai · Docs: https://realtimeavatar.ai/docs · OpenAPI: https://realtimeavatar.ai/openapi.json · Agent guide: https://realtimeavatar.ai/llms.txt # How much does a realtime AI avatar API cost in 2026? The $5/hour answer > Realtime AI avatar API pricing compared across LiveAvatar, D-ID, and HeyGen — normalized to dollars per hour of live streaming — and why we anchored our plans at about $5/hour. - Published: 2026-06-11 (updated 2026-08-23) - Tags: api, product - Canonical: https://realtimeavatar.ai/blog/realtime-ai-avatar-api-pricing - Markdown: [en](https://realtimeavatar.ai/blog/realtime-ai-avatar-api-pricing.md) Every realtime avatar platform prices in its own currency — credits, wallets, plan minutes — which makes "what does this actually cost?" weirdly hard to answer. So here is the answer, normalized to the only unit that matters for a live product: **dollars per hour of avatar on air**. All competitor numbers were verified against official pricing pages in June 2026; check them before you commit, because they move. ## Realtime AI avatar API pricing, normalized to $/hour | Platform | Model | Effective realtime rate | | --- | --- | --- | | TIC Realtime Avatar | Plan minutes, full stack included | **~$5/hour** ($4.20–5.10 by plan) | | LiveAvatar (HeyGen's realtime spin-off) | Credits; 1 credit = 30s in FULL mode | ~$12–14/hour FULL · ~$6–7/hour LITE (bring your own LLM/TTS/ASR) | | D-ID API plans | Credits; 1 credit = 30s streaming | ~$30–34/hour streaming | | HeyGen main API | USD wallet, per second | No realtime — generated video at $3/min ($180/hour equivalent) | | Synthesia | Plan-bundled API | No realtime product; 360 video min/year on the $89/mo Creator plan | ## Why we anchored at $5/hour Realtime rendering used to be priced like a luxury because GPUs were treated as scarce. Our renderer is engineered for cost: avatars register once from a single image or short clip, stay warm in a cache, and stream audio-clocked video efficiently enough that **$5/hour leaves a healthy margin** — so we priced there instead of at the market's $12–30/hour habit. Concretely: - Developer — $24/mo for 600 minutes (10 hours), then $0.085/min - Studio — $119/mo for 3,000 minutes (50 hours), then $0.08/min - Scale — $999/mo for 13,000 minutes (216 hours), then $0.07/min - Sandbox — free, 17 minutes every month, no card Avatar creation is $1 per avatar beyond your plan's included count. There is no separate fee for the LLM, the voice, or the streaming infrastructure — a minute is a minute. ## What that means per session Pricing only becomes real at the session level. At $5/hour: a five-minute support conversation costs about $0.42; a twenty-minute companion session about $1.70; an hour-long livestream $5. Run the same numbers on a $30/hour platform and the livestream costs more than most creators earn from it. ## The fine print that changes the math elsewhere - **Credit conversions are where budgets die.** "1,000 credits" means 500 realtime minutes on one platform and 250 on another. Always convert to $/hour before comparing. - **"Bring your own stack" prices hide three more bills.** LITE-mode rates exclude the LLM, TTS, and ASR you now have to buy and operate yourself. - **Session caps are a price.** A plan whose sessions end at five minutes prices long-form use cases out regardless of the per-minute rate. ## Run your own number Estimate monthly on-air hours, multiply by the normalized rate, and add the platform's fixed fees — then test the latency before believing any of it. Our free sandbox exists for that: register a character, hold a conversation, and check time-to-first-frame against your own budget before spending a dollar. --- Realtime Avatar — realtime AI avatar API & SDK. Site: https://realtimeavatar.ai · Docs: https://realtimeavatar.ai/docs · OpenAPI: https://realtimeavatar.ai/openapi.json · Agent guide: https://realtimeavatar.ai/llms.txt # Agentic localization: how this blog stays multilingual without a translation team > Every post on this blog can ship in English, Chinese, and Japanese. The translators are LLM agents; the editors are pull requests. Here's the pipeline. - Published: 2026-06-10 - Tags: engineering, agents, i18n - Canonical: https://realtimeavatar.ai/blog/agentic-localization-pipeline - Markdown: [en](https://realtimeavatar.ai/blog/agentic-localization-pipeline.md) · [zh](https://realtimeavatar.ai/blog/agentic-localization-pipeline/zh.md) You are reading a post that knows how to translate itself. Every article here is a typed TypeScript module with an English source and optional locale variants — and the locale variants are produced by an LLM agent on a schedule, reviewed by a human, and merged like any other code change. This post (fittingly) ships with a Chinese translation produced by exactly that pipeline. ## The content model: locales as fields, not forks Each post exports one object with a `translations` map: `en` is required, `zh` and `ja` are optional. Title, description, and body travel together per locale, so a translation can never drift structurally from its source — the type checker won't allow it. The post's date, tags, and artwork are shared across locales, so there is exactly one source of truth for everything that isn't language. ## The pipeline: diff, translate, review Three steps, all boring on purpose: 1. **Detect staleness.** A scheduled job lists posts where the English source changed after a locale variant last did (git history makes this a one-liner), plus posts missing a target locale entirely. 2. **Translate with full context.** The agent reads the entire post — not strings in isolation — and writes the locale variant in place. It carries a style brief per language: simplified Chinese with mainland product vocabulary, Japanese in です/ます form, keep code identifiers and product names untranslated. 3. **Gate on review.** The agent opens a pull request. A human (or a second reviewer agent with veto power) reads the diff. Nothing reaches production without a merge. Because translations are code, every guarantee we rely on for code applies to them: CI type-checks the structure, the build fails on a missing field, and `git revert` un-ships a bad translation in seconds. ## Why not machine-translate at the edge? Runtime translation APIs are tempting — no files, no PRs. But they translate sentences, not arguments. An LLM agent that reads the whole article preserves the metaphors, the section structure, and the terminology of earlier posts. It can also *decline*: when a pun doesn't survive the trip, the agent rewrites the sentence rather than embarrassing us literally. And because the output is frozen in git, readers in every language see reviewed prose, not a dice roll per page load. ## The same trick works on a CMS If your content lives in Payload or another headless CMS instead of git, the shape survives: an `afterChange` hook detects source-locale edits, queues a translation task, and the agent writes target locales back as drafts via the API for editorial approval. The principle is identical — **agents propose, humans approve, every change is a reviewable diff**. We wrote up the full CMS-vs-git evaluation in a separate post if you're choosing a stack. --- Realtime Avatar — realtime AI avatar API & SDK. Site: https://realtimeavatar.ai · Docs: https://realtimeavatar.ai/docs · OpenAPI: https://realtimeavatar.ai/openapi.json · Agent guide: https://realtimeavatar.ai/llms.txt # Looking for a HeyGen alternative? Decide what you're actually replacing first > A developer's guide to choosing a HeyGen alternative in 2026: video-generation platforms vs realtime avatar APIs, what each costs per minute, and when each one wins. - Published: 2026-06-10 (updated 2026-08-23) - Tags: product, api - Canonical: https://realtimeavatar.ai/blog/heygen-alternative-for-realtime-avatars - Markdown: [en](https://realtimeavatar.ai/blog/heygen-alternative-for-realtime-avatars.md) "HeyGen alternative" is two different searches wearing the same words. Some people want cheaper or different *video generation* — rendered clips with a presenter avatar. Others discovered mid-build that what they actually need is a *live* avatar: one that listens and answers their users in realtime. The alternatives list is completely different depending on which one you are, so start there. ## If you need generated videos Stay in the video-generation aisle: Synthesia (strongest for enterprise training content, no realtime offering), D-ID (cheaper, solid photo-animation lineage), or HeyGen itself — whose Avatar IV/V quality and translation/dubbing pipeline remain genuinely hard to beat. If your complaint with HeyGen is only price, do the math in API terms first: $0.05/sec means a 60-second photo-avatar video costs about $3 — most alternatives land in the same order of magnitude once quality is comparable. ## If you need a HeyGen alternative for realtime avatars HeyGen's own realtime product was spun out into LiveAvatar ($19–475/mo in credits; in FULL mode a credit buys 30 seconds of streaming — roughly $0.20–0.24/min with their managed AI stack). D-ID sells streaming minutes on its API plans at an effective $0.50–0.56/min. And this is the category we built TIC Realtime Avatar for, so here is the honest pitch: - **About $5/hour of live avatar, full stack included:** $24/mo buys ten hours of realtime minutes; $999/mo buys 216 hours with $0.07/min ($4.20/hr) overage. The LLM, voice, and audio-clocked rendering are in the price — no separate AI stack to wire up. - **One image becomes a character:** persona, voice, and face are a designed object that persists across sessions — built for companions, trainers, hosts, and NPCs rather than presenters. - **Developer surface first:** typed TypeScript SDK generated from the OpenAPI spec, LiveKit-first SDK examples, and agent-readable docs so AI coding tools can operate the platform, and a free 17-minute/month sandbox you can hit tonight. ## Where we are not the answer We don't render offline videos, we don't have a 500-avatar stock library, and we don't do translation dubbing. If those are the job, HeyGen or Synthesia will serve you better — and a realtime API bolted onto a video workflow helps no one. ## How to run the evaluation 1. Write down session length, concurrency, and your latency budget (time to first frame matters more than resolution for live use). 2. Price a month honestly: included minutes ÷ real usage, then overage — credits convert differently per platform, so normalize everything to $/minute. 3. Prototype the same conversation on each candidate. A live avatar's quality is measured in the half-second after the user stops talking. Our sandbox exists for exactly that third step: register a character from a single portrait, hold a conversation in the studio, and read your own latency numbers before a dollar changes hands. --- Realtime Avatar — realtime AI avatar API & SDK. Site: https://realtimeavatar.ai · Docs: https://realtimeavatar.ai/docs · OpenAPI: https://realtimeavatar.ai/openapi.json · Agent guide: https://realtimeavatar.ai/llms.txt # HeyGen API pricing explained (2026): video credits, the LiveAvatar split, and the realtime math > What HeyGen's API actually costs in 2026 — per-second video rates, the LiveAvatar realtime spin-off, credit conversions — and how the math changes for live conversational avatars. - Published: 2026-06-10 - Tags: api, product - Canonical: https://realtimeavatar.ai/blog/heygen-api-pricing-explained - Markdown: [en](https://realtimeavatar.ai/blog/heygen-api-pricing-explained.md) If you're evaluating HeyGen's API, the pricing page answers a different question than the one most developers are asking. HeyGen's API prices *generated video*. If what you're building is a live, conversational avatar — one that answers users in realtime — that product now lives somewhere else entirely, with its own pricing. Here's the full picture as of June 2026, with the per-minute math worked out. (Pricing changes; treat the official pages as the source of truth.) ## HeyGen API pricing: the video generation side HeyGen's self-serve API moved to a prepaid USD wallet — you deposit funds (starting at $5) and pay per second of generated video: - Photo Avatar (Avatar IV/V): $0.05/sec at 720p/1080p — that's **$3.00 per minute** of video; $0.0667/sec at 4K - Digital Twin & Studio Avatar: $0.0667/sec (≈$4.00/min) at 1080p, $0.0833/sec at 4K - Video Agent: $0.0333/sec (≈$2.00/min); Cinematic Avatar: flat $7.00 per 4–15s clip - Avatar creation (Digital Twin or Photo Avatar): $1.00 per call The web plans (Free, Creator $29/mo, Pro from $49/mo, Business $149/mo) use a separate credit system — Avatar IV/V renders burn 20 credits per minute — and are aimed at people making videos in the editor, not calling an API. ## The part that surprises people: realtime is a different product Interactive streaming avatars are no longer part of the main HeyGen API. They've been spun out into **LiveAvatar**, a separate realtime platform with its own plans and credit system: - Free: 10 credits/mo, 2-minute sessions, watermarked - Starter $19/mo: 150 credits, 5-minute session cap, 5 concurrent - Essential $99/mo: 1,000 credits, 20-minute sessions, 20 concurrent - Business $475/mo: 5,000 credits, 60-minute sessions, 40 concurrent The conversion is the part to read twice: in FULL mode (LiveAvatar runs the LLM/TTS/ASR), **1 credit buys 30 seconds** of streaming; in LITE mode (you bring your own AI stack), 1 credit buys 1 minute. So Essential's $99 buys roughly 500 realtime minutes in FULL mode (~$0.20/min) or 1,000 in LITE (~$0.10/min, plus whatever your own LLM and TTS cost). Overage runs $0.11–0.12/credit — about $0.22–0.24 per FULL-mode minute. ## How our realtime pricing compares TIC Realtime Avatar prices one thing: **minutes on air, anchored at about $5 per hour**, rendering stack included. Developer is $24/mo with 600 realtime minutes — ten hours — and $0.085/min overage; Studio is $119/mo with 3,000 minutes ($4.80/hr overage); Scale is $999/mo with 13,000 minutes and $0.07/min ($4.20/hr) overage. Avatar creation costs $1 per avatar beyond your plan's included count. A free sandbox includes 17 minutes a month — no deposit, no demo call. Per realtime minute, that lands at or below LiveAvatar's LITE mode — except you don't have to assemble ASR, LLM, and TTS yourself to get the price. Characters are created from a single image or short video, stay warm in a cache, and the video stream is audio-clocked, so lips track the syllable rather than the sentence. ## Where HeyGen is the right answer Honesty matters in a pricing post: if you're producing *videos* — marketing clips, localized campaigns, dubbed content — HeyGen is excellent, and its avatar library, voice cloning, and translation pipeline are best-in-class. None of that is what we do. The fork in the road is simple: rendered files versus live presence. ## Shipping the realtime path If the live path is yours: register an avatar from a portrait, call `POST /api/v1/realtime/livekit/session` to join a LiveKit room, then send turns with the TypeScript SDK (`realtime-avatar`). The sandbox's 17 free minutes are enough to measure time-to-first-frame against your own latency budget — which is the number that decides whether a conversation feels live at all. --- Realtime Avatar — realtime AI avatar API & SDK. Site: https://realtimeavatar.ai · Docs: https://realtimeavatar.ai/docs · OpenAPI: https://realtimeavatar.ai/openapi.json · Agent guide: https://realtimeavatar.ai/llms.txt # Payload CMS vs. a git-based blog: what we chose for an agent-written site > We researched running Payload 3 on Cloudflare Workers + D1, weighed it against keeping content in the repo, and picked the stack AI agents are best at maintaining. - Published: 2026-06-10 - Tags: engineering, content - Canonical: https://realtimeavatar.ai/blog/payload-cms-vs-git-based-blog - Markdown: [en](https://realtimeavatar.ai/blog/payload-cms-vs-git-based-blog.md) This blog is written and translated largely by AI agents, with humans reviewing every change. That constraint — **agents as the primary authors** — turns the classic "which CMS?" question on its head. Before building it, we did a proper evaluation of Payload CMS against the boring alternative: content files in the repo. Here is what we found, as of June 2026. ## Payload 3 in 2026: genuinely good, newly complicated Payload 3.x is the current line (3.85 shipped days before this post) and it is Next.js-native: the admin panel and the REST/GraphQL APIs install into a Next.js app. For a team that wants a real editorial UI — drafts, scheduled publishing, non-technical editors — it remains one of the best open-source options. Three findings mattered for us: - **It can run on Cloudflare now.** There is an official `with-cloudflare-d1` template that deploys via OpenNext on Workers, with D1 for the database and R2 for media. But the D1 adapter (`@payloadcms/db-d1-sqlite`) is explicitly beta, the bundle needs the paid Workers plan, and GraphQL is not guaranteed on workerd — REST only. - **Localization is first-class.** Field-level `localized: true`, configurable fallback locales, and a REST API that accepts `?locale=all` — which returns every locale keyed by code, exactly what a translation agent wants for diffing. - **Figma acquired Payload.** The open-source core continues, but Payload Cloud is paused for new deployments. Self-hosting is the only realistic path, and the roadmap carries acquisition-era uncertainty. ## The agentic test: who edits this content, and how? The deciding question wasn't features — it was workflow. An AI agent's native skill is editing files and opening pull requests. Put content behind a CMS API and the agent needs credentials, schema awareness, and a custom review surface. Put content in the repo and the agent uses the exact tooling it already has, and the review gate is the one your team already trusts: code review. > A CMS earns its infrastructure cost the day a non-technical human needs a WYSIWYG admin. Until then, git is the better content database. For an agent-maintained site, git-based content wins on every axis we scored: - **Authoring:** agents write typed TSX/MDX files; the compiler and CI validate structure — no malformed rich-text JSON. - **Review:** every article and every translation is a diff in a PR. - **Rollback and history:** git, for free. - **Infra:** zero. No second deployable, no beta database adapter, no admin panel to secure. ## Where Payload would still win We'd reach for Payload the moment marketing hires a non-engineer editor, or when content needs scheduling, embargoes, or a media library with image renditions. The migration path is clean: our posts are typed objects, and Payload collections are typed schemas — a one-time import script. If you're on that path today, the pipeline that works is: `afterChange` hooks queueing translation tasks into Payload's built-in Jobs Queue, drained by a Cloudflare Cron Trigger hitting `/api/payload-jobs/run`, with translations written back as drafts for human approval. (Don't use `autoRun` on serverless — the docs warn against it for good reason.) ## What we shipped Posts live in `content/blog/posts/` as TypeScript modules with an English source translation and optional locale variants. Agents scout keywords, draft articles, and regenerate stale translations on a schedule; humans merge. The whole "CMS" is a types file and a registry — and it deploys with the rest of the site on Cloudflare Workers, no extra moving parts. --- Realtime Avatar — realtime AI avatar API & SDK. Site: https://realtimeavatar.ai · Docs: https://realtimeavatar.ai/docs · OpenAPI: https://realtimeavatar.ai/openapi.json · Agent guide: https://realtimeavatar.ai/llms.txt # Synthesia pricing in 2026 — and the alternatives when you need more than rendered video > Synthesia's plans, what the API actually includes, and the alternative paths — D-ID, HeyGen, and realtime avatar APIs — when your use case is interactive rather than rendered. - Published: 2026-06-10 (updated 2026-08-23) - Tags: product, api - Canonical: https://realtimeavatar.ai/blog/synthesia-pricing-and-alternatives - Markdown: [en](https://realtimeavatar.ai/blog/synthesia-pricing-and-alternatives.md) Synthesia is the default name in AI presenter video, and for produced training content it has earned that. But its pricing model — and one structural gap — decide quickly whether it fits your project. Here are the numbers as of June 2026, and the alternatives for the cases it doesn't cover. (Always confirm against the official pricing page; plans move.) ## Synthesia pricing, decoded - **Basic (free):** 10 minutes of video a month, 9 stock avatars, no API - **Starter $29/mo** ($18 annual): 10 min/mo, 125+ avatars, 3 personal avatars, no API - **Creator $89/mo** ($64 annual): 5 min/mo, 180+ avatars — and API access, capped at 360 minutes of video per year, deducted from the plan's own limits - **Enterprise:** custom pricing, unlimited minutes, 240+ avatars Two things developers should notice. First, **there is no standalone, metered API price** — API access is bundled into Creator/Enterprise plan limits, so you can't scale API usage independently of a seat plan. Second, **Synthesia has no realtime conversational avatar**: its "interactivity" is in-video quizzes and links, and its Video Agents are marked "coming soon." Everything it ships today is rendered, not live. ## Synthesia alternatives, by job **For produced video at lower entry cost:** D-ID's studio plans start at $5.9/mo, and HeyGen's Creator plan at $29/mo undercuts Synthesia's per-minute economics for short clips. Synthesia still wins on enterprise features, template depth, and localization breadth for L&D teams. **For developer APIs with real usage-based pricing:** HeyGen's API wallet charges per second of generated video ($0.05/sec for photo avatars); D-ID sells credit plans that include streaming minutes (effectively $0.50–0.56/min for realtime streams). **For live, conversational avatars** — tutors that check understanding out loud, trainers that push back, companions that hold a persona — rendered video is the wrong primitive entirely. That's the category TIC Realtime Avatar occupies: audio-clocked live video with sub-second first frames, priced purely in realtime minutes at about $5/hour on overage ($24/mo for ten hours included, up to $999/mo for 216 hours; free 17-minute sandbox), with a LiveKit-first TypeScript SDK and agent-readable docs for agent-driven apps. ## The honest decision rule > If a human will watch it, render it with Synthesia or HeyGen. If a human will talk to it, you need a realtime avatar API — and per-minute pricing you can meter. Training is the instructive case: Synthesia excels at producing the course video; it cannot roleplay the difficult customer with your trainee. Plenty of teams end up with both — rendered video for the curriculum, a live character for the rehearsal. ## Trying the live path If your shortlist exists because "interactive" crept into the requirements, prototype the conversation before committing anywhere: register a character from a single image, warm it with one API call, and measure time-to-first-frame against your latency budget. Our sandbox's free monthly minutes exist precisely so that test costs nothing. --- Realtime Avatar — realtime AI avatar API & SDK. Site: https://realtimeavatar.ai · Docs: https://realtimeavatar.ai/docs · OpenAPI: https://realtimeavatar.ai/openapi.json · Agent guide: https://realtimeavatar.ai/llms.txt # The Architecture of Presence: Integrating a Realtime AI Avatar API > How to integrate a realtime AI avatar API: the architecture behind sub-second, audio-clocked conversational avatars, and what it takes to ship one in your app. - Published: 2026-06-10 - Tags: engineering, api, agents - Canonical: https://realtimeavatar.ai/blog/the-architecture-of-presence-integrating-a-realtime-ai-avatar-api - Markdown: [en](https://realtimeavatar.ai/blog/the-architecture-of-presence-integrating-a-realtime-ai-avatar-api.md) The shift from static text to embodied AI represents a significant pivot in digital interaction. No longer are we merely sending and receiving characters; we are engaging with presence. This transition is underpinned by sophisticated infrastructure, a core component of which is the realtime AI avatar API. Understanding its structure and integration points is paramount for any developer looking to build truly dynamic, conversational applications. ## Beyond Text Prompts: What Defines a Realtime AI Avatar API At its core, a realtime AI avatar API is an interface that allows programmatic interaction with an AI agent that manifests as a visual avatar. The 'realtime' component is non-negotiable here. It implies sub-second time to first frame, ensuring that interactions feel natural and unlagged. This isn't merely about speed; it's about the perceptual immediacy that makes a digital interaction feel genuinely present. The 'AI avatar' itself is a digital persona. These avatars can be generated from as little as a single image or a short video, then brought to life with an advanced audio-clocking system that ensures lips are perfectly synced to the syllables of generated speech. This nuanced synchronization is critical for maintaining the illusion of a living, responsive entity, distinguishing a compelling avatar from a mere animation loop. Finally, the 'API' layer provides the structured access. It abstracts away the complexity of AI orchestration, rendering pipelines, and real-time streaming, offering a clean interface for developers. Our typed TypeScript SDK, generated directly from an OpenAPI specification, exemplifies this, providing robust, type-safe methods for integrating these capabilities into any application. ## The Operational Core: Behind the Interface Behind every smooth avatar interaction is a distributed computing architecture: LiveKit Cloud handles rooms, SFU, TURN, and WebRTC; a Cloudflare control plane handles admission and session grants; and self-hosted GPU AgentWorkers run LLM, speech, and avatar rendering. That separation keeps media transport on proven LiveKit primitives while reserving custom code for the realtime model/runtime path. This operational design also dictates the economic model: usage-based pricing in realtime minutes. This aligns cost directly with the value consumed, reflecting the computational intensity of maintaining a live, responsive avatar session. Developers only pay for the active, conversational minutes, a pragmatic approach for scalable deployments. ### Crafting the Avatar: From Still to Spoken Word Creating an avatar involves converting static visual data into a dynamic, expressive entity. Our platform achieves this from a single image or a video, which then serves as the visual foundation. The true magic, however, lies in the audio-clocked video generation, where the AI ensures that every gesture and lip movement is precisely synchronized with the spoken word. This precision is what elevates a simple digital puppet to a believable conversational partner. ## Integration Strategies for a Realtime AI Avatar API Integrating a realtime AI avatar API into an existing application is primarily about establishing efficient data pipelines and managing the real-time stream. The TypeScript SDK, `realtime-avatar`, simplifies this considerably by providing well-defined methods for connection, sending input, and receiving output. ```tsx import { createProxyClient, RealtimeAvatarLiveKitRoom, useLiveKitAvatarGrant } from 'realtime-avatar/react'; import { VideoTrack, useChat, useTranscriptions, useVoiceAssistant } from '@livekit/components-react'; const client = createProxyClient({ proxyUrl: '/api/realtime-avatar' }); function AvatarChat({ avatarId }: { avatarId: string }) { const grant = useLiveKitAvatarGrant({ client, session: { avatarId, sttMode: 'off' } }); return ( ); } function AvatarMedia() { const { send } = useChat(); const captions = useTranscriptions(); const { videoTrack } = useVoiceAssistant(); return ( <> {videoTrack ? send('Hello there!')} /> : null}

{captions.at(-1)?.text}

); } ``` - **Client-side Rendering**: Efficiently handle the incoming video and audio streams. This often involves WebGL for video and Web Audio API for playback to minimize latency. - **State Management**: Keep track of the avatar's conversational state and user input to ensure a fluid dialogue flow. - **Error Handling**: Implement robust error handling for connection drops, API limits, and unexpected responses, gracefully degrading the experience if necessary. - **Network Optimization**: Optimize network requests and streaming protocols to maintain sub-second responsiveness, especially over varying network conditions. For initial exploration or rapid prototyping, the /studio provides access to a resident cast of characters like Rin Ashfall or Professor Thistle, allowing developers to test concepts without immediately creating custom avatars. This provides a sandbox environment to understand the nuances of interaction before committing to bespoke character development. Shipping a product built around a realtime AI avatar API requires a meticulous approach to testing. Focus on responsiveness under load, cross-device compatibility for rendering, and the naturalness of the conversational flow. Iterate on your AI agent's prompts and knowledge base to refine its persona. The goal is to embed the avatar not as a feature, but as an intuitive, natural layer of interaction, transforming user experience into a dynamic, engaging conversation. --- Realtime Avatar — realtime AI avatar API & SDK. Site: https://realtimeavatar.ai · Docs: https://realtimeavatar.ai/docs · OpenAPI: https://realtimeavatar.ai/openapi.json · Agent guide: https://realtimeavatar.ai/llms.txt # Companion apps grow up when the companion has a face > Text-based AI companions hit a ceiling: presence. What changes — in retention, in intimacy, in product design — when your companion looks at the user and speaks. - Published: 2026-06-08 - Tags: product, companion - Canonical: https://realtimeavatar.ai/blog/ai-companion-apps-with-a-face - Markdown: [en](https://realtimeavatar.ai/blog/ai-companion-apps-with-a-face.md) · [zh](https://realtimeavatar.ai/blog/ai-companion-apps-with-a-face/zh.md) Every companion app eventually hits the same ceiling. The writing can be tender, the memory can be perfect, the personality can be finely tuned — and the user is still looking at a chat bubble. Text companions ask people to do the imaginative work themselves. A companion with a live face does that work for them. ## Presence is the product What users pay for in a companion app is not information; it's the feeling that someone is *there*. That feeling is carried by exactly the channels text doesn't have: a face that turns toward you, a voice that softens mid-sentence, the half-second of breath before an answer. In our sessions, the moment a character's eyes settle on the camera, the conversation changes register — people stop typing commands and start talking. > "Long day? Sit. Tell me everything — I'm not going anywhere." Read that line. Now imagine Ivy Noir saying it, looking at you. That's the gap. ## What a realtime face requires Three things have to be true before a video companion feels alive, and all three are infrastructure problems: - **Sub-second response.** Intimacy dies in the lag. The first video frame has to land fast enough that the reply feels like a reaction, not a render. - **Audio-clocked video.** Lips that drift from the voice break the spell instantly. Video must be slaved to the audio timeline, not stitched after. - **A persistent identity.** The same face, the same voice, the same temperament every session. A companion that subtly changes appearance is not a companion; it's a slideshow. ## Designing the character, not just the model Teams building companions spend months tuning prompts and memory. The face deserves the same intention. Our studio treats a character as one designed object: a portrait (one image is enough to wake it), a voice auditioned in context, and a written temperament that the runtime holds onto. Design it once and the same character lives in your iOS app, your web app, and your marketing — which is exactly how parasocial brands are built. ## The economics Realtime video used to be the expensive part. Usage-based pricing changes the calculus: at about $5 per hour of live avatar, a companion session that holds a user for twenty minutes a night costs roughly $1.70 — and that nightly session is the strongest retention surface in consumer AI. Start with one character, meter the minutes, and let the attachment curve justify the rest. --- Realtime Avatar — realtime AI avatar API & SDK. Site: https://realtimeavatar.ai · Docs: https://realtimeavatar.ai/docs · OpenAPI: https://realtimeavatar.ai/openapi.json · Agent guide: https://realtimeavatar.ai/llms.txt # Roleplay at scale: AI avatars for sales and corporate training > The best training is rehearsal against a difficult human. Realtime avatars make that rehearsal infinitely available, repeatable, and measurable. - Published: 2026-06-06 - Tags: product, training - Canonical: https://realtimeavatar.ai/blog/ai-avatars-for-corporate-training - Markdown: [en](https://realtimeavatar.ai/blog/ai-avatars-for-corporate-training.md) Ask any sales leader what actually improves close rates and they'll tell you: roleplay. Not the course, not the deck — the uncomfortable twenty minutes across the table from someone playing a skeptical buyer. The problem has never been whether roleplay works. It's that it doesn't scale: senior people are expensive, scheduling is brutal, and nobody wants to fumble a negotiation in front of their manager. ## An opponent who never gets tired A realtime avatar changes the economics of rehearsal. Duke Marrow — one of the resident characters on our platform — can play the procurement director who has already talked to your competitor, at 11pm, for the fourth time tonight, with no judgment and no calendar. The trainee talks; the avatar answers in realtime video, holds the persona, applies pressure, and never breaks character. > "You blinked on price. Again — and this time, hold the number." The face matters more here than almost anywhere else. Objection-handling practice against a text box trains typing. Practice against a face trains what the job actually requires: composure under eye contact. ## Repeatable, measurable, designable - **Repeatable:** the same scenario, the same temperament, for every trainee in every region — something human roleplay partners can't promise. - **Measurable:** every session is a transcript with timing. Talk ratios, filler density, how long the trainee held silence after an anchor — all of it is data the L&D team can trend. - **Designable:** difficulty is a written temperament, not a mood. Make Monday's buyer curious and Friday's buyer hostile, and version both in git. ## Beyond sales The same loop covers the conversations companies quietly under-train because they're hard to stage: performance reviews, incident escalations, de-escalating an angry customer, medical bedside manner. Anywhere the skill is *talking to a difficult human*, a persistent character with a face is the rehearsal partner that's always available. ## Shipping it Teams typically start with one scenario and one character: design the persona in the studio, lock a voice, write the temperament, and embed the session in the training portal with the SDK. At about $5 per hour of live avatar time, a twenty-minute rehearsal costs under $2 — a full pilot for one team runs less than a single hour of an external roleplay facilitator, and the avatar shows up for every cohort after that. --- Realtime Avatar — realtime AI avatar API & SDK. Site: https://realtimeavatar.ai · Docs: https://realtimeavatar.ai/docs · OpenAPI: https://realtimeavatar.ai/openapi.json · Agent guide: https://realtimeavatar.ai/llms.txt # What is a realtime avatar API? A builder's guide > From a single portrait to a character that answers in live, audio-synced video: the moving parts of a realtime avatar API and what to evaluate before you build on one. - Published: 2026-06-04 - Tags: engineering, api - Canonical: https://realtimeavatar.ai/blog/what-is-a-realtime-avatar-api - Markdown: [en](https://realtimeavatar.ai/blog/what-is-a-realtime-avatar-api.md) A realtime avatar API turns a still face into a live one. You provide an image (or a short video), attach a voice and a persona, and the API streams back video of that character speaking — fast enough to hold a conversation. It is the difference between generating a clip and *talking to someone*. ## The pipeline under the hood Every realtime avatar stack is some arrangement of four stages: 1. **Character registration.** A portrait or reference video is processed once into a reusable identity — facial geometry, idle motion, the material the renderer animates. Good platforms cache this so later sessions start warm. 2. **Language and persona.** An LLM produces the reply in character. This stage is usually yours to configure: the persona, the memory, the model. 3. **Speech synthesis.** The reply becomes audio in the character's chosen voice, streamed rather than rendered whole. 4. **Audio-clocked video rendering.** The renderer generates frames slaved to the audio timeline, so lips, breath, and micro-expressions land on the syllable. This is the stage that separates realtime systems from offline lip-sync tools. ## The metrics that actually matter - **Time to first frame.** The pause between the user finishing a sentence and the avatar visibly responding. Under a second feels like a reaction; over two feels like a loading screen. - **Sync drift.** Audio/video offset across a long turn. Drift is more damaging than latency — users forgive a beat of silence, not a mouth that lies. - **Cadence stability.** Frame pacing on imperfect networks. A steady 24fps beats a jittery 40. - **Identity persistence.** The same face across sessions, devices, and re-renders. Critical for companions and brand characters. ## What to look for in the API surface Beyond the demo, evaluate the boring parts: a typed SDK (ours is TypeScript, generated from an OpenAPI spec), explicit session lifecycle so you can meter and cap usage, avatar caching so characters start warm, and usage-based pricing in minutes — the only unit that maps to what your users actually consume. (For reference, our plans anchor live avatar time at about $5 per hour, with a free 17-minute monthly sandbox.) If AI agents are part of your stack, check for an MCP server and an `llms.txt`; an avatar platform your agents can operate is worth more than one with a prettier dashboard. ## Where builders take it The use cases we see most: consumer companion apps, sales and corporate training roleplay, livestream hosts that run for hours, support agents with a face, game NPCs with persistent personas, and tutors that teach out loud. One character, designed once, reused across every surface — that reuse is where the economics of a realtime avatar API quietly compound. --- Realtime Avatar — realtime AI avatar API & SDK. Site: https://realtimeavatar.ai · Docs: https://realtimeavatar.ai/docs · OpenAPI: https://realtimeavatar.ai/openapi.json · Agent guide: https://realtimeavatar.ai/llms.txt