Interactive Avatar: LiveKit Plugin reference

Complete reference for livekit-plugins-synthesia — every parameter, method, event, and exception.

Use this page when you're already building and need exact signatures, defaults, and failure behavior. For the mental model behind these objects, see Concepts; for runnable code, see the Quickstarts.

All symbols live in livekit.plugins.synthesia.

Requirements

  • Python 3.10 or later. Tested on 3.10, 3.11 and 3.12.
  • livekit-agents 1.8.2 or later, the plugin's only dependency, installed automatically.

Install with pip:

pip install "livekit-plugins-synthesia~=1.8"

If you're following LiveKit's docs, livekit-agents[synthesia]~=1.8 installs the same package.

Environment variables

Read when the corresponding argument isn't passed explicitly.

VariableUsed for
SYNTHESIA_API_KEYYour Synthesia workspace API key
SYNTHESIA_API_URLAPI base URL. Defaults to https://developers.synthesia.io
LIVEKIT_URLYour LiveKit project URL
LIVEKIT_API_KEYLiveKit API key
LIVEKIT_API_SECRETLiveKit API secret

All five are secrets and belong in your agent's environment or a secret manager. SYNTHESIA_API_KEY is workspace-bound and must never reach frontend code.

LIVEKIT_URL accepts https:// as well as wss://. The scheme is normalised before it reaches the avatar worker, so the https:// value LiveKit Cloud injects into deployed agents works unchanged.

Minimal usage

from livekit.plugins import synthesia

avatar = synthesia.AvatarSession(
    synthesia.AvatarConfig(avatar_ids=["<avatar-id>"]),
)
await avatar.start(session, room=ctx.room)   # before session.start()

The plugin replaces session.output.audio, so the avatar lip-syncs whatever speech your agent produces. You can swap STT, LLM or TTS providers freely and these two lines never change.

synthesia.AvatarSession

AvatarSession(
    avatar_config,
    *,
    api_key=None,
    api_url=None,
    join_timeout=30.0,
    avatar_participant_identity=None,
    avatar_participant_name=None,
)
ParameterTypeDefaultDescription
avatar_configAvatarConfigrequiredThe avatars to render. See below.
api_keystr | NoneNoneSynthesia workspace API key. Falls back to SYNTHESIA_API_KEY.
api_urlstr | NoneNoneAPI base URL. Falls back to SYNTHESIA_API_URL, then https://developers.synthesia.io.
join_timeoutfloat30.0Seconds to wait for the avatar to join and publish before raising SynthesiaError with type=ErrorType.TIMEOUT. Raise it if the worker cold-starts slowly.
avatar_participant_identitystr | None"synthesia-avatar-agent"The LiveKit identity the avatar joins under. Must be unique per concurrent avatar in a room, because LiveKit evicts an existing participant when a second joins with the same identity.
avatar_participant_namestr | None"Synthesia avatar"The LiveKit display name the avatar joins under.

Passing an empty or whitespace-only string for either participant field raises SynthesiaError. Omit them to take the defaults.

Properties

PropertyDescription
avatar_identityThe LiveKit identity the avatar joins under.
providerAlways "synthesia".

synthesia.AvatarConfig

AvatarConfig(avatar_ids)
FieldTypeDescription
avatar_idsSequence[str]One to five ids of avatars available to your workspace, each prefixed av_…. The first is the active avatar; the rest are precomputed so swap_avatar() can switch to them mid-session.

avatar_ids is validated when you construct the config, not when the session starts:

  • A bare string instead of a list raises ValueError.
  • Fewer than one or more than five ids raises ValueError.

An id your workspace can't access raises SynthesiaError with type=ErrorType.UNKNOWN_AVATAR later, at start().

Pass every avatar you might swap to up front. Adding one afterwards requires a new session.

Methods

await avatar.start(agent_session, room, *, livekit_url=None, livekit_api_key=None, livekit_api_secret=None)

Mounts the avatar into room, launches the worker, and wires session.output.audio. Returns once the avatar has joined and published its video track.

Call this before AgentSession.start(). LiveKit credentials fall back to LIVEKIT_URL, LIVEKIT_API_KEY and LIVEKIT_API_SECRET.

The room must already be connected, because the plugin mints the avatar's token on behalf of your agent's identity and needs one to exist. Connect the room, then attach the avatar.

Calling start() on an already-started session returns immediately and does nothing. Calling it while another start() is still running raises SynthesiaError.

await avatar.swap_avatar(avatar_id, *, timeout=15.0)

Switches the rendered avatar mid-session to another id from avatar_ids. Pass "default" to return to the first id. Returns the now-active avatar id once the swap has taken effect.

await avatar.swap_avatar("<another-id-from-avatar-ids>")
await avatar.swap_avatar("default")

Raises SynthesiaError if the session hasn't started or is shutting down, type=ErrorType.UNKNOWN_AVATAR if the target wasn't in avatar_ids, and type=ErrorType.CONNECTION if the swap request to the worker fails. A swap the worker rejects outright raises SynthesiaError with no type set.


await avatar.aclose()

Cooperative shutdown; restores normal audio routing. Called automatically when the room disconnects, or when the avatar drops unexpectedly.

Events

EventFires when
session_endedThe room disconnected. A clean end.
errorThe avatar stopped publishing video while the room was still connected, meaning the worker failed. Carries a SynthesiaConnectionError.
avatar.on("session_ended", lambda: ...)
avatar.on("error", lambda exc: ...)

session_ended means you or your user ended the session; error means the avatar went away on its own. Either one triggers shutdown automatically, so you don't need to call aclose() in the handler.

Exceptions

Every failure raises SynthesiaError, which subclasses LiveKit's livekit.agents.APIError, so you can catch Synthesia failures alongside every other plugin's. There is no exception hierarchy below it. To tell failures apart, read the type attribute.

AttributeDescription
typeAn ErrorType member identifying the failure, or None when the failure happened before a request was made.
retryableWhether retrying the same call could plausibly succeed.
retry_afterSeconds to wait, when the backend supplied a value. Populated for rate-limit and concurrency-limit failures only.
statusThe HTTP status the API answered, or None if no answer arrived.
request_idThe API's requestId, when the response carried one. Quote it when contacting support.
bodyThe raw response body, when there was one.

ErrorType

MemberMeaning
AUTHThe API key is invalid, expired, or lacks the scope this endpoint requires.
FEATURE_NOT_IN_PLANYour workspace's plan doesn't include interactive avatars.
UNKNOWN_AVATARAn avatar isn't accessible to your workspace. Also raised by swap_avatar() for a target that wasn't in avatar_ids.
QUOTA_EXCEEDEDYour workspace's session quota is exhausted.
RATE_LIMITEDThrottled. Carries retry_after when the backend supplied one.
CONCURRENCY_LIMITEvery concurrent-session slot for your plan is in use.
TIMEOUTThe avatar didn't join within join_timeout.
CONNECTIONNo usable response, or the avatar dropped mid-session.

Some failures carry no type at all, including a missing API key and a swap the worker rejects. Handle type is None rather than assuming every SynthesiaError is classified.

try:
    await avatar.start(session, room=ctx.room)
except synthesia.SynthesiaError as e:
    if e.type in (
        synthesia.ErrorType.QUOTA_EXCEEDED,
        synthesia.ErrorType.FEATURE_NOT_IN_PLAN,
    ):
        ...   # fall back to an audio-only session
    raise

Retry guidance

Branch on retryable, not on type; the plugin sets it per failure for exactly this decision. Honor retry_after when it's set. For a TIMEOUT failure, raise join_timeout before retrying, since cold starts are the usual cause.

Starting a session does not retry internally. A transient server error surfaces immediately as SynthesiaError with type=ErrorType.CONNECTION, so retrying is yours to do.

Never wrap auth, plan, quota, unknown-avatar, or validation failures in a retry loop.

For the HTTP-level error codes behind these error types, see Errors.

Troubleshooting

📘

Prefer automated help?

The Claude Code / Cursor skill can scan your codebase and suggest fixes directly.

SymptomLikely causeFix
SynthesiaError: a Synthesia API key is requiredNo SYNTHESIA_API_KEY and no api_key=.Set the env var or pass api_key=.
SynthesiaError: LiveKit url, API key, and API secret are requiredOne of the three is missing or blank at start().Set the env vars or pass them to start(). A blank secret is caught here deliberately: it still mints a token Synthesia accepts, and LiveKit would only reject it much later.
ValueError: avatar_ids must be a list of ids, not a single stringAvatarConfig(avatar_ids="<id>").Wrap it in a list: avatar_ids=["<id>"].
ValueError: avatar_ids must contain between 1 and 5 idsAn empty list, or more than five.Pass one to five ids.
SynthesiaError: livekit_url … is not a ws:// or wss:// URLA malformed LIVEKIT_URL.Use your project's wss:// URL. https:// is normalized automatically.
SynthesiaError: the room's local participant has no identitystart() ran before the room finished connecting.Connect the room first, then attach the avatar.
SynthesiaError: start() is already in progressTwo concurrent start() calls.Await the first one.
type=ErrorType.AUTHKey invalid or expired, or missing the scope this endpoint requires.Check the key and its scopes.
type=ErrorType.FEATURE_NOT_IN_PLANYour plan doesn't include interactive avatars.Retrying won't help. Contact Synthesia.
type=ErrorType.UNKNOWN_AVATAR at start()An avatar id isn't accessible to your workspace.Use an id your workspace has access to.
type=ErrorType.UNKNOWN_AVATAR on swap_avatar()The target id wasn't passed to AvatarConfig.Include every swappable id, up to five, in avatar_ids up front.
SynthesiaError: swap_avatar() requires a started avatar sessionCalled before start(), or during shutdown.Only swap while the session is live.
type=ErrorType.CONCURRENCY_LIMITAll concurrent session slots are in use.End an active session, or wait for one to end.
type=ErrorType.TIMEOUTThe avatar didn't join in time. Usually a cold start or network problem.Raise join_timeout and retry.
Avatar never appears, no errorTerminal console run mode.Run with dev or connect against a real room, and attach the avatar first. See Testing your integration.
Avatar joins but doesn't lip-syncThe agent isn't producing audio, or session.output.audio was reassigned after start().Confirm the agent speaks without the avatar attached; never reassign output.audio after attaching.
Avatar video looks low-res or blurrySubscriber-side adaptive streaming downscaled the track.Create the room with adaptiveStream: false, or render the avatar large and call setVideoQuality(VideoQuality.HIGH). Resolution is set by the hosted worker; there's no plugin-side control.
Works locally, fails when deployedSecrets missing from the deploy environment.Confirm all five environment variables exist in the runtime.
type=ErrorType.TIMEOUT on most sessions, not just cold startsTypical join time has been running ~45 seconds against a 30-second default join_timeout.Raise join_timeout well above the default until this improves.
Session drops and the agent doesn't recoverThe plugin has no built-in reconnect.Handle the error event yourself and decide whether to retry.
A room refuses a new session, or the avatar won't (re)join a room it was just inA known issue can leave a closed session active in LiveKit, blocking a new one in the same room.Check the LiveKit dashboard; close the stale room manually, then retry.

Choosing an avatar

Only synthetic and personal avatars work. Stock actor-based avatars (for example Ryan or Ada) can't be used as interactive avatars, whatever your plan.

For an eligible avatar your plan gives you access to:

  1. Copy its ID from the Avatars page: open the ••• menu and select Copy ID.
  2. Convert it with POST /api/interactive-avatars/avatars. Poll until status is completed, then pass the returned id in avatar_ids.

A repeat request for the same source returns the existing interactive avatar rather than starting a new conversion. Converting a stock avatar, or one outside your plan, returns an error rather than an interactive avatar.

Additional resources