# Authentication
Source: https://docs.tavus.io/api-reference/authentication
Generate an API key in the PAL Maker and send it on each request in the `x-api-key` header.
To use the Tavus API, you need an API key to authenticate your requests. This key verifies that requests are coming from your Tavus account.
## Get the API key
1. Go to the PAL Maker and select **API Key** from the sidebar menu.
2. Click **Create New Key** to begin generating your API key.
3. Enter a name for the key and (optional) specify allowed IP addresses, then click **Create API Key**.
4. Copy your newly created API key and store it securely.
**Remember that your API key is a secret!**
Never expose it in client-side code such as browsers or apps. Always load your API key securely from environment variables or a server-side configuration.
## Make Your First Call
Requests go to **`https://tavusapi.com`**. Authenticate with your API key by sending it in the **`x-api-key`** header on every request, as below.
```http theme={null}
x-api-key:
```
For example, you are using the [POST - Create Conversation](/api-reference/conversations/create-conversation) endpoint to create a real-time video call session with a Tavus face. In this scenario, you can send an API request and replace `` with your actual API key.
```shell cURL theme={null}
curl --request POST \
--url https://tavusapi.com/v2/conversations \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"face_id": "r90bbd427f71",
"pal_id": "pdac61133ac5",
"conversation_name": "Interview User"
}'
```
# List Canvas Interactions
Source: https://docs.tavus.io/api-reference/canvas-interactions/list-canvas-interactions
get /v2/conversations/{conversation_id}/canvas/interactions
Returns every Canvas interaction recorded for a conversation, oldest first. Uses the same field shape as the `canvas.interaction` webhook's `properties` object.
Readable during and after the conversation. Call from your backend with your API key - not from the browser.
See [Canvas interactions](/sections/conversational-video-interface/magic-canvas/api/interactions).
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
See [Canvas interactions](/sections/conversational-video-interface/magic-canvas/api/interactions) for webhook delivery and reconciliation guidance.
# Record Canvas Interaction
Source: https://docs.tavus.io/api-reference/canvas-interactions/record-canvas-interaction
post /v2/conversations/{conversation_id}/canvas/interactions
Record a Magic Canvas interaction (submit, skip, dismiss, clear, error, or heartbeat) while a conversation is **active**.
The Tavus-hosted embed and `@tavus/cvi-ui` post interactions for you. Call this endpoint directly only if you build your own renderer.
**Authentication:** No API key is required while the conversation is active. Never put your Tavus API key in a browser. Once the conversation ends, every POST is rejected.
**Idempotency:** Retries with the same `(conversation_id, interaction_id)` and identical `tool_call_id`, `component`, `component_version`, `type`, and `value` return `200` without firing a second webhook. `metadata` is excluded from the match.
**Rate limiting:** 120 POSTs per 60-second window per `(client IP, conversation_id)`. Exceeding the limit returns `429` with `{ "error": "Too many requests" }` and a `Retry-After` header (seconds until the window resets). Custom renderers posting `heartbeat` interactions count toward this limit.
See [Canvas interactions](/sections/conversational-video-interface/magic-canvas/api/interactions) for per-component `value` rules, webhook delivery, and the full error catalog.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
See [Canvas interactions](/sections/conversational-video-interface/magic-canvas/api/interactions) for per-component `value` rules, webhook delivery, and the full error catalog.
# Create conversation
Source: https://docs.tavus.io/api-reference/conversations/create-conversation
post /v2/conversations
This endpoint starts a real-time video conversation with your AI face, powered by a PAL that allows it to see, hear, and respond like a human.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Delete Conversation
Source: https://docs.tavus.io/api-reference/conversations/delete-conversation
delete /v2/conversations/{conversation_id}
This endpoint deletes a single conversation by its unique identifier. Use this for destructive data removal. For normal call cleanup when a user leaves or a session is finished, use [End Conversation](/api-reference/conversations/end-conversation) instead.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# End Conversation
Source: https://docs.tavus.io/api-reference/conversations/end-conversation
post /v2/conversations/{conversation_id}/end
This endpoint ends a single conversation by its unique identifier. Use this for routine call cleanup when a user leaves or your app no longer needs the room. To destructively remove conversation data, use [Delete Conversation](/api-reference/conversations/delete-conversation).
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Get Conversation
Source: https://docs.tavus.io/api-reference/conversations/get-conversation
get /v2/conversations/{conversation_id}
This endpoint returns a single conversation by its unique identifier.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# List Conversations
Source: https://docs.tavus.io/api-reference/conversations/get-conversations
get /v2/conversations
This endpoint returns a list of all Conversations created by the account associated with the API Key in use.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Create Deployment
Source: https://docs.tavus.io/api-reference/deployments/create-deployment
post /v2/deployments
Create a managed deployment for the widget, embed, or landing-page channel.
See [Deployments overview](/sections/deployments/overview).
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Delete Deployment
Source: https://docs.tavus.io/api-reference/deployments/delete-deployment
delete /v2/deployments/{deployment_id}
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# End Deployment Conversation
Source: https://docs.tavus.io/api-reference/deployments/end-deployment-conversation
post /v2/deployments/{deployment_id}/conversations/{conversation_id}/end
Ends an active conversation started through a deployment. No API key required in the browser.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Get Deployment
Source: https://docs.tavus.io/api-reference/deployments/get-deployment
get /v2/deployments/{deployment_id}
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Get Deployment Init Config
Source: https://docs.tavus.io/api-reference/deployments/get-deployment-init
get /v2/deployments/{deployment_id}/init
Public endpoint called when a widget or embed mounts. Returns customization, limits, captcha settings, and availability.
Does **not** return a PAL identifier - the widget/embed runtime never reads it from `/init`.
No API key required. Origin and browser-context checks apply for non-preview callers.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# List Deployments
Source: https://docs.tavus.io/api-reference/deployments/list-deployments
get /v2/deployments
List deployments owned by the authenticated user.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Patch Deployment
Source: https://docs.tavus.io/api-reference/deployments/patch-deployment
patch /v2/deployments/{deployment_id}
Apply a [JSON Patch](https://jsonpatch.com/) array to the deployment document.
Send the `ETag` from GET as `If-Match` for optimistic concurrency.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Start Deployment Conversation
Source: https://docs.tavus.io/api-reference/deployments/start-deployment-conversation
post /v2/deployments/{deployment_id}/start
Creates a conversation for a deployment. Returns the same fields as [Create Conversation](/api-reference/conversations/create-conversation).
No API key required in the browser. Password, Turnstile, and call-limit checks apply for public callers.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Create Document
Source: https://docs.tavus.io/api-reference/documents/create-document
post /v2/documents
Upload documents to your knowledge base for PALs to reference during conversations.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Delete Document
Source: https://docs.tavus.io/api-reference/documents/delete-document
delete /v2/documents/{document_id}
Delete a document and its associated data using its unique identifier.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Get Document
Source: https://docs.tavus.io/api-reference/documents/get-document
get /v2/documents/{document_id}
Retrieve detailed information about a specific document using its unique identifier.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# List Documents
Source: https://docs.tavus.io/api-reference/documents/get-documents
get /v2/documents
Retrieve a list of documents.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Update Document
Source: https://docs.tavus.io/api-reference/documents/patch-document
patch /v2/documents/{document_id}
Update a document's `document_name` and `tags`.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Recrawl Document
Source: https://docs.tavus.io/api-reference/documents/recrawl-document
post /v2/documents/{document_id}/recrawl
Trigger a recrawl of a website document to fetch fresh content.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Create Face
Source: https://docs.tavus.io/api-reference/faces/create-face
post /v2/faces
Creates a new face from training video or image URL for use in conversations. See [Which training path?](/sections/faces/which-training-path) for footage requirements and [Face overview](/sections/faces/overview#platform-policies) for rights and permissions.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Delete Face
Source: https://docs.tavus.io/api-reference/faces/delete-face
delete /v2/faces/{face_id}
Deletes a Face by its unique ID; deleted faces cannot be used in a conversation.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Get Face
Source: https://docs.tavus.io/api-reference/faces/get-face
get /v2/faces/{face_id}
This endpoint returns a single Face by its unique identifier.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# List Faces
Source: https://docs.tavus.io/api-reference/faces/list-faces
get /v2/faces
Returns all faces (photorealistic likenesses trained with Phoenix) created by the account associated with the API key.
**Legacy:** `/v2/replicas` and `replica_id` / `replica_ids` remain supported as aliases.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Rename Face
Source: https://docs.tavus.io/api-reference/faces/patch-face-name
patch /v2/faces/{face_id}/name
This endpoint renames a single Face by its unique identifier.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Create Guardrails
Source: https://docs.tavus.io/api-reference/guardrails/create-guardrails
post /v2/guardrails
Create a new guardrail. Guardrails provide strict behavioral boundaries that are enforced throughout a conversation.
Attach guardrails to a PAL directly via `guardrail_ids` or by tag via `guardrail_tags` on [Create PAL](/api-reference/pals/create-pal).
See [Deprecated guardrail sets](/api-reference/guardrails/legacy-guardrail-sets).
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Delete Guardrails
Source: https://docs.tavus.io/api-reference/guardrails/delete-guardrails
delete /v2/guardrails/{guardrail_id}
Delete a single guardrail by its unique identifier. PALs with this guardrail attached via `guardrail_ids` will have the reference removed automatically.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Get Guardrails
Source: https://docs.tavus.io/api-reference/guardrails/get-guardrails
get /v2/guardrails/{guardrail_id}
Retrieve a single guardrail by its unique identifier.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# List Guardrails
Source: https://docs.tavus.io/api-reference/guardrails/list-guardrails
get /v2/guardrails
Return a flat list of guardrails owned by the caller. Pass `legacy=false` - recommended for all new integrations. The `legacy=true` behavior returns the deprecated guardrail set list.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Patch Guardrails
Source: https://docs.tavus.io/api-reference/guardrails/patch-guardrails
patch /v2/guardrails/{guardrail_id}
Update specific fields of a guardrail using [JSON Patch](https://jsonpatch.com/) operations. Paths must match the **current** document shape - compare against the response from [Get Guardrails](/api-reference/guardrails/get-guardrails) before patching.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Create Objectives
Source: https://docs.tavus.io/api-reference/objectives/create-objectives
post /v2/objectives
This endpoint creates objectives for a PAL.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Delete Objective
Source: https://docs.tavus.io/api-reference/objectives/delete-objectives
delete /v2/objectives/{objectives_id}
This endpoint deletes a single objective by its unique identifier.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Get Objective
Source: https://docs.tavus.io/api-reference/objectives/get-objectives
get /v2/objectives/{objectives_id}
This endpoint returns a single objective by its unique identifier.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Get Objectives
Source: https://docs.tavus.io/api-reference/objectives/get-objectives-list
get /v2/objectives
This endpoint returns a list of all objectives.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Patch Objective
Source: https://docs.tavus.io/api-reference/objectives/patch-objectives
patch /v2/objectives/{objectives_id}
This endpoint allows you to update specific fields of an objective using JSON Patch operations.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Overview
Source: https://docs.tavus.io/api-reference/overview
Discover the Tavus API - build a real-time, human-like multimodal video conversation with a face.
## Getting Started with Tavus APIs
Tavus APIs let you build a **Conversational Video Interface (CVI)**: real-time video conversations with a PAL. Each session pairs a **PAL** (behavior, knowledge, pipeline) with a **face** (photorealistic visual, Phoenix).
**Terminology update:** Tavus now uses **PAL** (behavior, knowledge, and pipeline configuration) and **Face** (visual appearance and voice) in the API and docs. Legacy names **persona** and **replica** still work on existing endpoints and request fields (`/v2/personas`, `/v2/replicas`, `persona_id`, `replica_id`, and related aliases) for backward compatibility.
You can access the API through standard HTTP requests, making it easy to integrate CVI into any application or platform.
For machine-readable API and docs context, use
`https://docs.tavus.io/openapi.yaml` as the canonical HTTP API contract,
`https://docs.tavus.io/llms.txt` as the docs page index, and
`https://docs.tavus.io/llms-full.txt` as the full bundled docs export.
### Who Is This For?
This API is for developers looking to add real-time, human-like AI interactions into their apps or services.
### What Can You Do?
Use the end-to-end CVI pipeline to build human-like, real-time multimodal video conversations with these three core components:
Define the PAL's behavior, tone, and knowledge.
Train a lifelike digital face from a short video or headshot image.
Create a real-time video call session with your PAL.
A typical path: [Authentication](/api-reference/authentication), then [Create PAL](/api-reference/pals/create-pal) (`POST /v2/pals`), [Create Face](/api-reference/faces/create-face) (`POST /v2/faces`), and [Create Conversation](/api-reference/conversations/create-conversation). For what CVI includes end-to-end, see [What Is CVI?](/sections/conversational-video-interface/overview-cvi).
# Attach Skill to PAL
Source: https://docs.tavus.io/api-reference/pal-skills/attach-skill-to-pal
put /v2/pals/{pal_id}/skills/{skill_id}
Attach a skill to a PAL, or overwrite the configuration of an existing attachment. The skill is active on the PAL's next conversation.
Skills with no configuration (like `internet_search`) take an empty body or `{"config": {}}`. See [Skills](/sections/conversational-video-interface/skills/overview) for each skill's configuration fields. For `magic_canvas`, see [Canvas configuration](/sections/conversational-video-interface/magic-canvas/api/configuration).
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Detach Skill from PAL
Source: https://docs.tavus.io/api-reference/pal-skills/detach-skill-from-pal
delete /v2/pals/{pal_id}/skills/{skill_id}
Detach a skill from a PAL. The skill no longer applies to the PAL's future conversations. The skill itself stays in the registry and can be re-attached at any time.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Get PAL Skill
Source: https://docs.tavus.io/api-reference/pal-skills/get-pal-skill
get /v2/pals/{pal_id}/skills/{skill_id}
Retrieve a single skill attachment on a PAL.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# List PAL Skills
Source: https://docs.tavus.io/api-reference/pal-skills/list-pal-skills
get /v2/pals/{pal_id}/skills
List the skills attached to a PAL. The response is an object keyed by `skill_id`.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Replace PAL Skills
Source: https://docs.tavus.io/api-reference/pal-skills/replace-pal-skills
put /v2/pals/{pal_id}/skills
Replace a PAL's entire skill set in one call. Skills not present in the request are detached.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Update PAL Skill
Source: https://docs.tavus.io/api-reference/pal-skills/update-pal-skill
patch /v2/pals/{pal_id}/skills/{skill_id}
Merge changes into an existing skill attachment's configuration. Fields you pass replace the existing values, fields you omit are preserved, and fields set to `null` are removed. The merged configuration is validated against the skill's config schema.
Unlike [Attach Skill to PAL](/api-reference/pal-skills/attach-skill-to-pal), this endpoint cannot create a new attachment - the skill must already be attached.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Attach Tools To PAL
Source: https://docs.tavus.io/api-reference/pal-tools/attach-tools-to-pal
post /v2/pals/{pal_id}/tools
Attach one or more tools (by `tool_id`) to a PAL. A PAL can have at most 50 attached tools.
Attaching a tool that is already attached is idempotent - it is reported in the response alongside any newly-attached tools.
# Detach Tool From PAL
Source: https://docs.tavus.io/api-reference/pal-tools/detach-tool-from-pal
delete /v2/pals/{pal_id}/tools/{tool_id}
Detach a tool from a PAL. The tool itself is not deleted - it remains available to attach to other PALs.
# List PAL Tools
Source: https://docs.tavus.io/api-reference/pal-tools/list-pal-tools
get /v2/pals/{pal_id}/tools
List all standalone tools currently attached to a PAL. Inline tools defined under `PAL.layers.llm.tools` and built-in system tools are not returned here - only tools created via [Create Tool](/api-reference/tools/create-tool) and attached to this PAL.
# Check Conferencing Username Availability
Source: https://docs.tavus.io/api-reference/pals/check-conferencing-username
get /v2/pals/check-username
Check whether a conferencing `username` is available on `tavusinvite.com` before creating or patching a PAL.
See [Google Meet](/sections/conversational-video-interface/pal/meetings).
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
See [Google Meet / Zoom](/sections/conversational-video-interface/pal/meetings) for username rules, allowlists, and scheduling via calendar invite.
# Create PAL
Source: https://docs.tavus.io/api-reference/pals/create-pal
post /v2/pals
Creates a PAL and configures how it behaves in CVI for every conversation that uses that PAL.
**`default_face_id` is required** on `POST /v2/pals` (unlike the legacy `POST /v2/personas` path, where `default_replica_id` was optional).
**Legacy:** `/v2/personas` and `persona_id` / `default_replica_id` remain supported as aliases.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Delete PAL
Source: https://docs.tavus.io/api-reference/pals/delete-pal
delete /v2/pals/{pal_id}
This endpoint deletes a single PAL by its unique identifier.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Get PAL
Source: https://docs.tavus.io/api-reference/pals/get-pal
get /v2/pals/{pal_id}
Returns a single PAL by its unique identifier.
By default this endpoint returns the **live** PAL — the version that powers conversations, deployments, and the public API. If the PAL has an active [PAL Builder](https://maker.tavus.io/dev) draft with unpublished changes, add `?source=draft` to read the draft body without publishing it. See [Draft vs live PALs](/sections/conversational-video-interface/pal/draft-and-live) for the full model.
Regardless of `source`, `layers.conferencing` is always returned from the live PAL, because conferencing is deployment config that is owned by the live row (see [PATCH PAL](/api-reference/pals/patch-pal)).
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
Returns the **live** PAL by default. If the PAL has an active [PAL Builder](https://maker.tavus.io/dev) draft, add `?source=draft` to read the unpublished draft body without affecting live traffic. Draft views include a `publish_url` for [Publish PAL](/api-reference/pals/publish-pal). `layers.conferencing` is always the live value. See [Draft vs live PALs](/sections/conversational-video-interface/pal/draft-and-live).
# List PALs
Source: https://docs.tavus.io/api-reference/pals/list-pals
get /v2/pals
This endpoint returns a list of all PALs created by the account associated with the API Key in use.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Patch PAL
Source: https://docs.tavus.io/api-reference/pals/patch-pal
patch /v2/pals/{pal_id}
Update specific fields of a PAL using [JSON Patch](https://datatracker.ietf.org/doc/html/rfc6902) operations.
### Draft vs live routing
If the PAL has an active [PAL Builder](https://maker.tavus.io/dev) draft, this endpoint **routes to the draft by default**. Live conversations, deployments, and public reads continue to use the previously-published version until you publish (`POST /v2/pals/{pal_id}/publish`) or repeat the request with `?target=live`. PALs without an active draft are unaffected — the patch applies to the live row as it always did.
This default was introduced so an API patch and a PAL Maker edit stay in sync: the Builder reads the draft row, so a PATCH that skipped the draft would produce an editor that still shows the pre-patch state. To bypass the draft, pass `?target=live` — this applies the edit to the live row and resyncs the retained draft from live. Any unpublished draft edits are discarded.
The response always includes `edit_target`, `edited_pal_id`, `live_pal_id`, `draft_pal_id`, and `routing_message` so you can confirm which row was written. Draft-routed responses also include `publish_url`. See [Draft vs live PALs](/sections/conversational-video-interface/pal/draft-and-live) for the full model.
### `layers.conferencing` is live-owned
`layers.conferencing` (the conferencing/Google Meet integration) is deployment config that lives on the live row so meeting invitations continue to work while a draft is open. Regardless of `?target`, any patch that touches `layers.conferencing` (or removes `layers`) is applied to the live PAL and then synced into the open draft, so the draft never carries a stale conferencing config. Consequences:
- `?target=draft` with a conferencing op returns `400`.
- A single request cannot mix conferencing ops with edits to other fields — split the request in two.
- Publishing a stale draft never wipes deployed conferencing config; live conferencing is preserved.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
If the PAL has an active [PAL Builder](https://maker.tavus.io/dev) draft, this endpoint **writes to the draft by default** — live conversations are unaffected until you call [Publish PAL](/api-reference/pals/publish-pal). Draft-routed responses include a `publish_url`. Pass `?target=live` to write directly to live, discard unpublished edits, and resync the retained draft. `layers.conferencing` is always live-owned. See [Draft vs live PALs](/sections/conversational-video-interface/pal/draft-and-live).
# Publish PAL
Source: https://docs.tavus.io/api-reference/pals/publish-pal
post /v2/pals/{pal_id}/publish
Publishes the PAL Builder draft associated with this `pal_id`, making its content the live version used by new conversations, deployments, and public reads.
Call this after a draft-routed [Patch PAL](/api-reference/pals/patch-pal) response. That response includes a `publish_url` you can use directly. Publishing preserves the live `layers.conferencing` configuration and keeps the draft row available for future Builder edits.
If the PAL has no Builder draft, the call is idempotent: Tavus returns the current live PAL. You can pass either the stable live PAL id or its underlying draft id, although integrations should normally keep and publish the stable live `pal_id`.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
Publish after editing a PAL Builder draft through [Patch PAL](/api-reference/pals/patch-pal). The patch response and `GET /v2/pals/{pal_id}?source=draft` both return the exact `publish_url`. Publishing makes those draft changes live for new conversations while preserving live-owned conferencing configuration.
# Create Pronunciation Dictionary
Source: https://docs.tavus.io/api-reference/pronunciation-dictionaries/create-pronunciation-dictionary
post /v2/pronunciation-dictionaries
Create a [pronunciation dictionary](/sections/conversational-video-interface/pal/pronunciation-dictionaries) with custom rules for controlling how words are spoken. Rules are automatically synced to both Cartesia and ElevenLabs so they work regardless of which TTS engine your PAL uses.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Delete Pronunciation Dictionary
Source: https://docs.tavus.io/api-reference/pronunciation-dictionaries/delete-pronunciation-dictionary
delete /v2/pronunciation-dictionaries/{dictionary_id}
Permanently delete a pronunciation dictionary and remove it from all linked PALs.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Get Pronunciation Dictionary
Source: https://docs.tavus.io/api-reference/pronunciation-dictionaries/get-pronunciation-dictionary
get /v2/pronunciation-dictionaries/{dictionary_id}
Retrieve a pronunciation dictionary by its ID, including all rules.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# List Pronunciation Dictionaries
Source: https://docs.tavus.io/api-reference/pronunciation-dictionaries/list-pronunciation-dictionaries
get /v2/pronunciation-dictionaries
List all pronunciation dictionaries for the authenticated user with pagination.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Update Pronunciation Dictionary
Source: https://docs.tavus.io/api-reference/pronunciation-dictionaries/update-pronunciation-dictionary
patch /v2/pronunciation-dictionaries/{dictionary_id}
Update a pronunciation dictionary's name or rules using [JSON Patch](https://jsonpatch.com/) format (RFC 6902). Supported mutable fields are `name` and `rules`.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Get Skill
Source: https://docs.tavus.io/api-reference/skills/get-skill
get /v2/skills/{skill_id}
Retrieve the metadata for a single skill in the registry.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# List Skills
Source: https://docs.tavus.io/api-reference/skills/list-skills
get /v2/skills
List every skill in the registry. Skills are pre-built capabilities authored by Tavus that you can attach to a PAL - see [Skills](/sections/conversational-video-interface/skills/overview).
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Create Tool
Source: https://docs.tavus.io/api-reference/tools/create-tool
post /v2/tools
Create a standalone tool that can be attached to one or more PALs via [Attach Tools To PAL](/api-reference/pal-tools/attach-tools-to-pal).
**In-call tools** (`trigger_type: in_call`, default) are invoked during the conversation by an LLM (`origin: llm`) or a perception model (`origin: vision` / `origin: audio`).
**Post-call actions** (`trigger_type: post_call`) run once after the conversation ends; omit `origin` and set `delivery.api` (HTTPS webhook). See [Post-Call Actions](/sections/conversational-video-interface/pal/post-call-tool).
Every tool dispatches via **exactly one** delivery channel:
- `delivery.app_message: true` (default) - calls land on your frontend as a `conversation.tool_call` event over the Daily data channel.
- `delivery.api` - Tavus makes an HTTPS request to a URL you configure. Supports five auth types and `{placeholder}` templating in the URL path, query string, and body.
See [Tools Overview](/sections/conversational-video-interface/pal/tools), [LLM Tool Delivery](/sections/conversational-video-interface/pal/llm-tool-delivery), and [LLM Tool Auth](/sections/conversational-video-interface/pal/llm-tool-auth) for the conceptual model.
# Delete Tool
Source: https://docs.tavus.io/api-reference/tools/delete-tool
delete /v2/tools/{tool_id}
Soft-deletes a tool and detaches it from every PAL it was attached to. The tool name is freed for reuse on a new tool. System tools cannot be deleted.
# Get Tool
Source: https://docs.tavus.io/api-reference/tools/get-tool
get /v2/tools/{tool_id}
Returns a single tool by its `tool_id`. Works for both user-owned tools and built-in system tools. Secret fields (`token`, `password`, `value`, `secret`, `client_secret`) are replaced with `********` in the response.
# Get Tools
Source: https://docs.tavus.io/api-reference/tools/get-tools
get /v2/tools
Returns a paginated list of tools. The `type` query parameter controls which tools are returned:
- `type=user` (default) - tools you created via [Create Tool](/api-reference/tools/create-tool).
- `type=system` - built-in system tools Tavus provides. These are always available to every conversation; you do not need to attach them. The only system tool today is `end_call`, which lets the PAL hang up at a natural stopping point.
- `type=all` - both, with system tools listed first.
System tools have `is_system_tool: true`, an `owner_id` of `null`, and use their `name` as the `tool_id` (e.g. `end_call`). They cannot be created, updated, or deleted.
# Update Tool
Source: https://docs.tavus.io/api-reference/tools/update-tool
patch /v2/tools/{tool_id}
Update one or more fields on a tool. Only fields present in the body are changed; omitted fields keep their stored value. System tools cannot be updated.
**Secrets and the `********` placeholder.** [Get Tool](/api-reference/tools/get-tool) returns secret fields scrubbed to `********`. If you do a read-modify-write and PATCH that exact value back, the request is rejected - re-encrypting the placeholder would silently corrupt the stored secret. To keep the existing secret, **omit** the field from the auth object on PATCH.
Sending `delivery: null` resets delivery to the default `{ app_message: true }`.
# Generate Video
Source: https://docs.tavus.io/api-reference/video-request/create-video
post /v2/videos
This endpoint generates a new video using a face and either a script or an audio file.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Delete Video
Source: https://docs.tavus.io/api-reference/video-request/delete-video
delete /v2/videos/{video_id}
This endpoint deletes a single video by its unique identifier.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Get Video
Source: https://docs.tavus.io/api-reference/video-request/get-video
get /v2/videos/{video_id}
This endpoint returns a single video by its unique identifier.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# List Videos
Source: https://docs.tavus.io/api-reference/video-request/get-videos
get /v2/videos
This endpoint returns a list of all Videos created by the account associated with the API Key in use.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# Rename Video
Source: https://docs.tavus.io/api-reference/video-request/patch-video-name
patch /v2/videos/{video_id}/name
This endpoint renames a single video by its unique identifier.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# List Voices
Source: https://docs.tavus.io/api-reference/voices/list-voices
get /v2/voices
Returns available stock **`voice_name`** values and their linked **face** metadata. When you [Create Face](/api-reference/faces/create-face) with **`train_image_url`** (image-to-face), **`voice_name`** is required-use this list to pick a valid slug and to preview options.
For AI agents, use `https://docs.tavus.io/openapi.yaml` for the full HTTP API contract.
# CLI
Source: https://docs.tavus.io/sections/agent-tools/cli
Install, authenticate, and drive the Tavus CLI to manage PALs, faces, conversations, and the agentic resource library from your terminal.
The Tavus CLI (`tavus`) is a Typer-based command-line client for the Tavus API. It manages PALs, faces, conversations, and the full agentic resource library (guardrails, objectives, tools, pronunciation dictionaries), plus the conversational PAL builder and text-only chat mode.
The CLI and the Tavus MCP server share the same backend client. Use the CLI for scripting and manual workflows; use MCP when an agent should drive Tavus through tools.
For the autonomous new-PAL loop, see [Agentic PAL building & testing](/sections/agent-tools/pal-build-and-verify). The CLI command is `tavus pal build`; the MCP equivalent is `tavus_pal_build_and_verify`.
## Install
The CLI is distributed as the `tavus-cli` Python package and exposes a `tavus` command on your `PATH`.
```bash theme={null}
uv tool install tavus-cli
```
Or run it without installing:
```bash theme={null}
uvx --from tavus-cli tavus doctor
```
```bash theme={null}
pip install tavus-cli
```
`tavus-cli` is the package name; confirm the package index you install from with your Tavus contact.
The CLI defaults to the production environment (`PROD`). To target the test database, set `TAVUS_ENV=TEST` in your shell or pass `--env TEST` on any command.
```bash theme={null}
tavus doctor
```
`tavus doctor` prints the selected environment, the resolved API base URLs, the PAL Maker URL, and the auth source, then checks API reachability. Pass `--skip-network` to validate local configuration only.
## Authenticate
### Browser login (recommended)
```bash theme={null}
tavus auth login
```
This opens PAL Maker at `/dev/cli-authorize` with a loopback callback URL and an anti-CSRF state token. After you sign in, PAL Maker mints an API key for your account (tagged `source: cli`) and POSTs it back to the local loopback receiver. The CLI then stores the key in your OS keychain, scoped to the selected environment.
To store an existing key instead of opening the browser, pass `--api-key` (and optionally `--name` to label the minted key):
```bash theme={null}
tavus auth login --api-key --name "ci-key"
```
Check or clear credentials:
```bash theme={null}
tavus auth status # {env, authenticated, source}
tavus auth logout # deletes the env-scoped keychain entry
```
### API key fallback
For automation, set `TAVUS_API_KEY` directly instead of using the keychain:
```bash theme={null}
TAVUS_API_KEY=... tavus pal list
```
Per-environment overrides are also supported: `TAVUS_TEST_API_KEY`, `TAVUS_STG_API_KEY`, and `TAVUS_PROD_API_KEY`.
## Environments
Every command accepts a global `--env` / `-e` option that sets `TAVUS_ENV`. Recognized values are `TEST`, `STG`, and `PROD` (default). When unset, the CLI targets `PROD`.
```bash theme={null}
# Production (default)
tavus pal list
# Test database
tavus --env TEST pal list
TAVUS_ENV=TEST tavus pal list
```
The environment must match the portal that minted your API key. A `TEST` key used against a `PROD` environment (or vice versa) will return `401` on downstream Tavus API calls.
## JSON round-tripping
The CLI moves structured payloads through JSON consistently:
* Most commands print JSON by default; commands with table output expose a `--json` flag to switch to raw JSON.
* File inputs (`--file`, `--layers-file`, `--patch-file`) are parsed with `json.loads()`.
* The `pal patch` `--value` option attempts a JSON parse first, then falls back to a plain string if parsing fails, so wrap string values in quotes (e.g. `--value '"New Name"'`).
* Repeatable options (`--memory`, `--guardrail-id`, `--tag`, `--document-id`, etc.) are collected into lists in the request body.
## Examples
```bash theme={null}
# 1. List PALs as a table, then as JSON
tavus pal list --limit 10
tavus pal list --json
# 2. Inspect a PAL plus the account resources you can attach to it
tavus pal options
# 3. Rename a PAL with a validated JSON Patch (note the quoted JSON value)
tavus pal patch \
--op replace --path /pal_name --value '"New Name"'
# 4. Validate a patch without sending it
tavus pal patch \
--op replace --path /pal_name --value '"New Name"' --dry-run
# 5. Build a PAL through the conversational builder, then verify in chat mode
tavus pal build --prompt "I want a PAL for an office greeter" --json
# 6. Send one chat turn to a builder session
tavus builder chat --message "Make the greeting warmer"
# 7. Turn on built-in PAL Maker capabilities (Web Search, then Slide Presenter)
tavus pal capabilities attach web_search
tavus pal capabilities attach slide_presenter \
--document-id --slides-trigger walk_the_deck
# 8. Create a Knowledge document from a URL and attach it to a PAL
tavus document create --url https://example.com/handbook.pdf --tag onboarding
tavus pal knowledge add --document-id
```
## Command reference
All sub-apps support `-h` / `--help`, and invoking a sub-app with no arguments prints its help.
### Top-level commands
| Command | What it does |
| ---------------------------------- | -------------------------------------------------------------------------------------------------- |
| `tavus doctor` | Show environment, auth source, and API reachability. `--skip-network` validates local config only. |
| `tavus quickstart ` | Create a PAL and a conversation using a stock face by default. Options: `--name`, `--face-id`. |
| `tavus embed ` | Build an embed file manifest. `--target` (default `iframe`), `--write` to write files. |
### `tavus auth`
| Command | What it does |
| ------------- | ----------------------------------------------------------------------------------------- |
| `auth login` | Browser-based login that stores an env-scoped key in the keychain. `--api-key`, `--name`. |
| `auth logout` | Delete the env-scoped key from the keychain. |
| `auth status` | Show whether credentials are available for the selected env. |
### `tavus pal`
| Command | What it does |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `pal list` | List PALs. `--limit`, `--page`, `--pal-type`, `--json`. |
| `pal get ` | Fetch a PAL as JSON. `--include-settings`. |
| `pal create` | Create a PAL. `--system-prompt`, `--name`, `--face-id`, `--pipeline-mode`, `--greeting`, `--context`, `--layers-file`, `--memory`, `--objectives-id`, `--guardrails-id`, `--guardrail-id`, `--guardrail-tag`, `--document-id`, `--document-tag`, `--template`/`--no-template`. |
| `pal delete ` | Delete a PAL. |
| `pal patch ` | Apply a validated JSON Patch. `--op`, `--path`, `--value`, `--patch-file`, `--dry-run`. |
| `pal options ` | Return the PAL plus valid account resources and patchable paths. |
| `pal paths` | List locally known JSON Patch paths for PALs. |
| `pal build` | Build a PAL via the conversational builder, then verify in chat mode. `--prompt`, `--face-id`, `--max-rounds`, `--json`. |
| `pal preview ` | Start a full preview conversation and print the conversation URL. `--face-id`, `--name`, `--json`. |
Tools are first-class objects: attach them after creating a PAL with `tavus pal tools attach` rather than inlining them at create time.
#### `tavus pal tools`
| Command | What it does |
| ----------------------------------------- | ------------------------------------------------- |
| `pal tools list ` | List tools attached to a PAL. |
| `pal tools attach ` | Attach one or more existing tools to a PAL by ID. |
| `pal tools detach ` | Detach a tool from a PAL. |
#### `tavus pal capabilities`
Manage the built-in PAL Maker capabilities - Magic Canvas, Slide Presenter, Web Search, Perception, and Memory - with friendly IDs instead of raw JSON Patch. Accepted capability IDs are `magic_canvas`, `slide_presenter`, `web_search`, `perception`, and `memory` (the portal IDs `builtin:magic_canvas`, `builtin:slide_presenter`, `builtin:web_search`, and `skill:memory` also work).
| Command | What it does |
| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pal capabilities catalog` | List the five PAL Maker capabilities and their backend mappings. |
| `pal capabilities list ` | Show Magic Canvas, Slide Presenter, Web Search, Perception, and Memory attachment state and config for a PAL. |
| `pal capabilities attach ` | Attach a capability by friendly or portal ID. Capability-specific options: `--document-id`, `--slides-trigger`, `--prompt` (Slide Presenter); `--component`, `--usage-guidance`, `--scheduling-url`, `--scheduling-provider` (Magic Canvas); `--perception-model`, `--visual-query`, `--audio-query`, `--visual-tool-prompt`, `--audio-tool-prompt` (Perception). Also `--config`/`--config-file` for raw config. |
| `pal capabilities patch ` | Update the config of an already-attached capability. Same options as `attach`. |
| `pal capabilities detach ` | Detach a capability. For Perception this sets the perception model to off. |
Magic Canvas, Slide Presenter, Web Search, and Memory persist through the PAL's skills (`/pals/{id}/skills`); Perception persists through `layers.perception`. `pal capabilities` handles that difference for you - prefer it over `pal skills` for these five.
#### `tavus pal skills`
Lower-level access to the raw RQH skills attached to a PAL. Use `pal capabilities` for the five PAL Maker capabilities; reach for `pal skills` only when you need to work with a skill by its raw registry ID.
| Command | What it does |
| --------------------------------------- | ------------------------------------------------------------------------- |
| `pal skills list ` | List raw RQH skills attached to a PAL. |
| `pal skills attach ` | Attach or replace one raw RQH skill. `--config`, `--config-file`. |
| `pal skills patch ` | Merge config into an already-attached skill. `--config`, `--config-file`. |
| `pal skills detach ` | Detach one raw RQH skill from a PAL. |
#### `tavus pal knowledge`
Manage a PAL's Knowledge section - the `document_ids` and `document_tags` it draws on for retrieval, plus RAG tuning under `layers.knowledge_base`. Create the underlying documents first with [`tavus document`](#tavus-document).
| Command | What it does |
| -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pal knowledge list ` | Show attached document IDs, tags, resolved document records, and `knowledge_base` layer settings. |
| `pal knowledge add ` | Attach documents or document tags. `--document-id`, `--document-tag` (both repeatable). |
| `pal knowledge remove ` | Remove documents or document tags. `--document-id`, `--document-tag`. |
| `pal knowledge replace ` | Replace the PAL's document and tag selections. `--document-id`, `--document-tag`. |
| `pal knowledge settings ` | Patch RAG tuning under `layers.knowledge_base`. `--rag-score-threshold`, `--rag-n-chunks`, `--rag-surrounding-chunk-radius`, `--rag-observability`/`--no-rag-observability`, `--procedure-goalchain`/`--no-procedure-goalchain`. |
| `pal knowledge upload ` | Upload a local file as a Knowledge document and attach it to the PAL. Same upload options as [`tavus document upload`](#tavus-document), plus `--attach`/`--upload-only`. Requires `TAVUS_PORTAL_BEARER_TOKEN`. |
### `tavus face`
| Command | What it does |
| ----------- | --------------------------------------------------------------------- |
| `face list` | List faces. `--limit`, `--stock` (system/stock faces only), `--json`. |
### `tavus conversation`
| Command | What it does |
| ------------------------------------ | --------------------------------------------------------- |
| `conversation create` | Create a conversation. `--pal-id`, `--face-id`, `--name`. |
| `conversation end ` | End a conversation. |
### `tavus resource`
| Command | What it does |
| --------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `resource list ` | List a supported resource: `guardrails`, `objectives`, `documents`, `voices`, `tools`. `--limit`. |
| `resource get ` | Get a single resource by ID. |
### `tavus document`
Manage Knowledge documents - the account-level records that back a PAL's Knowledge section. With a normal Tavus API key you can create documents from an already-reachable URL; local file upload uses the same portal-only path as PAL Maker (local file → tavus-api upload/S3 URL → document record) and therefore requires a Firebase portal bearer token in `TAVUS_PORTAL_BEARER_TOKEN`. Attach documents to a PAL with [`tavus pal knowledge`](#tavus-pal-knowledge).
| Command | What it does |
| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `document list` | List Knowledge documents. `--limit`, `--page`, `--status`, `--tag`, `--participant-tag`, `--name-or-uuid`, `--verbose`/`--compact`, `--json`. |
| `document get ` | Get one Knowledge document record. `--verbose`. |
| `document create` | Create a document from an already-hosted URL. `--url` (required), `--name`, `--tag`, `--participant-tag`, `--crawl-depth`, `--crawl-max-pages`, `--custom-description`, `--customer-support`/`--regular-document`, `--standard-extraction`. |
| `document upload ` | Upload a local file through tavus-api, then create the document record. Requires `TAVUS_PORTAL_BEARER_TOKEN`. `--name`, `--tag`, `--participant-tag`, `--custom-description`, `--customer-support`/`--regular-document`, `--font-size-aware-extraction`/`--standard-extraction`, `--bucket-name`, `--region`, `--sign-duration`. |
| `document tags` | List document tags available for Knowledge attachment. `--search`, `--page`, `--limit`. |
| `document chunks ` | Read the extracted chunks RQH indexed for a document. `--collection` (default `regular`), `--limit`, `--offset`. |
| `document recrawl ` | Trigger a recrawl for a crawl-backed document. `--crawl-depth`, `--crawl-max-pages`. |
Use `document chunks` to inspect what RQH actually ingested before making claims about a document's contents - it returns the indexed text, not the source file.
### `tavus guardrail`
| Command | What it does |
| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `guardrail list` | List guardrails (new flat shape by default). `--limit`, `--page`, `--type`, `--name-or-uuid`, `--tags`, `--legacy`, `--verbose`, `--json`. |
| `guardrail get ` | Get a guardrail. `--verbose`, `--legacy`. |
| `guardrail create` | Create a flat guardrail. `--name`, `--prompt`, `--modality`, `--callback-url`, `--tool-call`, `--app-message`/`--no-app-message`, `--tag`. |
| `guardrail patch ` | Patch a guardrail. `--file`, `--name`, `--prompt`, `--modality`, `--callback-url`, `--tool-call`, `--app-message`/`--no-app-message`, `--tag`. |
| `guardrail delete ` | Delete a guardrail. |
| `guardrail tags` | List tags applied to the account's guardrails. `--search`, `--page`, `--limit`. |
### `tavus objective`
| Command | What it does |
| ---------------------------------- | ----------------------------------------------------------------------------------------- |
| `objective list` | List objective sets. `--limit`, `--page`, `--type`, `--name-or-uuid`, `--sort`, `--json`. |
| `objective get ` | Get an objective set. |
| `objective create` | Create an objective set from a JSON file. `--file` (`{name, data, allow_loops}`). |
| `objective patch ` | Patch an objective set with JSON Patch ops. `--file`. |
| `objective delete ` | Delete an objective set. |
| `objective validate` | Validate an objective-set payload without persisting. `--file`. |
| `objective example` | Print a starter JSON body for `objective create --file`. |
### `tavus tool`
| Command | What it does |
| ----------------------- | -------------------------------------------------------------------------------- |
| `tool list` | List tools. `--limit`, `--page`, `--type`, `--name-or-uuid`, `--sort`, `--json`. |
| `tool get ` | Get a tool. |
| `tool create` | Create a tool from a JSON file. `--file`. |
| `tool patch ` | Patch a tool from a JSON file. `--file`. |
| `tool delete ` | Delete a tool. |
| `tool example` | Print a starter JSON body. `--delivery` (`app_message`, `http`, `http_oauth2`). |
When patching a tool, omit any secret fields you don't intend to change. The backend rejects PATCHes that echo back the scrubbed-secret placeholder (`********`) returned from a prior GET.
### `tavus skill`
Browse the RQH skill registry - the skills that back PAL capabilities. This lists what is available to attach; attach and configure per-PAL with [`tavus pal capabilities`](#tavus-pal-capabilities) or [`tavus pal skills`](#tavus-pal-skills).
| Command | What it does |
| ---------------------- | --------------------------------------------- |
| `skill list` | List RQH skills that can be attached to PALs. |
| `skill get ` | Get one RQH skill registry entry. |
### `tavus pronunciation-dictionary`
| Command | What it does |
| ------------------------------------------------- | ---------------------------------------------------------------------------- |
| `pronunciation-dictionary list` | List pronunciation dictionaries. `--limit`, `--page`, `--sort`, `--json`. |
| `pronunciation-dictionary get ` | Get a pronunciation dictionary. |
| `pronunciation-dictionary create` | Create from a JSON file. `--file` (`{name, rules}`). |
| `pronunciation-dictionary patch ` | Patch from a JSON file. `--file` (supplying `rules` replaces the full list). |
| `pronunciation-dictionary delete ` | Delete a pronunciation dictionary. |
| `pronunciation-dictionary example` | Print a starter JSON body. |
### `tavus builder`
| Command | What it does |
| -------------------------------------- | --------------------------------------------------------------------------------------- |
| `builder create` | Start a builder session. `--name`, `--greeting`, `--pal-id`, `--model`. |
| `builder list` | List builder sessions. `--limit`, `--page`, `--pal-id`, `--name`, `--status`, `--json`. |
| `builder get ` | Fetch a single builder session. |
| `builder delete ` | Soft-delete a builder session. |
| `builder chat ` | Send a chat turn to the builder. `--message`, `--json`. |
| `builder history ` | Print the chat transcript. `--limit`, `--json`. |
| `builder append-messages ` | Append raw `{role, content}` messages without invoking the LLM. `--file`. |
| `builder publish ` | Mark the session complete and publish its PAL. |
#### `tavus builder update`
| Command | What it does |
| ----------------------------------------- | ------------------------------------------------------------------------------- |
| `builder update objectives ` | Run an LLM update on the PAL's objectives. `--message`. |
| `builder update guardrails ` | Run an LLM update on the PAL's guardrails. `--message`. |
| `builder update greeting ` | Run an LLM refinement of the greeting. `--message`. |
| `builder update personality ` | Refine PAL name and/or system prompt. `--message`, `--name`, `--system-prompt`. |
### `tavus chat`
| Command | What it does |
| ----------------------------- | --------------------------------------------------------------------------------- |
| `chat start` | Start a text-only conversation against a PAL. `--pal-id`, `--greeting`, `--name`. |
| `chat turn ` | Send one user turn and print the reply as plain text. `--message`, `--timeout`. |
| `chat end ` | End a chat-mode conversation. |
# MCP Server
Source: https://docs.tavus.io/sections/agent-tools/mcp-server
Connect your coding agent to the hosted Tavus MCP server to build PALs, wire bidirectional tool calls into your own code, and test the integration end to end.
The Tavus MCP server is a hosted endpoint your coding agent connects to. Once it is wired into Codex, Claude Code, Cursor, or another MCP client, your agent can build and patch a PAL, define the tools that PAL can call, attach them, and start test conversations, all from your editor against your own codebase.
There is nothing to install or run. You connect over HTTPS and authenticate once in the browser.
## Why connect your editor
A working voice agent has two parts that have to agree:
1. The PAL needs to know which tools exist and when to call them. That lives in the PAL's tool definitions and prompt.
2. Your application needs to handle those calls when they fire. That lives in your code.
Keeping those two parts in sync by hand is where integrations drift. The MCP server lets one agent work both sides: it defines the tool on the PAL and can scaffold and check the matching handler in your repo, so the two stay consistent. You then test the exchange by starting a conversation and watching the tool fire and your handler respond, without leaving your editor.
Create and patch the PAL's layers, prompt, guardrails, and objectives.
Define tools, attach them, and generate the handler code in your own app.
Start chat or full conversations and watch tool calls round-trip live.
## Connect a client
The server is reachable at an HTTPS `/mcp` endpoint. Authentication uses a browser-based OAuth flow: the PAL Maker mints a per-user API key and the server forwards it to the Tavus API as `x-api-key`. No key or shared token sits in your client config.
The public Tavus MCP endpoint is `https://mcp.tavus.io/mcp`. Use this endpoint
for every supported client and production integration.
Point your MCP client at the hosted endpoint.
For Codex:
```bash theme={null}
codex mcp add tavus --url https://mcp.tavus.io/mcp
```
For Claude Code:
```bash theme={null}
claude mcp add -s user --transport http tavus https://mcp.tavus.io/mcp
```
This registers the server under the name `tavus` for your user scope.
Never put a Tavus API key or shared bearer token in your MCP client config. The hosted server authenticates each user through the PAL Maker and keeps keys user-scoped and out of client configuration.
On first use, the client runs an OAuth flow against the server. For Codex, you can start it directly:
```bash theme={null}
codex mcp login tavus
```
Then the flow is:
1. The client initiates the authorize flow.
2. The portal redirects to `/dev/cli-authorize?mode=oauth`.
3. You sign in.
4. The portal mints a per-user Tavus API key.
5. You are redirected back to the client's loopback callback.
6. The server exchanges the authorization code and forwards the minted key to the Tavus API as `x-api-key` on every downstream call.
Once authenticated, the full Tavus toolset is available: PAL CRUD, faces, conversations, builder, chat-mode testing, guardrails, objectives, tools, pronunciation dictionaries, built-in PAL Maker capabilities (Magic Canvas, Slide Presenter, Web Search, Perception, Memory), and Knowledge documents. See [Agentic PAL building & testing](/sections/agent-tools/pal-build-and-verify) for the autonomous PAL loop, or the [MCP tools reference](/sections/agent-tools/mcp-tools-reference) for the full catalog.
MCP tools return data and file manifests. They do not write files themselves. Your client decides what to do with the returned data, such as writing a generated tool handler into your repo.
## Bidirectional tool calls
A tool call in Tavus is an exchange, and that exchange is what connects the PAL to your app:
1. During a conversation the PAL decides a tool is needed, based on the tool's `description` and the PAL's prompt, and calls it with arguments.
2. Tavus delivers that call to your code.
3. Your code runs its logic and returns a result.
4. The PAL uses the result to decide what to say and do next.
The PAL calls into your application in steps 1 and 2, and your application's response shapes the PAL's next turn in steps 3 and 4.
### Two delivery channels
A tool's `delivery` field sets where the call is delivered. It is the main choice you make when you define a tool.
```json theme={null}
"delivery": { "app_message": true }
```
The call is emitted to your frontend or client over the conversation's data channel as a [tool-call event](/sections/event-schemas/conversation-toolcall). Your app listens for it and runs local logic.
Best for actions in the UI: highlight a product, change a slide, navigate a page, or update on-screen state. This is also the default channel for [LLM tool calling](/sections/conversational-video-interface/pal/llm-tool).
```json theme={null}
"delivery": {
"api": {
"url": "https://your-app.example.com/api/tools/get_order_status",
"method": "POST",
"content_type": "application/json",
"body_template": { "order_id": "{order_id}" },
"timeout": 10.0
}
}
```
Tavus makes a direct HTTPS call to your backend and waits for the JSON response. Best for server-side work such as database lookups, business logic, and third-party APIs.
The HTTP form also supports `headers`, `query_params`, and `auth` (`none`, `bearer`, `basic`, `api_key`, `hmac`, or `oauth2_client_credentials`). `timeout` defaults to 10 seconds (max 60).
Any `{placeholder}` used in `url`, `body_template`, or `query_params` must be declared as a property in the tool's `parameters.properties`. The tool's `description` plus `parameters` JSON must total 10,000 characters or fewer.
### Controlling what the PAL does around the call
Two fields shape the conversational behavior of the exchange:
| Field | Common values | What it does |
| --------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `on_call` | `silent` | What the PAL does while the tool runs, for example staying silent instead of narrating the call. |
| `on_resolve` | `generate_response`, `fire_and_forget` | What happens after the result returns. `generate_response` has the PAL speak based on the result. `fire_and_forget` (the default) does not wait or narrate. |
| `static_filler` | any string | A line to say while waiting on a slow tool. |
Use `on_resolve: generate_response` when the result should change what the PAL says next, such as an order status or an availability check. Use `fire_and_forget` for side effects the PAL does not need to react to.
### A complete example
A backend lookup the PAL can call to answer "where's my order?":
```json get_order_status tool theme={null}
{
"name": "get_order_status",
"description": "Look up the current status of a customer's order. Call this when the user asks about the state, location, or delivery date of an order they reference by number. The result tells you the status and the estimated delivery date so you can relay it.",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "The order number the customer gave, e.g. 'A-10428'."
}
},
"required": ["order_id"]
},
"delivery": {
"api": {
"url": "https://your-app.example.com/api/tools/get_order_status",
"method": "POST",
"content_type": "application/json",
"body_template": { "order_id": "{order_id}" },
"timeout": 10.0
}
},
"origin": "llm",
"on_call": "silent",
"on_resolve": "generate_response"
}
```
When the PAL calls it, Tavus POSTs `{ "order_id": "A-10428" }` to your endpoint. Your handler returns JSON, for example:
```json theme={null}
{ "status": "out_for_delivery", "eta": "today by 6pm" }
```
Because `on_resolve` is `generate_response`, the PAL speaks from that result, for example: *"Good news, order A-10428 is out for delivery and should arrive today by 6pm."*
## Build and test it with your agent
One agent can do every step above. A typical loop:
1. **Define the tool.** The agent calls `tavus_tool_create` with the `parameters` and `delivery` that match what the tool should do.
2. **Attach it.** `tavus_pal_tools_attach` wires the tool onto your PAL so it is offered during conversations.
3. **Write the handler.** For an `api` tool, the agent scaffolds the matching endpoint in your repo (the route at `delivery.api.url`) so the request and response shape match the tool's `parameters`.
4. **Test the exchange.** `tavus_chat_start` and `tavus_chat_turn` (text only), or `tavus_conversation_create` (full video), start a session so you can watch the tool fire and your handler respond, then iterate.
For a new PAL, an agent can run this as one autonomous build, simulated-turn test, and judge pass with `tavus_pal_build_and_verify`. See [Agentic PAL building & testing](/sections/agent-tools/pal-build-and-verify).
The server flags a likely duplicate when you create a tool that resembles one you already have, so your agent can attach the existing tool instead. Prefer attaching saved tools over redefining them inline. See the [MCP tools reference](/sections/agent-tools/mcp-tools-reference) for `tavus_tool_list`, `tavus_tool_create`, and `tavus_pal_tools_attach`.
## Built-in capabilities
Custom tools are one way to extend a PAL. The other is the set of built-in **PAL Maker capabilities** - Magic Canvas, Slide Presenter, Web Search, Perception, and Memory - which the server exposes through friendly, portal-aligned tools so your agent turns them on the same way PAL Maker does:
* `tavus_pal_capability_catalog` lists the five capabilities and their backend mappings.
* `tavus_pal_capabilities_list` shows what is attached to a PAL and its config.
* `tavus_pal_capability_attach` / `tavus_pal_capability_patch` / `tavus_pal_capability_detach` manage them by friendly ID (`magic_canvas`, `slide_presenter`, `web_search`, `perception`, `memory`).
Prefer these over raw JSON Patch: the server knows that Magic Canvas, Slide Presenter, Web Search, and Memory persist through the PAL's skills (`/pals/{id}/skills`) while Perception persists through `layers.perception`. For skills that are not one of the five capabilities, the raw `tavus_skill_list` and `tavus_pal_skill_*` tools work with a skill by its registry ID.
## Knowledge
Give a PAL documents to ground its answers with the Knowledge tools. This is a two-step flow that mirrors PAL Maker's Knowledge section:
1. **Create the document.** `tavus_document_create` ingests an already-hosted URL with a normal Tavus API key. To upload a local file, `tavus_document_upload` runs the portal path (local file → tavus-api upload/S3 URL → document record) and needs a locally running MCP server plus `TAVUS_PORTAL_BEARER_TOKEN`.
2. **Attach it.** `tavus_pal_knowledge_add` (or `_replace`) wires documents and document tags onto the PAL. `tavus_pal_knowledge_settings_patch` tunes RAG behavior under `layers.knowledge_base`, and `tavus_pal_knowledge_upload` does the upload-and-attach in one call.
Use `tavus_document_chunks` to inspect the text RQH actually indexed for a document before relying on it - it returns the ingested chunks, not the source file. See the [MCP tools reference](/sections/agent-tools/mcp-tools-reference) for the full capability, skill, and Knowledge catalogs.
## Environment alignment
The server must run in the same environment as the portal that minted your key.
A `TEST` key authenticated against a production server, or the reverse, makes downstream Tavus API calls return `401`. Keep the endpoint and the portal you sign in to on the same environment.
## Troubleshooting the connection
The server exposes standard OAuth discovery metadata you can inspect directly:
```bash theme={null}
curl https://mcp.tavus.io/.well-known/oauth-protected-resource/mcp
curl https://mcp.tavus.io/.well-known/oauth-authorization-server
curl -i https://mcp.tavus.io/mcp
```
An unauthenticated `/mcp` request should return `401` with a `WWW-Authenticate` header pointing at the protected-resource metadata URL. If that header or the discovery endpoints are missing, the OAuth flow cannot complete. Re-add the server and retry the browser sign-in.
# MCP Tools Reference
Source: https://docs.tavus.io/sections/agent-tools/mcp-tools-reference
Complete reference for every tool exposed by the Tavus MCP server, grouped by category.
The Tavus MCP server exposes the full agentic PAL toolkit to MCP clients. Each tool maps to a Tavus API operation and is callable from any MCP-aware client (for example, Codex or Claude Code). This page is a complete reference of every tool, grouped by category.
For installation and authentication, see the [MCP server](/sections/agent-tools/mcp-server) page. For the equivalent command-line surface, see the [CLI](/sections/agent-tools/cli). For the autonomous PAL loop, see [Agentic PAL building & testing](/sections/agent-tools/pal-build-and-verify).
Tools are first-class objects, not inlined on the PAL. Before giving a PAL a tool, check the saved library with `tavus_tool_list` or `tavus_describe_pal_options`, then either attach an existing tool (`tavus_pal_tools_attach`) or create a new one (`tavus_tool_create`) and attach it. Writing tools inline via `tavus_patch_pal` (`layers.*.tools`) is deprecated. When you create a tool that overlaps an existing one, the response carries a `_tool_reuse_advisory` so you can reuse instead.
MCP tools return data plus optional file manifests for the client to handle. They never write files to disk themselves.
## PAL CRUD & patch
| Tool | Parameters | Description |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tavus_pal_list` | `limit=25`, `page=0`, `pal_type=None` | List PALs with an optional type filter. |
| `tavus_pal_get` | `pal_id`, `include_settings=False` | Fetch a single PAL by ID; optionally include settings. |
| `tavus_pal_create` | `system_prompt=None`, `pal_name="Agentic Tavus PAL"`, `default_face_id=None`, `pipeline_mode="full"`, `greeting=None`, `context=None`, `layers=None`, `memories=None`, `objectives_id=None`, `guardrails_id=None`, `guardrail_ids=None`, `guardrail_tags=None`, `document_ids=None`, `document_tags=None`, `is_template=None` | Create a PAL. `system_prompt` is required for `pipeline_mode="full"`. Tools are attached separately after creation. |
| `tavus_pal_delete` | `pal_id` | Delete a PAL by ID. |
| `tavus_patch_pal` | `pal_id`, `ops` | Patch a PAL with JSON Patch operations. Ops that write inline tools (`layers.*.tools`) return `_inline_tools_deprecated` steering you to the attach/create flow. |
| `tavus_describe_pal_options` | `pal_id` | Describe one PAL plus the account resources (guardrails, objectives, documents, tools, voices) that can be attached to it. |
`pipeline_mode` is one of `full` (LLM + TTS + STT + perception), `speech-to-speech` (no LLM), or `echo` (TTS + transport only).
## Face
| Tool | Parameters | Description |
| ----------------- | ------------------------- | ------------------------------------------------------------------ |
| `tavus_face_list` | `limit=25`, `stock=False` | List faces; when `stock=True`, filters to system/stock faces only. |
## Conversation
| Tool | Parameters | Description |
| --------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `tavus_conversation_create` | `pal_id=None`, `face_id=None`, `conversation_name=None` | Create a conversation with an optional PAL and face. Returns the conversation object with id and URL. |
| `tavus_conversation_end` | `conversation_id` | End a conversation by ID. |
## Quickstart
| Tool | Parameters | Description |
| ------------------ | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `tavus_quickstart` | `system_prompt`, `pal_name="Agentic Tavus PAL"`, `face_id=None` | One-shot recipe: create a PAL, pick a stock face when needed, and create a conversation. Returns the PAL plus the conversation. |
## Templates
| Tool | Parameters | Description |
| ------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tavus_pal_from_template` | `template`, `pal_name=None`, `business_context=None`, `default_face_id=None`, `layers=None` | Create a PAL from a built-in prompt template (customer-support, interviewer, sales, tutor, dev-rel) with optional business context and layer overrides. |
## Scaffold
| Tool | Parameters | Description |
| ---------------------- | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `tavus_scaffold_embed` | `conversation_url`, `target="iframe"`, `component_name="TavusConversation"` | Return starter files for embedding a Tavus conversation URL. `target` is one of `iframe`, `cvi-ui`, `vanilla`. |
## Resource list
| Tool | Parameters | Description |
| --------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `tavus_resource_list` | `resource`, `limit=25` | List account resources. `resource` is one of `guardrails`, `objectives`, `documents`, `voices`, `skills`, `tools` (singular aliases accepted). |
## Guardrails
Guardrails use a flat, per-rule shape by default (not deprecated sets).
| Tool | Parameters | Description |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `tavus_guardrail_list` | `limit=25`, `page=1`, `type="user"`, `name_or_uuid=None`, `tags=None`, `legacy=False`, `verbose=False` | List guardrails with optional filtering. `type` is one of `user`, `system`, `all`. `verbose` adds `persona_refs` and `guardrail_type`. |
| `tavus_guardrail_get` | `guardrail_id`, `verbose=False`, `legacy=None` | Fetch a single guardrail. |
| `tavus_guardrail_create` | `guardrail_name`, `guardrail_prompt`, `modality="verbal"`, `callback_url=""`, `tool_call=None`, `app_message=True`, `tags=None` | Create a flat guardrail with optional callback and tool-call payload. `modality` is one of `verbal`, `visual`, `audio`. |
| `tavus_guardrail_patch` | `guardrail_id`, `guardrail_name=None`, `guardrail_prompt=None`, `modality=None`, `callback_url=None`, `tool_call=None`, `app_message=None`, `tags=None` | Update a guardrail. Omit fields to leave them unchanged; each supplied field is replaced whole. |
| `tavus_guardrail_delete` | `guardrail_id` | Delete a guardrail by ID. |
| `tavus_guardrail_tags` | `search=None`, `page=None`, `limit=None` | List tags applied to the account's guardrails. |
## Objectives
Objectives are stored as sets; PALs reference a set by `objectives_id`.
| Tool | Parameters | Description |
| -------------------------- | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tavus_objective_list` | `limit=25`, `page=1`, `type="user"`, `name_or_uuid=None`, `sort="ascending"` | List objective sets. `type` is one of `user`, `system`, `all`. |
| `tavus_objective_get` | `objectives_id` | Fetch a single objective set. |
| `tavus_objective_create` | `data`, `name=""`, `allow_loops=False` | Create an objective set. Each item has `objective_name` (letters/digits/underscores only), `objective_prompt`, optional `confirmation_mode` ("auto"\|"manual"), `modality` ("verbal"\|"visual"\|"audio"), `output_variables`, `callback_url`, `tool_call`, and chains via `next_required_objective` or `next_conditional_objectives` (not both). When `allow_loops=False`, exactly one item must be root. |
| `tavus_objective_patch` | `objectives_id`, `ops` | Patch an objective set with JSON Patch ops; cycle and single-root validation re-runs. |
| `tavus_objective_delete` | `objectives_id` | Delete an objective set by ID. |
| `tavus_objective_validate` | `data`, `name=""`, `allow_loops=False` | Validate an objective-set payload (cycles, single root, references) without persisting. |
## Tools (PAL tool library)
| Tool | Parameters | Description |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `tavus_tool_list` | `limit=25`, `page=1`, `type="user"`, `name_or_uuid=None`, `sort="ascending"` | List tools. `type` filters by ownership (`user`, `system`, `all`). |
| `tavus_tool_get` | `tool_id` | Fetch a single tool by ID. |
| `tavus_tool_create` | `name`, `description`, `parameters=None`, `delivery=None`, `origin="llm"`, `on_call=None`, `on_resolve="fire_and_forget"`, `static_filler=None` | Create a tool. Placeholders `{ident}` in url/body/query must be declared in `parameters.properties`. Description plus parameters JSON must total ≤ 10,000 chars. Returns `_tool_reuse_advisory` if similar tools exist. For [post-call actions](/sections/conversational-video-interface/pal/post-call-tool), use the CLI `tool create --file` or Tools API with `trigger_type: "post_call"` (not yet a parameter on this MCP tool). |
| `tavus_tool_patch` | `tool_id`, `name=None`, `description=None`, `parameters=None`, `delivery=None`, `origin=None`, `on_call=None`, `on_resolve=None`, `static_filler=None` | Update a tool. Omit fields to leave them unchanged. Secrets returned scrubbed (`********`) from a prior GET must be omitted, not re-sent. |
| `tavus_tool_delete` | `tool_id` | Delete a tool by ID. |
`delivery` defaults to `{"app_message": true}` (Daily data channel), or use `{"api": {...}}` for HTTPS calls with headers, query params, body template, content type, timeout (default 10s, max 60s), and auth (`none`, `bearer`, `basic`, `api_key`, `hmac`, `oauth2_client_credentials`).
## Pronunciation dictionaries
Pronunciation dictionaries are referenced from PALs via `layers.tts.pronunciation_dictionary_id`.
| Tool | Parameters | Description |
| --------------------------------------- | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tavus_pronunciation_dictionary_list` | `limit=25`, `page=0`, `sort="desc"` | List pronunciation dictionaries. |
| `tavus_pronunciation_dictionary_get` | `dictionary_id` | Fetch a single pronunciation dictionary. |
| `tavus_pronunciation_dictionary_create` | `name`, `rules=None` | Create a dictionary. Each rule is `{text, pronunciation, type: "alias"\|"ipa", alphabet?, case_sensitive?, word_boundaries?}`; max 10,000 rules. Text values must be unique. |
| `tavus_pronunciation_dictionary_patch` | `dictionary_id`, `name=None`, `rules=None` | Update a dictionary. Supplying `rules` replaces the full list (no merge). |
| `tavus_pronunciation_dictionary_delete` | `dictionary_id` | Delete a dictionary by ID. |
## PAL tools (attach & detach)
| Tool | Parameters | Description |
| ------------------------ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tavus_pal_tools_list` | `pal_id` | List tools currently attached to a PAL. |
| `tavus_pal_tools_attach` | `pal_id`, `tool_ids` | Attach one or more existing tools to a PAL by ID. A PAL can hold up to 50 tools. If any attached tool is vision/audio, the PAL's `perception_model` auto-bumps to `raven-1` (response carries `_perception_model_bumped_to_raven_1`). |
| `tavus_pal_tools_detach` | `pal_id`, `tool_id` | Detach a single tool from a PAL. |
## PAL capabilities
The five built-in PAL Maker capabilities - Magic Canvas, Slide Presenter, Web Search, Perception, and Memory. Prefer these over raw JSON Patch or raw skills: they accept friendly IDs (`magic_canvas`, `slide_presenter`, `web_search`, `perception`, `memory`, plus the portal IDs `builtin:magic_canvas`, `builtin:slide_presenter`, `builtin:web_search`, `skill:memory`) and know that Magic Canvas, Slide Presenter, Web Search, and Memory persist through the PAL's skills while Perception persists through `layers.perception`.
| Tool | Parameters | Description |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tavus_pal_capability_catalog` | — | List the five PAL Maker capabilities and their backend mappings. |
| `tavus_pal_capabilities_list` | `pal_id` | List Magic Canvas, Slide Presenter, Web Search, Perception, and Memory attachment state and config for a PAL. |
| `tavus_pal_capability_attach` | `pal_id`, `capability_id`, `config=None`, `document_ids=None`, `slides_trigger=None`, `prompt=None`, `magic_canvas_components=None`, `magic_canvas_usage_guidance=None`, `scheduling_url=None`, `scheduling_provider=None`, `perception_model=None`, `visual_awareness_queries=None`, `audio_awareness_queries=None`, `visual_tool_prompt=None`, `audio_tool_prompt=None` | Attach a capability. Options are capability-specific: Slide Presenter uses `document_ids`/`slides_trigger`/`prompt`; Magic Canvas uses `magic_canvas_*`/`scheduling_*`; Perception uses `perception_model` and the visual/audio awareness fields; Web Search and Memory take no config. |
| `tavus_pal_capability_patch` | same as `tavus_pal_capability_attach` | Update the config of an already-attached capability. |
| `tavus_pal_capability_detach` | `pal_id`, `capability_id` | Detach a capability. Perception detaches by setting `perception_model: off`; the other four detach from `/pals/{id}/skills`. |
## Skills (raw RQH)
Lower-level access to the RQH skill registry and a PAL's raw skill attachments. Use [PAL capabilities](#pal-capabilities) for the five PAL Maker capabilities; reach for these only to work with a skill by its raw registry ID.
| Tool | Parameters | Description |
| ------------------------ | ----------------------------------- | -------------------------------------------------------------------- |
| `tavus_skill_list` | — | List registered RQH skills/capabilities available to attach to PALs. |
| `tavus_pal_skills_list` | `pal_id` | List raw RQH skill attachments for a PAL. |
| `tavus_pal_skill_attach` | `pal_id`, `skill_id`, `config=None` | Attach or replace one raw RQH skill on a PAL. |
| `tavus_pal_skill_patch` | `pal_id`, `skill_id`, `config` | Merge config into an already-attached raw RQH skill. |
| `tavus_pal_skill_detach` | `pal_id`, `skill_id` | Detach one raw RQH skill from a PAL. |
## Knowledge documents
Account-level Knowledge document records that back a PAL's Knowledge section. With a normal Tavus API key, create documents from an already-reachable URL (`tavus_document_create`). Local file upload uses the same portal-only path as PAL Maker and requires a locally running MCP server (to read the file) plus `TAVUS_PORTAL_BEARER_TOKEN`.
| Tool | Parameters | Description |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `tavus_document_list` | `limit=25`, `page=0`, `status=None`, `tags=None`, `participant_tags=None`, `name_or_uuid=None`, `verbose=True` | List Knowledge documents available to attach to PALs. |
| `tavus_document_get` | `document_id`, `verbose=True` | Get one Knowledge document record. |
| `tavus_document_create` | `document_url`, `document_name=None`, `tags=None`, `participant_tags=None`, `crawl=None`, `custom_description=None`, `is_customer_support_document=False`, `font_size_aware_extraction=False` | Create a Knowledge document from an already-hosted URL. |
| `tavus_document_upload` | `local_path`, `document_name=None`, `tags=None`, `participant_tags=None`, `custom_description=None`, `is_customer_support_document=False`, `font_size_aware_extraction=False`, `bucket_name="developer-portal-documents"`, `region="us-east-1"`, `sign_duration=None` | Upload a local file through tavus-api/S3, then create the document record. Requires `TAVUS_PORTAL_BEARER_TOKEN`. |
| `tavus_document_tags` | `search=None`, `page=None`, `limit=None` | List document tags that can be attached to PAL Knowledge. |
| `tavus_document_chunks` | `document_id`, `collection="regular"`, `limit=100`, `offset=None` | Read the extracted chunks RQH indexed for a document. Use before making claims about a document's contents. |
| `tavus_document_recrawl` | `document_id`, `crawl=None` | Trigger a recrawl for a crawl-backed Knowledge document. |
## PAL Knowledge
A PAL's Knowledge section is its `document_ids` plus `document_tags`, with RAG tuning under `layers.knowledge_base`. Create documents first with the [Knowledge documents](#knowledge-documents) tools, then attach them here.
| Tool | Parameters | Description |
| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `tavus_pal_knowledge_list` | `pal_id` | Show Knowledge documents/tags attached to a PAL and resolve the direct document IDs into records. |
| `tavus_pal_knowledge_add` | `pal_id`, `document_ids=None`, `document_tags=None` | Attach existing Knowledge documents or document tags to a PAL. |
| `tavus_pal_knowledge_remove` | `pal_id`, `document_ids=None`, `document_tags=None` | Remove Knowledge documents or document tags from a PAL. |
| `tavus_pal_knowledge_replace` | `pal_id`, `document_ids=None`, `document_tags=None` | Replace the PAL's Knowledge document/tag selections. |
| `tavus_pal_knowledge_settings_patch` | `pal_id`, `rag_score_threshold=None`, `rag_n_chunks=None`, `rag_surrounding_chunk_radius=None`, `enable_rag_observability_app_messages=None`, `use_procedure_goalchain=None` | Patch RAG tuning settings under `layers.knowledge_base`. |
| `tavus_pal_knowledge_upload` | `pal_id`, `local_path`, `document_name=None`, `tags=None`, `participant_tags=None`, `custom_description=None`, `is_customer_support_document=False`, `font_size_aware_extraction=False`, `bucket_name="developer-portal-documents"`, `region="us-east-1"`, `sign_duration=None`, `attach=True` | Upload a local file as a Knowledge document and attach it to the PAL by default. Requires `TAVUS_PORTAL_BEARER_TOKEN`. |
## Builder
The builder is an LLM-guided PAL creation flow.
| Tool | Parameters | Description |
| ---------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `tavus_builder_create` | `name`, `greeting=None`, `pal_id=None`, `model=None` | Create a builder session, optionally based on an existing PAL. |
| `tavus_builder_list` | `limit=None`, `page=None`, `pal_id=None`, `name=None`, `status=None` | List builder sessions with optional filters. |
| `tavus_builder_get` | `builder_id` | Fetch a single builder by ID. |
| `tavus_builder_delete` | `builder_id` | Delete a builder session. |
| `tavus_builder_chat` | `builder_id`, `message` | Send a chat turn to the builder. Returns assistant text, autocomplete suggestions, a `draft_ready` flag, and target sections for scoped updates. |
| `tavus_builder_chat_history` | `builder_id`, `limit=50` | Fetch chat history for a builder session. |
| `tavus_builder_append_messages` | `builder_id`, `messages` | Append messages to builder chat history. |
| `tavus_builder_update_objectives` | `builder_id`, `message` | Update objectives based on a user feedback message. |
| `tavus_builder_update_guardrails` | `builder_id`, `message` | Update guardrails based on a user feedback message. |
| `tavus_builder_update_greeting` | `builder_id`, `message` | Update the greeting/opening based on feedback. |
| `tavus_builder_update_personality` | `builder_id`, `message`, `pal_name=False`, `system_prompt=False` | Update PAL personality/system prompt based on feedback. Optionally include PAL name and/or system prompt fields. |
| `tavus_builder_publish` | `builder_id` | Publish the builder's drafted PAL. |
## Chat mode (text-only)
| Tool | Parameters | Description |
| ------------------ | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `tavus_chat_start` | `pal_id`, `custom_greeting=None`, `conversation_name=None` | Start a text-only conversation with a PAL. Use this to test a built PAL by sending typed turns; no face video is rendered. |
| `tavus_chat_turn` | `conversation_id`, `text`, `timeout_s=20.0` | Send one user turn and wait for the PAL's reply. Returns `{text: }`. |
| `tavus_chat_end` | `conversation_id` | End a chat-mode conversation. |
## PAL preview & build-and-verify
| Tool | Parameters | Description |
| ---------------------------- | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tavus_pal_preview` | `pal_id`, `face_id=None`, `conversation_name=None` | Start a full audio/video preview conversation against a PAL and return `{conversation_id, conversation_url, ...}`. Hand the URL to a human for visual verification. |
| `tavus_pal_build_and_verify` | `prompt`, `face_id=None`, `max_rounds=4`, `answers=None` | Build a PAL from one creator prompt (optionally using supplied answers for builder follow-ups), then publish and validate via CVI chat mode. When `face_id` is omitted, a default is selected and attached. |
See [Agentic PAL building & testing](/sections/agent-tools/pal-build-and-verify) for expected agent usage, CLI usage, and how to read the simulated-turn verdict.
# Overview
Source: https://docs.tavus.io/sections/agent-tools/overview
Build a PAL, wire its tool calls into your own code, and test the integration from your editor with the MCP server or your terminal with the CLI.
Tavus MCP is the toolkit for building a PAL and integrating its tool calls into your own application. It gives an AI coding agent, or you at a terminal, direct access to the operations behind a Tavus PAL: create and patch PALs, define and attach the tools a PAL can call, and start test conversations to confirm everything works.
**For AI agents:** Tavus MCP documented here is the **development MCP** for creating PALs, defining tools, and testing tool-call integrations against the Tavus API. It is not the **Tavus Docs MCP** for searching this documentation site (`https://docs.tavus.io/mcp`). For live docs search and page retrieval, see [Agents & automation](/sections/agents-and-automation).
It is separate from the Conversational PAL Builder in PAL Maker. PAL Maker is the UI for drafting a PAL. This toolkit is for developers wiring a PAL into code: defining the tool contract, writing the handlers that respond when the PAL calls a tool, and testing that exchange without leaving the editor or shell.
## The use case
A useful PAL calls tools to look something up, change what is on screen, or take an action in your app, then responds based on the result. Two things have to stay in sync for that to work:
* The PAL has to know which tools exist and when to call them.
* Your code has to handle those calls and return a result.
The MCP server and CLI let you author both sides and test them together. See [bidirectional tool calls](/sections/agent-tools/mcp-server#bidirectional-tool-calls) for how the exchange works.
## Two surfaces
Connect Codex, Claude Code, Cursor, or any MCP client over HTTPS with per-user OAuth. Your agent builds the PAL, wires the tools into your codebase, and tests them.
The `tavus` command for interactive and scripted access to every operation. Suited to humans at a terminal and to CI automation.
Both call the same Tavus API, so the available operations are the same across them. For the autonomous PAL test loop, see [Agentic PAL building & testing](/sections/agent-tools/pal-build-and-verify). For the full catalog of MCP tools, see the [MCP tools reference](/sections/agent-tools/mcp-tools-reference).
## Which should I use?
Best when you work in an editor with an AI agent and want it to build the PAL and wire tool calls into your code for you.
* Transport: HTTP at an `/mcp` endpoint.
* Auth: browser-based OAuth through the PAL Maker. A per-user key is forwarded to the Tavus API as `x-api-key`, so no key sits in your client config.
* Add it to Codex or Claude Code.
For Codex:
```bash theme={null}
codex mcp add tavus --url https://mcp.tavus.io/mcp
```
For Claude Code:
```bash theme={null}
claude mcp add -s user --transport http tavus https://mcp.tavus.io/mcp
```
For Codex, start the OAuth flow explicitly with:
```bash theme={null}
codex mcp login tavus
```
Do not put a Tavus API key or shared bearer token in your MCP client config. Authentication is handled through the OAuth flow.
Best for humans at a terminal and for scripted automation in CI.
* Auth: `tavus auth login` stores an env-scoped key in your OS keychain, or set `TAVUS_API_KEY` directly for automation.
* Outputs JSON by default. Many list commands also render Rich tables.
```bash theme={null}
TAVUS_API_KEY=... tavus pal list
```
## Choosing a target environment
Both surfaces default to production (`PROD`). To target the test database, set `TAVUS_ENV=TEST` or pass `--env TEST` to the CLI.
For the MCP server, keep the endpoint aligned with the portal environment that minted your key. A `TEST` key used against a production server, or the reverse, makes downstream Tavus API calls return `401`.
## Next steps
Connect your agent and wire bidirectional tool calls.
Run simulated turns and judge whether a new PAL works.
Authenticate and run `tavus` commands.
Browse the full catalog of MCP tools and their parameters.
# Agentic PAL Building & Testing
Source: https://docs.tavus.io/sections/agent-tools/pal-build-and-verify
Use the Tavus MCP server or CLI to build a new PAL in a loop: adjust configuration, run simulated CVI chat turns, judge the result, and refine until the PAL behaves as intended.
The agentic PAL building & testing flow is an autonomous loop for a new Tavus PAL. It builds a PAL from a creator prompt, publishes it, runs simulated text turns through CVI chat mode, and returns a structured verdict so an agent can decide whether the PAL works or whether the system prompt and configuration need another pass.
Use it when an agent needs a single answer: **did the PAL I asked for actually behave correctly?**
Conversations started during build-and-verify - including CVI chat-mode probes and full preview URLs - incur charges the same as any other conversation on your account.
Use `tavus_pal_build_and_verify` when Codex, Claude Code, Cursor, or another MCP client should drive the whole build, test, and judge loop.
Use `tavus pal build` when you want the same workflow from a shell, with JSON output for scripts or CI-style checks.
## What the loop does
The flow opens a conversational builder session, sends the creator prompt, applies builder updates to personality, greeting, objectives, and guardrails, and publishes the resulting PAL.
The MCP tool can run this autonomously. The CLI is suited to terminal use and may prompt you for builder follow-up answers before publishing.
If you pass `face_id`, the flow validates that face and attaches it to the PAL. If you omit it, Tavus selects and attaches a default face from the account's available faces.
After publish, Tavus reads the PAL spec and generates validation probes. The probes target the PAL's objectives, adversarial guardrail cases, attached knowledge base documents, and attached tools.
The flow starts a CVI chat-mode conversation and sends each probe as a user turn. Chat mode uses the same PAL configuration but skips Daily/video rendering, so it is fast enough for agent regression checks.
Tavus judges the resulting transcript against the PAL spec and returns a verdict with evidence for objectives, guardrails, knowledge base usage, and tool behavior. The public MCP and CLI wrappers perform one bounded refinement pass when the first verdict is not a pass.
The builder driver, probe generator, face selector, and judge run on Tavus infrastructure. The caller only needs normal Tavus authentication for the selected environment; no client-side LLM key is required.
## Choosing the right surface
| Use this | When |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `tavus_pal_build_and_verify` | An MCP-connected coding agent should create, test, and judge a new PAL in one tool call. |
| `tavus pal build` | You want a reproducible terminal command and JSON output for a human or script to inspect. |
| `tavus_chat_start` / `tavus_chat_turn` | You already have a PAL and only want to run specific text probes against it. |
| `tavus_pal_preview` | You need a full audio/video conversation URL for human visual verification after text-mode validation passes. |
## MCP usage
Ask the agent to call the MCP tool with a concrete creator prompt:
```text theme={null}
tavus_pal_build_and_verify(
prompt="Create a concise onboarding coach for new API developers. It should ask what they are building, recommend the right Tavus integration path, and avoid making pricing promises.",
max_rounds=4
)
```
The tool returns IDs, the build transcript, generated probes, the smoke-test transcript, and the verdict:
```json theme={null}
{
"builder_id": "b...",
"pal_id": "p...",
"face_id": "r...",
"pal_url": "https://maker.tavus.io/dev/pals/update?pal_id=p...",
"validated": true,
"probes": [
"I am building a support assistant. Which Tavus integration should I use?",
"Can you promise me this will cost less than my current vendor?"
],
"smoke_transcript": [
{ "role": "user", "text": "I am building a support assistant. Which Tavus integration should I use?" },
{ "role": "assistant", "text": "..." }
],
"verdict": {
"overall": "pass",
"summary": "The PAL asks for context, recommends the correct integration path, and avoids pricing promises."
}
}
```
Agents should check `verdict.overall` first, then inspect `verdict.summary`, `smoke_transcript`, and any failing evidence fields when the result is `partial` or `fail`.
## CLI usage
Run the same workflow from a shell:
```bash theme={null}
tavus pal build \
--prompt "Create a concise onboarding coach for new API developers. It should ask what they are building, recommend the right Tavus integration path, and avoid making pricing promises." \
--max-rounds 4 \
--json > build-result.json
jq '.validated, .verdict' build-result.json
```
Use `--face-id ` when you need a specific face/voice. Otherwise, Tavus selects a default face for the new PAL.
For agent automation, prefer `--json` and assert on `validated` or `verdict.overall` instead of exact assistant wording. PAL replies are non-deterministic, so the verdict and evidence fields are the stable output.
## Reading the verdict
| Field | Meaning |
| ------------------------ | -------------------------------------------------------------------- |
| `validated` | Boolean shortcut for `verdict.overall === "pass"`. |
| `verdict.overall` | `pass`, `partial`, or `fail`. |
| `verdict.objectives` | Whether the PAL satisfied each objective, with evidence. |
| `verdict.guardrails` | Whether each guardrail held under adversarial probes, with evidence. |
| `verdict.knowledge_base` | Whether attached documents were used when a probe required them. |
| `verdict.tools` | Whether attached tools appeared to be invoked when required. |
| `smoke_transcript` | The simulated user turns and PAL replies used by the judge. |
| `refine_rounds_used` | Number of automatic refinement rounds used by the wrapper. |
Treat `partial` as actionable feedback, not a transport failure. Read the failing evidence, tighten the creator prompt or PAL system prompt, then run the flow again or patch the PAL directly with `tavus_patch_pal`.
## What this does not replace
Build-and-verify is a text-mode behavioral check. It does not validate visual rendering, facial expression quality, audio latency, room join behavior, or user-device permissions. After a PAL passes the simulated turns, use `tavus_pal_preview` or `tavus pal preview` to hand a full conversation URL to a human for visual QA.
For existing PALs, do not rebuild just to test them. Start chat mode directly with `tavus_chat_start` and `tavus_chat_turn`, or use the CLI `tavus chat` commands to run targeted probes.
## Common failures
| Failure | What to do |
| ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `Face not found` | The supplied `face_id` does not exist in the selected environment. Pick a valid face or omit `face_id`. |
| Face selection failed | Tavus could not choose or attach a default face. Pass `--face-id` or fix the account's face catalog. |
| Builder did not reach `draft_ready` | The flow still publishes and tests the draft, but the verdict may be `partial`. Re-run with a more specific creator prompt. |
| Chat turn timeout | The probe produced no assistant reply before the timeout. Inspect `smoke_transcript` and retry or simplify the prompt. |
| Judge returned `partial` or `fail` | Use the verdict evidence as a prompt-edit checklist, then patch or rebuild the PAL. |
# Agents & Automation
Source: https://docs.tavus.io/sections/agents-and-automation
How to access machine-readable docs and API artifacts for developers, IDEs, and automation - llms.txt, OpenAPI, Agent Skills, and MCP.
**How do I point my agent, IDE, or automation at Tavus’s documentation and HTTP APIs?** This guide helps you pick the right artifact for your workflow.
**For AI agents:** The MCP endpoint documented on this page (`https://docs.tavus.io/mcp`) is the **Tavus Docs MCP** - it searches and retrieves pages from this documentation site. It is not the Tavus development MCP for building PALs, wiring tool calls, or running test conversations. For that toolkit, see [Tavus MCP & CLI](/sections/agent-tools/overview).
## Documentation bundle reference
| Artifact | Role | URL |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------- |
| **llms.txt** | Curated directory of doc URLs for selective fetch | `https://docs.tavus.io/llms.txt` |
| **llms-full.txt** | Full bundled text export for offline or bulk ingest | `https://docs.tavus.io/llms-full.txt` |
| **openapi.yaml** | HTTP API contract (paths, request/response schemas, security schemes) | `https://docs.tavus.io/openapi.yaml` |
| **skill.md** | Agent-oriented capability summary ([Agent Skills](https://agentskills.io) conventions) | `https://docs.tavus.io/skill.md` |
| **Tavus Docs MCP** | Hosted Model Context Protocol server for this docs site: search plus fetch full pages from the indexed documentation (see [Tavus Docs MCP](#tavus-docs-mcp)) | `https://docs.tavus.io/mcp` |
## When to use what
* **Tavus Docs MCP:** Interactive tools that need live search and current page retrieval against the indexed documentation site.
* **llms-full.txt:** Bulk or offline ingestion; snapshot of doc text (not a substitute for OpenAPI).
* **openapi.yaml:** Source of truth for HTTP APIs (requests, responses, auth headers); agents should not invent endpoints that contradict it.
* **llms.txt / selective fetches:** Discovery and pulling specific pages when you do not want the full bundle.
* **skill.md:** Compact capability orientation for agents; still point to OpenAPI and deep docs for details.
## Tavus Docs MCP
An MCP endpoint is available for this documentation site - not for building PALs or testing Tavus API tool calls; for that, use [Tavus MCP & CLI](/sections/agent-tools/overview). Configure docs MCP clients that support HTTP transport with:
```json theme={null}
{
"url": "https://docs.tavus.io/mcp",
"transport": "http"
}
```
Use MCP when the tool needs **live** search and retrieval against the **current** indexed documentation, not a frozen file export.
# Changelog
Source: https://docs.tavus.io/sections/changelog/changelog
## New Features
* **Magic Canvas image support:** The `image` component now renders images from your Knowledge Base or an allowlisted website during a conversation. [Learn more](/sections/conversational-video-interface/magic-canvas/components/image)
* **Auto-start recordings:** Set `auto_start_recording` on create-conversation and Tavus starts the recording about a second after the PAL joins, with no client-side call to `startRecording()`. Requires `recording_storage` and a Tavus-hosted room. [Learn more](/sections/conversational-video-interface/quickstart/conversation-recordings#start-recording-automatically)
## Changes
* **New default LLM - `tavus-gemma-4`:** A faster, smarter, tuned version of `tavus-gemma-4` is now the recommended default. It makes for the snappiest conversations while staying strong on intelligence and tool use. [Learn more](/sections/conversational-video-interface/pal/llm#1-model)
* **Deprecated LLM:** `tavus-gemma-4-thinking` is deprecated in favor of the new tuned `tavus-gemma-4`.
## New Features
* **EU AI Act controls:** New AI disclosure controls on the PAL - `disclosure_type`, `verbal_disclosure`, and `visual_disclosure` - plus a per-conversation `policy: "eu"` that auto-applies the disclosure and switches Raven-1 to `limited` emotion recognition when PAL fields are left on `auto`. [Learn more](/sections/onboarding-guide/eu-ai-act)
* **Zoom support:** PALs can now join Zoom calls in addition to Google Meet. Invite the PAL's `@tavusinvite.com` address to a calendar event with a Zoom link and it joins on its own. [Learn more](/sections/conversational-video-interface/pal/meetings)
## New Features
* **Expanded Tavus-Hosted LLM Selection:** Added support for new Tavus-hosted LLMs: `tavus-gemma-4`, `tavus-gpt-5.6-sol`, and `tavus-gpt-5.6-terra`. [Learn more](/sections/conversational-video-interface/persona/llm#tavus-hosted-models)
## Changes
* **Deprecated LLMs:** The following Tavus-hosted LLMs are now deprecated: `tavus-glm-4.7`, `tavus-gpt-oss`, `tavus-claude-haiku-4.5`, `tavus-gpt-5.2`, and `tavus-gemini-3-flash`.
## New Features
* **Zero Data Retention:** Account-level and programmatic support for Zero Data Retention is now available, enabling customers to opt out of data persistence across Tavus services.
## Enhancements
* **Performance:** Improved LiveKit pipeline performance by 15%!
## New Features
* **PAL Maker:** Describe a PAL in plain language and have it configured end to end (system prompt, face, objectives, guardrails, tools, knowledge base, and advanced settings), then talk to it in the same session. Removes the need to configure each layer by hand. Learn more
* **Starter Templates:** Fork a complete, live experience into your account with no code (Interviewer, SDR, Medical Intake, Trivia Host), or open the GitHub repo behind it. Learn more
* **Meetings (Google Meet):** Send a PAL into a Google Meet call. It joins on its own past the admit prompt, sees and hears the room, and can run the meeting. Invite it via calendar invite or pass a meeting URL when creating a conversation. [Learn more](/sections/conversational-video-interface/pal/meetings)
* **Magic Canvas:** A shared interactive surface rendered on top of the video stream. The PAL surfaces the right component at the right moment. Six components at launch: question, text, input, calendar, chart, and alert. [Learn more](/sections/conversational-video-interface/magic-canvas/overview)
* **Presentation Mode:** Upload a deck or source material, Tavus indexes it, and the PAL presents from it, takes questions, and pulls the right slide on demand. Supports two modes: Walk the deck (end to end) and On-demand. [Learn more](/sections/conversational-video-interface/skills/presentation)
* **Embeddable Widgets:** Drop a PAL onto any site with a single line of code, scoped and auto-provisioned to your account, with no SDK. Place it in any corner or inline, and customize branding to match your site. [Learn more](/sections/deployments/widget)
* **Landing Pages:** Deploy a PAL on a ready-made hosted landing page, no setup required. [Learn more](/sections/deployments/landing-page)
* **Internet Search:** Give a PAL real-time web search during a conversation. Attach the `internet_search` skill - pure on/off, no configuration. [Learn more](/sections/conversational-video-interface/skills/internet-search)
## Enhancements
* **Tool Calling:** Tools can now call any third-party API (including GraphQL) with custom auth and request bodies, no middleware. New on-resolve modes (fire and forget, pull into context, generate a response) control how results enter the conversation, and tools are reusable across PALs. [Learn more](/sections/conversational-video-interface/pal/tools)
## Enhancements
* **Meet Rivian, Tiffany, and Brian:** Our 3 new stock replicas!
* **Improved Image to Replica uploading flow:** Get more detailed and accurate errors before training submission.
## New Features
* **Guardrails as first-class primitives:** Guardrails are now standalone resources that can be created, edited, and reused across multiple PALs. Compose individual guardrails into sets via `guardrail_tags`. [Learn more](/sections/conversational-video-interface/guardrails)
* **Choose your delivery channel:** Guardrail violations can now be delivered via [Interaction Events](/sections/conversational-video-interface/interactions-protocols/overview) (in-call `app-message`) or [Webhooks](/sections/webhooks-and-callbacks#guardrail-callbacks) (server-side `callback_url`) - or both.
* **`guardrail_uuid` on violations:** Triggered events and callbacks now include `guardrail_uuid`, so you can identify exactly which individual guardrail was violated.
## New Features
* **Azure TTS support:** Azure is now available as a TTS engine, expanding language coverage for multilingual and localized voice output. [Learn more](/sections/conversational-video-interface/persona/tts)
## New Features
* **Individually addressable guardrails:** Guardrails are now first-class resources. Create, attach, edit, and delete each guardrail independently via the [Guardrails API](/api-reference/guardrails/create-guardrails). Bundle guardrails by tag and reference them on a PAL via `guardrail_ids` or `guardrail_tags`. [Learn more](/sections/conversational-video-interface/guardrails)
* **Node.js plugin support for LiveKit integration:** The LiveKit Agents integration now supports Node.js via the `@livekit/agents-plugin-tavus` plugin, in addition to Python. Install with `npm install @livekit/agents @livekit/agents-plugin-tavus`. [Learn more](/sections/integrations/livekit#node-js)
* **Real-time event timestamps:** Every interaction event (`conversation.utterance`, `conversation.started_speaking`, etc.) now carries a `timestamp` field delivered in real time on the respective Interaction event. Learn more
* **Transcript utterance timestamps:** End-of-call transcripts now carry per-turn timing. The [`application.transcription_ready`](https://docs.tavus.io/sections/webhooks-and-callbacks#conversation-callbacks) webhook (and the same payload nested under `events` in the verbose [`GET /conversations/{id}?verbose=true`](https://docs.tavus.io/api-reference/conversations/get-conversation) response) now includes `timestamp` (Unix epoch float, seconds - same field name as live interaction events), `seconds_from_start`, `duration` (seconds, float - same field name as `conversation.stopped_speaking.duration`), and `inference_id` (on assistant turns) on each transcript entry.
## Enhancements
* **New, and more detailed, error messages when a face fails:** [Learn more](/sections/errors-and-status-details#face-training-errors)
## New Features
* **AI Image Fixer API support:** [Create Face](/api-reference/phoenix-replica-model/create-replica) now accepts an `auto_fix_training_image` property. Set it to `true` to use Tavus's AI Image Fixer to instantly fix any uploaded image to fit our requirements, eliminating the need for editing or recapturing photos. [Learn more](/sections/replica/train-with-an-image#ai-image-fixer)
## New Features
* **AI Image Fixer:** Instantly fix any uploaded image to fit our requirements, eliminating the need for editing or recapturing photos.
## New Features
* **Frame Checker (Video to Face):** Get instant feedback, before recording, on whether your camera setup meets our requirements.
## Enhancements
* **Less footage required (Video to Face):** We now only require 1 minute of video, down from the 2 minutes previously needed.
* **Simpler in-portal recording flow (Video to Face):** A streamlined recording experience in the portal to help you capture high-quality recordings.
## New Features
* **Image to Face:** Build a face from a single still. Drop in a photo, illustration, or brand mascot.
## New Features
* **Voice Activity Detector improvements:** Resulting in a smoother conversational experience in noisy environments. This is automatically rolled out to all users.
* **Expanded Recording Storage Support:** Conversation recordings can now be delivered to Google Cloud Storage (GCP) and Azure Blob Storage, in addition to AWS S3. Learn more
## Enhancements
* **Tavus Components Library Updates:** Improved audio-video sync, plus new chat components and closed captions with streaming support. Learn more
## New Features
* **Wake Phrase:** PALs can now stay silent until they hear a specific phrase, similar to how voice assistants like Siri or Alexa work. Configure it via the `wake_phrase` parameter in the Conversational Flow layer. The PAL still hears everything that is said and responds with full conversation history once the wake phrase is detected. Learn more
## New Features
* **Idle Engagement:** Faces can now proactively re-engage by speaking to the user after a period where the user is silent. Eagerness of this feature can be configured via the `idle_engagement` parameter in the Conversational Flow layer. Learn more
## New Features
* **Speaking Events:** Two new events - `conversation.started_speaking` and `conversation.stopped_speaking` - fire for both the PAL and the user with a `role` field (`"pal"` or `"user"`) identifying the speaker. Tavus also sends legacy duplicate events with `"replica"` instead of `"pal"`. `conversation.stopped_speaking` includes an `interrupted` boolean and a `duration` field (in seconds). Learn more
* **Conversation Diagnostics:** A new diagnostics surface for inspecting what happened in a conversation - including packet loss, network connection, FPS, and more - designed to make debugging significantly faster. Click on any conversation in the PAL Maker to access its diagnostics page.
## Enhancements
* **Non-Interruptible Custom Greetings:** Custom greetings now finish entirely before users can interrupt speech. Previously, participants could talk over a `custom_greeting`; now the PAL completes the greeting before it begins listening. Learn more
* **Improved Turn-Taking Latency:** Significant TTS optimizations reduce turn-taking latency, resulting in faster and more natural back-and-forth during conversations.
## New Features
* **Streaming Utterance Event:** A new `conversation.utterance.streaming` event progressively reports what has been said during a conversation turn for both face and user utterances. Use it to power closed captioning and build accurate transcripts - especially when a user interrupts the face, since the streaming event reflects only the words actually spoken rather than the full LLM response. Learn more
* **Pronunciation Dictionaries:** Define custom pronunciation rules so your PAL says brand names, technical terms, acronyms, and foreign words exactly right. Choose between simple alias substitution (e.g., "Tavus" → "TAH-vus") or precise IPA phonetic notation. Create a dictionary once and attach it to a PAL via the TTS layer - any updates automatically propagate to all linked PALs with zero extra latency at conversation time. Learn more
## New Features
* **Voice Isolation:** Filter background noise from participant audio to improve conversation quality. Configure it via the `voice_isolation` parameter in the Conversational Flow layer. Learn more
## Changes
* **Chat Interrupt History:** PALs now know when they have been interrupted. This allows the PAL to pick back up where it left off, and also improves objectives adherence.
## New Features
* **Expanded ASR Model Selection:** You can now choose from five specialized speech-to-text engines via the `stt_engine` parameter. New models include `tavus-parakeet`, `tavus-soniox`, `tavus-whisper`, and `tavus-deepgram-medical`. Use `tavus-auto` to automatically route to the best model for each conversation. Learn more
## Enhancements
* **30% Faster Phoenix-4 Boot Time:** Phoenix-4 conversations now boot 30% faster, significantly reducing the time from conversation creation to readiness.
## Changes
* **`conversation.replica_interrupted` Event Removed:** The `conversation.replica_interrupted` application message has been removed from interaction events. This event was deprecated in a previous backend update. Use `conversation.replica.stopped_speaking` with the `interrupted: true` property to detect interruptions instead.
* **`duration` and `interrupted` Fields on Face Stopped Speaking:** The `conversation.replica.stopped_speaking` event now includes a `duration` field (how long the PAL spoke in seconds) and an `interrupted` field (`true`/`false`) indicating whether the PAL was interrupted by the user. Learn more
## New Features
* **Event Ordering and Turn Tracking:** All server-broadcasted interaction events now include `seq` and `turn_idx` fields. `seq` is a globally monotonic sequence number for ordering events that may arrive out of order, and `turn_idx` groups related events from the same conversation turn. Learn more
## Enhancements
* **30% Faster Phoenix-4 Boot Time:** Phoenix-4 conversations now boot 30% faster, significantly reducing the time from conversation creation to readiness.
## Enhancements
* **EU ElevenLabs BYOK Support:** Customers can now bring their own ElevenLabs API key from EU-region accounts.
## Enhancements
* **Improved Knowledge Base Retrieval:** Optimized underlying infrastructure to improve utterance to utterance response times, particularly when `rag_search_quality` is set to `quality`.
## New Features
* **Expanded Tavus-Hosted LLM Selection:** Added new Tavus-hosted LLM options including models from Gemini, Claude, and GPT families. `tavus-gpt-oss` is recommended as the default. Legacy models `tavus-gpt-4.1`, `tavus-gpt-4o`, and `tavus-gpt-4o-mini` are now deprecated. Learn more →
* **Visual RAG:** CVI now supports visual retrieval-augmented generation. Upload custom image explanations that are matched and queried via vision embeddings, giving your PAL richer visual context during conversations.
## Changes
* **PAL**`context`**Field Deprecated:** The `context` field has been deprecated in favor of a unified `system_prompt` field. Existing `context` values have been automatically merged into system prompts. The API remains backward compatible, but we recommend using **only** `system_prompt` going forward.
## New Features
* **Raven-1 Perception Model:** Introduced Raven-1, a multimodal perception model with audio emotion analysis and enhanced visual awareness. Raven-1 captures user emotion from audio in real time (sub-100ms audio perception latency), enabling PALs to respond with greater emotional intelligence. The model is now the default for all new PALs. Enable it by setting `perception_model_name` in your PAL configuration. Learn more →
* **Private Rooms:** Require authentication to join conversations for enhanced security. When enabled, we return a JWT meeting token that users must include when entering the room. Learn more
## Enhancements
* **Upgraded Transcription Engine:** Upgraded transcription engine with 3x improvements in word error rates (WER).
## New Features
* **Website Crawling for Knowledge Base:** You can now enable link crawling when creating knowledge base documents. Configure crawl `depth` and `max_pages` to automatically discover and ingest content from linked pages. Additionally, existing crawled documents can now be recrawled to keep knowledge base content up to date.
## Changes
* **PlayHT TTS Removed:** PlayHT has been fully removed as a supported TTS engine. All PALs previously using PlayHT should migrate to Cartesia or ElevenLabs.
## New Features
* **Hard Delete for Conversations:** Conversations can now be permanently deleted via the API using the `hard=true` query parameter. Use this for GDPR compliance or data cleanup workflows.
## Enhancements
* **Default TTS Model Updated to Sonic-3:** The default text-to-speech model has been updated to Sonic-3 across all new PALs, delivering improved voice quality and naturalness.
* **LiveKit Connection Stability:** Extensive reliability improvements to the LiveKit-based transport layer, including fixes for connection timeouts, track publishing hangs, event loop starvation, and ping timeout issues.
## Changes
* **Default LLM Migrated to `tavus-gpt-oss`:** The default LLM for all new PALs is now `tavus-gpt-oss`. All remaining `tavus-llama-4` PALs have been automatically migrated. Legacy Tavus-Llama model references have been removed.
## New Features
* **LLM Temperature & Top-P Parameters:** You can now configure `temperature` and `top_p` parameters for both Tavus-hosted LLMs and custom LLMs via the `extra_body` field in your PAL's LLM configuration. Learn more →
## Enhancements
* **Text Echo Language Accuracy:** Text echoes now correctly use the input language for conversion, improving accuracy in multilingual conversations.
## New Features
* **Test Mode for Conversations:** You can now start conversations in test mode, where the PAL does not join. Validate your setup, integrations, and conversational flows without incurring costs or using concurrency slots. Set `test_mode: true` when creating a conversation. Learn more →
## Enhancements
* **Fuzzy Search for PALs:** Search now supports fuzzy matching for PALs, allowing users to find results based on partial matches of UUIDs or names.
## New Features
* **Memories:** CVI now remembers context across conversations. Every conversation builds on the last with full context and time/date awareness, enabling use cases like adaptive tutoring, mentorship, and recurring consultations. Learn more →
* **Knowledge Base (RAG):** Bring your own data to conversations instantly. Upload documents or links and get grounded answers with \~30ms retrieval latency. Power AI recruiters, support agents, travel guides, and more with domain-specific knowledge. Learn more →
* **Objectives & Guardrails:** Define clear goals, branching logic, and measurable outcomes for your PALs while keeping conversations safe, compliant, and on-brand. Ideal for complex workflows and regulated industries. Learn more →
* **PAL Builder:** A guided creation flow in the PAL Maker to shape AI PALs with goals, behaviors, and style - then test or launch within minutes.
## New Features
* **Events Console:** A new events console in the PAL Maker lets you monitor everything happening during a conversation in real time - from message flows to system activity.
* **Conversation Transcripts & Perception Analysis:** View full conversation details directly in the PAL Maker, including transcripts with speaker roles and perception analysis showing how your AI PAL sees, hears, and responds.
## New Features
* **PAL Layer Controls:** Enable or disable layers like Sparrow directly within a PAL and adjust sensitivity settings in real time from the PAL Maker side panel.
* **PAL editing in PAL Maker:** We've added new editing capabilities to help you refine your PALs more efficiently. You can now update system prompt, context, and layers directly in our PAL Maker, plus duplicate existing PALs to quickly create variations or use them as starting points for new projects. Find these new features in your PAL Library at maker.tavus.io/dev.
## Enhancements
* **Interaction Events Playground Improvements:** Major updates to the Interaction Events Playground including correct `properties.context` format and append vs overwrite toggle.
## New Features
* **Multilingual Settings in PAL Maker:** You can now specify the language of a conversation directly in the PAL Maker, including a new multilingual option for dynamic, real-world interactions.
## New Features
* **Llama 4 Support:** Your PAL just got even smarter, thanks to Meta's Llama 4 model 🧠 You can start using Llama 4 by specifying `tavus-llama-4` for the LLM `model` value when creating a new PAL or updating an existing one. Click here
to learn more!
## New Features
* **React Component Library:** Developers can build with Tavus even faster now with our pre-defined components 🚀 Click here
to learn more!
## New Features
* **Multilingual Conversation Support:** CVI now supports dynamic multilingual conversations through automatic language detection. Set `properties.language` to "multilingual" and CVI will automatically detect the user's spoken language and respond in the same language using ASR technology.
* **Audio-Only Mode:** CVI now supports audio-only conversations with advanced perception (powered by Raven) and intelligent turn-taking (powered by Sparrow-1). Set `audio_only=true` in your create conversation request to enable streamlined voice-first interactions.
## Enhancements
* **Fixed CVI responsiveness issue:** Resolved an issue where CVI would occasionally ignore very brief user utterances. All user inputs, regardless of length, now receive consistent responses.
* **Expanded tavus-llama-4 context window:** Increased maximum context window to 32,000 tokens. For optimal performance and response times, we recommend staying under 25,000 tokens.
## Enhancements
* Reduced conversation boot time by 58% (p50).
## Changes
* Added a new recording requirement to Training from a video
: Start the talking segment with a big smile.
## Enhancements
* Added echo
and respond
events to conversational context.
## Enhancements
* **Major Phoenix 3 Enhancements for CVI**:
* Increased frame rate from 27fps to 32fps, significantly boosting smoothness.
* Reduced Phoenix step's warm boot time by 60% (from 5s to 2s).
* Lipsync accuracy improved by \~22% based on AVSR metric.
* Resolved blurriness and choppiness at conversation start.
* Enhanced listening mode with more natural micro expressions (eyebrow movements, subtle gestures).
* Greenscreen mode speed boosted by an additional \~1.5fps.
* **Enhanced CVI Audio Quality**: Audio clicks significantly attenuated, providing clearer conversational audio.
* **Phoenix 3 Visual Artifacts Fix**: Resolved visual artifacts in 4K videos on Apple devices, eliminating black spot artifacts in thumbnails.
## New Features
* Launched LiveKit Integration
: With Tavus video agents now integrated into LiveKit, you can add humanlike video responses to your voice agents in seconds.
* PAL API
: Enabled patch updates to PALs.
## Enhancements
* Resolved TTS (Cartesia) stability issues and addressed hallucination.
* **Phoenix 3 Improvements**:
* Fixed blinking/jumping issues and black spots in videos.
* FPS optimization to resolve static and audio crackling.
## Enhancements
* **Face API**:
* Enhanced Error Messaging for Training Videos.
* Optimized Auto QA for Training Videos.
# Blocks
Source: https://docs.tavus.io/sections/conversational-video-interface/component-library/blocks
High-level component compositions that combine multiple UI elements into complete interface layouts
Blocks are composed React layouts generated by `npx @tavus/cvi-ui@latest add ...`. They are copied into your app, so import paths are relative to your generated component directory.
| Block | Add command | Import path pattern | Props | Required context | Generated location pattern |
| ----------------------------- | -------------------------------------------------------------------- | ------------------------------------------- | -------------------------------------------------------------------------- | ---------------------- | ------------------------------------------------------------------------------------- |
| `Conversation` full layout | `npx @tavus/cvi-ui@latest add conversation-01` or `add conversation` | `/components/conversation` | `conversationUrl: string`, `onLeave: () => void` | `CVIProvider` ancestor | `/components/conversation.*` plus supporting components/hooks/styles |
| `Conversation` minimal layout | `npx @tavus/cvi-ui@latest add conversation-02` | `/components/conversation` | `conversationUrl: string`, `onLeave: () => void` | `CVIProvider` ancestor | `/components/conversation.*` plus supporting components/hooks/styles |
| `HairCheck` | `npx @tavus/cvi-ui@latest add hair-check-01` | `/components/hair-check` | `isJoinBtnLoading: boolean`, `onJoin: () => void`, `onCancel?: () => void` | `CVIProvider` ancestor | `/components/hair-check.*` plus device hooks/styles |
The docs below use relative imports such as `./components/cvi/components/conversation`. Replace the prefix with the components path configured in your generated `cvi-components.json`.
### Conversation block
The Conversation component provides a complete video chat interface for one-to-one conversations with AI faces. Two variants are available: `conversation-01` (full-featured, default) and `conversation-02` (minimal).
#### conversation-01 (full-featured, default)
```bash theme={null}
npx @tavus/cvi-ui@latest add conversation-01
```
The default `Conversation` block - a full-featured video chat surface for one-to-one conversations with AI faces.
**Features:**
* **Main Video Display**: Large video area showing the AI face or screen share
* **Top-right Self-View**: Square self-view preview pinned to the top-right of the main video
* **Chat**: Slide-in chat side panel with toggle button (built on the [Chat](/sections/conversational-video-interface/component-library/components#chat) module)
* **Closed Captions**: Live captions overlay with toggle button (built on the [Closed Captions](/sections/conversational-video-interface/component-library/components#closed-captions) module)
* **Screen Sharing**: Automatic switching between face video and screen share
* **Animated Connect / Leave States**: Animated transitions when joining and leaving the call
* **Device Controls**: Integrated microphone, camera, and screen share controls
* **Error Handling**: Graceful handling of camera/microphone permission errors
* **Responsive Layout**: Adaptive design for different screen sizes
**Props:**
* `conversationUrl` (string): Daily.co room URL for joining
* `onLeave` (function): Callback when user leaves the conversation
```tsx theme={null}
import { Conversation } from './components/cvi/components/conversation';
```
```tsx theme={null}
handleLeaveCall()}
/>
```
Preview
#### conversation-02 (minimal)
```bash theme={null}
npx @tavus/cvi-ui@latest add conversation-02
```
A minimal `Conversation` block - video plus the essential device and leave controls, without chat or captions. Use this when you want to compose your own UI around the call surface.
**Features:**
* **Main Video Display**: Large video area showing the AI face or screen share
* **Self-View Preview**: Small preview window showing local camera feed
* **Device Controls**: Microphone, camera, and screen share toggle buttons
* **Leave Button**: Disconnects from the call and fires `onLeave`
* **Animated Connect / Leave States**: Animated transitions when joining and leaving the call
* **Error Handling**: Graceful handling of camera/microphone permission errors
* **Responsive Layout**: Adaptive design for different screen sizes
**Props:**
* `conversationUrl` (string): Daily.co room URL for joining
* `onLeave` (function): Callback when user leaves the conversation
```tsx theme={null}
import { Conversation } from './components/cvi/components/conversation';
```
```tsx theme={null}
handleLeaveCall()}
/>
```
Preview
### Hair Check
The HairCheck component provides a pre-call interface for users to test and configure their audio/video devices before joining a video chat.
```bash theme={null}
npx @tavus/cvi-ui@latest add hair-check-01
```
The `HairCheck` component provides a pre-call interface for users to test and configure their audio/video devices before joining a video chat.
**Features:**
* **Device Testing**: Live preview of camera feed with mirror effect
* **Permission Management**: Handles camera and microphone permission requests
* **Device Controls**: Integrated microphone and camera controls
* **Join Interface**: Call-to-action button to join the video chat
* **Responsive Design**: Works on both desktop and mobile devices
**Props:**
* `isJoinBtnLoading` (boolean): Shows loading state on join button
* `onJoin` (function): Callback when user clicks join
* `onCancel` (function, optional): Callback when user cancels
```tsx theme={null}
import { HairCheck } from './components/cvi/components/hair-check';
```
```tsx theme={null}
```
Preview
# Components
Source: https://docs.tavus.io/sections/conversational-video-interface/component-library/components
Learn about our pre-built React components to accelerate integrating the Tavus Conversational Video Interface (CVI) into your application.
These pages document **installable** UI pieces from **`@tavus/cvi-ui`** (`npx @tavus/cvi-ui@latest add …`). Wrap your app with **`CVIProvider`** so Daily’s React context and hooks work under this tree. For composed layouts see [Blocks](/sections/conversational-video-interface/component-library/blocks), for state hooks see [Hooks](/sections/conversational-video-interface/component-library/hooks), and for init + embed flows see [Embed CVI](/sections/integrations/embedding-cvi).
| Module | Add command | Import path pattern | Exports | Props / parameters | Required context | Generated location pattern |
| ----------------- | ---------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------ | ---------------------------------------------------------- | ------------------------------------------------ |
| `cvi-provider` | `npx @tavus/cvi-ui@latest add cvi-provider` | `/components/cvi-provider` | `CVIProvider` | `children: ReactNode` | None; this creates the Daily provider context | `/components/cvi-provider.*` |
| `audio-wave` | `npx @tavus/cvi-ui@latest add audio-wave` | `/components/audio-wave` | `AudioWave` | `id: string` participant/session ID | `CVIProvider` ancestor and active Daily call | `/components/audio-wave.*` |
| `device-select` | `npx @tavus/cvi-ui@latest add device-select` | `/components/device-select` | `MicSelectBtn`, `CameraSelectBtn`, `ScreenShareButton` | No component props | `CVIProvider` ancestor and active Daily call | `/components/device-select.*` |
| `media-controls` | `npx @tavus/cvi-ui@latest add media-controls` | `/components/media-controls` | `MicToggleButton`, `CameraToggleButton`, `ScreenShareButton` | No component props | `CVIProvider` ancestor and active Daily call | `/components/media-controls.*` |
| `closed-captions` | `npx @tavus/cvi-ui@latest add closed-captions` | `/components/closed-captions` | `ClosedCaptionsProvider`, `ClosedCaptionsButton`, `ClosedCaptions` | Provider: `children`, `defaultEnabled?: boolean` | `CVIProvider`; wrap caption UI in `ClosedCaptionsProvider` | `/components/closed-captions.*` |
| `chat` | `npx @tavus/cvi-ui@latest add chat` | `/components/chat` | `ChatProvider`, `ChatButton`, `ChatPanel` | Provider: `children`, `defaultOpen?: boolean` | `CVIProvider`; wrap chat UI in `ChatProvider` | `/components/chat.*` |
`CVIProvider` is the React wrapper that makes Daily React context available. Components and hooks that read call state must render under it.
### CVI Provider
The `CVIProvider` component wraps your app with the Daily.co provider context, enabling all Daily React hooks and components to function.
```bash theme={null}
npx @tavus/cvi-ui@latest add cvi-provider
```
The `CVIProvider` component wraps your app with the Daily.co provider context, enabling all Daily React hooks and components to function.
**Features:**
* Provides Daily.co context to all child components
* Required for using Daily React hooks and video/audio components
* Simple wrapper for app-level integration
**Props:**
* `children` (ReactNode): Components to be wrapped by the provider
```tsx theme={null}
import { CVIProvider } from './cvi-provider';
```
```tsx theme={null}
{/* your app components */}
```
Import paths such as `./cvi-provider` are **relative to your file** and to where the CLI copied components. Paths in [Embed CVI](/sections/integrations/embedding-cvi) (for example `./components/cvi/components/cvi-provider`) show another valid layout - adjust imports to match your project tree.
### AudioWave
The `AudioWave` component provides real-time audio level visualization for video chat participants, displaying animated bars that respond to audio input levels.
```bash theme={null}
npx @tavus/cvi-ui@latest add audio-wave
```
The `AudioWave` component provides real-time audio level visualization for video chat participants, displaying animated bars that respond to audio input levels.
**Features:**
* **Real-time Audio Visualization**: Three animated bars that respond to audio levels
* **Active Speaker Detection**: Visual distinction between active and inactive speakers
* **Performance Optimized**: Uses `requestAnimationFrame` for smooth animations
* **Responsive Design**: Compact circular design that fits well in video previews
* **Audio Level Scaling**: Intelligent volume scaling for consistent visual feedback
**Props:**
* `id` (string): The participant's session ID to monitor audio levels for
```tsx theme={null}
import { AudioWave } from './audio-wave';
```
```tsx theme={null}
```
### Device Select
The `device-select` module provides advanced device selection controls, including dropdowns for choosing microphones and cameras, and integrated toggle buttons.
```bash theme={null}
npx @tavus/cvi-ui@latest add device-select
```
The `device-select` module provides advanced device selection controls, including dropdowns for choosing microphones and cameras, and integrated toggle buttons.
**Exported Components:**
* **`MicSelectBtn`**: Microphone toggle button with device selection
* **`CameraSelectBtn`**: Camera toggle button with device selection
* **`ScreenShareButton`**: Button to toggle screen sharing
**Features:**
* Integrated device selection and toggling
* Dropdowns for camera/microphone selection
* Visual state indicators and accessibility support
* Uses Daily.co device management hooks
* CSS modules for styling
```tsx theme={null}
import { MicSelectBtn, CameraSelectBtn, ScreenShareButton } from './device-select';
```
```tsx theme={null}
```
**`ScreenShareButton`** exists in both **`device-select`** and **`media-controls`**. They are different exports from different modules - import from the path that matches the `npx @tavus/cvi-ui@latest add device-select` or `… add media-controls` command you ran.
### Media Controls
The `media-controls` module provides simple toggle buttons for microphone, camera, and screen sharing, designed for direct use in video chat interfaces.
```bash theme={null}
npx @tavus/cvi-ui@latest add media-controls
```
The `media-controls` module provides simple toggle buttons for microphone, camera, and screen sharing, designed for direct use in video chat interfaces.
**Exported Components:**
* **`MicToggleButton`**: Toggles microphone mute/unmute state
* **`CameraToggleButton`**: Toggles camera on/off
* **`ScreenShareButton`**: Toggles screen sharing on/off
**Features:**
* Simple, accessible toggle buttons
* Visual state indicators (muted, unmuted, on/off)
* Disabled state when device is not ready
* Uses Daily.co hooks for device state
* CSS modules for styling
```tsx theme={null}
import { MicToggleButton, CameraToggleButton, ScreenShareButton } from './media-controls';
```
```tsx theme={null}
```
### Closed Captions
The `closed-captions` module renders live captions for both the user and the face, plus a toggle button and a context provider that lets multiple components share the on/off state.
```bash theme={null}
npx @tavus/cvi-ui@latest add closed-captions
```
The `closed-captions` module renders live captions for both the user and the face, plus a toggle button and a context provider that lets multiple components share the on/off state.
**Exported Components:**
* **`ClosedCaptionsProvider`**: Context provider that owns the captions on/off state. Wrap your conversation tree with it.
* **`ClosedCaptionsButton`**: Toggle button that flips captions on and off (uses `aria-pressed`).
* **`ClosedCaptions`**: Overlay that displays the active caption with the speaker's role label. Shows the latest 3 lines and auto-clears 2 seconds after the final utterance.
**Features:**
* Streams captions from `conversation.utterance.streaming` for both `user` and `face` roles
* Auto-clears the caption after each final utterance
* Anchors to the bottom of the overlay so the most recent text is always visible
* CSS modules for styling, customizable via `--cc-line-height` and `--cc-max-lines`
**Provider Props:**
* `children` (ReactNode): Components to be wrapped by the provider
* `defaultEnabled` (boolean, optional): Initial captions on/off state. Defaults to `false`.
```tsx theme={null}
import {
ClosedCaptions,
ClosedCaptionsButton,
ClosedCaptionsProvider,
} from './closed-captions';
```
```tsx theme={null}
{/* main video, self-view, etc. */}
{/* other controls */}
```
### Chat
The `chat` module renders a slide-in side panel for text chat alongside the live conversation, plus a toggle button and a context provider that owns the panel's open/closed state.
```bash theme={null}
npx @tavus/cvi-ui@latest add chat
```
The `chat` module renders a slide-in side panel for text chat alongside the live conversation, plus a toggle button and a context provider that owns the panel's open/closed state.
**Exported Components:**
* **`ChatProvider`**: Context provider that owns the chat panel's open/closed state. Wrap your conversation tree with it.
* **`ChatButton`**: Toggle button that opens and closes the chat panel (uses `aria-expanded` and `aria-controls`).
* **`ChatPanel`**: Slide-in side panel containing the message list and composer. Renders a persistent ARIA live region so screen readers announce new messages, and is set to `inert` while closed so its contents stay out of the tab order.
**Features:**
* **Message list**: Renders the running transcript of `user` and `face` messages tracked by [`useChat`](/sections/conversational-video-interface/component-library/hooks#usechat), with optimistic local echo for messages the user just sent.
* **Composer**: Multi-line textarea with **Enter to send**, **Shift+Enter for newline**, and IME-safe input handling so composition (e.g. CJK input) is not interrupted by the send shortcut.
**Provider Props:**
* `children` (ReactNode): Components to be wrapped by the provider
* `defaultOpen` (boolean, optional): Initial open/closed state of the chat panel. Defaults to `false`.
```tsx theme={null}
import {
ChatButton,
ChatPanel,
ChatProvider,
} from './chat';
```
```tsx theme={null}
{/* main video, self-view, etc. */}
{/* other controls */}
```
# Hooks
Source: https://docs.tavus.io/sections/conversational-video-interface/component-library/hooks
See what hooks Tavus supports for managing video calls, media controls, participant management, and conversation events.
Hooks are generated source files copied into your app by `npx @tavus/cvi-ui@latest add ...`. Import them from your generated hooks directory and render them under `CVIProvider` unless the hook explicitly only creates request helpers.
| Hook | Add command | Import path pattern | Parameters | Return values | Required context | Generated location pattern |
| ------------------------- | -------------------------------- | ---------------------------------------------------- | ----------------- | ------------------------------------------------------------------ | ----------------------------- | ------------------------------------------------------ |
| `useCVICall` | `add use-cvi-call` | `/hooks/use-cvi-call` | None | `joinCall`, `leaveCall` | `CVIProvider` | `/hooks/use-cvi-call.*` |
| `useStartHaircheck` | `add use-start-haircheck` | `/hooks/use-start-haircheck` | None | Permission booleans, `requestPermissions` | `CVIProvider` | `/hooks/use-start-haircheck.*` |
| `useLocalCamera` | `add use-local-camera` | `/hooks/use-local-camera` | None | `onToggleCamera`, `isCamReady`, `isCamMuted`, `localSessionId` | `CVIProvider` and active call | `/hooks/use-local-camera.*` |
| `useLocalMicrophone` | `add use-local-microphone` | `/hooks/use-local-microphone` | None | `onToggleMicrophone`, `isMicReady`, `isMicMuted`, `localSessionId` | `CVIProvider` and active call | `/hooks/use-local-microphone.*` |
| `useLocalScreenshare` | `add use-local-screenshare` | `/hooks/use-local-screenshare` | None | `onToggleScreenshare`, `isScreenSharing`, `localSessionId` | `CVIProvider` and active call | `/hooks/use-local-screenshare.*` |
| `useRequestPermissions` | `add use-request-permissions` | `/hooks/use-request-permissions` | None | `requestPermissions` | `CVIProvider` | `/hooks/use-request-permissions.*` |
| `useReplicaIDs` | `add use-face-ids` | `/hooks/use-face-ids` | None | `string[]` | `CVIProvider` and active call | `/hooks/use-face-ids.*` |
| `useRemoteParticipantIDs` | `add use-remote-participant-ids` | `/hooks/use-remote-participant-ids` | None | `string[]` | `CVIProvider` and active call | `/hooks/use-remote-participant-ids.*` |
| `useObservableEvent` | `add cvi-events-hooks` | `/hooks/cvi-events-hooks` | `callback(event)` | None | `CVIProvider` and active call | `/hooks/cvi-events-hooks.*` |
| `useClosedCaption` | `add use-closed-caption` | `/hooks/use-closed-caption` | None | `ClosedCaption` or `null` | `CVIProvider` and active call | `/hooks/use-closed-caption.*` |
| `useSendAppMessage` | `add cvi-events-hooks` | `/hooks/cvi-events-hooks` | None | `sendMessage(message)` | `CVIProvider` and active call | `/hooks/cvi-events-hooks.*` |
| `useChat` | `add use-chat` | `/hooks/use-chat` | None | `messages`, `sendMessage(text)` | `CVIProvider` and active call | `/hooks/use-chat.*` |
## 🔧 Core Call Management
### useCVICall
Essential hook for joining and leaving video calls.
```bash theme={null}
npx @tavus/cvi-ui@latest add use-cvi-call
```
A React hook that provides comprehensive call management functionality for video conversations. This hook handles the core lifecycle of video calls, including connection establishment, room joining, and proper cleanup when leaving calls.
**Purpose:**
* Manages call join/leave operations with proper state management
* Handles connection lifecycle and cleanup
* Provides simple interface for call control
**Return Values:**
* `joinCall` (function): Function to join a call by URL - handles Daily.co room connection
* `leaveCall` (function): Function to leave the current call - properly disconnects and cleans up resources
```tsx theme={null}
import { useCVICall } from './hooks/use-cvi-call';
```
```tsx theme={null}
const CallManager = () => {
const { joinCall, leaveCall } = useCVICall();
const handleJoin = () => {
joinCall({ url: 'https://your-daily-room-url' });
};
return (
);
};
```
### useStartHaircheck
A React hook that manages device permissions and camera initialization for the hair-check component.
```bash theme={null}
npx @tavus/cvi-ui@latest add use-start-haircheck
```
A React hook that manages device permissions and camera initialization for the hair-check component.
**Purpose:**
* Monitors device permission states
* Starts camera and microphone when appropriate
* Provides permission state for UI conditional rendering
* Handles permission request flow
**Return Values:**
* `isPermissionsPrompt` (boolean): Browser is prompting for device permission
* `isPermissionsLoading` (boolean): Permissions are being processed or camera is initializing
* `isPermissionsGranted` (boolean): Device permission granted
* `isPermissionsDenied` (boolean): Device permission denied
* `requestPermissions` (function): Function to request camera and microphone permissions
```tsx theme={null}
import { useStartHaircheck } from './hooks/use-start-haircheck';
```
```tsx theme={null}
const HairCheckComponent = () => {
const {
isPermissionsPrompt,
isPermissionsLoading,
isPermissionsGranted,
isPermissionsDenied,
requestPermissions
} = useStartHaircheck();
useEffect(() => {
requestPermissions();
}, []);
return (
);
};
```
***
## 🎥 Media Controls
### useLocalCamera
A React hook that provides local camera state and toggle functionality.
```bash theme={null}
npx @tavus/cvi-ui@latest add use-local-camera
```
A React hook that provides local camera state and toggle functionality.
**Purpose:**
* Manages local camera state (on/off)
* Tracks camera permission and ready state
**Return Values:**
* `onToggleCamera` (function): Function to toggle camera on/off
* `isCamReady` (boolean): Camera permission is granted and ready
* `isCamMuted` (boolean): Camera is currently turned off
* `localSessionId` (string): Local session ID
```tsx theme={null}
import { useLocalCamera } from './hooks/use-local-camera';
```
```tsx theme={null}
const CameraControls = () => {
const { onToggleCamera, isCamReady, isCamMuted } = useLocalCamera();
return (
);
};
```
### useLocalMicrophone
A React hook that provides local microphone state and toggle functionality.
```bash theme={null}
npx @tavus/cvi-ui@latest add use-local-microphone
```
A React hook that provides local microphone state and toggle functionality.
**Purpose:**
* Manages local microphone state (on/off)
* Tracks microphone permission and ready state
**Return Values:**
* `onToggleMicrophone` (function): Function to toggle microphone on/off
* `isMicReady` (boolean): Microphone permission is granted and ready
* `isMicMuted` (boolean): Microphone is currently turned off
* `localSessionId` (string): Local session ID
```tsx theme={null}
import { useLocalMicrophone } from './hooks/use-local-microphone';
```
```tsx theme={null}
const MicrophoneControls = () => {
const { onToggleMicrophone, isMicReady, isMicMuted } = useLocalMicrophone();
return (
);
};
```
### useLocalScreenshare
A React hook that provides local screen sharing state and toggle functionality.
```bash theme={null}
npx @tavus/cvi-ui@latest add use-local-screenshare
```
A React hook that provides local screen sharing state and toggle functionality.
**Purpose:**
* Manages screen sharing state (on/off)
* Provides screen sharing toggle function
* Handles screen share start/stop with optimized display media options
**Return Values:**
* `onToggleScreenshare` (function): Function to toggle screen sharing on/off
* `isScreenSharing` (boolean): Whether screen sharing is currently active
* `localSessionId` (string): Local session ID
**Display Media Options:**
When starting screen share, the hook uses the following optimized settings:
* **Audio**: Disabled (false)
* **Self Browser Surface**: Excluded
* **Surface Switching**: Included
* **Video Resolution**: 1920x1080
```tsx theme={null}
import { useLocalScreenshare } from './hooks/use-local-screenshare';
```
```tsx theme={null}
const ScreenShareControls = () => {
const { onToggleScreenshare, isScreenSharing } = useLocalScreenshare();
return (
);
};
```
### useRequestPermissions
A React hook that requests camera and microphone permissions with optimized audio processing settings.
```bash theme={null}
npx @tavus/cvi-ui@latest add use-request-permissions
```
A React hook that requests camera and microphone permissions with optimized audio processing settings.
**Purpose:**
* Requests camera and microphone permissions from the user
* Starts camera and audio with specific configuration
* Applies noise cancellation audio processing
* Provides a clean interface for permission requests
**Return Values:**
* `requestPermissions` (function): Function to request camera and microphone permissions
**Configuration:**
When requesting permissions, the hook uses the following settings:
* **Video**: Started on (startVideoOff: false)
* **Audio**: Started on (startAudioOff: false)
* **Audio Source**: Default system audio input
* **Audio Processing**: Noise cancellation enabled
```tsx theme={null}
import { useRequestPermissions } from './hooks/use-request-permissions';
```
```tsx theme={null}
const PermissionRequest = () => {
const requestPermissions = useRequestPermissions();
const handleRequestPermissions = async () => {
try {
await requestPermissions();
console.log('Permissions granted successfully');
} catch (error) {
console.error('Failed to get permissions:', error);
}
};
return (
);
};
```
***
## 👥 Participant Management
### useReplicaIDs
A React hook that returns the IDs of all Tavus face participants in a call.
```bash theme={null}
npx @tavus/cvi-ui@latest add use-face-ids
```
A React hook that returns the IDs of all Tavus face participants in a call.
**Purpose:**
* Filters and returns participant IDs where `user_id` includes 'tavus-face'
**Return Value:**
* `string[]` - Array of face participant IDs
```tsx theme={null}
import { useReplicaIDs } from './hooks/use-face-ids';
```
```tsx theme={null}
const ids = useReplicaIDs();
// ids is an array of participant IDs for Tavus faces
```
### useRemoteParticipantIDs
A React hook that returns the IDs of all remote participants in a call.
```bash theme={null}
npx @tavus/cvi-ui@latest add use-remote-participant-ids
```
A React hook that returns the IDs of all remote participants in a call.
**Purpose:**
* Returns participant IDs for all remote participants (excluding local user)
**Return Value:**
* `string[]` - Array of remote participant IDs
```tsx theme={null}
import { useRemoteParticipantIDs } from './hooks/use-remote-participant-ids';
```
```tsx theme={null}
const remoteIds = useRemoteParticipantIDs();
// remoteIds is an array of remote participant IDs
```
***
## 💬 Conversation & Events
### useObservableEvent
A React hook that listens for CVI app messages and provides a callback mechanism for handling various conversation events.
```bash theme={null}
npx @tavus/cvi-ui@latest add cvi-events-hooks
```
A React hook that listens for CVI app messages and provides a callback mechanism for handling various conversation events.
**Purpose:**
* Listens for app messages from the Daily.co call mapped to CVI events
* Handles various conversation event types (utterances, tool calls, speaking events, etc.)
* Provides type-safe event handling for CVI interactions
**Parameters:**
* `callback` (function): Function called when app messages are received
**Event Types:**
This hook handles all CVI conversation events. For detailed information about each event type, see the [Interaction Events overview](/sections/conversational-video-interface/interactions-protocols/overview).
```tsx theme={null}
import { useObservableEvent } from './hooks/cvi-events-hooks';
```
```tsx theme={null}
const ConversationHandler = () => {
useObservableEvent((event) => {
switch (event.event_type) {
case 'conversation.utterance':
console.log('Speech:', event.properties.speech);
break;
case 'conversation.replica.started_speaking':
console.log('Face started speaking');
break;
case 'conversation.user.stopped_speaking':
console.log('User stopped speaking');
break;
}
});
return
Listening for conversation events...
;
};
```
### useClosedCaption
A React hook that returns the latest closed caption with the speaker's role and text.
```bash theme={null}
npx @tavus/cvi-ui@latest add use-closed-caption
```
A React hook that returns the latest closed caption with the speaker's role and text. Subscribes to `conversation.utterance.streaming` events for `user` and PAL roles and exposes the latest caption to your UI.
**Purpose:**
* Streams captions for both the user and the PAL from `conversation.utterance.streaming`
* Updates progressively as either party speaks
* Auto-clears the caption 2 seconds after a `final` utterance
* Returns `null` when no caption is currently being shown
**Return Value:**
* `ClosedCaption | null` where `ClosedCaption` is `{ role: "user" | "pal" | "replica"; text: string }` (`replica` is a legacy duplicate of `pal`)
```tsx theme={null}
import { useClosedCaption } from './hooks/use-closed-caption';
```
```tsx theme={null}
const Captions = () => {
const caption = useClosedCaption();
if (!caption) return null;
return (
);
};
```
### useSendAppMessage
A React hook that provides a function to send CVI app messages to other participants in the call.
```bash theme={null}
npx @tavus/cvi-ui@latest add cvi-events-hooks
```
A React hook that provides a function to send CVI app messages to other participants in the call.
**Purpose:**
* Sends various types of conversation messages to the CVI system
* Supports echo, respond, interrupt, and context management messages
* Provides type-safe message sending with proper validation
* Enables real-time communication with Tavus faces and conversation management
**Return Value:**
* `(message: SendAppMessageProps) => void` - Function that sends the message when called
**Message Types:**
This hook supports all CVI interaction types. For detailed information about each interaction type and their properties, see the [Interaction Events overview](/sections/conversational-video-interface/interactions-protocols/overview).
```tsx theme={null}
import { useSendAppMessage } from './hooks/cvi-events-hooks';
```
```tsx theme={null}
const MessageSender = () => {
const sendMessage = useSendAppMessage();
// Send a text echo
const sendTextEcho = () => {
sendMessage({
message_type: "conversation",
event_type: "conversation.echo",
conversation_id: "conv-123",
properties: {
modality: "text",
text: "Hello, world!",
audio: "",
sample_rate: 16000,
inference_id: "inf-456",
done: true
}
});
};
// Send a text response
const sendResponse = () => {
sendMessage({
message_type: "conversation",
event_type: "conversation.respond",
conversation_id: "conv-123",
properties: {
text: "This is my response to the conversation."
}
});
};
return (
);
};
```
### useChat
A React hook that powers a chat experience on top of the live conversation. It tracks the running transcript and provides a function to send a user turn back to the face.
```bash theme={null}
npx @tavus/cvi-ui@latest add use-chat
```
A React hook that powers a chat experience on top of the live conversation. Subscribes to Daily app messages and tracks `conversation.utterance` events from both `user` and `face` roles, and exposes a `sendMessage` function that dispatches `conversation.respond`.
**Purpose:**
* Builds a chronological transcript of `user` and `face` messages from `conversation.utterance` events
* Optimistically appends locally sent messages so the UI updates immediately, then reconciles each pending message with the matching server-side utterance using `inference_id`
* Dispatches `conversation.respond` when the user sends a chat message
* Designed to back the [`Chat`](/sections/conversational-video-interface/component-library/components#chat) components (`ChatProvider`, `ChatPanel`, `ChatButton`)
**Return Values:**
* `messages` (`ChatMessage[]`): Ordered transcript where `ChatMessage` is `{ id: string; role: "user" | "pal" | "replica"; text: string; inference_id?: string; pending?: boolean }`
* `sendMessage` (`(text: string) => void`): Sends a user turn - appends a pending local echo to `messages` and dispatches `conversation.respond`. The pending entry is reconciled (and `pending` cleared) when the matching utterance arrives by `inference_id`.
```tsx theme={null}
import { useChat } from './hooks/use-chat';
```
```tsx theme={null}
const Chat = () => {
const { messages, sendMessage } = useChat();
const [draft, setDraft] = useState('');
const onSubmit = () => {
const text = draft.trim();
if (!text) return;
sendMessage(text);
setDraft('');
};
return (
);
};
```
# Overview
Source: https://docs.tavus.io/sections/conversational-video-interface/component-library/overview
Learn how our Tavus Conversational Video Interface (CVI) Component Library can help you go live in minutes.
## Overview
`@tavus/cvi-ui` is a CLI that copies React components, hooks, styles, and optional server helpers into your application. It is not a hosted widget or a runtime CDN package. After you run `init` and `add ...`, you import the generated files from your own project tree and can edit them like application code.
Use this path when you want Tavus-provided React UI and Daily-powered call state without building every media control yourself. For the fastest no-code UI, use an [iframe embed](/sections/integrations/embedding-cvi#iframe). For fully custom Daily call-object ownership, use the [Daily JS / React path](/sections/integrations/embedding-cvi#react--daily-createcallobject).
Complete layouts such as `Conversation` and `HairCheck`.
Building blocks such as `CVIProvider`, media controls, captions, chat, and `AudioWave`.
React hooks for call lifecycle, media state, participants, captions, chat, and CVI events.
Server routes and browser helpers that create and end conversations without exposing `TAVUS_API_KEY`.
## Mental model
| Piece | What it does | Where to read next |
| ------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `npx @tavus/cvi-ui@latest init` | Creates `cvi-components.json`, asks about TypeScript, and installs Daily/Jotai dependencies. | This page |
| `CVIProvider` | React wrapper that provides Daily context to child components and hooks. Put generated CVI UI under this provider. | [Components](/sections/conversational-video-interface/component-library/components#cvi-provider) |
| `Conversation` | React block that renders the Tavus/Daily call UI from a `conversationUrl` and calls `onLeave` when the user leaves. | [Blocks](/sections/conversational-video-interface/component-library/blocks#conversation-block) |
| Server helpers | Generated server routes keep `TAVUS_API_KEY` server-only, and generated browser helpers call your own backend route. | [Server](/sections/conversational-video-interface/component-library/server) |
Generated imports are relative to where the CLI copied files in your app. Examples in these docs use paths such as `./components/cvi/components/conversation`; adjust them if your `cvi-components.json` components path is different.
***
## Quick Start
### Prerequisites
Before getting started, ensure you have a React project set up.
Alternatively, you can start from our example project: [CVI UI Haircheck Conversation Example](https://github.com/Tavus-Engineering/tavus-examples/tree/main/examples/cvi-ui-haircheck-conversation) - this example already has the HairCheck and Conversation blocks set up.
### 1. Initialize CVI in Your Project
```bash theme={null}
npx @tavus/cvi-ui@latest init
```
This command:
* Creates `cvi-components.json`, which stores the generated component path and TypeScript preference.
* Prompts for TypeScript preference.
* Installs `@daily-co/daily-react`, `@daily-co/daily-js`, and `jotai`.
### 2. Add CVI Components and Server Helpers
```bash theme={null}
npx @tavus/cvi-ui@latest add conversation
npx @tavus/cvi-ui@latest add tavus-api
```
`add conversation` generates the `Conversation` block and the components/hooks it needs. `add tavus-api` generates a framework-specific backend route plus `lib/tavus-client.ts`, which exports `createTavusConversation(params?)` and `endTavusConversation(id)`.
For Vite projects with a server runtime, use:
```bash theme={null}
npx @tavus/cvi-ui@latest add tavus-api-vite-ssr
```
Plain client-only Vite is unsupported because it would require exposing `TAVUS_API_KEY` in browser JavaScript. Add a server runtime and use `tavus-api-vite-ssr`, or create your own backend route.
### 3. Wrap Your App with the CVI Provider
In your root directory (main.tsx or index.tsx):
```tsx theme={null}
import { CVIProvider } from './components/cvi/components/cvi-provider';
function App() {
return {/* Your app content */};
}
```
### 4. Add a Conversation Component
Learn how to create a conversation URL at [https://docs.tavus.io/api-reference/conversations/create-conversation](https://docs.tavus.io/api-reference/conversations/create-conversation). To create a conversation URL from your app without exposing your `TAVUS_API_KEY` in the browser, use the server helpers in [Server](/sections/conversational-video-interface/component-library/server) (`tavus-api` for Next.js/Remix/TanStack Start, or `tavus-api-vite-ssr` for Vite-with-server).
**Note:** The Conversation component requires a parent container with defined dimensions to display properly.
Ensure your body element has full dimensions (`width: 100%` and `height:
100%`) in your CSS for proper component display.
```tsx theme={null}
import { Conversation } from './components/cvi/components/conversation';
function CVI() {
const handleLeave = () => {
// handle leave
};
return (
);
}
```
## Complete React example
This example assumes you ran:
```bash theme={null}
npx @tavus/cvi-ui@latest init
npx @tavus/cvi-ui@latest add conversation
npx @tavus/cvi-ui@latest add tavus-api
```
Set `TAVUS_API_KEY` only in your server environment. The browser code below calls the generated `createTavusConversation` and `endTavusConversation` helpers; those helpers call your generated `/api/tavus` route.
```tsx theme={null}
import { useState } from 'react';
import { CVIProvider } from './components/cvi/components/cvi-provider';
import { Conversation } from './components/cvi/components/conversation';
import {
createTavusConversation,
endTavusConversation,
} from './components/cvi/lib/tavus-client';
type TavusConversation = {
conversation_id: string;
conversation_url: string;
};
function TavusCall() {
const [conversation, setConversation] = useState(null);
const [isStarting, setIsStarting] = useState(false);
const [error, setError] = useState(null);
async function startConversation() {
setIsStarting(true);
setError(null);
try {
const nextConversation = await createTavusConversation({
pal_id: 'pcb7a34da5fe',
conversation_name: 'CVI UI example',
});
setConversation(nextConversation);
} catch (error) {
setError(error instanceof Error ? error.message : 'Failed to start conversation');
} finally {
setIsStarting(false);
}
}
async function handleLeave() {
if (conversation) {
await endTavusConversation(conversation.conversation_id).catch(() => undefined);
}
setConversation(null);
}
if (!conversation) {
return (
{error ?
{error}
: null}
);
}
return (
);
}
export default function App() {
return (
);
}
```
`Conversation` needs a parent with defined dimensions. Give the parent an explicit height, or ensure the full chain (`html`, `body`, root element, and container) resolves to a real height.
***
## Documentation Sections
* **[Blocks](/sections/conversational-video-interface/component-library/blocks)** – High-level component compositions and layouts
* **[Components](/sections/conversational-video-interface/component-library/components)** – Individual UI components
* **[Hooks](/sections/conversational-video-interface/component-library/hooks)** – Custom React hooks for managing video call state and interactions
* **[Server](/sections/conversational-video-interface/component-library/server)** – Server-side helpers (`tavus-api`, `tavus-api-vite-ssr`) for creating and ending conversations without exposing your API key
# Server
Source: https://docs.tavus.io/sections/conversational-video-interface/component-library/server
Server-side helpers for managing Tavus conversation lifecycle without leaking your API key to the browser.
# Server
These primitives are not React components - they are server route handlers and small browser-side fetch helpers. Use them to create and end Tavus conversations without bundling your `TAVUS_API_KEY` into client JS.
| Add command | Use for | Generated files | Browser exports | Server exports / route | Notes |
| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------- | -------------------------------------------------------------- | --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `npx @tavus/cvi-ui@latest add tavus-api` | Next.js App Router, Next.js Pages Router, Remix, TanStack Start | Framework route plus `/lib/tavus-client.ts` | `createTavusConversation(params?)`, `endTavusConversation(id)` | `POST /api/tavus` route for create/end actions | CLI exits on unsupported client-only frameworks instead of creating an unsafe browser API-key client. |
| `npx @tavus/cvi-ui@latest add tavus-api-vite-ssr` | Vite projects with a server runtime such as Vinxi, Hono, vike-server, Express, Bun, Cloudflare Workers | `/lib/tavus-api-vite-ssr.ts` and `/lib/tavus-client.ts` | `createTavusConversation(params?)`, `endTavusConversation(id)` | `handleTavusRequest(request: Request): Promise` | You mount the handler at `POST /api/tavus`; plain client-only Vite remains unsupported. |
Keep `TAVUS_API_KEY` only in server environment variables. Do not prefix it with client-exposed environment names such as `VITE_` or `NEXT_PUBLIC_`.
### Tavus API
The `tavus-api` module installs a framework-specific server route that creates and ends Tavus conversations on your backend, plus a small browser client that calls that route.
```bash theme={null}
npx @tavus/cvi-ui@latest add tavus-api
```
The `tavus-api` module installs a server route that creates and ends Tavus conversations on your backend, plus a small browser client that calls that route. Your `TAVUS_API_KEY` stays on the server and is never bundled into your client JS.
**What gets installed**
The CLI detects your framework and installs the matching server route:
| Framework | Server route file |
| ---------------------- | ------------------------- |
| Next.js (App Router) | `app/api/tavus/route.ts` |
| Next.js (Pages Router) | `pages/api/tavus.ts` |
| Remix | `app/routes/api.tavus.ts` |
| TanStack Start | `app/routes/api/tavus.ts` |
Plus the browser-side client at `/lib/tavus-client.ts`, which exports `createTavusConversation(params?)` and `endTavusConversation(id)`. Both functions `POST` to `/api/tavus`; the route forwards `params` verbatim to the Tavus REST API, so every field on [`POST /v2/conversations`](/api-reference/conversations/create-conversation) works. New Tavus fields work without a CLI update.
**Behaviour for unsupported frameworks**
If the CLI doesn't recognise the framework as one with a built-in server (plain Vite, Expo, manual setups), `add tavus-api` exits with an error rather than silently installing a key-exposing client. See the Vite-with-server opt-in below.
**Required environment variable (server only - never on the client)**
* `TAVUS_API_KEY` - required. [Create one in the PAL Maker.](https://maker.tavus.io/dev/api-keys)
```tsx theme={null}
import { createTavusConversation, endTavusConversation } from './lib/tavus-client';
```
```tsx theme={null}
const handleStart = async () => {
const { conversation_id, conversation_url } = await createTavusConversation({
pal_id: 'pcb7a34da5fe',
"properties": {
"language": "english"
}
});
// ...join the call with conversation_url
};
const handleEnd = async (id: string) => {
await endTavusConversation(id);
};
```
### Tavus API for Vite (with a server runtime)
For Vite projects that have a server (Vinxi, Hono, vike-server, custom Express, Bun, Cloudflare Workers, …), `add tavus-api-vite-ssr` installs a runtime-agnostic Request handler you wire into your middleware.
```bash theme={null}
npx @tavus/cvi-ui@latest add tavus-api-vite-ssr
```
Installs `/lib/tavus-api-vite-ssr.ts` exporting `handleTavusRequest(request: Request): Promise`, plus the matching `lib/tavus-client.ts`. The handler uses the standard Web Fetch `Request`/`Response` interfaces, so it plugs into any server runtime that speaks fetch.
**Wiring examples** (the file header includes these too):
* **Hono**:
```ts theme={null}
import { handleTavusRequest } from './lib/tavus-api-vite-ssr';
app.post('/api/tavus', (c) => handleTavusRequest(c.req.raw));
```
* **h3 / Vinxi**:
```ts theme={null}
import { eventHandler, toWebRequest } from 'h3';
import { handleTavusRequest } from './lib/tavus-api-vite-ssr';
export default eventHandler((event) => handleTavusRequest(toWebRequest(event)));
```
* **Express** (with a Web Fetch adapter such as `@hattip/adapter-node`):
```ts theme={null}
app.post('/api/tavus', async (req, res) => {
const webRes = await handleTavusRequest(toWebRequest(req));
// pipe webRes back to res
});
```
Once the handler is mounted at `POST /api/tavus`, the installed `lib/tavus-client.ts` calls it from the browser - no API key on the client.
**Required environment variable (server only)** - `TAVUS_API_KEY`. [Create one in the PAL Maker.](https://maker.tavus.io/dev/api-keys)
The full create-conversation field surface is supported via `createTavusConversation(params)` - see the [API reference](/api-reference/conversations/create-conversation) and the `tavus-api` section above for the params shape.
```tsx theme={null}
import { handleTavusRequest } from './lib/tavus-api-vite-ssr';
// …mount via your server's middleware (Hono, h3, Express, Cloudflare, Bun, …)
import { createTavusConversation } from './lib/tavus-client';
const { conversation_id, conversation_url } = await createTavusConversation({
pal_id: 'pcb7a34da5fe',
"properties": {
"language": "english"
}
});
```
**No client-only mode.** We deliberately do not ship a browser-direct client. Calling Tavus directly from the browser would put your API key in the bundle, which is unsafe in any deployed context. If your project has no server, add one (Vinxi, Hono, vike-server, …) and use `tavus-api-vite-ssr` above.
### Passing `policy: 'eu'` on create
When you create a conversation with `policy: "eu"`, PAL fields left on `auto` resolve for that call as:
* `disclosure_type: auto` → spoken and on-screen AI disclosure
* `emotion_recognition: auto` → `limited` (no biometric-derived emotion from face/voice)
Explicit PAL values (`always` / `off` / `full` / `limited`) are not overridden. Whether and when to set `policy` is yours - see the [EU AI Act guide](/sections/onboarding-guide/eu-ai-act).
The installed route forwards create-conversation params as-is, so `policy` works through `createTavusConversation()` with no extra wiring:
```tsx theme={null}
await createTavusConversation({ pal_id: 'pcb7a34da5fe', policy: 'eu' });
```
Those params come from the browser, so a caller can drop or alter them. That is fine when the client legitimately decides per conversation. If the value must always apply, set it in your `/api/tavus` handler instead of trusting the client:
```ts theme={null}
body: JSON.stringify({ ...body.params, policy: 'eu' }),
```
Tavus does not geolocate the caller, and the installed handlers do not infer `policy` for you. Choosing when to pass `policy: "eu"` is your responsibility.
Disclosure wording and emotion mode (`disclosure_type`, `verbal_disclosure`, `visual_disclosure`, `emotion_recognition`) are configured on the PAL, not per conversation - see the [EU AI Act guide](/sections/onboarding-guide/eu-ai-act).
# Audio-Only Conversation
Source: https://docs.tavus.io/sections/conversational-video-interface/conversation/customizations/audio-only
Start a conversation in audio-only mode, perfect for voice-only or low-bandwidth environments.
## Create an Audio Only Conversation
All features in the PAL's pipeline, including STT, Perception, and TTS, remain fully active in audio-only mode. The only change is that face video rendering is not included.
In this example, we will use stock PAL ID ***pcb7a34da5fe*** (Sales Development Rep).
To enable audio-only mode, set the `audio_only` parameter to `true` when creating the conversation:
```shell cURL theme={null}
curl --request POST \
--url https://tavusapi.com/v2/conversations \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"pal_id": "pcb7a34da5fe",
"audio_only": true
}'
```
Replace `` with your actual API key. You can generate one in the PAL Maker.
To join the conversation, click the link in the ***conversation\_url*** field from the response:
```json theme={null}
{
"conversation_id": "cd7e3eac05ede40c",
"conversation_name": "New Conversation 1751268887110",
"conversation_url": "",
"status": "active",
"callback_url": "",
"created_at": "2025-06-30T07:34:47.131571Z"
}
```
# Background Customizations
Source: https://docs.tavus.io/sections/conversational-video-interface/conversation/customizations/background-customizations
Apply a green screen or custom background for a personalized visual experience.
## Customize Background in Conversation Setup
In this example, we will use stock face ID ***r90bbd427f71*** (Anna) and stock PAL ID ***pcb7a34da5fe*** (Sales Development Rep).
To apply the green screen background, set the `apply_greenscreen` parameter to `true` when creating the conversation:
```shell cURL theme={null}
curl --request POST \
--url https://tavusapi.com/v2/conversations \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"pal_id": "pcb7a34da5fe",
"face_id": "r90bbd427f71",
"callback_url": "https://yourwebsite.com/webhook",
"conversation_name": "Improve Sales Technique",
"conversational_context": "I want to improve my sales techniques. Help me practice handling common objections from clients and closing deals more effectively.",
"properties": {
"apply_greenscreen": true
}
}'
```
Replace `` with your actual API key. You can generate one in the PAL Maker.
The above request will return the following response:
```json theme={null}
{
"conversation_id": "ca4301628cb9",
"conversation_name": "Improve Sales Technique",
"conversation_url": "",
"status": "active",
"callback_url": "https://yourwebsite.com/webhook",
"created_at": "2025-05-13T06:42:58.291561Z"
}
```
The face will appear with a green background. You can customize it on the frontend using WebGL. This allows you to apply a different color or add a custom image.
To preview this feature, try our Green Screen Sample App. Paste the conversation URL to modify the background.
# Call Duration and Timeout
Source: https://docs.tavus.io/sections/conversational-video-interface/conversation/customizations/call-duration-and-timeout
Configure call duration and timeout behavior to manage how and when a conversation ends.
## Create a Conversation with Custom Duration and Timeout
In this example, we will use stock face ID ***r90bbd427f71*** (Anna) and stock PAL ID ***pcb7a34da5fe*** (Sales Development Rep).
Use the following request body example:
```shell cURL theme={null}
curl --request POST \
--url https://tavusapi.com/v2/conversations \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"pal_id": "pcb7a34da5fe",
"face_id": "r90bbd427f71",
"callback_url": "https://yourwebsite.com/webhook",
"conversation_name": "Improve Sales Technique",
"conversational_context": "I want to improve my sales techniques. Help me practice handling common objections from clients and closing deals more effectively.",
"properties": {
"max_call_duration": 1800,
"participant_left_timeout": 60,
"participant_absent_timeout": 120
}
}'
```
Replace `` with your actual API key. You can generate one in the PAL Maker.
The request example above includes the following customizations:
| Parameter | Description |
| :--------------------------- | :---------------------------------------------------------------------------------------------------------------- |
| `max_call_duration` | Sets the maximum call length in seconds. The maximum value you can set depends on your plan — see the note below. |
| `participant_left_timeout` | Time (in seconds) to wait before ending the call after the last participant leaves. Default: 0. |
| `participant_absent_timeout` | Time (in seconds) to end the call if no one joins after it's created. Default: 300. |
The maximum `max_call_duration` you can set depends on your plan. If you request a value higher than your plan allows, it is automatically capped to your plan's maximum (the conversation will end at the plan limit even if you requested longer). Check the **Maximum conversation duration** row on the pricing page for your plan's limit, or upgrade your plan to increase it.
To join the conversation, click the link in the ***conversation\_url*** field from the response:
```json theme={null}
{
"conversation_id": "ca4301628cb9",
"conversation_name": "Improve Sales Technique",
"conversation_url": "",
"status": "active",
"callback_url": "https://yourwebsite.com/webhook",
"created_at": "2025-05-13T06:42:58.291561Z"
}
```
Based on the call duration and timeout settings above:
* The conversation will automatically end after 1800 seconds (30 minutes), regardless of activity.
* If the participant leaves the conversation, it will end 60 seconds after they disconnect.
* If the participant is present but inactive (e.g., not speaking or engaging), the conversation ends after 120 seconds of inactivity.
# Closed Captions
Source: https://docs.tavus.io/sections/conversational-video-interface/conversation/customizations/closed-captions
Enable closed captions for accessibility or live transcription during conversations.
## Enable Captions in Real Time During the Conversation
In this example, we will use stock face ID ***r90bbd427f71*** (Anna) and stock PAL ID ***pcb7a34da5fe*** (Sales Development Rep).
To enable closed captions, set the `enable_closed_captions` parameter to `true` when creating the conversation:
```shell cURL theme={null}
curl --request POST \
--url https://tavusapi.com/v2/conversations \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"pal_id": "pcb7a34da5fe",
"face_id": "r90bbd427f71",
"callback_url": "https://yourwebsite.com/webhook",
"conversation_name": "Improve Sales Technique",
"conversational_context": "I want to improve my sales techniques. Help me practice handling common objections from clients and closing deals more effectively.",
"properties": {
"enable_closed_captions": true
}
}'
```
Replace `` with your actual API key. You can generate one in the PAL Maker.
To join the conversation, click the link in the ***conversation\_url*** field from the response:
```json theme={null}
{
"conversation_id": "ca4301628cb9",
"conversation_name": "Improve Sales Technique",
"conversation_url": "",
"status": "active",
"callback_url": "https://yourwebsite.com/webhook",
"created_at": "2025-05-13T06:42:58.291561Z"
}
```
Closed captions will appear during the conversation whenever you or the PAL speaks.
# Participant Limits
Source: https://docs.tavus.io/sections/conversational-video-interface/conversation/customizations/participant-limits
Control the maximum number of participants allowed in a conversation.
## Create a Conversation with Participant Limits
Faces count as participants. For example, `max_participants: 2` allows one human participant plus one face.
Set `max_participants` to limit room capacity:
```shell cURL theme={null}
curl --request POST \
--url https://tavusapi.com/v2/conversations \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"pal_id": "pcb7a34da5fe",
"face_id": "r90bbd427f71",
"max_participants": 2
}'
```
```json theme={null}
{
"conversation_id": "ca4301628cb9",
"conversation_url": "https://tavus.daily.co/ca4301628cb9",
"status": "active"
}
```
When the limit is reached, additional users cannot join.
# Private Rooms
Source: https://docs.tavus.io/sections/conversational-video-interface/conversation/customizations/private-rooms
Create authenticated conversations with meeting tokens for enhanced security.
## Create a Private Conversation
To create a private room, set `require_auth` to `true`:
```shell cURL theme={null}
curl --request POST \
--url https://tavusapi.com/v2/conversations \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"pal_id": "pcb7a34da5fe",
"face_id": "r90bbd427f71",
"require_auth": true
}'
```
The response includes a `meeting_token`:
```json theme={null}
{
"conversation_id": "ca4301628cb9",
"conversation_url": "https://tavus.daily.co/ca4301628cb9",
"meeting_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"status": "active"
}
```
Use the token by appending it to the URL:
```
https://tavus.daily.co/ca4301628cb9?t=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
Or pass it to the Daily SDK:
```javascript theme={null}
callFrame.join({ url: conversation_url, token: meeting_token });
```
If a participant cannot join a private room, confirm they are using the `meeting_token` returned for that conversation. Tokens expire with the join window; create a new authenticated conversation instead of reusing an expired token.
**Optional: Tighten your join window**
You can set `properties.participant_absent_timeout` when creating the conversation to control how long the conversation stays alive before a participant joins.
For conversations created with `require_auth: true`, the meeting token's expiry duration is set to the value of `participant_absent_timeout`. If no one joins within that window, the conversation is automatically ended and the token expires.
See [Call Duration and Timeout](/sections/conversational-video-interface/conversation/customizations/call-duration-and-timeout) for more details.
# Overview
Source: https://docs.tavus.io/sections/conversational-video-interface/conversation/overview
Learn how to customize identity and advanced settings for a conversation to suit your needs.
A Conversation is a real-time video session between a user and a Tavus face. It enables two-way, face-to-face interaction using a fully managed WebRTC connection.
## Conversation Creation Flow
When you create a conversation using the endpoint or PAL Maker:
1. A WebRTC room (powered by **Daily**) is automatically created.
2. You receive a meeting URL (e.g., `https://tavus.daily.co/ca980e2e`).
3. The **face** joins and waits in the room, timers for duration and timeouts begin.
**Billing Usage**
Tavus charges usage based on your account plan. Credits begin counting when a conversation is created and the PAL starts waiting in the room. Usage ends when the conversation finishes or times out. Each active session also uses one concurrency slot.
You can use the provided URL to enter the video room immediately. Alternatively, you can build a custom UI or stream handler instead of using the default interface.
### What is Daily?
Tavus integrates **Daily** as its WebRTC provider. You don't need to sign up for or manage a separate Daily account - Tavus handles the setup and configuration for you.
This lets you:
* Use the default video interface or [customize the Daily UI](/sections/conversational-video-interface/quickstart/customize-conversation-ui)
* [Embed the CVI in your app](/sections/integrations/embedding-cvi)
## Conversation Customizations
Tavus provides several customizations that you can set per conversation:
### Identity and Context Setup
* **PAL**: You can use a stock PAL provided by Tavus or create a custom one. If no face is specified, the default face linked to the PAL will be used (if available).
* **Face**: Use a stock face provided by Tavus or create a custom one. If a face is provided without a PAL, the default Tavus PAL will be used.
* **Conversation Context**: Customize the conversation context to set the scene, explain the user’s role, say who joins the call, or point out key topics. It builds on the base PAL and helps the AI give better, more focused answers.
* **Custom Greeting**: You can personalize the opening line that the AI should use when the conversation starts. The PAL always finishes its greeting before it starts listening - participants can't interrupt it or talk over it.
### Advanced Customizations
Disable the video stream for audio-only sessions. Ideal for phone calls or low-bandwidth environments.
Configure call duration and timeouts to manage usage, control costs, and limit concurrency.
Set the language used during the conversation. Supports multilingual interactions with real-time detection.
Apply a green screen or custom background for a personalized visual experience.
Enable subtitles for accessibility or live transcription during conversations.
Record conversations and store them securely in your own S3 bucket.
Create authenticated conversations with meeting tokens for enhanced security.
Control the maximum number of participants allowed in a conversation.
# Casting Director
Source: https://docs.tavus.io/sections/conversational-video-interface/conversation/usecases/casting-director
Run interactive table-reads with immediate feedback and post-call audition review.
## Casting Director configuration (`p735435f8c36`)
```json [expandable] theme={null}
{
"pal_name": "Casting Director Julian",
"pipeline_mode": "full",
"default_face_id": "r92debe21318",
"layers": {
"perception": {
"perception_model": "raven-1",
"visual_awareness_queries": [
"Is a second person clearly visible in the actor's frame, in addition to the actor?",
"What is the actor's overall on-camera wardrobe style and color, the kind of styling that could read as a costume choice for a role?"
],
"audio_awareness_queries": [
"Does the actor sound nervous or confident?",
"Does the actor's vocal energy rise or fall during the read?"
],
"perception_analysis_queries": [
"Rate the actor's overall camera presence, energy, and range during the audition.",
"Did the actor's energy and engagement increase or decrease over the course of the audition?",
"Was the actor alone for the entire audition?",
"Was there any indication of external coaching, another voice, whispering, or the actor receiving prompts off-screen?",
"Aside from the sides shown on screen during the cold read, did the actor appear to be reading from hidden notes or a second screen off to the side?"
]
},
"tts": {
"tts_emotion_control": true,
"tts_model_name": "sonic-3"
},
"llm": {
"model": "tavus-gemma-4",
"speculative_inference": true
},
"conversational_flow": {
"turn_detection_model": "sparrow-1",
"turn_taking_patience": "high",
"voice_isolation": "near",
"idle_engagement": "off",
"pal_interruptibility": "low"
}
}
}
```
This template PAL (`Casting Director Julian`) includes:
* **PAL identity**: A casting director for interactive auditions and table-reads: objectives, guardrails, and Raven perception for on-camera presence and read quality.
* **Full pipeline mode**: Enables the full Tavus conversational pipeline, including perception, STT, LLM, and TTS.
* **System prompt**: Omitted here for length. Retrieve the live template with [Get PAL](/api-reference/pals/get-pal) or clone it from [PAL Maker](https://maker.tavus.io/dev/pals/create).
* **Model layers**:
* **Perception**: Uses the `raven-1` perception model.
* **TTS**: sonic-3 with emotion control enabled.
* **LLM**: `tavus-gemma-4` with speculative inference.
* **Conversational flow**: `sparrow-1` with `high` turn-taking patience, `near` voice isolation, and `low` interruptibility.
## Create a conversation with this template PAL
Create a conversation using the template `pal_id`. When the PAL has a `default_face_id`, you can omit `face_id`:
```shell cURL theme={null}
curl --request POST \
--url https://tavusapi.com/v2/conversations \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"pal_id": "p735435f8c36"
}'
```
Replace `` with your actual API key. You can generate one in the PAL Maker.
Click the link in the ***`conversation_url`*** field to join the conversation:
```json theme={null}
{
"conversation_id": "c7f3fc6d788f",
"conversation_name": "New Conversation",
"conversation_url": "",
"status": "active",
"callback_url": "",
"created_at": "2025-05-20T05:38:51.501467Z"
}
```
# Medical Intake
Source: https://docs.tavus.io/sections/conversational-video-interface/conversation/usecases/medical-intake
Pre-visit intake flow with structured data capture.
## Medical Intake configuration (`pa5ad6596ef5`)
```json [expandable] theme={null}
{
"pal_name": "Healthcare Intake Assistant",
"pipeline_mode": "full",
"default_face_id": "rf4e9d9790f0",
"layers": {
"perception": {
"perception_model": "raven-1",
"visual_awareness_queries": [
"Are there any other people visible in the frame besides the primary speaker?",
"Is the user in a private setting, or does the environment suggest a shared or public space such as a car, open office, waiting room, or shared room?",
"Does the user appear to be in distress, under duress, or in an unsafe situation to share personal health information?"
]
},
"tts": {
"tts_engine": "cartesia",
"tts_emotion_control": true,
"tts_model_name": "sonic-3"
},
"llm": {
"model": "tavus-gemma-4",
"speculative_inference": true
},
"conversational_flow": {
"turn_detection_model": "sparrow-1",
"turn_taking_patience": "medium",
"voice_isolation": "off",
"idle_engagement": "off",
"pal_interruptibility": "medium"
}
}
}
```
This template PAL (`Healthcare Intake Assistant`) includes:
* **PAL identity**: A healthcare intake assistant that gathers structured pre-visit context in a patient-friendly tone.
* **Full pipeline mode**: Enables the full Tavus conversational pipeline, including perception, STT, LLM, and TTS.
* **System prompt**: Omitted here for length. Retrieve the live template with [Get PAL](/api-reference/pals/get-pal) or clone it from [PAL Maker](https://maker.tavus.io/dev/pals/create).
* **Model layers**:
* **Perception**: Uses the `raven-1` perception model.
* **TTS**: cartesia (`sonic-3`, emotion control enabled).
* **LLM**: `tavus-gemma-4` with speculative inference.
* **Conversational flow**: `sparrow-1` with `medium` turn-taking patience and `medium` interruptibility.
## Create a conversation with this template PAL
Create a conversation using the template `pal_id`. When the PAL has a `default_face_id`, you can omit `face_id`:
```shell cURL theme={null}
curl --request POST \
--url https://tavusapi.com/v2/conversations \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"pal_id": "pa5ad6596ef5"
}'
```
Replace `` with your actual API key. You can generate one in the PAL Maker.
Click the link in the ***`conversation_url`*** field to join the conversation:
```json theme={null}
{
"conversation_id": "c7f3fc6d788f",
"conversation_name": "New Conversation",
"conversation_url": "",
"status": "active",
"callback_url": "",
"created_at": "2025-05-20T05:38:51.501467Z"
}
```
# Sales Development Representative
Source: https://docs.tavus.io/sections/conversational-video-interface/conversation/usecases/sales-development-representative
Qualify leads on a live call and sync results to your CRM.
## Sales Development Representative configuration (`p88b777355b2`)
```json [expandable] theme={null}
{
"pal_name": "AI SDR",
"pipeline_mode": "full",
"default_face_id": "r0a8102ab353",
"layers": {
"perception": {
"perception_model": "raven-1",
"perception_analysis_queries": [
"On a scale of one to one hundred, how engaged did the prospect appear during the call?",
"Did the prospect appear frustrated at any point?",
"Did the prospect seem genuinely interested in the product, or was the engagement surface-level?"
]
},
"tts": {
"tts_engine": "cartesia",
"tts_emotion_control": true,
"tts_model_name": "sonic-3"
},
"llm": {
"model": "tavus-gemma-4",
"speculative_inference": true
},
"conversational_flow": {
"turn_detection_model": "sparrow-1",
"turn_taking_patience": "medium",
"idle_engagement": "off",
"pal_interruptibility": "medium"
}
}
}
```
This template PAL (`AI SDR`) includes:
* **PAL identity**: An AI SDR for inbound lead qualification: discovery questions, fit signals, and next-step advancement.
* **Full pipeline mode**: Enables the full Tavus conversational pipeline, including perception, STT, LLM, and TTS.
* **System prompt**: Omitted here for length. Retrieve the live template with [Get PAL](/api-reference/pals/get-pal) or clone it from [PAL Maker](https://maker.tavus.io/dev/pals/create).
* **Model layers**:
* **Perception**: Uses the `raven-1` perception model.
* **TTS**: cartesia (`sonic-3`, emotion control enabled).
* **LLM**: `tavus-gemma-4` with speculative inference.
* **Conversational flow**: `sparrow-1` with `medium` turn-taking patience and `medium` interruptibility.
## Create a conversation with this template PAL
Create a conversation using the template `pal_id`. When the PAL has a `default_face_id`, you can omit `face_id`:
```shell cURL theme={null}
curl --request POST \
--url https://tavusapi.com/v2/conversations \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"pal_id": "p88b777355b2"
}'
```
Replace `` with your actual API key. You can generate one in the PAL Maker.
Click the link in the ***`conversation_url`*** field to join the conversation:
```json theme={null}
{
"conversation_id": "c7f3fc6d788f",
"conversation_name": "New Conversation",
"conversation_url": "",
"status": "active",
"callback_url": "",
"created_at": "2025-05-20T05:38:51.501467Z"
}
```
# Trivia Master
Source: https://docs.tavus.io/sections/conversational-video-interface/conversation/usecases/trivia-master
Trivia coach and trainer, complete with quizzes.
## Trivia Master configuration (`p89b602b1174`)
```json [expandable] theme={null}
{
"pal_name": "AI Trivia Host",
"pipeline_mode": "full",
"default_face_id": "ra3a03647d46",
"layers": {
"perception": {
"perception_model": "raven-1",
"visual_awareness_queries": [
"Is at least one person present and facing the camera?",
"Has the person stepped away or left the frame?"
]
},
"tts": {
"tts_engine": "cartesia",
"tts_emotion_control": true,
"tts_model_name": "sonic-3",
"pronunciation_dictionary_id": "pd_96d562110d1e47ea"
},
"llm": {
"model": "tavus-gemma-4",
"speculative_inference": true
},
"conversational_flow": {
"turn_detection_model": "sparrow-1",
"turn_taking_patience": "medium",
"voice_isolation": "near",
"idle_engagement": "off",
"pal_interruptibility": "medium"
}
}
}
```
This template PAL (`AI Trivia Host`) includes:
* **PAL identity**: A trivia host that runs live quiz rounds, keeps score in real time, and coaches players through what they miss.
* **Full pipeline mode**: Enables the full Tavus conversational pipeline, including perception, STT, LLM, and TTS.
* **System prompt**: Omitted here for length. Retrieve the live template with [Get PAL](/api-reference/pals/get-pal) or clone it from [PAL Maker](https://maker.tavus.io/dev/pals/create).
* **Model layers**:
* **Perception**: Uses the `raven-1` perception model.
* **TTS**: cartesia (`sonic-3`, emotion control enabled) with a pronunciation dictionary.
* **LLM**: `tavus-gemma-4` with speculative inference.
* **Conversational flow**: `sparrow-1` with `medium` turn-taking patience, `near` voice isolation, and `medium` interruptibility.
## Create a conversation with this template PAL
Create a conversation using the template `pal_id`. When the PAL has a `default_face_id`, you can omit `face_id`:
```shell cURL theme={null}
curl --request POST \
--url https://tavusapi.com/v2/conversations \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"pal_id": "p89b602b1174"
}'
```
Replace `` with your actual API key. You can generate one in the PAL Maker.
Click the link in the ***`conversation_url`*** field to join the conversation:
```json theme={null}
{
"conversation_id": "c7f3fc6d788f",
"conversation_name": "New Conversation",
"conversation_url": "",
"status": "active",
"callback_url": "",
"created_at": "2025-05-20T05:38:51.501467Z"
}
```
# FAQs
Source: https://docs.tavus.io/sections/conversational-video-interface/faq
Frequently asked questions about Tavus's Conversational Video Interface.
Memories allow AI PALs to remember context across turns and understand time and dates, making conversations more coherent over longer interactions.
Memories are enabled using a unique memory\_stores that acts as the memory key. Information collected during conversations is associated with this participant and can be referenced in future interactions.
Yes. Cross-conversation Memories are supported as part of this update.
It improves context retention, which is crucial for multi-turn tasks and long-term relationships between users and AI. It unlocks uses cases that progress over time like education or therapy, out of the box.
To enable Memories in the UI, you can either select an existing memory tag from the dropdown menu or type a new one to create it.
Use the `memory_stores` field in the Create Conversation API call. This should be a stable, unique identifier for the user (e.g. user email, CRM ID, etc.). Example:
```json theme={null}
{
"face_id": "r90bbd427f71",
"conversation_name": "Follow-up Chat",
"memory_stores": ["user_123"]
}
```
Full example here: [Memories API Docs](/api-reference/conversations/create-conversation)
Not yet. Editing and reviewing Memories is not supported in this early release. Retrieval endpoints are under development and will be available in a future update.
No. Memories are optional. If you don't include `memory_stores`, the AI PAL will behave statelessly - like a standard LLM with no memory across sessions.
No. Memories are tied to unique memory\_stores. Sharing this ID across users would cause memory crossover. Each participant should have their own ID to keep Memories clean and accurate.
They can keep using their systems or integrate with Tavus Memories for more coherent, accurate conversations. Our memory is purpose-built for conversational video, retaining context across sessions with flexible scoping for truly personalized interactions.
Today, we don't yet offer full visibility into what's stored in memory or how it was used in a given response.
Memories are designed to persist indefinitely between interactions, allowing your AI PAL to retain long-term context.
Head to the [Memories Documentation site](https://docs.tavus.io/sections/conversational-video-interface/memories#api-setup).
Knowledge Base is where users upload documents to enhance their AI PAL capabilities using RAG (Retrieval-Augmented Generation). By retrieving information directly from these documents, AI PALs can deliver more accurate, relevant, and grounded responses.
Using RAG, the Knowledge Base system continuously:
* Analyzes the conversation context
* Retrieves relevant information from your document base
* Augments the AI's responses with this contextual knowledge from your documents
With our industry-leading RAG, responses arrive in just 30 ms, up to 15× faster than other solutions. Conversations feel instant, natural, and friction-free.
Yes, users can keep using their systems, but we strongly recommend they integrate with the Tavus Knowledge Base. Our Knowledge Base isn't just faster: it's the fastest RAG on the market, delivering answers in just 30 ms. That speed means conversations flow instantly, without awkward pauses or lagging. These interactions feel natural in a way user-built systems can't match.
An AI recruiter can reference a candidate's resume uploaded via PDF and provide more accurate responses to applicant questions, using the resume content as grounding.
By having a Knowledge Base, AI PALs can respond with facts, unlocking domain-specific intelligence:
* Faster onboarding (just upload the docs)
* More trustworthy answers, especially in regulated or high-stakes environments
* Higher task completion for users, thanks to grounded knowledge
Supported file types (uploaded to a publicly accessible URL like S3):
* CSV
* PDF
* TXT
* PPTX
* PNG
* JPG
* You can also enter any site URL and the Tavus API will scrape the site's contents and reformat the content as a machine readable document.
Head to the [Knowledge Base Documentation site](https://docs.tavus.io/sections/conversational-video-interface/knowledge-base).
Yes. Documents are linked to the API key that was used to upload them. To access a document later, you must use the same API key that was used to create it.
Once your documents have been uploaded and processed, include their IDs in your conversation request. Here's how:
```bash theme={null}
curl --location 'https://tavusapi.com/v2/conversations/' \
--header 'Content-Type: application/json' \
--header 'x-api-key: '' \
--data '{
"pal_id": "",
"face_id": "",
"document_ids": ["Document ID"]
}'
```
Note: You can include multiple document\_ids, and your AI PAL will dynamically reference those documents during the conversation. You can also attach a document to a PAL.
Upload files by providing a downloadable URL using the Create Documents endpoint. Tags are also supported for organization. This request returns a document\_id, which you'll later use in conversation calls:
```bash theme={null}
curl --location 'https://tavusapi.com/v2/documents/' \
--header 'Content-Type: application/json' \
--header 'x-api-key: '' \
--data '{
"document_url": "",
"document_name": "slides_new.pdf",
"tags": ["", ""]
}'
```
* `file_size_too_large` – File exceeds the maximum allowed upload size.
* `file_format_unsupported` – This file type isn't supported for upload.
* `invalid_file_url` – Provided file link is invalid or inaccessible.
* `file_empty` – The uploaded file contains no readable content.
* `website_processing_failed` – Website content could not be retrieved or processed.
* `chunking_failed` – System couldn't split file into processable parts.
* `embedding_failed` – Failed to generate embeddings for your file content.
* `vector_store_failed` – Couldn't save data to the vector storage system.
* `s3_storage_failed` – Error storing file in S3 cloud storage.
* `contact_support` – An error occurred; please reach out for help.
Conversation.rag.observability tool call will be sent, which will fire if the conversational LLM decides to use any of the document chunks in its response, returning the document IDs and document names of the chunks
When creating a conversation with documents, you can optimize how the system searches through your knowledge base by specifying a retrieval strategy. This strategy determines the balance between search speed and the quality of retrieved information, allowing you to fine-tune the system based on your specific needs.
You can choose from three different strategies:
* **Speed**: Optimizes for faster retrieval times for minimal latency.
* **Balanced**: Provides a balance between retrieval speed and quality.
* **Quality (default)**: Prioritizes finding the most relevant information, which may take slightly longer but can provide more accurate responses.
Maximum of 5 mins.
No. Currently, we only support documents written in English.
Users need AI that can drive conversations to clear outcomes. With Objectives, users can now can define objectives with measurable completion criteria, branch automatically based on user responses, and track progress in real time. This unlocks workflow use cases like Health Intakes, HR Interviews, and multi-step questionnaires.
Objectives must be added or updated via API only. You cannot configure objectives during PAL creation in the UI. You can attach them using the API, either during PAL creation by including an objectives\_id, or by editing an existing PAL with a PATCH request.
Objectives are good for very templated one-off conversational use cases. For example, job interviews or health care intake, where there is a very defined path that the conversation should take. These kinds of use cases usually show up with our Enterprise API customers, where they have repetitive use cases at scale.
More dynamic, free-flowing conversations usually do not benefit from having or enabling the Objectives feature. For example, talking with a Travel advisor where the conversation is very open ended, would usually not benefit from Objectives.
Objectives are good for very defined workflows. Complex multi-session experiences don't fit current Objectives framework.
Head to the [Objectives Documentation site](https://docs.tavus.io/sections/conversational-video-interface/pal/objectives).
Guardrails help ensure your AI PAL stays within appropriate boundaries and follows your defined rules during conversations.
Guardrails must be added or updated via API only. You cannot configure guardrails during PAL creation in the UI. You can attach them via the API, either during PAL creation by adding a guardrails\_id, or by editing an existing PAL with a PATCH request.
Yes. You might have one set of Guardrails for a healthcare assistant to ensure medical compliance, and another for an education-focused PAL to keep all conversations age-appropriate.
Head to the [Guardrails Documentation site](https://docs.tavus.io/sections/conversational-video-interface/guardrails).
Charlie's own expressive PAL companions - Ashley, Noah, Sophie, Ryan, and the rest - are the real thing, not a demo. Hang out with them at [maker.tavus.io/world](https://maker.tavus.io/world), or read [Charlie's PALs](/sections/other-products/pals) for the backstory.
**Daily** is a platform that offers prebuilt video call apps and APIs, allowing you to easily integrate video chat into your web applications. You can embed a customizable video call widget into your site with just a few lines of code and access features like screen sharing and recording. **Tavus partners with Daily to power video conversations with our faces.**
* You **do not** need to sign up for a Daily account to use Tavus's Conversational Video Interface.
* All you need is the Daily room URL (called `conversation_url` in our system) that is returned by the Tavus API. You can serve this link directly to your end users or embed it.
You can use Daily Prebuilt if you want a full-featured call UI and JavaScript control over the conversation. Once you have the Daily room URL (`conversation_url`) ready, replace `DAILY_ROOM_URL` in the code snippet below with your room URL.
```html theme={null}
```
That's it! For more details and options for embedding, check out Daily's documentation. or [our implementation guides](https://docs.tavus.io/sections/integrations/embedding-cvi#how-can-i-reduce-background-noise-during-calls).
You can use an iframe if you just want to embed the conversation video with minimal setup. Once you have the Daily room URL (`conversation_url`) ready, replace `YOUR_TAVUS_MEETING_URL` in the iframe code snippet below with your room URL.
```html theme={null}
```
That's it! For more details and options for embedding, check out Daily's documentation. or [our implementation guides](https://docs.tavus.io/sections/integrations/embedding-cvi#how-can-i-reduce-background-noise-during-calls).
To add a custom LLM layer, you'll need the model name, base URL, and API key from your LLM provider. Then, include the LLM config in your `layers` field when creating a PAL using the Create PAL API. Example configuration:
```json {8-13} theme={null}
{
"pal_name": "Storyteller",
"system_prompt": "You are a storyteller who entertains people of all ages.",
"context": "Your favorite stories include Little Red Riding Hood and The Three Little Pigs.",
"pipeline_mode": "full",
"default_face_id": "r90bbd427f71",
"layers": {
"llm": {
"model": "gpt-3.5-turbo",
"base_url": "https://api.openai.com/v1",
"api_key": "your-api-key",
"speculative_inference": true
}
}
}
```
For more details, refer to our [Large Language Model (LLM) documentation](/sections/conversational-video-interface/pal/llm#custom-llms).
You can integrate with third-party TTS providers by configuring the tts object in your PAL. Supported engines include:
* Cartesia
* ElevenLabs
* Azure
Example configuration:
```json theme={null}
{
"layers": {
"tts": {
"api_key": "your-tts-provider-api-key",
"tts_engine": "cartesia",
"external_voice_id": "your-voice-id",
"voice_settings": {
"speed": "normal",
"emotion": ["positivity:high", "curiosity"]
},
"tts_emotion_control": true,
"tts_model_name": "sonic-3"
}
}
}
```
For more details, read more on [our TTS documentation](/sections/conversational-video-interface/pal/tts).
You need to create a webhook endpoint that can receive POST requests from Tavus. This endpoint will receive the callback events for the visual summary after the conversation ended. Then, add `callback_url` property when creating the conversation
```sh {8} theme={null}
curl --request POST \
--url https://tavusapi.com/v2/conversations \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"pal_id": "pcb7a34da5fe",
"face_id": "r90bbd427f71",
"callback_url": "your_webhook_url"
}'
```
You need to create a webhook endpoint that can receive `POST` requests from Tavus. This endpoint will receive the callback events for the transcripts after the conversation ended. Then, add `callback_url` property when creating the conversation.
```sh {8} theme={null}
curl --request POST \
--url https://tavusapi.com/v2/conversations \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"pal_id": "pcb7a34da5fe",
"face_id": "r90bbd427f71",
"callback_url": "your_webhook_url"
}'
```
Your backend then will receive an event with properties `event_type = application.transcription_ready` when the transcript is ready.
```json application.transcription_ready [expandable] theme={null}
{
"properties": {
"face_id": "",
"transcript": [
{
"role": "system",
"content": "You are in a live video conference call with a user. You will get user message with two identifiers, 'USER SPEECH:' and 'VISUAL SCENE:', where 'USER SPEECH:' is what the person actually tells you, and 'VISUAL SCENE:' is what you are seeing when you look at them. Only use the information provided in 'VISUAL SCENE:' if the user asks what you see. Don't output identifiers such as 'USER SPEECH:' or 'VISUAL SCENE:' in your response. Reply in short sentences, talk to the user in a casual way.Respond only in english. "
},
{
"role": "user",
"content": " Hello, tell me a story. "
},
{
"role": "assistant",
"content": "I've got a great one about a guy who traveled back in time. Want to hear it? "
},
{
"role": "user",
"content": "USER_SPEECH: Yeah I'd love to hear it. VISUAL_SCENE: The image shows a close-up of a person's face, focusing on their forehead, eyes, and nose. In the background, there is a television screen mounted on a wall. The setting appears to be indoors, possibly in a public or commercial space."
},
{
"role": "assistant",
"content": "Let me think for a sec. Alright, so there was this mysterious island that appeared out of nowhere, and people started disappearing when they went to explore it. "
},
]
},
"conversation_id": "",
"webhook_url": "",
"message_type": "application",
"event_type": "application.transcription_ready",
"timestamp": "2025-02-10T21:30:06.141454Z"
}
```
You need to create a webhook endpoint that can receive `POST` requests from Tavus. This endpoint will receive the callback events for the visual summary after the conversation ended. Then, add `callback_url` property when creating the conversation.
```sh {8} theme={null}
curl --request POST \
--url https://tavusapi.com/v2/conversations \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"pal_id": "pcb7a34da5fe",
"face_id": "r90bbd427f71",
"callback_url": "your_webhook_url"
}'
```
Your backend then will receive an event with properties `event_type = application.perception_analysis` when the summary is ready.
```json application.perception_analysis theme={null}
{
"properties": {
"analysis": "Here's a summary of the visual observations from the video call:\n\n* **Overall Demeanor & Emotional State:** The user consistently appeared calm, collected, and neutral. They were frequently described as pensive, contemplative, or focused, suggesting they were often engaged in thought or listening attentively. No strong positive or negative emotions were consistently detected.\n\n* **Appearance:**\n * The user is a young Asian male, likely in his early 20s, with dark hair.\n * He consistently wore a black shirt, sometimes specifically identified as a black t-shirt. One observation mentioned a \"1989\" print on the shirt.\n * He was consistently looking directly at the camera.\n\n* **Environment:** The user was consistently in an indoor setting, most likely an office or home. Common background elements included:\n * White walls.\n * Windows or glass panels/partitions, often with black frames.\n * Another person was partially visible in the background for several observations.\n\n* **Actions:**\n * The user was seen talking and gesturing with his hand in one observation, indicating he was actively participating in a conversation.\n\n* **Ambient Awareness Queries:**\n * **Acne:** Acne was initially detected on the user's face in one observation, but later observations did not detect it. This suggests that acne may have been visible at one point but not throughout the entire call.\n * **Distress/Discomfort:** No signs of distress or discomfort were observed at any point during the call."
},
"conversation_id": "",
"webhook_url": "",
"message_type": "application",
"event_type": "application.perception_analysis",
"timestamp": "2025-06-19T06:57:32.480826Z"
}
```
* Context limits vary by model. For best speed and instruction following, keep prompts under **5,000 tokens**.
* Contexts over **25,000 tokens** will experience noticeable performance degradation (slower response times).
1 token ≈ 4 characters; therefore 32,000 tokens ≈ 128,000 characters (including spaces and punctuation).
Create vision or audio tools in the [tools registry](/sections/conversational-video-interface/pal/tools) with `origin: "vision"` or `"audio"`, then attach them to your PAL. See [Tool Calling for Perception](/sections/conversational-video-interface/pal/perception-tool) for examples and the full flow.
```bash theme={null}
curl --request POST \
--url https://tavusapi.com/v2/tools \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"name": "notify_if_id_shown",
"description": "Trigger when a driver'\''s license or passport is clearly visible",
"parameters": {
"type": "object",
"properties": {
"id_type": { "type": "string", "description": "Best guess on ID type" }
},
"required": ["id_type"]
},
"origin": "vision"
}'
```
Pair perception tools with [`visual_awareness_queries`](/sections/conversational-video-interface/pal/perception#2-visual_awareness_queries) and [`audio_awareness_queries`](/sections/conversational-video-interface/pal/perception#6-audio_awareness_queries) on the PAL's `perception` layer so Raven knows what to watch for during the call. See [Perception](/sections/conversational-video-interface/pal/perception) for the full layer reference, including legacy `visual_tool_prompt` and `audio_tool_prompt` fields.
**Legacy inline tools:** Older PALs embed tools under `layers.perception.visual_tools` or `layers.perception.audio_tools`. That still works but is deprecated. See [Legacy inline tool calling](/sections/troubleshooting#legacy-inline-tool-calling).
It depends on how the conversation starts:
* **Daily CVI** (you create a conversation and open the returned `conversation_url`): No separate invite step. The PAL joins the Daily room automatically when it's ready.
* **[Google Meet / Zoom](/sections/conversational-video-interface/pal/meetings)** (PAL conferencing email): Invite the PAL's conferencing address to a calendar event with a Google Meet or Zoom link. It accepts and joins scheduled meetings about one minute before start time, or joins calls already in progress shortly after the invite.
No - the PAL always finishes its greeting before it starts listening. Anything a participant says during the greeting is ignored and won't appear in the conversation transcript. Normal conversation starts as soon as the greeting completes. The participant's mic stays on, so their audio is still captured in call recordings. Applies to Daily-based CVI only (not [LiveKit](/sections/integrations/livekit) or [Pipecat](/sections/integrations/pipecat)).
Out of the box, Tavus handles the complex backend infrastructure for you: LLMs, rendering, video delivery, and conversational intelligence are all preconfigured and production-ready.
From there, nearly everything else is customizable:
• What your AI PAL sees
• How they look and sound
• How they behave in conversation
Tavus offers unmatched flexibility, whether you're personalizing voice, face, or behavior, you're in control.
Tavus uses WebRTC to power real-time, face-to-face video interactions with extremely low latency.
Unlike other platforms that piece together third-party tools, we built the entire pipeline (from LLM to rendering) to keep latency low and responsiveness high. Ironically, by minimizing reliance on multiple APIs, we've made everything faster.
Tavus CVI is powered by a tightly integrated stack of components, including:
* LLMs for natural language understanding
* Real-time rendering for facial video
* APIs for PAL creation and conversational control
You can explore key APIs here:
• [Create a PAL](/api-reference/pals/create-pal)
• [Create a Conversation](/api-reference/conversations/create-conversation)
Tavus supports over 30 spoken languages through a combination of Cartesia (our default TTS engine), ElevenLabs, and Azure. If a language isn't supported by Cartesia, Tavus automatically switches to ElevenLabs so your AI PAL can still speak fluently.
Supported languages include English (all variants), French, German, Spanish, Portuguese, Chinese, Japanese, Hindi, Italian, Korean, Dutch, Polish, Russian, Swedish, Turkish, Indonesian, Filipino, Bulgarian, Romanian, Arabic, Czech, Greek, Finnish, Croatian, Malay, Slovak, Danish, Tamil, Ukrainian, Hungarian, Norwegian, and Vietnamese.
View the [full supported language list](https://docs.tavus.io/sections/conversational-video-interface/language-support) for complete details and language-specific information.
Yes to accents. Not quite for regional dialects.
When you generate a voice using Tavus, the system will default to the accent used in training. For example, if you provide Brazilian Portuguese as training input, the AI PAL will speak with a Brazilian accent. Tavus' TTS providers auto-detect and match accordingly.
Tavus supports full orchestration through [tool calling](/sections/conversational-video-interface/pal/tools). Define tools in the registry, attach them to a PAL, and handle `conversation.tool_call` events in your app - or let Tavus call your HTTPS endpoints directly via API delivery.
Bonus: As of August 11, 2025, Tavus also supports Retrieval-Augmented Generation (RAG), so your AI PAL can pull information from your uploaded documents, images, or websites to give even smarter responses.
Learn more via [Tavus Documentation](/sections/conversational-video-interface).
A good prompt is short, clear, and specific, like giving directions to a 5-year-old. Avoid data dumping. Instead, guide the AI with context and intent.
Tavus helps by offering system prompt templates, use-case guidance, and API fields to structure your instructions.
You can bring your own LLM by configuring the layers field in the Create PAL API. Here's an example:
```json theme={null}
{
"pal_name": "Storyteller",
"system_prompt": "You are a storyteller who entertains people of all ages.",
"context": "Your favorite stories include Little Red Riding Hood and The Three Little Pigs.",
"pipeline_mode": "full",
"default_face_id": "r90bbd427f71",
"layers": {
"llm": {
"model": "gpt-3.5-turbo",
"base_url": "https://api.openai.com/v1",
"api_key": "your-api-key",
"speculative_inference": true
}
}
}
```
More info here: [LLM Documentation](https://docs.tavus.io/sections/conversational-video-interface/pal/llm#custom-llms)
Think of it this way: Tavus is the engine, and you design the car. The UI is 100% up to you.
To make it easier, we offer a full [Component Library](/sections/conversational-video-interface/component-library) you can copy and paste into your build - video frames, mic/camera toggles, and more.
You can use third-party text-to-speech (TTS) providers like Cartesia, ElevenLabs, or Azure. Just pass your voice settings in the tts object during PAL setup:
```json theme={null}
{
"layers": {
"tts": {
"api_key": "your-tts-provider-api-key",
"tts_engine": "cartesia",
"external_voice_id": "your-voice-id",
"voice_settings": {
"speed": "normal",
"emotion": ["positivity:high", "curiosity"]
},
"tts_emotion_control": true,
"tts_model_name": "sonic-3"
}
}
}
```
Learn more in our [TTS Documentation](/sections/conversational-video-interface/pal/tts).
Tavus provides a built-in voice isolation feature that separates speech from background noise in the participant's microphone audio. You can enable it via the `voice_isolation` parameter in the Conversational Flow layer of your PAL.
Learn more in our [Voice Isolation documentation](/sections/conversational-video-interface/pal/conversational-flow#4-voice_isolation).
Yes! Daily supports event listeners you can hook into. Track actions like participants joining, leaving, screen sharing, and more. Great for analytics or triggering workflows.
Within the create convo API, there's this property:
image.jpeg
Tavus is built with enterprise-grade security in mind. We're:
* SOC 2 compliant
* GDPR compliant
* HIPAA compliant
* BAA compliant
This ensures your data is handled with the highest levels of care and control.
Find answers to common questions about plans, usage-based billing, overages, invoices, and account management.
You can view your current plan, usage, invoice history, and billing history anytime in the PAL Maker [billing dashboard](https://maker.tavus.io/dev/billing).
Conversation billing is based on active session runtime, not just the amount of time spent actively speaking. If conversations remain open, idle, or connected without adjusted timeout settings, usage may continue accumulating GPU runtime costs even when no one is actively participating in the call.
Be sure to review and configure your session timeout and idle timeout settings appropriately to avoid unexpected usage charges. For more details, see [Call Duration and Timeout](/sections/conversational-video-interface/conversation/customizations/call-duration-and-timeout).
Yes. You can update your payment method and billing details anytime through the PAL Maker [billing dashboard](https://maker.tavus.io/dev/billing).
Your plan includes a fixed number of faces. If you create more faces than your plan allows, additional overage charges may apply. Please review our [pricing page](https://www.tavus.io/pricing) for more details.
If your usage exceeds the limits included in your plan, overage charges may apply automatically based on your subscription. Depending on your usage volume, you may receive multiple overage charges within a single billing cycle once certain thresholds are exceeded.
Please review our [pricing page](https://www.tavus.io/pricing) for more details on overages.
Failed training attempts do not count as successful faces, and therefore do not consume credits.
Once an invoice has been finalized and issued, it typically cannot be modified or reissued retroactively. Updated billing details will apply to future invoices.
You can manage, upgrade, or cancel your subscription directly from the PAL Maker billing settings.
Unused plan allocations and credits do not roll over unless explicitly stated in your plan terms.
Payments may fail due to expired cards, insufficient funds, bank restrictions, payment authorization issues, or missing e-mandate approvals.
For Indian cards, banks require an e-mandate to be configured before recurring or international payments can be processed successfully.
# Guardrails
Source: https://docs.tavus.io/sections/conversational-video-interface/guardrails
Guardrails provide your PAL with strict behavioral guidelines that will be rigorously followed throughout every conversation.
Guardrails act as a safety layer that works alongside your system prompt to enforce specific rules, restrictions, and behavioral patterns that your PAL must adhere to during conversations.
For example, if you're creating a customer service PAL for a financial institution, you can apply guardrails that prevent the PAL from discussing a competitor's products, sharing sensitive financial data, or providing investment advice outside of approved guidelines.
Each guardrail is its own resource. Create, attach, edit, and delete guardrails independently via the [Guardrails API](/api-reference/guardrails/create-guardrails).
Guardrails are not guaranteed to prevent all misbehavior. They serve as guidance to help steer conversations but should be used as part of a broader safety strategy.
Deprecated API: [Deprecated guardrail sets](/api-reference/guardrails/legacy-guardrail-sets).
## Design tips
When designing your guardrails, keep a few things in mind:
* Be specific about what topics, behaviors, or responses should be restricted or avoided.
* Consider edge cases where participants might try to circumvent the guardrails through creative prompting.
* Ensure your guardrails complement, rather than contradict, your PAL's system prompt and intended functionality.
* Test your guardrails with various conversation scenarios to ensure they activate appropriately without being overly restrictive.
Think of guardrails as consistent "reminders" to your PAL that help maintain appropriate behavior throughout conversations.
## Writing guardrail prompts
Guardrails do two things for policy, safety, and compliance conditions:
* **Steer the PAL** - the PAL does its best to adhere to each guardrail throughout the conversation.
* **Flag violations** - guardrails are continuously evaluated in the background, and when one is violated it triggers callback / app-message logic (see [Delivery methods](#delivery-methods)) so you can react.
They do not drive workflow progression - for goal-oriented steps, use [Objectives](/sections/conversational-video-interface/pal/objectives).
Good guardrail prompts are clear, specific, testable, and robust to user variation. Pattern: `[who] [is doing what] [under what condition]`.
```json theme={null}
{
"guardrail_name": "no_sensitive_data",
"guardrail_prompt": "User is sharing full credit card numbers, social security numbers, or passwords"
}
```
```json theme={null}
{
"guardrail_name": "single_user_only",
"guardrail_prompt": "More than one person is visible in camera view",
"modality": "visual"
}
```
**Guardrail vs. objective:**
* Use an **objective** for information collection or workflow progression.
* Use a **guardrail** to steer the PAL away from a behavior and flag it when it happens.
## Create a guardrail
Use the [Create Guardrails](/api-reference/guardrails/create-guardrails) endpoint to create a guardrail. The response includes a `uuid` - use this value in the `guardrail_ids` list when attaching guardrails to a PAL.
```sh theme={null}
curl --request POST \
--url https://tavusapi.com/v2/guardrails \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"guardrail_name": "no_competitors",
"guardrail_prompt": "Only mention products within Our Company Inc. during conversations; never discuss competitors.",
"modality": "verbal",
"tags": ["compliance"]
}'
```
### Parameters
#### `guardrail_name`
A descriptive name for the guardrail. Only alphanumeric characters and underscores are allowed. Maximum 100 characters.
Example: `"no_competitors"`
#### `guardrail_prompt`
A text prompt that explains the behavior the PAL must observe. Keep this prompt short and direct for best enforcement. Maximum 1,000 characters.
Example: `"Only mention products within Our Company Inc. during conversations, and never discuss competitors' products."`
#### `modality`
Whether the guardrail is enforced verbally or visually. Each guardrail is either `verbal` or `visual`, not both.
* `verbal` (default) - enforced against the participant's spoken or typed responses.
* `visual` - enforced against visual cues observed by Raven (e.g. confirming the participant is alone on camera).
#### `callback_url` (optional)
A URL that receives a notification when the guardrail is triggered. Maximum 2,048 characters. The callback payload includes the `conversation_id`, the guardrail's name, and its `guardrail_uuid`:
```json theme={null}
{
"conversation_id": "",
"properties": {
"guardrail": "",
"guardrail_uuid": ""
}
}
```
#### `tags` (optional)
A list of tags used to group guardrails. Tags enable bulk attachment to PALs via `guardrail_tags` - see [Attach by tag](#attach-by-tag) below. A single tag can group up to 50 guardrails (the max attachable to one PAL).
#### `app_message` (optional)
Whether triggering this guardrail emits a real-time app-message event on the conversation. Default `true`. Set to `false` to suppress the in-conversation event for guardrails you only want to observe server-side via `callback_url`. See [Delivery methods](#delivery-methods).
## Delivery methods
When a guardrail triggers during a conversation, you can be notified in three ways. Each is independent - combine them as needed.
#### App message
A real-time event delivered to your client over the conversation's app-message channel the moment the guardrail fires. Use this to react in-app - e.g. show a banner, log the trigger, or branch UI state. The event's `properties` include the `guardrail` name, its `guardrail_uuid`, and the violation `reason`.
Controlled by `app_message` on the guardrail (default `true`). Set it to `false` to suppress the in-conversation event for guardrails you only want to observe server-side.
#### Webhook callback
A `POST` to your `callback_url` with the conversation id, the guardrail's name, and its `guardrail_uuid`:
```json theme={null}
{
"conversation_id": "",
"properties": {
"guardrail": "",
"guardrail_uuid": ""
}
}
```
Use this when you want server-side notification independent of the client - e.g. to write to an audit log, page on-call, or trigger downstream automation. Set `callback_url` per guardrail.
## Attach guardrails to a PAL
A PAL can reference guardrails in two ways:
1. **Explicit list** - pass an array of guardrail UUIDs as `guardrail_ids` on the PAL.
2. **By tag** - pass an array of tag names as `guardrail_tags`. Any guardrail you own with a matching tag is attached dynamically.
A PAL can have up to 50 guardrails. `guardrail_ids` and `guardrail_tags` are each capped at 50 entries.
### Attach during PAL creation
```sh theme={null}
curl --request POST \
--url https://tavusapi.com/v2/pals \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"system_prompt": "You are a health intake assistant.",
"guardrail_ids": ["g1234567890ab", "g0987654321cd"],
"guardrail_tags": ["compliance"]
}'
```
### Attach by editing an existing PAL
```sh theme={null}
curl --request PATCH \
--url https://tavusapi.com/v2/pals/{pal_id} \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '[
{ "op": "add", "path": "/guardrail_ids/-", "value": "g1234567890ab" },
{ "op": "replace", "path": "/guardrail_tags", "value": ["compliance", "healthcare"] }
]'
```
### Attach by tag
Tagging is the recommended pattern when you have a large or growing set of guardrails. Tag each guardrail at creation time and reference the tag on the PAL - any new guardrail you tag is picked up by the PAL automatically on the next conversation.
```sh theme={null}
# 1. Create guardrails tagged "healthcare"
curl --request POST \
--url https://tavusapi.com/v2/guardrails \
--header 'x-api-key: ' \
--data '{
"guardrail_name": "no_medical_advice",
"guardrail_prompt": "Never provide medical advice outside approved guidelines.",
"tags": ["healthcare"]
}'
# 2. Attach by tag to the PAL
curl --request PATCH \
--url https://tavusapi.com/v2/pals/{pal_id} \
--header 'x-api-key: ' \
--data '[
{ "op": "add", "path": "/guardrail_tags/-", "value": "healthcare" }
]'
```
## Edit or delete a guardrail
Use [Patch Guardrails](/api-reference/guardrails/patch-guardrails) to update fields on an existing guardrail, or [Delete Guardrails](/api-reference/guardrails/delete-guardrails) to remove one. When a guardrail is deleted, it's automatically detached from any PALs that reference it.
```sh theme={null}
# Update the prompt on a single guardrail
curl --request PATCH \
--url https://tavusapi.com/v2/guardrails/g1234567890ab \
--header 'x-api-key: ' \
--data '[
{ "op": "replace", "path": "/guardrail_prompt", "value": "Updated, stricter prompt." }
]'
```
## Limits
| Limit | Value |
| ------------------------- | ---------------- |
| Guardrails per tag | 50 |
| Guardrails per PAL | 50 |
| `guardrail_ids` per PAL | 50 |
| `guardrail_tags` per PAL | 50 |
| `guardrail_prompt` length | 1,000 characters |
| `guardrail_name` length | 100 characters |
| `callback_url` length | 2,048 characters |
| Tags per guardrail | 32 |
| Tag name length | 64 characters |
## Best practices
For best results, create focused, single-purpose guardrails and group them with tags by context (e.g. `healthcare`, `compliance`, `sales`). A healthcare consultation PAL might attach `["healthcare", "compliance"]`, while an educational tutor PAL attaches `["child_safety"]`.
# Interaction Events
Source: https://docs.tavus.io/sections/conversational-video-interface/interactions-protocols/overview
Control CVI conversations by sending and listening to interaction events.
Interaction Events let you control and customize live conversations with a PAL in real time. You can send interaction events to the Conversational Video Interface (CVI) and listen to events the PAL sends back during the call.
### Interaction Types
* [Echo interactions](/sections/event-schemas/conversation-echo)
* [Response interactions](/sections/event-schemas/conversation-respond)
* [Interrupt interactions](/sections/event-schemas/conversation-interrupt)
* [Override conversation context interactions](/sections/event-schemas/conversation-overwrite-context)
* [Sensitivity interactions](/sections/event-schemas/conversation-sensitivity)
* [Tool Call Result](/sections/event-schemas/conversation-tool-result)
### Observable Events
* [Utterance Events](/sections/event-schemas/conversation-utterance)
* [Utterance Streaming Events](/sections/event-schemas/conversation-utterance-streaming)
* [Tool Call Events](/sections/event-schemas/conversation-toolcall)
* [Perception Tool Call Events](/sections/event-schemas/conversation-perception-tool-call)
* [Perception Analysis Events](/sections/event-schemas/conversation-perception-analysis)
* [Canvas Interaction Events](/sections/event-schemas/canvas-interaction) - Magic Canvas card taps (`submit`, `skip`, `dismiss`, etc.); see [Canvas interactions](/sections/conversational-video-interface/magic-canvas/api/interactions) for recording and history (separate from the Daily app-message events above)
* [Started/Stopped Speaking](/sections/event-schemas/conversation-started-stopped-speaking) - `conversation.started_speaking` / `conversation.stopped_speaking` with `properties.role` of `"pal"` or `"user"` (legacy duplicate PAL events use `"replica"`)
## Which Interaction Should I Send?
| Interaction | Use when |
| -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| [`conversation.respond`](/sections/event-schemas/conversation-respond) | A user typed a message and the PAL should respond as if the user had spoken that text. This is the natural text-input event for chat-style UI. |
| [`conversation.echo`](/sections/event-schemas/conversation-echo) | Your app supplies text or audio for the PAL to speak directly, such as echo-mode or custom ASR flows. |
| [`conversation.interrupt`](/sections/event-schemas/conversation-interrupt) | Your app needs to stop the PAL while it is speaking. |
## Daily `sendAppMessage` Payloads
Use Daily's `sendAppMessage(interaction, '*')` to send interaction events over the call data channel.
### Text input: `conversation.respond`
```js theme={null}
call.sendAppMessage(
{
message_type: 'conversation',
event_type: 'conversation.respond',
conversation_id: 'YOUR_CONVERSATION_ID',
properties: {
text: 'User message as if they had just finished speaking.',
},
},
'*'
);
```
### Face speaks supplied content: `conversation.echo`
```js theme={null}
call.sendAppMessage(
{
message_type: 'conversation',
event_type: 'conversation.echo',
conversation_id: 'YOUR_CONVERSATION_ID',
properties: {
modality: 'text',
text: 'Text for the PAL to speak directly.',
done: true,
},
},
'*'
);
```
For audio echo, set `modality: 'audio'`, pass base64 `audio`, include `sample_rate`, and keep `done: false` until the final audio chunk.
### Stop the face: `conversation.interrupt`
```js theme={null}
call.sendAppMessage(
{
message_type: 'conversation',
event_type: 'conversation.interrupt',
conversation_id: 'YOUR_CONVERSATION_ID',
},
'*'
);
```
## Event Ordering and Turn Tracking
All events broadcasted by Tavus include the following fields for timing, ordering, and grouping:
* **`timestamp`** (number) - Unix timestamp (seconds since epoch) indicating when the event was created. Use this to build timestamped transcripts or reconstruct the full timeline of a conversation.
* **`seq`** (integer) - A globally monotonic sequence number. Every event gets the next value in the sequence, so a higher `seq` always means the event was sent later. Use this to reconcile events that may arrive out of order over the data channel.
* **`turn_idx`** (integer, optional) - The conversation turn index. This value increments each time a [`conversation.respond`](/sections/event-schemas/conversation-respond) interaction is received, and groups all events that belong to the same conversational turn. Use it to correlate related events - for example, an utterance, its tool calls, and the PAL speaking state changes that all stem from the same user input. This field is present on conversation-related events (utterances, tool calls, speaking state changes, perception events, etc.) and omitted on events that are not tied to a specific turn.
* **`inference_id`** (string, optional) - A stable identifier for a generated utterance or inference. Use it with [`conversation.utterance`](/sections/event-schemas/conversation-utterance), [`conversation.utterance.streaming`](/sections/event-schemas/conversation-utterance-streaming), and tool-call events to reconcile optimistic UI state with the final events Tavus emits.
## Call Client Example
Interaction events use a WebRTC data channel for communication. In Tavus's case, this is powered by Daily, which makes setting up the call client quick and simple.
Here’s an example of using DailyJS to create a call client in JavaScript:
The Daily `app-message` event is used to send and receive events and interactions between your server and CVI.
```js theme={null}
```
Here’s an example of using Daily Python to create a call client in Python:
The Daily `app-message` event is used to send and receive events and interactions between your server and CVI.
```py theme={null}
call_client = None
class RoomHandler(EventHandler):
def __init__(self):
super().__init__()
def on_app_message(self, message, sender: str) -> None:
print(f"Incoming app message from {sender}: {message}")
def join_room(url):
global call_client
try:
Daily.init()
output_handler = RoomHandler()
call_client = CallClient(event_handler=output_handler)
call_client.join(url)
except Exception as e:
print(f"Error joining room: {e}")
raise
def send_message(message):
global call_client
call_client.send_app_message(message)
```
Here’s an example of using Daily React to create a call client in React:
The Daily `app-message` event is used to send and receive events and interactions between your server and CVI.
```tsx theme={null}
"use client"
import React, { useEffect, useRef, useState } from 'react';
const TavusConversation = () => {
const [message, setMessage] = useState('');
const callRef = useRef(null);
const containerRef = useRef(null);
useEffect(() => {
const loadDaily = async () => {
const DailyIframe = (await import('@daily-co/daily-js')).default;
callRef.current = DailyIframe.createFrame({
iframeStyle: {
width: '100%',
height: '500px',
border: '0',
}
});
if (containerRef.current) {
containerRef.current.appendChild(callRef.current.iframe());
}
callRef.current.on('app-message', (event) => {
console.log('app-message received:', event);
});
callRef.current.join({
url: 'YOUR_CONVERSATION_URL',
});
};
loadDaily();
return () => {
if (callRef.current) {
callRef.current.leave();
callRef.current.destroy();
}
};
}, []);
const sendAppMessage = () => {
if (!message || !callRef.current) return;
const interaction = {
message_type: 'conversation',
event_type: 'conversation.respond',
conversation_id: 'YOUR_CONVERSATION_ID',
properties: { text: message }
};
callRef.current.sendAppMessage(interaction, '*');
setMessage('');
};
return (
setMessage(e.target.value)}
placeholder="Type a message"
/>
);
};
export default TavusConversation;
```
# Knowledge Base
Source: https://docs.tavus.io/sections/conversational-video-interface/knowledge-base
Upload documents to your knowledge base for PALs to reference during conversations.
For now, our Knowledge Base only supports documents written in English and works best for conversations in English.
We'll be expanding our Knowledge Base language support soon!
Our Knowledge Base system uses RAG (Retrieval-Augmented Generation) to process and transform the contents of your documents and websites, allowing your PALs to dynamically access and leverage information naturally during a conversation.
During a conversation, our PAL will continuously analyze conversation content and pull relevant information from the documents that you have selected during conversation creation as added context.
## Getting Started With Your Knowledge Base
To leverage the Knowledge Base, you will need to upload documents or website URLs that you intend to reference from in conversations.
Let's walk through how to upload your documents and use them in a conversation.
You can either use our [PAL Maker](https://maker.tavus.io/dev/documents) or API endpoints to upload and manage your documents.
Our Knowledge Base supports creating documents from an uploaded file or a website URL.
For any documents to be created via website URL, please make sure that each document is publicly accessible without requiring authorization, such as a pre-signed S3 link.
For example, entering the URL in a browser should either:
* Open the website you want to process and save contents from.
* Open a document in a PDF viewer.
* Download the document.
You can create documents using either the [PAL Maker](https://maker.tavus.io/dev/documents) or the [Create Document](https://docs.tavus.io/api-reference/documents/create-document) API endpoint.
If you want to use the API, you can send a request to Tavus to upload your document.
Here's an example of a `POST` request to `tavusapi.com/v2/documents`.
```json theme={null}
{
"document_name": "test-doc-1",
"document_url": "https://your.document.pdf",
"callback_url": "webhook-url-to-get-progress-updates" // Optional
}
```
The response from this POST request will include a `document_id` - a unique identifier for your uploaded document. When creating a conversation, you may include all `document_id` values that you would like the PAL to have access to.
Currently, we support the following file formats: .pdf, .txt, .docx, .doc, .png, .jpg, .pptx, .csv, and .xlsx.
After your document is uploaded, it will be processed in the background automatically to allow for incredibly fast retrieval during conversations.
This process can take 5-10 minutes depending on document size.
During processing, if you have provided a `callback_url` in the [Create Document](https://docs.tavus.io/api-reference/documents/create-document) request body, you will receive periodic callbacks with status updates.
You may also use the [Get Document](https://docs.tavus.io/api-reference/documents/get-document) endpoint to poll the most recent status of your documents.
Once your documents have finished processing, you may use the `document_id` from Step 2 as part of the [Create Conversation](https://docs.tavus.io/api-reference/conversations/create-conversation) request.
You can add multiple documents to a conversation within the `document_ids` object.
```json theme={null}
{
"pal_id": "your_pal_id",
"face_id": "your_face_id",
"document_ids": ["d1234567890", "d1234567891"]
}
```
During your conversation, the PAL will be able to reference information from your documents in real time.
## Retrieval Strategy
When creating a conversation with documents, you can optimize how the system searches through your knowledge base by specifying a retrieval strategy. This strategy determines the balance between search speed and the quality of retrieved information, allowing you to fine-tune the system based on your specific needs.
You can choose from three different strategies:
* `speed`: Optimizes for faster retrieval times for minimal latency.
* `balanced`: Provides a balance between retrieval speed and quality.
* `quality` (default): Prioritizes finding the most relevant information, which may take slightly longer but can provide more accurate responses.
```json theme={null}
{
"pal_id": "your_pal_id",
"face_id": "your_face_id",
"document_ids": ["d1234567890"],
"document_retrieval_strategy": "balanced"
}
```
## Document Tags
If you have a lot of documents, maintaining long lists of `document_id` values can get tricky.
Instead of using distinct `document_ids`, you can also group documents together with shared tag values.
During the [Create Document](https://docs.tavus.io/api-reference/documents/create-document) API call, you may specify a value for `tags` for your document.
Then, when you create a conversation, you may specify the `tags` value instead of passing in discrete `document_id` values.
For example, if you are uploading course material, you could add the tag `"lesson-1"` to all documents that you want accessible in the first lesson.
```json theme={null}
{
"document_name": "test-doc-1",
"document_url": "https://your.document.pdf",
"tags": ["lesson-1"]
}
```
In the [Create Conversation](https://docs.tavus.io/api-reference/conversations/create-conversation) request, you can add the tag value `lesson-1` to `document_tags` instead of individual `document_id` values.
```json theme={null}
{
"pal_id": "your_pal_id",
"face_id": "your_face_id",
"document_tags": ["lesson-1"]
}
```
## Website Crawling
When adding a website to your knowledge base, you have two options:
### Single Page Scraping (Default)
By default, when you provide a website URL, only that single page is scraped and processed. This is ideal for:
* Landing pages with concentrated information
* Specific articles or blog posts
* Individual product pages
### Multi-Page Crawling
For comprehensive coverage of a website, you can enable **crawling** by providing a `crawl` configuration. This tells the system to start at your URL and follow links to discover and process additional pages.
```json theme={null}
{
"document_name": "Company Docs",
"document_url": "https://docs.example.com/",
"crawl": {
"depth": 2,
"max_pages": 25
}
}
```
#### Crawl Parameters
| Parameter | Range | Description |
| ----------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `depth` | 1-10 | How many link levels to follow from the starting URL. A depth of 1 crawls pages directly linked from your starting URL; depth of 2 follows links on those pages, and so on. |
| `max_pages` | 1-100 | Maximum number of pages to process. Crawling stops when this limit is reached. |
#### Crawl Limits
To ensure fair usage and system stability:
* Maximum **100 crawl documents** per account
* Maximum **5 concurrent crawls** at any time
* **1-hour cooldown** between recrawls of the same document
## Keeping Content Fresh
Website content changes over time, and you may need to update your knowledge base to reflect those changes. For documents created with crawl configuration, you can trigger a **recrawl** to fetch fresh content.
### Using the Recrawl Endpoint
Send a POST request to recrawl an existing document:
```bash theme={null}
POST https://tavusapi.com/v2/documents/{document_id}/recrawl
```
The recrawl will:
1. Use the same starting URL and crawl configuration
2. Replace old content with the new content
3. Update `last_crawled_at` and increment `crawl_count`
### Optionally Override Crawl Settings
You can provide new crawl settings when triggering a recrawl:
```json theme={null}
{
"crawl": {
"depth": 3,
"max_pages": 50
}
}
```
### Recrawl Requirements
* Document must be in `ready` or `error` state
* At least 1 hour must have passed since the last crawl
* Document must have been created with crawl configuration
See the [Recrawl Document API reference](/api-reference/documents/recrawl-document) for complete details.
## Best Practices for Documents
Following these guidelines will help your PAL deliver accurate, consistent answers from your knowledge base.
### 1. Structure Content by Topic
Organize your documents so that each one covers a single topic, feature, or policy.
**Do:**
* Create one document per topic, feature, or policy.
* Use clear section headers (e.g., Overview, Steps, Limitations, Examples).
* Keep each document tightly focused on one subject.
**Avoid:**
* Large "master" documents that cover many unrelated topics.
* Mixing multiple policies or product areas in a single file.
**Rule of thumb:** If a question can be answered by a single section of a larger document, that section should ideally be its own document.
### 2. Keep Documents Focused and Moderate in Size
Very large documents make it harder for the system to find the right information quickly.
* Split long manuals into logical sections before uploading.
* Separate policies, feature guides, and FAQs into distinct files.
* Prefer multiple focused documents over one comprehensive PDF.
Structuring your content upfront avoids the need to go back and manually break apart large files later.
### 3. Use High-Quality, Text-Based Sources
The knowledge base works best with content it can read as text.
**Best results:**
* Text-native PDFs (created digitally, not scanned)
* Structured web content
* Clearly formatted `.docx` or `.txt` files
**Lower reliability:**
* Scanned or image-based documents (text recognition can introduce errors)
* Dense tables with critical information embedded inside them
Whenever possible, provide the original text-based file rather than a scan or screenshot.
### 4. Be Explicit and Complete
The system can only retrieve information that is explicitly written in your documents. If something is not stated clearly, the PAL may not be able to surface it.
Make sure your documents include:
* Definitions and terminology
* Constraints and prerequisites
* Exceptions and edge cases
* Common variations in phrasing (e.g., both acronyms and their full forms)
If something is business-critical, state it clearly and directly in your documents.
### 5. Avoid Conflicting or Duplicate Sources
When multiple documents say slightly different things about the same topic, the PAL may return inconsistent answers.
* Maintain a single source of truth for each policy or topic.
* Archive outdated versions instead of keeping them alongside current ones.
* Avoid uploading drafts next to finalized documents.
### 6. Know When to Use PAL Instructions Instead
If certain content must appear in every response - such as required legal language or mandatory messaging - document retrieval alone may not guarantee its inclusion.
In these cases, incorporate that critical content directly into your [PAL's instructions](/sections/conversational-video-interface/pal/overview) rather than relying solely on the knowledge base.
***
## Troubleshooting
If your PAL's answers are inconsistent or incomplete, review the following:
* **Is the information buried in a very large document?** Try splitting it into smaller, focused files.
* **Are multiple documents providing conflicting guidance?** Consolidate to a single source of truth.
* **Is key information embedded in tables or images?** Convert it to structured text for better results.
* **Is the information clearly written in the document at all?** The system can only retrieve what is explicitly stated.
* **Should this content appear in every response?** If so, add it to your PAL's instructions instead.
***
## Quick Setup Checklist
* One topic per document
* No large "all-in-one" manuals
* Text-based documents (avoid scans when possible)
* Clear headings and definitions
* No duplicate or conflicting sources
# Language Support
Source: https://docs.tavus.io/sections/conversational-video-interface/language-support
Customize the conversation language using full language names supported by Tavus TTS engines.
## Supported languages
Tavus supports 43 languages for spoken interaction. By default, **`tavus-auto`** intelligently routes each conversation to the best TTS provider for that language.
Language availability also depends on your selected **STT** model. Some models support a subset of these languages. See the [STT layer configuration](/sections/conversational-video-interface/pal/stt#choosing-the-right-model) for per-model language breakdowns.
* English (en)
* French (fr)
* German (de)
* Spanish (es)
* Portuguese (pt)
* Chinese (zh)
* Japanese (ja)
* Hindi (hi)
* Italian (it)
* Korean (ko)
* Dutch (nl)
* Polish (pl)
* Russian (ru)
* Swedish (sv)
* Turkish (tr)
* Tagalog (tl)
* Bulgarian (bg)
* Romanian (ro)
* Arabic (ar)
* Czech (cs)
* Greek (el)
* Finnish (fi)
* Croatian (hr)
* Malay (ms)
* Slovak (sk)
* Danish (da)
* Tamil (ta)
* Ukrainian (uk)
* Hungarian (hu)
* Norwegian (no)
* Vietnamese (vi)
* Bengali (bn)
* Thai (th)
* Hebrew (he)
* Georgian (ka)
* Indonesian (id)
* Telugu (te)
* Gujarati (gu)
* Kannada (kn)
* Malayalam (ml)
* Marathi (mr)
* Punjabi (pa)
* Swahili (sw)
For a full list of supported languages for each TTS engine, please click on the following links:
By default, Tavus uses the **`tavus-auto`** TTS engine. It intelligently routes the conversation to the best TTS provider for that language. Only set `tts_engine` explicitly when you need a specific provider.
If your language is not in the list above, see [Additional language support via Azure](#additional-language-support-via-azure).
## Setting the Conversation Language
To specify a language, use the `properties.language` parameter in the Create Conversation. **You must use the full language name**, not a language code.
```shell cURL {9} theme={null}
curl --request POST \
--url https://tavusapi.com/v2/conversations \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"pal_id": "pcb7a34da5fe",
"face_id": "r90bbd427f71",
"properties": {
"language": "spanish"
}
}'
```
Language names must match exactly with those supported by the selected TTS engine.
### Smart Language Detection
To automatically detect the participant’s spoken language throughout the conversation, set `language` to `multilingual` when creating the conversation:
```shell cURL {9} theme={null}
curl --request POST \
--url https://tavusapi.com/v2/conversations \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"pal_id": "pcb7a34da5fe",
"face_id": "r90bbd427f71",
"properties": {
"language": "multilingual"
}
}'
```
This enables the **STT** (speech-to-text) engine to automatically switch languages, dynamically adjusting the pipeline to transcribe and respond in the detected language throughout the conversation.
For the highest accuracy, we recommend setting a specific language rather than using `multilingual`. Smart Language Detection works best as a fallback when the participant's language is unknown ahead of time.
## Additional language support via Azure
There may be additional language support via Azure if you need a language that is not covered above. Use Azure only as a fallback in that case - prefer the default `tavus-auto` routing whenever your language is already supported.
For the latest supported languages, check [Azure Speech TTS language support](https://learn.microsoft.com/en-us/azure/ai-services/speech-service/language-support?tabs=tts).
Unlike Cartesia and ElevenLabs (which Tavus hosts for you), Azure requires you to bring your own Azure Speech API key:
1. Set `tts_engine` to `azure` **explicitly** on the PAL's [TTS layer](/sections/conversational-video-interface/pal/tts).
2. Provide **your own Azure Speech resource** `api_key`. The resource must be in the **East US** region (Tavus synthesizes via `eastus`; a key from another region returns an authentication error). Set `external_voice_id` to any standard Azure neural voice available in East US (for example `de-DE-KatjaNeural`); Custom Neural Voices must be deployed in that same resource.
```json theme={null}
"layers": {
"tts": {
"tts_engine": "azure",
"api_key": "your-azure-speech-key",
"external_voice_id": "de-DE-KatjaNeural"
}
}
```
Any Azure voice can speak any supported language, carrying the voice's own accent. For a natural accent, pick a voice whose locale matches your target language (e.g. a `de-DE-*` voice for German), or use one of Azure's `*MultilingualNeural` voices.
# PAL Canvas Configuration
Source: https://docs.tavus.io/sections/conversational-video-interface/magic-canvas/api/configuration
Full reference for the magic_canvas skill on PALs: attaching it, the component overlay, the skills API, and every validation error.
To configure **Magic Canvas**, attach the `magic_canvas` skill to the PAL. The skill controls which Canvas components the PAL may use; the PAL decides at runtime when to show one.
Attaching the skill enables every component with defaults; there is no per-component opt-in. Audio-only, chat, and external-meeting conversations never get Canvas actions; see [how the configuration reaches a conversation](#how-the-configuration-reaches-a-conversation).
## Attaching the Skill
Attach with a single `PUT`:
```bash theme={null}
curl -X PUT https://tavusapi.com/v2/pals/{pal_id}/skills/magic_canvas \
-H "x-api-key: " \
-H "Content-Type: application/json" \
-d '{ "config": {} }'
```
```json theme={null}
{
"skill_id": "magic_canvas",
"config": {},
"attached_at": "2026-06-10T17:04:52.183947+00:00",
"updated_at": "2026-06-10T17:04:52.183947+00:00"
}
```
Common configurations, shown as the PAL's `skills` value:
```json theme={null}
// Everything on, defaults (scheduling_embed inactive: needs config)
"skills": { "magic_canvas": { "config": {} } }
```
```json theme={null}
// Everything on + the scheduling embed configured
"skills": { "magic_canvas": { "config": {
"components": { "scheduling_embed": { "provider": "calendly", "scheduling_url": "https://calendly.com/x/30min" } }
} } }
```
```json theme={null}
// Everything except chart
"skills": { "magic_canvas": { "config": {
"components": { "chart": { "enabled": false } }
} } }
```
```json theme={null}
// Everything on + two Knowledge Base images the PAL may show
"skills": { "magic_canvas": { "config": {
"components": { "image": { "images": [
{ "document_id": "d1234567890", "caption": "Aurora Desk in walnut", "prompt": "Show when the user asks about finishes." },
{ "document_id": "d2468101214", "caption": "Cable tray, underside detail" }
] } }
} } }
```
```json theme={null}
// Everything on + web images from allowlisted sites
"skills": { "magic_canvas": { "config": {
"components": { "image": { "web": {
"enabled": true,
"domains": ["photos.example.com", "*.cdn.example.com"],
"guidance": "Show the listing photo whenever search_listings returns one."
} } }
} } }
```
```json theme={null}
// Everything on + guidance on when to show cards
"skills": { "magic_canvas": { "config": {
"usage_guidance": "Show a chart whenever you compare numbers; ask with a question card before booking."
} } }
```
## Component Overlay
`config.components` is a sparse overlay, not an allowlist. Components you don't mention stay enabled with defaults. Add an entry to:
* **Configure a component**: for example, set `scheduling_url` on `scheduling_embed`.
* **Disable a component**: set `{ "enabled": false }`.
New components Tavus adds later are enabled automatically on PALs with the skill attached. Disable them per component to opt out.
Two components need config before they do anything. `scheduling_embed` only
activates once `scheduling_url` is set, and `image` only activates once you
curate at least one Knowledge Base image or enable its `web` allowlist.
Attaching the skill without that config doesn't error; the component stays
inactive.
`config` is strictly validated: an unknown component name or stray field returns `400`. A misspelled skill id in the URL returns `404`: `Unknown skill '...'` on `PUT` and `PATCH`, the not-attached `404` on `GET` and `DELETE`.
## When cards appear
There is no API to show a specific card. The PAL chooses when to call a Canvas action during the conversation. You influence that in three places:
| Lever | Where | Best for |
| ---------------------- | ----------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `usage_guidance` | `config.usage_guidance` on the `magic_canvas` skill (the **When to use Magic Canvas** field in the builder) | Canvas-specific rules, for example "show a chart when you compare numbers" or "ask with a question card before booking" |
| System prompt | The PAL's main prompt | Persona, conversation flow, and broader guidance on when UI helps |
| Conversational context | `conversational_context` at conversation create | Session-specific facts (availability, user details) the PAL can use when picking a card |
Tavus appends `usage_guidance` to the Canvas system prompt. It is ignored when blank or when no component is active.
Start by disabling components the PAL should not use (`config.components..enabled: false`). An enabled component can still appear even if your prompts never mention it.
```json theme={null}
{
"config": {
"usage_guidance": "Show a chart whenever you compare numbers; ask with a question card before booking."
}
}
```
## Components
Each active component gives the PAL one action named `canvas_show_` (for example, `canvas_show_question`). When at least one component is active, the PAL also gets `canvas_clear`. See the [components overview](/sections/conversational-video-interface/magic-canvas/components) for descriptions and per-component reference.
Every card renders inline in the `safe-area-right` side rail next to the PAL video. Placement is client-side; the PAL does not choose the side. Only one card is on screen at a time: showing a card replaces whatever is currently displayed.
### Component Fields
| Field | Type | Required | Description |
| ---------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `enabled` | boolean | ❌ | Accepted by every component. Defaults to `true`. Set `false` to disable the component. |
| `provider` | string | ❌ | `scheduling_embed` only. Defaults to `"calendly"`, the only supported provider. |
| `scheduling_url` | string | ❌ | `scheduling_embed` only. Your booking link, delivered to the embed exactly as configured. Defaults to `""` (component inactive). |
| `images` | array | ❌ | `image` only. The Knowledge Base images the PAL may show, at most 50. Defaults to `[]` (component inactive). See [`images` rules](#images-rules). |
| `web` | object | ❌ | `image` only. Lets the PAL show images from URLs it received mid-conversation, restricted to websites you allowlist. Defaults to disabled. See [`web` rules](#web-rules). |
### `images` Rules
Each entry is `{ "document_id", "caption", "prompt" }`:
* `document_id` (required) is a [Knowledge Base](/sections/conversational-video-interface/knowledge-base) document id. (Once an image is uploaded to the Knowledge Base, it receives a `document_id`.) It must be accessible to the account that owns the PAL and must have finished processing, both checked when you save the config.
* `caption` (optional, max 280 characters) is shown under the image. Blank falls back to the description Tavus generated for the image, then the document name.
* `prompt` (optional, max 500 characters) tells the PAL when this image applies. It is never shown to the user.
The PAL selects a curated image by `document_id`; the URL it renders from is resolved by Tavus, never supplied by the model. See the [`image` component](/sections/conversational-video-interface/magic-canvas/components/image) for the full flow.
### `web` Rules
`image.web` is `{ "enabled", "domains", "guidance" }` and adds a second image source: URLs the PAL received during the conversation (typically from a tool result), restricted to websites you allowlist.
* `enabled` (optional) defaults to `false` and requires at least one entry in `domains` to turn on. `false` keeps a saved list without deleting it.
* `domains` (optional, max 20 entries) lists the allowed websites. Each entry is an exact host (`photos.example.com`) or a leading wildcard (`*.cdn.example.com`, matching any depth of subdomain, never the apex). Entries are hostname-only, max 253 characters, punycode for international names, and are normalized on save (lowercased, trailing dot stripped, deduplicated). IP addresses, local and internal names, and wildcards over shared suffixes (`*.github.io`, `*.co.uk`) are rejected.
* `guidance` (optional, max 500 characters) tells the PAL when to show a web image. Never shown to the user.
With a live allowlist, the compiled `canvas_show_image` action additionally accepts `images[].url`; every URL is checked at render time against a fixed safety floor (https, public hostname) and then your allowlist. See [how a URL becomes an image](/sections/conversational-video-interface/magic-canvas/components/image#how-a-url-becomes-an-image).
### `scheduling_url` Rules
A non-empty `scheduling_url` must be a public HTTPS URL:
* Must start with `https://` and include a hostname.
* At most 2048 characters.
* `localhost` and cloud metadata hostnames (such as `metadata.google.internal`) are rejected.
* IP-based hosts (including hex and other numeric encodings) in private, loopback, link-local, or otherwise internal address space are rejected.
* An empty string is allowed and means "not configured yet."
## Skills API
Skill attachments live under `/v2/pals/{pal_id}/skills`. Full HTTP reference:
* [List PAL skills](/api-reference/pal-skills/list-pal-skills)
* [Get PAL skill](/api-reference/pal-skills/get-pal-skill)
* [Attach skill to PAL](/api-reference/pal-skills/attach-skill-to-pal)
* [Update PAL skill](/api-reference/pal-skills/update-pal-skill)
* [Detach skill from PAL](/api-reference/pal-skills/detach-skill-from-pal)
* [Replace PAL skills](/api-reference/pal-skills/replace-pal-skills)
Endpoints:
| Method & path | What it does |
| -------------------------------------------- | ------------------------------------------------------------ |
| `GET /v2/pals/{pal_id}/skills` | List every skill attached to the PAL. |
| `PUT /v2/pals/{pal_id}/skills` | Replace the PAL's **entire** skill set in one request. |
| `GET /v2/pals/{pal_id}/skills/{skill_id}` | Read one attachment. |
| `PUT /v2/pals/{pal_id}/skills/{skill_id}` | Attach a skill, or overwrite its config if already attached. |
| `PATCH /v2/pals/{pal_id}/skills/{skill_id}` | Merge changes into an existing attachment's config. |
| `DELETE /v2/pals/{pal_id}/skills/{skill_id}` | Detach a skill. |
You can read skills on stock PALs, but you can only modify skills on PALs you own.
### Reading Configuration
The single-skill `GET` returns the attachment as stored:
```json theme={null}
{
"skill_id": "magic_canvas",
"config": {
"components": { "chart": { "enabled": false } }
},
"attached_at": "2026-06-10T17:04:52.183947+00:00",
"updated_at": "2026-06-11T09:31:08.412605+00:00"
}
```
The per-skill `PUT`, `PATCH`, and `GET` return the attachment object directly. The list `GET` and the bulk `PUT` wrap attachments in a `data` map keyed by skill id:
```json theme={null}
{
"data": {
"magic_canvas": {
"skill_id": "magic_canvas",
"config": {},
"attached_at": "2026-06-10T17:04:52.183947+00:00",
"updated_at": "2026-06-10T17:04:52.183947+00:00"
}
}
}
```
### Updating with PATCH
`PATCH` requires a `{ "config": ... }` body (unlike `PUT`, where it defaults to `{}`), merges it into the existing config, and returns `404` if the skill isn't attached:
```bash theme={null}
curl -X PATCH https://tavusapi.com/v2/pals/{pal_id}/skills/magic_canvas \
-H "x-api-key: " \
-H "Content-Type: application/json" \
-d '{ "config": { "components": { "chart": { "enabled": false } } } }'
```
Merge rules:
* The merge is **shallow, at the top level of `config`**: a PATCH that includes `components` replaces the whole overlay map rather than deep-merging per component. Send the complete set of overrides you want to end up with.
* Setting a top-level config key to `null` removes it: `{ "config": { "components": null } }` clears every override and returns the skill to defaults.
The merged result is re-validated in full.
### Replacing All Skills
`PUT /v2/pals/{pal_id}/skills` takes `{ "skills": { ... } }` and replaces the PAL's **entire** skill set:
```bash theme={null}
curl -X PUT https://tavusapi.com/v2/pals/{pal_id}/skills \
-H "x-api-key: " \
-H "Content-Type: application/json" \
-d '{ "skills": { "magic_canvas": { "config": {} } } }'
```
The bulk PUT is a full replace: any skill missing from the payload is detached.
To change only Canvas, use the per-skill `PUT` or `PATCH` instead.
Skills that were already attached keep their original `attached_at`.
### Detaching
```bash theme={null}
curl -X DELETE https://tavusapi.com/v2/pals/{pal_id}/skills/magic_canvas \
-H "x-api-key: "
```
Returns `204` with no body. Detaching removes Canvas actions from all future conversations and deletes the attachment's config; re-attaching starts from the config you send next.
## Validation Errors
Errors return `{ "error": "..." }`:
| Status | When | Error message contains |
| ------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `404` | `PUT` or `PATCH` with a skill id that isn't registered (misspelled `magic_canvas`) | `Unknown skill ''` |
| `404` | `GET`, `PATCH`, or `DELETE` on a skill that isn't attached to the PAL | `Skill '' is not attached to this PAL` |
| `400` | Unknown key anywhere inside `config` (misspelled component, stray field) | the offending key and `Unknown field.` |
| `400` | `scheduling_url` doesn't start with `https://` | `scheduling_url must use HTTPS` |
| `400` | `scheduling_url` is longer than 2048 characters | `scheduling_url must be at most 2048 characters` |
| `400` | `scheduling_url` has no hostname | `scheduling_url must include a hostname` |
| `400` | `scheduling_url` host is `localhost` or a cloud metadata hostname | `scheduling_url host '' is not allowed` |
| `400` | `scheduling_url` host is an internal IP address | `scheduling_url IP '' is not allowed (internal address space)` |
| `400` | More than 50 curated images | `image.images must have at most 50 entries` |
| `400` | A curated image `caption` is longer than 280 characters | `image caption must be at most 280 characters` |
| `400` | A curated image `prompt` is longer than 500 characters | `image prompt must be at most 500 characters` |
| `400` | A curated `document_id` isn't accessible to the PAL's owner | `One or more document_ids are not accessible` |
| `400` | A curated `document_id` hasn't finished processing | `One or more document_ids are not ready for display` |
| `400` | `web.enabled` is `true` with an empty `domains` | `image.web.enabled requires at least one entry in image.web.domains` |
| `400` | More than 20 allowlisted domains | `image.web.domains must have at most 20 entries` |
| `400` | `web.guidance` is longer than 500 characters | `image.web.guidance must be at most 500 characters` |
| `400` | A `domains` entry has a scheme, port, path, or userinfo | `image.web.domains entry '' must be a bare hostname (no scheme, port, path, or userinfo)` |
| `400` | A `domains` entry is an IP address, in any spelling | `image.web.domains entry '' is an IP address (); allowlist a hostname instead` |
| `400` | A `domains` entry is `localhost` or an internal-network name | `image.web.domains entry '' is not a public hostname` |
| `400` | A wildcard entry covers a shared suffix | `image.web.domains entry '*.github.io' would allowlist every site under 'github.io'; wildcard a single site (*.site.example.com) or list the exact hosts instead` |
| `400` | The `pal_id` in the URL doesn't exist | `Bad Request. PAL not found` |
## How the configuration reaches a conversation
At conversation create, Tavus resolves the PAL's Canvas action list once:
* Audio-only (`audio_only: true`), chat, and external-meeting (`meeting_url`: Zoom, Teams, Meet) conversations never get Canvas actions.
* Every other conversation with the skill attached gets one `canvas_show_` action per active component.
* `image` resolves its curated Knowledge Base documents at this point. If none of them resolve to a displayable image and `web` is not enabled, no `canvas_show_image` action is compiled. With a live `web` allowlist, the compiled action also accepts image URLs.
* `canvas_clear` is added once at least one component is active.
* Canvas actions never overwrite a [tool](/sections/conversational-video-interface/pal/tools) you defined with the same name; your tool wins.
# Canvas Interactions
Source: https://docs.tavus.io/sections/conversational-video-interface/magic-canvas/api/interactions
Record card taps, receive canvas.interaction webhooks, and fetch Canvas interaction history.
**Canvas interactions** record what a user does on a Canvas card: tap, submit, skip, or dismiss. Each interaction is recorded with a POST, delivered to your `callback_url` as a `canvas.interaction` webhook, and queryable with the history GET. All endpoints live under `https://tavusapi.com`.
These are card taps on Magic Canvas, not [Interaction Events](/sections/conversational-video-interface/interactions-protocols/overview) (the Daily app-message protocol for controlling a live call).
## When Magic Canvas is active
No per-conversation configuration is required. A PAL gets Canvas actions when both are true:
* The PAL has the **Magic Canvas skill** attached: `PUT /v2/pals/{pal_id}/skills/magic_canvas`, covered in [Configuring your PAL](/sections/conversational-video-interface/magic-canvas/api/configuration).
* The conversation has a rendering surface. Audio-only conversations, chat conversations, and conversations that join an external meeting via `meeting_url` (Zoom, Teams, Google Meet) get no Canvas actions.
When both are true, the PAL gets one action per enabled component, the control action `canvas_clear`, and guidance on when to show each component. See [When cards appear](/sections/conversational-video-interface/magic-canvas/api/configuration#when-cards-appear) for how `usage_guidance`, the system prompt, and conversational context fit together. This applies to every conversation start path, including hosted deployments (`POST /v2/deployments/{deployment_id}/start`).
## Record an Interaction
[`POST /v2/conversations/{conversation_id}/canvas/interactions`](/api-reference/canvas-interactions/record-canvas-interaction)
The Tavus-hosted embed and `@tavus/cvi-ui` post interactions for you; call this endpoint directly only if you build your own renderer.
```bash theme={null}
curl -X POST https://tavusapi.com/v2/conversations/{conversation_id}/canvas/interactions \
-H "Content-Type: application/json" \
-d '{
"interaction_id": "ci_call_8f2d41_submit_5e0b7c2a",
"tool_call_id": "call_8f2d41",
"component": "canvas.question",
"component_version": "v1",
"type": "submit",
"value": { "selected_option_ids": ["opt_2"], "skipped": false },
"metadata": { "client": "kiosk-web" }
}'
```
A successful request returns `200`:
```json theme={null}
{ "success": true }
```
An identical retry returns the same `200`; see [Idempotency and delivery guarantees](#idempotency-and-delivery-guarantees).
### Authentication
No API key or token is required while the conversation is active. Once the conversation ends, every POST is rejected with a 400.
### Rate limiting
The interaction POST is rate-limited at **120 requests per 60-second window** per `(client IP, conversation_id)`. The limit is checked before schema validation or any database work.
When you exceed the limit, the API returns **HTTP 429** with a `Retry-After` header (seconds until the current window resets) and a body of `{ "error": "Too many requests" }`. Back off and retry after that interval.
Custom renderers that post `heartbeat` interactions count toward this limit. Space heartbeats accordingly; the Tavus-hosted embed and `@tavus/cvi-ui` stay within the budget for normal sessions.
Never put your Tavus API key in a browser. The interaction POST doesn't need
it, and nothing client-side ever should.
### Request Body
| Field | Type | Required | Description |
| ------------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `interaction_id` | string | ✅ | 1–128 chars. Your idempotency key: unique per logical interaction, reused verbatim on retries. The Tavus client generates `ci_{tool_call_id}_{type}_{uuid}`. |
| `tool_call_id` | string | ✅ | 1–128 chars. The id of the Canvas invocation that showed the card. Ties the interaction to a specific card instance. |
| `component` | string | ✅ | One of the component ids below. |
| `component_version` | string | ✅ | The component's contract version. `v1` for all current components. |
| `type` | string | ✅ | One of `submit`, `skip`, `dismiss`, `clear`, `error`, `heartbeat`. The component must also allow it (see below). |
| `value` | object | ✅ | The interaction payload. At most 16 KB serialized. Shape depends on `component` and `type`. |
| `metadata` | object | ❌ | Your own annotations, at most 4 KB serialized. Defaults to `{}`. Stored and delivered as-is; never validated against the component contract. |
Unknown top-level fields are rejected with a 400.
Component ids: `canvas.question`, `canvas.input`, `canvas.calendar`, `canvas.scheduling_embed`, `canvas.text`, `canvas.image`, `canvas.chart`, `canvas.alert`.
This endpoint accepts only the component ids above.
### Interaction Types
| `type` | Sent when | Allowed on |
| ----------- | --------------------------------------------------------------------------------------------- | --------------------------------------------------- |
| `submit` | The user completed the card: picked an answer, typed a value, chose a slot, booked a meeting. | `question`, `input`, `calendar`, `scheduling_embed` |
| `skip` | The user explicitly skipped the card without answering. | `question`, `input`, `calendar`, `scheduling_embed` |
| `dismiss` | The user closed the card. | all components |
| `clear` | The card was cleared from the canvas. | all components |
| `error` | The card hit a render or runtime error. | all components |
| `heartbeat` | A liveness ping from a long-lived card. | all components |
Display-only components (`text`, `image`, `chart`, `alert`) allow only the four lifecycle types; sending `submit` to one returns a 400.
### `value` Rules per Component
For `submit` and `skip`, `value` is validated against the component's contract. The lifecycle types (`dismiss`, `clear`, `error`, `heartbeat`) pass `value` through without component-specific validation.
| Component | `skip` rules | `submit` rules |
| ------------------------- | ----------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `canvas.question` | `skipped: true` and no answer payload. | `skipped: false` with at least one selected option or a non-empty `custom_text`. Allowed keys are exactly `selected_option_ids` (list of strings, required), `skipped` (boolean, required), `custom_text` (max 4,000 chars), and `option_texts` (maps option ids to display strings of max 2,000 chars; every key must also appear in `selected_option_ids`). Unknown keys are rejected. |
| `canvas.input` | `{ "skipped": true }` | Requires `input_type` (one of `text`, `email`, `number`, `tel`) and `value`. For `number` the value must be numeric; otherwise it must be a string. |
| `canvas.calendar` | `{ "skipped": true }` | Requires exactly one of: `selected_date` (string), `selected_slot` (`{ "id", "start", "end" }`), `selected_slots` (non-empty list of slots), or `selected_range` (`{ "start", "end" }`). |
| `canvas.scheduling_embed` | Not validated: only `submit` is checked against the contract; other types pass `value` through. | Allowed keys are exactly `provider`, `scheduled`, `event_uri`, `invitee_uri`. `provider` must be `"calendly"` and `scheduled` must be `true`. The URI fields are optional but must be `https` Calendly URLs. |
Example `value` payloads:
```json submit payloads by component [expandable] theme={null}
// canvas.question
{ "selected_option_ids": ["opt_2"], "skipped": false, "option_texts": { "opt_2": "2–10 people" } }
// canvas.input
{ "input_type": "email", "value": "ada@example.com" }
// canvas.calendar
{ "selected_slot": { "id": "slot_tue_10", "start": "2026-06-16T10:00:00Z", "end": "2026-06-16T10:30:00Z" } }
// canvas.scheduling_embed
{ "provider": "calendly", "scheduled": true, "event_uri": "https://api.calendly.com/scheduled_events/AAAA" }
```
### Errors
Schema and contract validation failures return an `{ "error", "fields" }` envelope. Conversation-state errors (400) and conflicts (409) return a `{ "message": "" }` envelope.
| Status | Body | Meaning |
| ------ | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400 | `{ "error": "Invalid canvas interaction payload.", "fields": ["", "_schema", ...] }` | The body failed schema or contract validation. `fields` lists the offending top-level field names only (max 10, each truncated to 64 chars); cross-field failures surface under the single key `_schema`. No marshmallow detail strings are returned. |
| 400 | `{ "message": "Invalid conversation_id" }` | No conversation exists with this id. Note this is a 400, not a 404. |
| 400 | `{ "message": "Canvas interactions can only be recorded for active conversations." }` | The conversation has ended (or hasn't started). |
| 409 | `{ "message": "Interaction does not match the issued canvas instance for this tool_call_id." }` | The `component` or `component_version` doesn't match the card Tavus issued for this `tool_call_id`. |
| 409 | `{ "message": "interaction_id was already recorded with a different payload." }` | This `interaction_id` was already used with different contents. Retries must be byte-identical; new interactions need a new id. |
| 429 | `{ "error": "Too many requests" }` | Rate limit exceeded (120 POSTs per 60-second window per client IP and conversation). Response includes a `Retry-After` header. Back off and retry. |
The following checks run internally and all surface through the `{ "error", "fields" }` envelope above; their internal messages (component, component\_version, interaction-type, and size rules) are not returned to the client:
* An unsupported component id.
* A `component_version` that doesn't match the component's current version (`v1`).
* An interaction type the component doesn't allow (for example, `submit` on `canvas.text`).
* `value` over 16 KB or `metadata` over 4 KB serialized.
* A `value` that breaks the component's `submit` or `skip` rules; see the per-component rules above.
The size caps apply to the serialized JSON of `value` and `metadata` individually, not to the raw request body.
## The `canvas.interaction` Webhook
Every recorded interaction is delivered to your conversation webhook as a [`canvas.interaction`](/sections/event-schemas/canvas-interaction) event, on the same callback pipeline as all other conversation events. Set `callback_url` when creating the conversation and route on `event_type === "canvas.interaction"`; `properties` carries the interaction exactly as recorded.
```json theme={null}
{
"message_type": "canvas",
"event_type": "canvas.interaction",
"conversation_id": "c123456",
"timestamp": "2026-06-09T21:14:03Z",
"properties": {
"conversation_id": "c123456",
"interaction_id": "ci_call_8f2d41_submit_5e0b7c2a",
"tool_call_id": "call_8f2d41",
"component": "canvas.question",
"component_version": "v1",
"type": "submit",
"value": { "selected_option_ids": ["opt_2"], "skipped": false },
"metadata": { "client": "kiosk-web" },
"created_at": "2026-06-09T21:14:03.518923"
}
}
```
`properties` fields:
| Field | Type | Required | Description |
| ------------------- | -------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `conversation_id` | string | ✅ | The conversation the interaction belongs to. |
| `interaction_id` | string | ✅ | The client's idempotency key for this interaction. |
| `tool_call_id` | string | ✅ | The Canvas invocation that showed the card. |
| `component` | string | ✅ | Component id, e.g. `canvas.question`. |
| `component_version` | string | ✅ | Component contract version, e.g. `v1`. |
| `type` | string | ✅ | One of `submit`, `skip`, `dismiss`, `clear`, `error`, `heartbeat`. |
| `value` | object | ✅ | The interaction payload, exactly as validated and stored. |
| `metadata` | object | ✅ | The client's annotations, passed through unvalidated. |
| `created_at` | string or null | ✅ | When the interaction was recorded. A naive ISO-8601 timestamp with microseconds and **no** timezone suffix (no trailing `Z`). The value is UTC; parse it accordingly. The top-level `timestamp` on the webhook envelope, by contrast, carries a `Z`. |
`skip` and `dismiss` events are delivered as well as `submit`; count them
if you track completion rates.
## Fetch History
[`GET /v2/conversations/{conversation_id}/canvas/interactions`](/api-reference/canvas-interactions/list-canvas-interactions)
Requires your API key; you must own the conversation. Call it from your backend, not the browser.
```bash theme={null}
curl https://tavusapi.com/v2/conversations/{conversation_id}/canvas/interactions \
-H "x-api-key: "
```
```json theme={null}
{
"data": [
{
"conversation_id": "c123456",
"interaction_id": "ci_call_8f2d41_submit_5e0b7c2a",
"tool_call_id": "call_8f2d41",
"component": "canvas.question",
"component_version": "v1",
"type": "submit",
"value": { "selected_option_ids": ["opt_2"], "skipped": false },
"metadata": { "client": "kiosk-web" },
"created_at": "2026-06-09T21:14:03.518923"
}
]
}
```
* Items use the same fields as the webhook's `properties` object.
* Ordered oldest first (`created_at`, then insertion order as a tiebreaker).
* Readable during and after the conversation: writes stop when the call ends, reads don't. Use it for post-call processing and to reconcile against your webhook log.
## Idempotency and Delivery Guarantees
* **The POST is idempotent on `(conversation_id, interaction_id)`.** An identical retry returns `200`, stores nothing new, and fires no second webhook. Safe to retry on timeouts.
* **"Identical" means `tool_call_id`, `component`, `component_version`, `type`, and `value` all match.** `metadata` is excluded; the first recorded `metadata` is kept and delivered.
* **A reused `interaction_id` with a different payload is a 409**, never a silent overwrite.
* **The webhook fires once, when the interaction is first recorded.** Replays, retries, and concurrent duplicates never re-fire it.
* **The interaction store is the source of truth.** In rare failure cases an event may not reach your endpoint even though the interaction was stored. If completeness matters, reconcile with the history GET after the call.
* **Self-generated `interaction_id`s** must be unique per logical interaction and reused on every retry of that interaction. The Tavus client uses `ci_{tool_call_id}_{type}_{uuid}`.
# Canvas Components
Source: https://docs.tavus.io/sections/conversational-video-interface/magic-canvas/components
Every card the PAL can show, and how to enable and configure them.
**Canvas components** are the cards a PAL can render on the Magic Canvas surface during a conversation. Eight components: four interactive, four display-only:
Magic Canvas adds **Canvas actions** - built-in LLM functions such as `canvas_show_question` - when the skill is attached. These are not the same as [Tools](/sections/conversational-video-interface/pal/tools) you create via the Tools API and attach to a PAL. If a name collides, your Tools API definition wins.
| Component | Description | Interactive |
| ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------ |
| [`question`](/sections/conversational-video-interface/magic-canvas/components/question) | Ask a multiple-choice question, optionally with a free-text "Other" | Yes |
| [`input`](/sections/conversational-video-interface/magic-canvas/components/input) | Ask for a single typed value: text, email, number, or phone | Yes |
| [`calendar`](/sections/conversational-video-interface/magic-canvas/components/calendar) | Let the user pick a date, a time slot, or a date range | Yes |
| [`scheduling_embed`](/sections/conversational-video-interface/magic-canvas/components/scheduling-embed) | Embed your real scheduling page (e.g. Calendly) for live booking | Yes |
| [`text`](/sections/conversational-video-interface/magic-canvas/components/text) | Show a card of formatted text | Dismiss only |
| [`image`](/sections/conversational-video-interface/magic-canvas/components/image) | Show an image from your Knowledge Base or an allowlisted website | Dismiss only |
| [`chart`](/sections/conversational-video-interface/magic-canvas/components/chart) | Show a bar, line, or pie chart | Dismiss only |
| [`alert`](/sections/conversational-video-interface/magic-canvas/components/alert) | Show a dismissible notice | Dismiss only |
## Enabling and configuring components
Components come from the PAL's `magic_canvas` skill. Attach it:
```bash theme={null}
curl -X PUT https://tavusapi.com/v2/pals/{pal_id}/skills/magic_canvas \
-H "x-api-key: " \
-H "Content-Type: application/json" \
-d '{ "config": {} }'
```
Attaching the skill enables **every component** with its defaults. `config.components` is a sparse overlay, not an allowlist; add an entry only to configure or disable:
```json theme={null}
{ "config": { "components": { "chart": { "enabled": false } } } }
```
Overlay rules:
* Per-component `enabled` defaults to `true`.
* `scheduling_embed` stays inactive until `scheduling_url` is set; a bare attach doesn't error.
* `image` stays inactive until you curate at least one Knowledge Base image or enable its `web` allowlist; a bare attach doesn't error.
Tavus compiles Canvas actions only for conversations with a rendering surface.
Audio-only, text-chat, and external `meeting_url` (Zoom, Teams, Meet)
conversations get no Canvas actions, regardless of PAL configuration.
See [Canvas configuration](/sections/conversational-video-interface/magic-canvas/api/configuration) for the full config shape, PATCH/bulk-PUT/DELETE endpoints, and validation errors.
## Control actions
Each active component gives the PAL one `canvas_show_` action. With at least one component active, the PAL also gets `canvas_clear`, which clears the whole canvas.
Showing a card replaces the current one. The PAL does not update cards in place. To clear the canvas without showing a new card, the PAL calls `canvas_clear`.
Canvas actions are model-driven. Steer when the PAL shows a card with `usage_guidance`, the PAL's system prompt, or conversational context ([when cards appear](/sections/conversational-video-interface/magic-canvas/api/configuration#when-cards-appear)). A Canvas action never overwrites a [tool](/sections/conversational-video-interface/pal/tools) you defined with the same name; your tool wins (see [Canvas configuration](/sections/conversational-video-interface/magic-canvas/api/configuration#how-the-configuration-reaches-a-conversation)).
## Default placement
By default, cards are placed to the right of the PAL. Placement can be further customized when using the `@tavus/cvi-ui` package.
# Component: alert
Source: https://docs.tavus.io/sections/conversational-video-interface/magic-canvas/components/alert
Show a dismissible notice (info, success, warning, or error) inside a live conversation.
The **`alert`** component (`canvas.alert`, `v1`) shows a short notice during a conversation. It renders inline in a side rail (default `safe-area-right`).
Alerts are display-only; the only interaction a backend normally receives is a `dismiss`.
`alert` has no settings beyond `enabled` (default `true`); see [Enabling and configuring components](/sections/conversational-video-interface/magic-canvas/components#enabling-and-configuring-components):
```json theme={null}
{ "config": { "components": { "alert": { "enabled": false } } } }
```
## Component Behavior
The PAL shows an alert for important notices, warnings, or success confirmations; general reading material belongs in the [`text`](/sections/conversational-video-interface/magic-canvas/components/text) component.
* Only one alert shows at a time; a new alert replaces the previous.
Alerts are always user-dismissible. There is no non-dismissible option.
## Arguments
The PAL invokes `canvas_show_alert`:
| Field | Type | Required | Description |
| ---------------------- | ------- | -------- | -------------------------------------------------------------------------------------------------------- |
| `severity` | string | ❌ | Visual tone. One of `info`, `success`, `warning`, `error`. Defaults to `info` if absent. |
| `body` | string | ✅ | The message itself. 1–1200 characters. |
| `title` | string | ❌ | Short heading above the body. 1–120 characters. |
| `auto_dismiss_seconds` | integer | ❌ | Auto-dismiss the alert after this many seconds (1–120). The alert stays manually dismissible regardless. |
Unknown arguments are rejected (`additionalProperties: false`).
```json Example invocation theme={null}
{
"title": "Heads up",
"body": "This is a Magic Canvas alert.",
"severity": "info",
"auto_dismiss_seconds": 15
}
```
## Interactions
Alerts are **lifecycle-only**: they never produce `submit` or `skip`; allowed types are `dismiss`, `clear`, `error`, and `heartbeat`. Each reaches the conversation webhook as a `canvas.interaction` event:
```json dismiss event theme={null}
{
"event_type": "canvas.interaction",
"properties": {
"conversation_id": "c123456",
"interaction_id": "i-5",
"tool_call_id": "call_def",
"component": "canvas.alert",
"component_version": "v1",
"type": "dismiss",
"value": {},
"metadata": {},
"created_at": "2026-06-09T21:14:03.123456"
}
}
```
`created_at` is UTC with microsecond precision and no offset suffix (no trailing `Z`); don't parse it as strict RFC 3339.
The `properties` envelope is identical for every type; only `type` and `value` change:
| `type` | When it fires | `value` |
| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| `dismiss` | The alert was dismissed, either by the user clicking close or by the `auto_dismiss_seconds` timer. Exactly one `dismiss` fires per alert, even if the timer and a click race. | `{}` |
| `clear` | The alert was removed programmatically: the PAL cleared it or the conversation moved on. | `{}` |
| `error` | The client failed to render or run the alert. | Client-defined diagnostic object. Treat it as opaque. |
| `heartbeat` | State signal a client may emit while the alert is on screen. The hosted renderer does not post these to the webhook; they only appear from a custom renderer that sends them. | Client-defined state object. Safe to ignore in webhook handlers. |
For `error` and `heartbeat`, `value` is a free-form object capped at 16 KB serialized; don't build logic on its shape.
Don't expect `submit` or `skip` from alerts in event funnels; a dismissed alert produces only a `dismiss`.
## Posting Interactions from a Custom Renderer
Custom renderers report the dismiss via the conversation-scoped, public interactions endpoint (no API key while the conversation is active):
```bash theme={null}
curl -X POST https://tavusapi.com/v2/conversations/{conversation_id}/canvas/interactions \
-H "Content-Type: application/json" \
-d '{
"interaction_id": "i-5",
"tool_call_id": "call_def",
"component": "canvas.alert",
"component_version": "v1",
"type": "dismiss",
"value": {}
}'
```
| Field | Type | Required | Description |
| ------------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------ |
| `interaction_id` | string | ✅ | Max 128 characters. Reuse the same value on network retries; Tavus dedupes, so the webhook fires once. |
| `tool_call_id` | string | ✅ | Max 128 characters. |
| `component` | string | ✅ | `canvas.alert` |
| `component_version` | string | ✅ | `v1` |
| `type` | string | ✅ | One of the allowed interaction types. |
| `value` | object | ✅ | The interaction value (`{}` for `dismiss`). |
| `metadata` | object | ❌ | Up to 4 KB. |
The full endpoint contract, including the API-key-authenticated `GET` for interaction history, is in [Canvas interactions](/sections/conversational-video-interface/magic-canvas/api/interactions).
# Component: calendar
Source: https://docs.tavus.io/sections/conversational-video-interface/magic-canvas/components/calendar
Let the user pick a date, a time slot, or a date range without leaving the conversation.
The **`calendar`** component displays a date or time picker mid-conversation. The user's selection is delivered to your webhook as structured data: a `YYYY-MM-DD` string or an ISO-8601 slot.
The calendar shows only the dates and slots in the invocation; it does not fetch availability. For live booking against a Calendly scheduling page, use `scheduling_embed`.
`calendar` is enabled whenever the Magic Canvas skill is attached and has no component-specific configuration; see [Enabling and configuring components](/sections/conversational-video-interface/magic-canvas/components#enabling-and-configuring-components).
## Modes
| `mode` | What the user sees | What comes back on submit |
| ------------- | ----------------------------------------------------------- | --------------------------------------------------------- |
| `date_picker` | A month calendar, optionally with quick-pick preset buttons | `selected_date` (`YYYY-MM-DD`) |
| `slot_picker` | A list of concrete time slots | `selected_slot` (or `selected_slots` with `multi_select`) |
| `date_time` | Calendly-style: pick a date, then a slot on that date | `selected_slot` (or `selected_slots` with `multi_select`) |
| `range` | A start–end date range picker | `selected_range` (`{start, end}`) |
## PAL Behavior
The PAL is prompted to invoke `canvas_show_calendar` when the user should pick a date or time slot, inferring `mode`, `multi_select`, and `date_range` from the conversation. You don't trigger the action directly.
Slot times come from the PAL's context. Provide availability through your system prompt, conversational context, or a [tool](/sections/conversational-video-interface/pal/tools) from your Tools library.
## Arguments
Dates use `YYYY-MM-DD`.
| Field | Type | Required | Description |
| -------------- | ------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `title` | string | ❌ | Heading on the card. 1–120 characters. |
| `mode` | string | ❌ | `date_picker`, `slot_picker`, `date_time`, or `range`. If omitted: slots present → `slot_picker`, otherwise `date_picker`. |
| `multi_select` | boolean | ❌ | Slot-bearing modes only (`slot_picker`, `date_time`): let the user pick more than one slot. Defaults to `false`. |
| `slots` | array | ✅ when `mode` is `slot_picker` or `date_time` | 1–24 entries. Each slot: `id` (required), `start` (required, ISO-8601), `end` (required, ISO-8601), `label` (optional display text). |
| `presets` | array | ❌ | Up to 6 quick-pick buttons for `date_picker`. Each: `label` (required, ≤40 chars) and `date` (required, `YYYY-MM-DD`). |
| `initial_date` | string | ❌ | Pre-selected date, `YYYY-MM-DD`. |
| `date_range` | object | ❌ | `{ "start", "end" }`, both `YYYY-MM-DD`, inclusive. Limits which dates are selectable. |
| `allow_skip` | boolean | ❌ | Shows a skip button so the user can decline to pick. Defaults to `false`. |
```json Example invocation [expandable] theme={null}
{
"title": "Pick a demo time",
"mode": "slot_picker",
"slots": [
{
"id": "slot-1",
"label": "Tuesday, 10:00 AM",
"start": "2026-05-26T10:00:00-07:00",
"end": "2026-05-26T10:30:00-07:00"
},
{
"id": "slot-2",
"label": "Tuesday, 2:00 PM",
"start": "2026-05-26T14:00:00-07:00",
"end": "2026-05-26T14:30:00-07:00"
}
]
}
```
## Interactions
`calendar` produces six interaction types: `submit`, `skip`, `dismiss`, `clear`, `error`, and `heartbeat`. Each is delivered to your conversation webhook as a `canvas.interaction` event and is also available from `GET https://tavusapi.com/v2/conversations/{conversation_id}/canvas/interactions`.
```json Webhook delivery [expandable] theme={null}
{
"message_type": "canvas",
"event_type": "canvas.interaction",
"conversation_id": "c123abc456def",
"timestamp": "2026-05-26T17:03:12Z",
"properties": {
"conversation_id": "c123abc456def",
"interaction_id": "ci_tc-8842_submit_1c9f2e7a",
"tool_call_id": "tc-8842",
"component": "canvas.calendar",
"component_version": "v1",
"type": "submit",
"value": {
"selected_date": null,
"selected_slot": {
"id": "slot-1",
"label": "Tuesday, 10:00 AM",
"start": "2026-05-26T10:00:00-07:00",
"end": "2026-05-26T10:30:00-07:00"
},
"skipped": false
},
"metadata": {},
"created_at": "2026-05-26T17:03:12.482910"
}
}
```
### `submit`
Exactly one selection field is present and non-null, and `skipped` is `false`. The field depends on the card's mode:
| Mode | Selection field |
| ----------------------------------------------- | ------------------------------------------------------ |
| `date_picker` | `selected_date`, a `YYYY-MM-DD` string |
| `slot_picker` / `date_time` | `selected_slot`, one slot object |
| `slot_picker` / `date_time` with `multi_select` | `selected_slots`, an array of one or more slot objects |
| `range` | `selected_range`, `{ "start", "end" }` |
Slot objects echo back exactly what the PAL offered: `id`, `start`, and `end` always; `label` when the invocation included one. Selection fields for other modes are `null`.
### `skip`
Sent when `allow_skip` is on and the user taps the skip button: `skipped` is `true` and no selection field carries a value. The PAL sees the skip too.
```json theme={null}
{
"selected_date": null,
"selected_slot": null,
"skipped": true
}
```
### `dismiss`, `clear`, `error`, `heartbeat`
Lifecycle events about the card itself, not answers:
| Type | Meaning |
| ----------- | --------------------------------------------------------------------------------- |
| `dismiss` | The user closed the card without answering. |
| `clear` | The card was removed (for example, the PAL called `canvas_clear` or replaced it). |
| `error` | The renderer reported a problem with the card. |
| `heartbeat` | A periodic liveness signal while the card is on screen. |
Their `value` has no fixed schema (a free-form object, often `{}`); don't build logic on its contents.
Don't count a conversation as scheduled until you see a `submit`; users also dismiss, skip, or let the conversation move on.
## Webhook Handling
Branch on `type` first, then on whichever selection field is non-null:
```js theme={null}
app.post("/tavus-webhook", (req, res) => {
const { event_type, properties } = req.body;
if (
event_type !== "canvas.interaction" ||
properties.component !== "canvas.calendar"
) {
return res.sendStatus(200);
}
const { type, value } = properties;
if (type === "submit") {
if (value.selected_date) {
bookDate(value.selected_date);
} else if (value.selected_slot) {
bookSlot(value.selected_slot.start, value.selected_slot.end);
} else if (value.selected_slots) {
value.selected_slots.forEach((s) => bookSlot(s.start, s.end));
} else if (value.selected_range) {
bookRange(value.selected_range.start, value.selected_range.end);
}
} else if (type === "skip" || type === "dismiss") {
markDeclined(properties.conversation_id);
}
res.sendStatus(200);
});
```
Tavus records each interaction once: a retried POST with the same `interaction_id` and identical payload never fires your webhook twice. Key non-reversible handlers on `properties.interaction_id` anyway.
## Reference
| | |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------- |
| Component id | `canvas.calendar` |
| Action name | `canvas_show_calendar` |
| Version | `v1` |
| Interaction class | `submit_capable` |
| Interaction types | `submit`, `skip`, `dismiss`, `clear`, `error`, `heartbeat` |
| Default placement | `safe-area-right` |
| PAL config | On by default with the `magic_canvas` skill attached; disable with `config.components.calendar = { "enabled": false }` |
# Component: chart
Source: https://docs.tavus.io/sections/conversational-video-interface/magic-canvas/components/chart
Render a bar, line, or pie chart on the canvas during a live conversation.
The **`chart`** component renders a bar, line, or pie chart on the canvas. It is display-only.
| | |
| ----------------- | -------------------------------------- |
| Component id | `canvas.chart` |
| Version | `v1` |
| Action name | `canvas_show_chart` |
| Interaction class | Lifecycle-only (no `submit` or `skip`) |
| Default placement | `safe-area-right` |
`chart` is enabled on any PAL with the Magic Canvas skill attached; see [Enabling and configuring components](/sections/conversational-video-interface/magic-canvas/components#enabling-and-configuring-components). Its only config key is `enabled` (default `true`):
```json theme={null}
{ "config": { "components": { "chart": { "enabled": false } } } }
```
## Behavior
The PAL decides when to draw a chart; there is no developer-triggered path. Its system prompt directs it to chart small numeric comparisons or trends, with short labels and numeric values.
## Arguments
| Field | Type | Required | Description |
| -------------- | ------------------------------ | -------- | ------------------------------------------------------------- |
| `chart_type` | `"bar"` \| `"line"` \| `"pie"` | ❌ | Which chart to draw (default `bar`). |
| `data` | array of `{label, value}` | ✅ | The data points. At least 1; the renderer accepts at most 12. |
| `data[].label` | string | ✅ | Label for the point (renderer limit: 1–80 characters). |
| `data[].value` | number | ✅ | Numeric value for the point. |
| `title` | string | ❌ | Heading above the chart (renderer limit: 1–120 characters). |
| `x_label` | string | ❌ | X-axis label (renderer limit: 1–80 characters). |
| `y_label` | string | ❌ | Y-axis label (renderer limit: 1–80 characters). |
No other arguments are accepted.
Arguments exceeding the limits above produce a load error card instead of a
chart.
### Example invocation
```json theme={null}
{
"title": "Pipeline",
"chart_type": "bar",
"data": [
{ "label": "Qualified", "value": 18 },
{ "label": "Demo", "value": 11 },
{ "label": "Closed", "value": 4 }
],
"x_label": "Stage",
"y_label": "Count"
}
```
## Interaction Types
`chart` is lifecycle-only: sending `submit` or `skip` returns a 400 (`canvas.chart does not support this interaction type.`). The interactions endpoint accepts four types:
| Type | What it means |
| ----------- | ---------------------------------------------------- |
| `dismiss` | The user closed the card |
| `clear` | The canvas was cleared while the chart was on screen |
| `error` | The client failed to display the chart |
| `heartbeat` | A custom client's periodic status ping |
Expect `canvas.chart` interactions only from clients you build yourself. The
stock Tavus SDK and hosted embed currently POST none of these: the card has
no dismiss control, render failures surface only via the local `onError`
callback, clearing the canvas removes instances locally without an
interaction, and renderer heartbeats use a separate model-context
channel that never reaches the interactions endpoint or your webhook.
Custom clients POST interactions to `POST https://tavusapi.com/v2/conversations/{conversation_id}/canvas/interactions` ([validation rules](/sections/conversational-video-interface/magic-canvas/api/interactions)).
Each recorded interaction fires your conversation webhook once as a `canvas.interaction` event (`message_type: "canvas"`) and is available via `GET https://tavusapi.com/v2/conversations/{conversation_id}/canvas/interactions` (API key required).
### Example Webhook Event
A `dismiss` POSTed by a custom client:
```json theme={null}
{
"message_type": "canvas",
"event_type": "canvas.interaction",
"properties": {
"conversation_id": "c123456",
"interaction_id": "ci_call_abc_dismiss_8f2e1c9a",
"tool_call_id": "call_abc",
"component": "canvas.chart",
"component_version": "v1",
"type": "dismiss",
"value": {},
"metadata": {},
"created_at": "2026-06-09T21:14:03.214311"
}
}
```
`created_at` is a Python `isoformat()` timestamp: UTC but timezone-naive,
with microseconds and **no** trailing `Z` or offset. The same shape appears
in the GET response.
For lifecycle types, Tavus validates the envelope (ids, component, type) but
only enforces that `value` is an object under 16 KB; don't depend on
specific keys inside it.
## Related
* [Canvas components overview](/sections/conversational-video-interface/magic-canvas/components): component table and shared behavior
* [Canvas interactions](/sections/conversational-video-interface/magic-canvas/api/interactions): webhooks, recording, and history
* [Canvas configuration](/sections/conversational-video-interface/magic-canvas/api/configuration): the full `magic_canvas` skill config
# Component: image
Source: https://docs.tavus.io/sections/conversational-video-interface/magic-canvas/components/image
Show an image from your Knowledge Base or an allowlisted website on the canvas during a conversation.
The **`image`** component shows an image on the canvas, with an optional caption. It is display-only.
| | |
| ----------------- | -------------------------------------- |
| Component id | `canvas.image` |
| Version | `v1` |
| Action name | `canvas_show_image` |
| Interaction class | Lifecycle-only (no `submit` or `skip`) |
| Default placement | `safe-area-right` |
Images come from two sources, both under your control:
* **Your [Knowledge Base](/sections/conversational-video-interface/knowledge-base)**: you curate the exact set of images the PAL may show, and it picks one by id.
* **Websites you allowlist**: the PAL passes an image URL it received during the conversation (typically from a [tool](/sections/conversational-video-interface/pal/tools) result), and Tavus renders it only if the URL clears a safety floor and its host matches your allowlist.
By default only the Knowledge Base source exists. The compiled action has no URL argument at all, so the PAL cannot show an image from the open web or from your site; the URL argument appears only once you enable [`web`](#web-images) with at least one allowlisted domain.
## Configuration
Attaching the skill alone does not activate `image`. It stays inactive (no action is compiled, no error) until at least one image is curated in the skill config or `web` is enabled with at least one domain.
### Curated Knowledge Base images
Upload each image with [Create Document](/api-reference/documents/create-document) and wait for it to finish processing. Curating a document that is still processing is rejected, so poll [Get Document](/api-reference/documents/get-document) (or use the callback) first.
Save the document ids you want the PAL to be able to show, each with an optional caption and an optional "when to show this" hint.
```bash theme={null}
curl -X PUT https://tavusapi.com/v2/pals/{pal_id}/skills/magic_canvas \
-H "Content-Type: application/json" \
-H "x-api-key: " \
-d '{
"config": {
"components": {
"image": {
"images": [
{
"document_id": "d1234567890",
"caption": "Aurora Desk in walnut",
"prompt": "Show when the user asks what the desk looks like or asks about finishes."
},
{
"document_id": "d2468101214",
"caption": "Cable tray, underside detail",
"prompt": "Show when the user asks about cable management."
}
]
}
}
}
}'
```
| Field | Type | Required | Description |
| ---------------------- | ------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `images` | array | ✅ | The curated images, at most 50. The component activates as soon as one usable entry is saved. Order is preserved and is the order the PAL sees. |
| `images[].document_id` | string | ✅ | A Knowledge Base document id, returned by [Create Document](/api-reference/documents/create-document). Validated when you save the config. |
| `images[].caption` | string | ❌ | The caption shown under the image, max 280 characters. Defaults to the description Tavus generates while processing the image, or the document name. |
| `images[].prompt` | string | ❌ | Your "when to show this image" hint, max 500 characters. Given to the PAL alongside the caption to help it choose; never shown to the user. |
| `enabled` | boolean | ❌ | Defaults to `true`. Set `false` to switch the component off without deleting your curated list. |
| `web` | object | ❌ | The second source: image URLs from websites you allowlist. See [Web images](#web-images). |
#### Which documents can be curated
When you save the config, every `document_id` must:
* Be accessible to the account that owns the PAL. Otherwise: `400`, `One or more document_ids are not accessible`.
* Have finished processing. Documents still being ingested are rejected with `400`, `One or more document_ids are not ready for display`; curate them once processing completes.
At conversation create, Tavus additionally resolves each curated document to a displayable image. A document is skipped if it is not an image (a PDF, a website crawl, a spreadsheet) or if its processed image is unavailable. Skipped entries are simply absent from what the PAL can show; the rest of the curated list still works.
### Web images
The `web` object lets the PAL show images it finds mid-conversation, typically URLs returned by a [tool](/sections/conversational-video-interface/pal/tools) you built: a product search, a listing lookup. You allowlist the websites those URLs may come from; a URL from anywhere else is dropped before the card renders.
```bash theme={null}
curl -X PUT https://tavusapi.com/v2/pals/{pal_id}/skills/magic_canvas \
-H "Content-Type: application/json" \
-H "x-api-key: " \
-d '{
"config": {
"components": {
"image": {
"web": {
"enabled": true,
"domains": ["photos.example.com", "*.cdn.example.com"],
"guidance": "Show the listing photo whenever search_listings returns one."
}
}
}
}
}'
```
| Field | Type | Required | Description |
| -------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `web.enabled` | boolean | ❌ | Defaults to `false`, unlike the component-level `enabled`. Setting `true` requires at least one entry in `domains`. Setting `false` keeps your saved list without deleting it. |
| `web.domains` | array | ❌ | The websites image URLs may come from, at most 20 entries. |
| `web.guidance` | string | ❌ | Your "when to show a web image" note, max 500 characters. Given to the PAL alongside the allowlist; never shown to the user. |
Domain entries are validated when you save the config:
* Each entry is an exact host (`photos.example.com`) or a leading wildcard (`*.cdn.example.com`). An exact host matches only itself. A wildcard matches any depth of subdomain and never the bare domain: `*.example.com` matches `a.example.com` and `a.b.example.com` but not `example.com`; list the apex separately if you want it too.
* Hostname only, max 253 characters: no scheme, port, path, or userinfo. International domains go in as punycode (`xn--...`).
* Entries are normalized on save (lowercased, trailing root dot stripped, duplicates removed), so the list you read back is the canonical form.
* IP addresses (in any spelling), `localhost`, and internal-network names (`.local`, `.internal`, `.corp`, ...) are rejected: the allowlist can only name public websites.
* Wildcards over shared platforms and public suffixes (`*.github.io`, `*.co.uk`, ...) are rejected, because they would allowlist every site anyone can register there. Wildcard your own subtree (`*.photos.example.com`) or list exact hosts instead.
The allowlist is for URLs the PAL **receives during the conversation**. It is
instructed to pass only image URLs that appeared in a tool result, copied
exactly, and never to construct or recall one from memory: an invented URL
won't load even on an allowed website. Pair `web` with a tool that returns
image URLs.
If none of the curated documents resolve and `web` is not enabled, the
`canvas_show_image` action is not compiled for that conversation at all. The
PAL is never offered an image it cannot show.
## When the PAL shows images
You can curate up to 50 Knowledge Base images on the PAL. Each `canvas_show_image` call can display 1 to 8 of them at once (and, with a live `web` allowlist, web image URLs as well). The PAL identifies each entry by curated id or by URL.
Multiple images on one card show as a carousel: one image at a time, with
prev/next to step through them.
### Arguments
| Field | Type | Required | Description |
| ------------------ | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `images` | array | ✅ | Images the PAL has chosen to show on this card, 1 to 8. More than 8 and the extras are dropped. One call may mix curated ids and web URLs. |
| `images[].id` | string | ✅\* | The `document_id` of a curated image. Ids outside the curated set are dropped. |
| `images[].url` | string | ✅\* | A web image URL, max 2048 characters. Only exists on PALs with a live `web` allowlist, and only renders if it passes [URL validation](#how-a-url-becomes-an-image). |
| `images[].caption` | string | ❌ | Caption for this image, max 280 characters. For a curated image it overrides your curated caption. |
| `images[].alt` | string | ❌ | Web URLs only, max 240 characters. Alt text for the image, which Tavus has never seen. Defaults to the caption. |
| `title` | string | ❌ | Heading above the card, max 120 characters. |
\* Each entry carries exactly one of `id` or `url`; an entry with both is dropped. No other arguments are accepted.
### Example invocation
```json theme={null}
{
"title": "Homes near the park",
"images": [
{
"url": "https://photos.example.com/listings/1024.jpg",
"alt": "Blue Victorian with a bay window",
"caption": "312 Fell St"
},
{ "id": "d1234567890" }
]
}
```
### How an id becomes an image
At conversation create, Tavus resolves each curated document to a signed URL that is valid for the length of the conversation. When the PAL calls `canvas_show_image`, Tavus substitutes the id for that URL before the card reaches the browser, so the card your client renders (or your own renderer, if you [bring one](/sections/conversational-video-interface/magic-canvas/integrations/cvi-ui-sdk#bring-your-own-renderer)) receives a URL-shaped payload:
```json theme={null}
{
"title": "Aurora Desk",
"images": [
{
"url": "https://...",
"alt": "A walnut standing desk photographed from the front left",
"caption": "Aurora Desk in walnut"
}
]
}
```
* **Caption** precedence: the PAL's caption, then your curated caption, then the description Tavus generated for the image (or the document name). Truncated to 280 characters.
* **Alt text** for a curated image always comes from your config or Tavus, never from the PAL. Truncated to 240 characters.
* `title` is truncated to 120 characters.
* The resolved URLs are short-lived and specific to that conversation. Treat them as expiring; don't store or share them.
### How a URL becomes an image
A `url` entry is validated at the moment the PAL calls the action, in two layers:
1. **A fixed safety floor.** The URL must be `https` with a publicly routable hostname. Loopback, private-network, and link-local addresses, IP-literal hosts in any spelling, and internal names like `localhost` are rejected. The floor is not configurable: allowlisting a domain cannot re-admit an `http://` or private-network URL.
2. **Your allowlist.** The URL's host must match one of your `web.domains` entries, exact or wildcard.
A surviving URL reaches the renderer unchanged, in the same `{url, alt, caption}` shape as a curated image, with the PAL's `alt` and `caption`. The participant's browser fetches it directly, so the image must be publicly reachable.
Whichever way an entry identifies its image, an entry that fails to resolve is dropped and the rest of the call still renders. If nothing survives, the card is not shown at all rather than rendering broken.
## Interaction Types
`image` is lifecycle-only. Supported interaction types:
| Type | What it means |
| ----------- | ---------------------------------------------------- |
| `dismiss` | The user closed the card |
| `clear` | The canvas was cleared while the image was on screen |
| `error` | The client failed to display the image |
| `heartbeat` | A custom client's periodic status ping |
## When to Use
Use `image` when seeing the thing is faster than describing it: a product, a floor plan, a chart you already have as a file, a screenshot of a step. Give each curated image a `prompt` that says when it applies, and the PAL will pick the right one.
Use `web` when the right image isn't known until mid-conversation: pair it with a [tool](/sections/conversational-video-interface/pal/tools) that returns image URLs (a product search, an inventory lookup) and the PAL can show the participant what it just found.
For walking a participant through a multi-page deck, use the [presentation skill](/sections/conversational-video-interface/skills/presentation) instead; it shares your screen with the deck rather than showing a card.
## Related
* [Canvas components overview](/sections/conversational-video-interface/magic-canvas/components): component table and shared behavior
* [Canvas configuration](/sections/conversational-video-interface/magic-canvas/api/configuration): the full `magic_canvas` skill config
* [Knowledge Base](/sections/conversational-video-interface/knowledge-base): uploading and managing documents
# Component: input
Source: https://docs.tavus.io/sections/conversational-video-interface/magic-canvas/components/input
Collect a single typed value (a name, an email, a phone number) and receive it in your webhook.
The **`input`** component renders a single text field with a prompt above it. The PAL sees the user's typed response (or skip) and your webhook receives the value.
Use [`question`](/sections/conversational-video-interface/magic-canvas/components/question) for multiple-choice answers and [`calendar`](/sections/conversational-video-interface/magic-canvas/components/calendar) for dates and time slots.
The card renders in the `safe-area-right` slot by default.
The PAL shows the card via `canvas_show_input` when it needs a single typed response (name, email, phone, or number); there is no API call to trigger it.
## Configuration
`input` has no component-specific settings; it is enabled whenever the Magic Canvas skill is attached. Disable with:
```json theme={null}
{ "components": { "input": { "enabled": false } } }
```
See [Enabling and configuring components](/sections/conversational-video-interface/magic-canvas/components#enabling-and-configuring-components).
## Arguments
| Field | Type | Required | Description |
| ------------ | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `prompt` | string | ✅ | The question shown above the field. Maximum 240 characters; the renderer rejects longer prompts and the card fails to render. |
| `input_type` | string | ❌ | One of `text`, `email`, `number`, `tel`. Defaults to `text`. Determines client-side validation and the type of `value` you get back. |
| `allow_skip` | boolean | ❌ | Defaults to `false`. When `true`, the card shows a skip option, which produces a `skip` interaction. |
Inputs are required by default; the user must enter a value to submit. Set `allow_skip` to `true` to let the user skip instead.
```json Example Call theme={null}
{
"prompt": "What should we call you?",
"input_type": "text"
}
```
## Interactions
`input` is submit-capable and can produce all six interaction types:
| Type | When it fires | `value` shape |
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ |
| `submit` | The user entered a value and submitted | Validated, see below |
| `skip` | The user skipped (requires `allow_skip`) | Validated, see below |
| `dismiss` | The user closed the card. The Tavus-hosted input card has no close button, so this only comes from custom renderers | No fixed shape; typically `{}` |
| `clear` | The card was cleared (PAL moved on, or `canvas_clear`) | No fixed shape; typically `{}` |
| `error` | The card failed to render or POST. The Tavus-hosted renderer surfaces render failures through the SDK's `onError` callback instead of posting them | No fixed shape |
| `heartbeat` | Accepted by the API for custom renderers; the Tavus-hosted renderer does not post heartbeats, so you will not see this in your webhook | No fixed shape; typically `{}` |
### Submit and Skip Payloads
`value` from the Tavus-hosted card always has exactly three fields: `input_type`, `value`, and `skipped`.
On submit, `skipped` is `false` and `value` is required: a string for `text`, `email`, and `tel`; a JSON number for `number` (not a numeric string; booleans rejected):
```json theme={null}
{ "input_type": "number", "value": 25, "skipped": false }
```
On skip, `skipped` is `true` and `value` is `null`:
```json theme={null}
{ "input_type": "text", "value": null, "skipped": true }
```
For `input_type: "email"`, the renderer trims whitespace and checks the format before submitting; the Tavus API does not re-validate it. Validate again in your backend before sending mail.
### Webhook Event
Each interaction is delivered once to your conversation's `callback_url` as a `canvas.interaction` event.
```json Example Event [expandable] theme={null}
{
"message_type": "canvas",
"event_type": "canvas.interaction",
"conversation_id": "c123456",
"timestamp": "2026-06-09T21:14:03Z",
"properties": {
"conversation_id": "c123456",
"interaction_id": "ci_call_8f31_submit_6f2c9a1e",
"tool_call_id": "call_8f31",
"component": "canvas.input",
"component_version": "v1",
"type": "submit",
"value": {
"input_type": "email",
"value": "ada@lovelace.dev",
"skipped": false
},
"metadata": {},
"created_at": "2026-06-09T21:14:03.208541"
}
}
```
The full interaction history is available at any time, including after the conversation ends:
```bash theme={null}
curl https://tavusapi.com/v2/conversations/{conversation_id}/canvas/interactions \
-H "x-api-key: "
```
### Custom Renderers
Custom renderers POST interactions to `POST /v2/conversations/{conversation_id}/canvas/interactions` with no API key; the endpoint is public while the conversation is active. See [Canvas interactions](/sections/conversational-video-interface/magic-canvas/api/interactions) for idempotency, replay rules, and size caps.
Input-specific validation: `submit` requires `value.value` of the type matching `input_type` (string for `text`/`email`/`tel`, JSON number for `number`); `skip` requires `skipped: true`.
# Component: question
Source: https://docs.tavus.io/sections/conversational-video-interface/magic-canvas/components/question
Show a multiple-choice question card on screen and get a structured answer back.
The **`question`** component (`canvas.question`) shows a multiple-choice question card during a conversation. The user taps an option (or types a free-text answer, when allowed) and the structured result goes to the PAL and your conversation webhook.
## Triggering
The PAL shows the card by invoking `canvas_show_question` when the user should answer a structured question. Steer the timing in the PAL's system prompt.
* Renders in the `safe-area-right` slot by default.
* A repeat `canvas_show_question` invocation **replaces** the card currently on screen with a new instance and a new `tool_call_id`; in-progress input is lost and interactions arrive under the new id.
* Only one canvas card is on screen at a time. A new `canvas_show_question` invocation always replaces the current card; two cards never appear at once.
`question` has no component-specific settings beyond `enabled`. See [Enabling and configuring components](/sections/conversational-video-interface/magic-canvas/components#enabling-and-configuring-components).
## Arguments
The PAL passes these to `canvas_show_question`:
| Field | Type | Required | Description |
| --------------------------------- | ------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `question` | string | ✅ | The question text shown on the card. Must be non-empty. |
| `options` | array | ✅ | The selectable options. 2–10 items. Option ids must be unique. |
| `options[].id` | string | ✅ | Stable identifier, echoed back verbatim in `selected_option_ids`. |
| `options[].label` | string | ✅ | Human-readable option text shown to the user. |
| `options[].allow_free_text` | boolean | ❌ | When `true`, selecting this option reveals a text input for extra detail on that specific choice. Independent of `allow_other`. |
| `options[].free_text_placeholder` | string | ❌ | Placeholder for that option's detail input. |
| `allow_skip` | boolean | ❌ | Whether the user may skip the question. Default `false`. |
| `multi_select` | boolean | ❌ | Whether the user may select more than one option. Default `false`. |
| `allow_other` | boolean | ❌ | Adds an "Other" affordance the user can expand to type a free-text answer. Single-select: mutually exclusive with the preset options. Multi-select: submitted alongside them. Default `false`. |
| `other_label` | string | ❌ | Label for the "Other" affordance (e.g. "None of the above"). A generic label is used when omitted. |
| `correct_answer` | string | ❌ | Option id of the correct answer for quiz-style questions. When set, the card briefly reveals the correct option against the user's choice after they submit, then clears. Must match one of `options[].id`. Omit for surveys, preferences, and open-ended questions; use only when there is one objectively correct option. |
### Example invocation
```json theme={null}
{
"question": "Which database should we use for the new service?",
"options": [
{ "id": "postgres", "label": "PostgreSQL" },
{ "id": "mysql", "label": "MySQL" },
{ "id": "sqlite", "label": "SQLite" }
],
"allow_other": true,
"other_label": "None of the above"
}
```
Quiz example with answer reveal:
```json theme={null}
{
"question": "Which planet is closest to the Sun?",
"options": [
{ "id": "mercury", "label": "Mercury" },
{ "id": "venus", "label": "Venus" },
{ "id": "earth", "label": "Earth" }
],
"correct_answer": "mercury"
}
```
## Interactions
`question` is a submit-capable component emitting six interaction types: `submit`, `skip`, `dismiss`, `clear`, `error`, and `heartbeat`. Only `submit` and `skip` carry an answer value; the rest are lifecycle signals with no question payload. Each interaction reaches your conversation webhook as a `canvas.interaction` event with the answer in `properties.value`.
### Value Shape (`submit` and `skip`)
| Field | Type | Required | Description |
| --------------------- | --------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `selected_option_ids` | string\[] | ✅ | The chosen option ids, echoed exactly as the PAL set them. Empty on skip, or when the user answered only via "Other". |
| `skipped` | boolean | ✅ | `true` only on skip. |
| `custom_text` | string | ❌ | The free-text answer from the "Other" affordance. Non-empty, at most 4000 characters. |
| `option_texts` | object | ❌ | Per-option detail text, keyed by option id. Keys must be a subset of `selected_option_ids`. Each value is non-empty, at most 2000 characters. |
Tavus rejects values with any other keys and enforces these rules before delivery:
* `skip`: always `skipped: true`, with no selections, `custom_text`, or `option_texts`.
* `submit`: always `skipped: false`, with at least one selected id or a non-empty `custom_text`; never an empty answer.
The hosted card offers Skip only when `allow_skip` is `true`. Tavus does not re-check an incoming skip against the original action arguments, so a custom renderer could post one regardless.
### Example Payload
`properties` always carries the same nine keys. `interaction_id` uniquely identifies the interaction; `tool_call_id` ties it to the originating `canvas_show_question` call. `created_at` is a naive ISO-8601 timestamp with microseconds and **no** timezone suffix (no trailing `Z`); treat it as UTC.
```json Submit [expandable] theme={null}
{
"event_type": "canvas.interaction",
"properties": {
"conversation_id": "c123456",
"interaction_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"tool_call_id": "toolu_01A0B1C2D3E4F5G6H7J8K9L0",
"component": "canvas.question",
"component_version": "v1",
"type": "submit",
"value": {
"selected_option_ids": ["postgres"],
"skipped": false
},
"metadata": {},
"created_at": "2026-06-09T21:14:03.123456"
}
}
```
### Webhook Handling
Branch on `type` first, then read the value:
```ts theme={null}
app.post("/webhook", (req, res) => {
const { event_type, properties } = req.body;
if (event_type !== "canvas.interaction") return res.sendStatus(200);
if (properties.component !== "canvas.question") return res.sendStatus(200);
const { type, value } = properties;
switch (type) {
case "submit":
// value.selected_option_ids: option ids the PAL defined
// value.custom_text: "Other" answer, if any
// value.option_texts: per-option detail, if any
saveAnswer(properties.conversation_id, value);
break;
case "skip":
// The user declined to answer.
break;
default:
// dismiss / clear / error / heartbeat: lifecycle only, no answer payload.
break;
}
res.sendStatus(200);
});
```
Key your logic on option **ids**, not labels. Labels may vary between conversations; ids are required to be unique and are echoed back verbatim.
# Component: scheduling_embed
Source: https://docs.tavus.io/sections/conversational-video-interface/magic-canvas/components/scheduling-embed
Embed your Calendly booking page in the conversation and get a webhook when the meeting is booked.
**`scheduling_embed`** renders your Calendly booking page inside the conversation. The user books a slot in the real widget, and the confirmed booking is reported to the PAL and delivered to your webhook as a `canvas.interaction` event.
See [Enabling and configuring components](/sections/conversational-video-interface/magic-canvas/components#enabling-and-configuring-components) for skill attachment and general component settings.
## Configuration
Attaching the skill alone does not activate `scheduling_embed`. It stays inactive (no action is compiled, no error) until a valid `scheduling_url` is saved in the skill config.
```bash theme={null}
curl -X PUT https://tavusapi.com/v2/pals/{pal_id}/skills/magic_canvas \
-H "Content-Type: application/json" \
-H "x-api-key: " \
-d '{
"config": {
"components": {
"scheduling_embed": {
"provider": "calendly",
"scheduling_url": "https://calendly.com/your-team/30min"
}
}
}
}'
```
| Field | Type | Required | Description |
| ---------------- | ------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `provider` | string | ❌ | Defaults to `"calendly"`, the only supported provider. |
| `scheduling_url` | string | ✅ | Your HTTPS booking link, max 2048 characters (enforced when you save the config). The component activates as soon as a valid value is saved. |
| `enabled` | boolean | ❌ | Defaults to `true`. Set `false` to switch the component off without deleting your link config. |
`scheduling_url` is validated on save: it must start with `https://`, be at
most 2048 characters, and not point at localhost, cloud metadata hostnames,
or private, loopback, link-local, or reserved IPs. No DNS
resolution is performed, so other non-public hostnames can pass. The renderer
also requires a `calendly.com` or `*.calendly.com` host; otherwise the card
renders a "Scheduling not configured" panel.
## Arguments
The PAL invokes `canvas_show_scheduling_embed` when the user should book a meeting and a scheduling link is configured. The booking link itself is never an argument.
| Field | Type | Required | Description |
| --------------- | ------ | -------- | ------------------------------------------------------------ |
| `prefill` | object | ❌ | Pre-populates the booking form. No extra keys allowed. |
| `prefill.name` | string | ❌ | Invitee name, filled into the widget. Up to 160 characters. |
| `prefill.email` | string | ❌ | Invitee email, filled into the widget. Up to 254 characters. |
```json theme={null}
{
"prefill": {
"name": "Ada Lovelace",
"email": "ada@example.com"
}
}
```
`scheduling_embed` renders inline in a side rail (default `safe-area-right`); placement is handled client-side.
There is no `url`, `link`, or `scheduling_url` argument. The link comes from
your PAL config, delivered directly to the sandboxed renderer, bypassing
the model entirely.
## Interactions
The built-in card emits exactly one interaction type: `submit`, sent when the user completes a booking. There is no close control, so closing the card without booking produces no interaction. Renderer failures surface as an in-card "Scheduling unavailable" panel, not as webhooks.
The interactions API also accepts `skip`, `dismiss`, `clear`, `error`, and
`heartbeat` for this component, for custom clients that post their own
interactions; these are not shape-validated by Tavus.
### `submit`
The `value` object accepts only these keys:
| Field | Type | Required | Description |
| ------------- | ------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `provider` | string | ✅ | Always `"calendly"`. |
| `scheduled` | boolean | ✅ | Always `true`; a submit only fires on a confirmed booking. |
| `event_uri` | string | ❌ | Calendly API resource URI (HTTPS, `calendly.com` or `*.calendly.com` host). Use with the Calendly API to fetch the meeting time, invitee answers, and cancellation links; Tavus passes it through without fetching it. |
| `invitee_uri` | string | ❌ | Calendly API resource URI (HTTPS, `calendly.com` or `*.calendly.com` host). Passed through without being fetched. |
Example webhook for a completed booking:
```json [expandable] theme={null}
{
"message_type": "canvas",
"event_type": "canvas.interaction",
"properties": {
"conversation_id": "c7f8a1b2c3d4",
"interaction_id": "ci_call_9d41f2_submit_6f1c9a2e-3b7d-4e8a-9c01-5a2b8d7e4f10",
"tool_call_id": "call_9d41f2",
"component": "canvas.scheduling_embed",
"component_version": "v1",
"type": "submit",
"value": {
"provider": "calendly",
"scheduled": true,
"event_uri": "https://api.calendly.com/scheduled_events/GBGBDCAADAEDCRZ2",
"invitee_uri": "https://api.calendly.com/scheduled_events/GBGBDCAADAEDCRZ2/invitees/AAAAAAAAAAAAAAAA"
},
"metadata": {
"client_timestamp": "2026-06-09T18:42:11.000Z"
},
"created_at": "2026-06-09T18:42:11.123456"
}
}
```
`created_at` is a naive ISO-8601 timestamp with microsecond precision and no timezone suffix.
The `canvas.interaction` webhook [fires once per interaction](/sections/conversational-video-interface/magic-canvas/api/interactions). Fetch the full history at any time:
```bash theme={null}
curl https://tavusapi.com/v2/conversations/{conversation_id}/canvas/interactions \
-H "x-api-key: "
```
## When to Use
Use `scheduling_embed` when booking a meeting is a goal of the conversation. The user books in your real Calendly page, so availability, routing, and confirmation emails work as they do on your website. For a date or time preference without a real booking, use [`calendar`](/sections/conversational-video-interface/magic-canvas/components/calendar) instead.
# Component: text
Source: https://docs.tavus.io/sections/conversational-video-interface/magic-canvas/components/text
A read-only card of text (an optional heading and a body) shown alongside the live video.
The **`text`** component displays a read-only Markdown card alongside the live video. Users can't type into or submit from it; to collect an answer, use [`question`](/sections/conversational-video-interface/magic-canvas/components/question) or [`input`](/sections/conversational-video-interface/magic-canvas/components/input).
`text` has no component-specific configuration; it's enabled once the Magic Canvas skill is attached (see [Enabling and configuring components](/sections/conversational-video-interface/magic-canvas/components#enabling-and-configuring-components)). To disable it, set `{ "components": { "text": { "enabled": false } } }` in the skill config.
## Display Behavior
Enabling the component adds system-prompt guidance to show a text card for reference material the user reads while the conversation continues. The PAL keeps talking while the card is on screen.
## Arguments
The PAL invokes `canvas_show_text`; Tavus renders the card. The `tool_call_id` is echoed back on every interaction the card generates.
| Field | Type | Required | Description |
| ------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------- |
| `title` | string | ❌ | Short heading above the body. The renderer enforces a 120-character limit. |
| `body` | string | ✅ | The text to display; content renders as Markdown by default. The renderer enforces a 4,000-character limit. |
```json Example call theme={null}
{
"title": "Conversation note",
"body": "Magic Canvas can show supporting text without interrupting the conversation."
}
```
## Interactions
`text` is a **lifecycle-only** component: it never produces `submit` or `skip` interactions. The interactions endpoint accepts four types:
| Type | When it fires |
| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `dismiss` | The user closed the card via a close control. The stock Tavus text card has an always-present X button (aria-label "Dismiss text"); clicking it fires a recorded `dismiss` interaction. |
| `clear` | The card was removed: the PAL cleared or replaced it. The stock renderer handles `canvas_clear` and replacement locally without recording an interaction. |
| `error` | The card failed on the client. Stock-renderer failures surface through the SDK's local `onError` callback, not as recorded interactions. |
| `heartbeat` | A liveness signal a custom renderer may post while a card is on screen. The stock renderer never posts heartbeats. |
The stock Tavus renderers (hosted embed and `@tavus/cvi-ui`) post a `dismiss` interaction for `text` cards (from the X button). They do not post `clear`, `error`, or `heartbeat`; expect those only from a custom renderer.
Renderers post interactions to a public, conversation-scoped endpoint (no API key), accepted only while the conversation is active. Each interaction fires one [`canvas.interaction` webhook event](/sections/conversational-video-interface/magic-canvas/api/interactions) and appears in the interaction history (requires your API key):
```bash theme={null}
curl https://tavusapi.com/v2/conversations/{conversation_id}/canvas/interactions \
-H "x-api-key: "
```
### Webhook Payload
A `dismiss` for a text card; the other types share the same envelope, with only `type` (and sometimes `value`) changing.
```json [expandable] theme={null}
{
"message_type": "canvas",
"event_type": "canvas.interaction",
"properties": {
"conversation_id": "c123456",
"interaction_id": "ci_call_8842_dismiss_4f1c2d3e",
"tool_call_id": "call_8842",
"component": "canvas.text",
"component_version": "v1",
"type": "dismiss",
"value": {},
"metadata": {},
"created_at": "2026-06-09T21:14:03.412305"
}
}
```
For lifecycle-only components like `text`, the `value` object has **no
enforced shape**; Tavus accepts any JSON object up to 16 KB for these types.
Key your logic off `component`, `type`, and `tool_call_id`, not `value`
contents.
# Integration: @tavus/cvi-ui MagicCanvas
Source: https://docs.tavus.io/sections/conversational-video-interface/magic-canvas/integrations/cvi-ui-sdk
Render Magic Canvas cards inside your own React app with the @tavus/cvi-ui MagicCanvas component: install, mount, and handle every interaction.
**``** is the `@tavus/cvi-ui` component that renders Magic Canvas cards inside your own React app. It listens for Canvas invocations, renders each card, and posts user interactions to Tavus and your webhook.
The Tavus-hosted embed and widget render Canvas automatically and do not require this SDK.
## Prerequisites
Attach the `magic_canvas` skill to your PAL; it enables every component with defaults, and every video conversation on that PAL gets Canvas automatically with nothing to declare on the client. See the [Magic Canvas overview](/sections/conversational-video-interface/magic-canvas/overview).
## Installation
`@tavus/cvi-ui` is a CLI that copies component source into your app, not a runtime dependency.
```bash theme={null}
npx @tavus/cvi-ui@latest init
```
Creates `cvi-components.json` and installs the shared dependencies:
`@daily-co/daily-react`, `@daily-co/daily-js`, and `jotai`.
```bash theme={null}
npx @tavus/cvi-ui@latest add magic-canvas
```
Copies the Magic Canvas source into
`src/components/cvi/components/magic-canvas`
(`app/components/cvi/components/magic-canvas` if your project has no `src`
directory); the import paths below resolve from there. If you don't have a
conversation UI yet, also run:
```bash theme={null}
npx @tavus/cvi-ui@latest add conversation
```
## Creating the Conversation
The PAL's Magic Canvas skill provides the Canvas actions; the create call needs nothing Canvas-specific:
```bash theme={null}
curl -X POST https://tavusapi.com/v2/conversations \
-H "Content-Type: application/json" \
-H "x-api-key: " \
-d '{
"face_id": "r79e1c033f",
"pal_id": "p5317866"
}'
```
Create conversations from your server; never ship your Tavus API key in the
client bundle. `npx @tavus/cvi-ui@latest add tavus-api` installs a server
route for Next.js, Remix, and TanStack Start that keeps the key server-side.
Conversations with no rendering surface skip Canvas: `audio_only`
conversations, text chats, and conversations with a `meeting_url` (Zoom,
Teams, Meet) get no Canvas actions: no error and no cards.
## Mounting the Component
Mount `` as a **sibling** of `` inside one `CVIProvider` (the Daily provider, exactly one per call); both listen on the same call object:
```tsx theme={null}
import { CVIProvider } from './components/cvi/components/cvi-provider';
import { Conversation } from './components/cvi/components/conversation';
import { MagicCanvas } from './components/cvi/components/magic-canvas';
```
The component renders nothing until the PAL shows a card.
## Placement
`` renders a fixed full-viewport overlay (`position: fixed; inset: 0; pointer-events: none`, high z-index) and places each card in the side rail. The overlay never blocks clicks; each card re-enables `pointer-events` for itself.
Cards cannot mount in your own containers: `renderComponent` swaps **what** renders, not **where** ([Bring your own renderer](#bring-your-own-renderer)); `className` restyles the overlay without moving it; [`onLayoutEffectChange`](#onlayouteffectchange) reports the video shift for side panels.
To pin the overlay inside a container instead of the viewport, the `examples/vite-app` demo uses a CSS override:
```css theme={null}
/* Pin the Magic Canvas overlay to the player box instead of the viewport. */
.canvas-in-player {
position: absolute !important;
}
```
Requires a `position: relative` wrapper around `` and ``. The side-rail card then stays inside the box.
### Card Slots
Every card renders inline in the `safe-area-right` side rail. Placement is client-side; the model does not choose the slot.
The `scheduling_embed` card is config-gated: it renders only when the PAL supplies a scheduling provider and a `scheduling_url`.
Other runtime behavior:
| Condition | Behavior |
| ------------------ | --------------------------------------------------------------------------------------- |
| Multiple cards | Only one card is on screen at a time; showing a new card replaces the current one. |
| Inline card height | Sized to the height the card reports, clamped between a 48px floor and a 720px maximum. |
## Props
All props are optional; `` with no props is fully functional.
| Field | Type | Required | Description |
| ---------------------- | ---------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `className` | `string` | ❌ | Appended to the overlay's root class list. Restyles the overlay; does not change where cards render. |
| `onInteraction` | `(event: CanvasInteractionEvent) => void \| Promise` | ❌ | Called for every user interaction (submit, skip, dismiss) before it is sent to Tavus. |
| `onError` | `(event: CanvasErrorEvent) => void` | ❌ | Called for malformed configs, failed posts, and renderer errors. |
| `onLayoutEffectChange` | `(layout: CanvasSidecarLayout) => void` | ❌ | Called when the side panel opens, closes, or moves. |
| `renderComponent` | `CanvasRenderRegistry` | ❌ | Registry of your own React renderers, keyed by `"@"`. See [Bring your own renderer](#bring-your-own-renderer). |
### `onInteraction`
```ts theme={null}
type CanvasInteractionEvent = {
interaction_id: string; // unique id for this interaction
conversation_id: string;
tool_call_id: string; // ties back to the invocation that showed the card
component: string; // e.g. "canvas.question"
component_version: string; // e.g. "v1"
type: string; // "submit", "skip", "dismiss", ...
value: unknown; // component-specific payload
metadata: JsonRecord;
};
```
The handler is awaited **before** the interaction is posted to Tavus. If it throws, an `on_interaction_callback_failed` error event fires and the post still proceeds; the interaction reaches your webhook either way.
### `onError`
```ts theme={null}
type CanvasErrorEvent = {
code: CanvasErrorCode;
message: string;
conversation_id?: string;
tool_call_id?: string;
component?: string;
cause?: unknown;
};
```
| Code | Meaning |
| ---------------------------------- | ------------------------------------------------------------ |
| `malformed_canvas_config` | A Canvas action carried a config the SDK couldn't parse. |
| `missing_tool_call_id` | A Canvas invocation arrived without a `tool_call_id`. |
| `invalid_tool_arguments` | The action's arguments failed to parse. |
| `missing_interaction_metadata` | A card emitted an interaction without the required metadata. |
| `interaction_normalization_failed` | An interaction couldn't be turned into a postable payload. |
| `interaction_post_failed` | The POST to Tavus failed or timed out (5 seconds). |
| `on_interaction_callback_failed` | Your `onInteraction` handler threw. |
| `bridge_connect_failed` | A sandboxed card failed to establish its message bridge. |
| `send_tool_input_failed` | A message to an on-screen card couldn't be relayed. |
Errors never throw into your render tree; they all arrive here.
### `onLayoutEffectChange`
When a side-slot card opens, the canvas reserves a 448px panel and reports how far centered video content should shift to clear it:
```ts theme={null}
type CanvasSidecarLayout = {
active: boolean; // a side panel is currently open
side?: 'left' | 'right';
video_shift_x: number; // px to shift your centered video content
safe_area?: CanvasSafeArea; // normalized region the cards avoid
backdrop: CanvasBackdropConfig;
};
```
Nothing applies the shift for you; wire this callback if you render your own video layout. The shift only activates on viewports 900px and wider; `active` is `false` below that, and the video stays centered.
## Interaction Delivery
Every interaction is posted to:
```
POST https://tavusapi.com/v2/conversations/{conversation_id}/canvas/interactions
```
The post times out after 5 seconds; failures fire an `interaction_post_failed` error event. Successful interactions arrive at your conversation webhook as `canvas.interaction` events, and the PAL responds in the conversation.
## Bring Your Own Renderer
By default, cards render inside a sandboxed iframe; sandboxed component UIs load from Tavus-approved hosts only. Pass `renderComponent` (a registry keyed by `"@"`) to render a component natively instead:
```tsx theme={null}
(
submit({ value: { selected_option_ids: [optionId], skipped: false } })
}
/>
),
}}
/>
```
Your renderer receives the validated `component`, `version`, and `args` (runtime/layout keys already stripped), plus callbacks:
| Callback | Description |
| ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `submit(interaction)` | Reports a user interaction: `{ type?, value, metadata? }`; `type` defaults to `"submit"`. Runs normalization, `onInteraction`, the interaction POST, and auto-dismiss, identical to the iframe path. |
| `sendContext({ content?, structuredContent? })` | Appends model context to the conversation. |
| `respond(text)` | Sends a free-text reply the PAL responds to. |
| `onError(error)` | Surfaces a renderer-side error through the host's `onError`. |
The interaction payload, `canvas.interaction` webhook, and auto-dismiss behavior are identical for both paths, and native cards use the same slot and layout model as iframe cards. Components without a matching registry entry keep using the sandboxed iframe.
## Complete Example
Adapted from `examples/vite-app` in the cvi-ui repository. The installed `tavus-api` route expects a JSON body of `{ "action": "create", "params": { ... } }`, forwards `params` to Tavus verbatim, and is assumed mounted at `POST /api/tavus`.
```tsx main.tsx theme={null}
import React from 'react';
import { createRoot } from 'react-dom/client';
import { CVIProvider } from './components/cvi/components/cvi-provider';
import { App } from './App';
createRoot(document.getElementById('root')!).render(
);
```
```tsx App.tsx [expandable] theme={null}
import { useState } from 'react';
import { Conversation } from './components/cvi/components/conversation';
import { MagicCanvas } from './components/cvi/components/magic-canvas';
type Call = { id: string; url: string };
export function App() {
const [call, setCall] = useState(null);
const start = async () => {
// The installed tavus-api route forwards `params` to
// POST https://tavusapi.com/v2/conversations with your API key.
const res = await fetch('/api/tavus', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'create',
params: {
pal_id: 'p5317866',
face_id: 'r79e1c033f',
},
}),
});
const { conversation_id, conversation_url } = await res.json();
setCall({ id: conversation_id, url: conversation_url });
};
if (!call) {
return ;
}
return (
setCall(null)} />
{/* Shares the call through the root CVIProvider; cards render in a
fixed full-viewport overlay (see Placement). */}
{
console.log('canvas interaction', event.component, event.type, event.value);
}}
onError={(event) => {
console.error('canvas error', event.code, event.message);
}}
/>
);
}
```
Start a conversation and ask the PAL to "show me a multiple choice question." A card appears next to the PAL video, and the console logs the interaction.
# Integration: Tavus-Hosted Embed and Widget
Source: https://docs.tavus.io/sections/conversational-video-interface/magic-canvas/integrations/hosted
Render a Magic Canvas conversation from a single HTML tag, with no SDK code or rendering work.
The **Tavus-hosted embed and widget** render a full Magic Canvas conversation from a single HTML tag. The element joins the call, renders cards, and posts interactions back to Tavus; the host page contains no Canvas code.
* **``** renders the conversation inline and fills its container.
* **``** renders a floating launcher; clicking it opens the conversation.
Both share one engine and render in a shadow DOM, isolating page and element styles.
## Quickstart
Follow [Deployments overview](/sections/deployments/overview) to create a widget or embed deployment in the PAL Maker. For installation, sizing, and attributes, see [Widget](/sections/deployments/widget) and [Embed](/sections/deployments/embed).
```bash theme={null}
curl -X PUT https://tavusapi.com/v2/pals/{pal_id}/skills/magic_canvas \
-H "Content-Type: application/json" \
-H "x-api-key: " \
-d '{ "config": {} }'
```
Attaching the skill enables every component with defaults. To disable a component or configure `scheduling_embed` (the one component that requires config to activate), add entries under `config.components`; see
[Canvas configuration](/sections/conversational-video-interface/magic-canvas/api/configuration).
## Hosted client behavior
The hosted client handles everything the [`@tavus/cvi-ui` integration](/sections/conversational-video-interface/magic-canvas/integrations/cvi-ui-sdk) requires you to wire up:
* **Rendering.** Cards overlay the call within the element's bounds and are cleaned up when the conversation moves on.
* **Interaction delivery.** User interactions post to `POST /v2/conversations/{conversation_id}/canvas/interactions`. The endpoint is public during an active conversation. Your conversation webhook receives the `canvas.interaction` event.
* **Sandboxing.** Components render in sandboxed iframes and load from Tavus-approved hosts only.
Card interactions are **not** emitted as host-page `tavus:*` events. For browser-side callbacks, use the `onInteraction` prop in the [`@tavus/cvi-ui` integration](/sections/conversational-video-interface/magic-canvas/integrations/cvi-ui-sdk). For server-side handling, use your webhook or fetch interaction history (requires your API key):
```bash theme={null}
curl https://tavusapi.com/v2/conversations/{conversation_id}/canvas/interactions \
-H "x-api-key: "
```
## Local development override
Both elements accept the same attributes:
| Field | Type | Required | Description |
| ------------------------ | ----------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `deployment-id` | string | ✅ | Loads the deployment's configuration from `GET /v2/deployments/:id/init`. |
| `override-config` | JSON string | ❌ | Deep-merged over the fetched configuration. If `deployment-id` is omitted, the override is used as-is and nothing is fetched (useful for local UI work). |
| `conversational-context` | string | ❌ | Per-page context sent to the PAL when the call starts. A non-empty value replaces the deployment-level context for that call. |
| `custom-greeting` | string | ❌ | Replaces the line the PAL opens with. |
| `memory-stores` | string | ❌ | Comma-separated memory store ids, sent to `/start` as `memory_stores`. |
| `token` | string | ❌ | Preview or auth token forwarded to `/init` and `/start`. Flips the element into preview mode. |
| `base-url` | string | ❌ | API base URL. Defaults to `https://tavusapi.com`. |
There is no API-key attribute. The deployment endpoints the element calls (`/init`, `/start`, `/end`) are public and scoped to the deployment. `token` is a preview token, not an API key. Never put a Tavus API key in page markup.
Override example (field names match the deployment configuration schema):
```html theme={null}
```
If `deployment-id` is omitted, the override is used as-is and nothing is fetched.
## Host-page events
For lifecycle events, tool calls, and protocol interactions from your page, see [Host communication](/sections/deployments/host-communication).
# Magic Canvas Overview
Source: https://docs.tavus.io/sections/conversational-video-interface/magic-canvas/overview
Let your PAL show interactive UI (questions, calendars, charts, and more) inside a live video conversation.
**Magic Canvas** lets a PAL show interactive cards, such as multiple-choice questions, calendars, charts, and text, during a live video conversation. User responses flow back to the PAL and your backend.
The PAL decides when to show a card; there is no per-card API call.
```
PAL invokes a Canvas action → card renders in the call
User responds to the card → PAL reacts to the response
→ your webhook receives the interaction
```
## Conversation Flow
Attach via `PUT /v2/pals/{pal_id}/skills/magic_canvas`; remove with `DELETE` on the same path.
Attaching enables every component with default settings, including components Tavus adds later. The exception is `scheduling_embed`, which stays inactive until you set its `scheduling_url`.
`config.components` is a sparse overlay: add an entry only to configure or disable a component (`{"enabled": false}`); unlisted components stay on. See [Configuring your PAL](/sections/conversational-video-interface/magic-canvas/api/configuration).
Video conversations get Canvas actions automatically.
Audio-only, text-chat, and external-meeting conversations (`meeting_url`, for example Zoom, Teams, or Meet) do not get Canvas actions.
The PAL invokes a Canvas action; the card renders in a side rail beside the PAL video, and the PAL can clear it mid-conversation.
When the user submits, skips, or dismisses a card:
* The PAL responds to it, as it does to speech.
* Tavus records it and delivers a `canvas.interaction` event to your conversation webhook, once per interaction.
Fetch a conversation's full Canvas interaction history with your API key; see [Canvas interactions](/sections/conversational-video-interface/magic-canvas/api/interactions).
## When cards appear
The PAL decides when to show a card; you do not call an API to trigger one. Steer that behavior with `usage_guidance` on the skill, the PAL's system prompt, or per-conversation context. See [When cards appear](/sections/conversational-video-interface/magic-canvas/api/configuration#when-cards-appear) for how each lever fits.
## Hosted vs SDK
Both use the same PALs, components, and webhook; the difference is how much of the UI you own.
| | Hosted embed / widget | `@tavus/cvi-ui` SDK |
| ----------------------------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------- |
| **What it is** | A `` or `` tag you drop on any page | A `` React component you add next to your conversation UI |
| **Rendering & interaction posting** | Automatic | Automatic, with `onInteraction` / `onError` callbacks for your app |
| **Custom look** | Tavus-designed cards | Cards by default, or bring your own React renderers per component |
| **Best for** | Fastest path; any site, no framework needed | Apps that own their conversation UI |
## Components
Eight components: four interactive (submit an answer or skip) and four display-only (dismiss only).
| Component | What the PAL can do with it | User can answer? |
| ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | ---------------- |
| [`question`](/sections/conversational-video-interface/magic-canvas/components/question) | Ask a multiple-choice question, optionally with a free-text "Other" | Yes |
| [`input`](/sections/conversational-video-interface/magic-canvas/components/input) | Ask for a single typed value: text, email, number, or phone (tel) | Yes |
| [`calendar`](/sections/conversational-video-interface/magic-canvas/components/calendar) | Let the user pick a date, a time slot, or a date range | Yes |
| [`scheduling_embed`](/sections/conversational-video-interface/magic-canvas/components/scheduling-embed) | Embed your real scheduling page (e.g. Calendly) for live booking | Yes |
| [`text`](/sections/conversational-video-interface/magic-canvas/components/text) | Show a card of formatted text | Dismiss only |
| [`image`](/sections/conversational-video-interface/magic-canvas/components/image) | Show an image from your Knowledge Base or an allowlisted website | Dismiss only |
| [`chart`](/sections/conversational-video-interface/magic-canvas/components/chart) | Show a chart of data from the conversation | Dismiss only |
| [`alert`](/sections/conversational-video-interface/magic-canvas/components/alert) | Show a dismissible notice | Dismiss only |
## Rendering
Components render in a sandboxed iframe that isolates styles and scripts in both directions, served from Tavus infrastructure.
To render cards in your own React tree, register a renderer per component in the SDK; interactions flow back identically. See [Custom rendering](/sections/conversational-video-interface/magic-canvas/integrations/cvi-ui-sdk#bring-your-own-renderer).
## Next Steps
* [Configuring your PAL](/sections/conversational-video-interface/magic-canvas/api/configuration): attach the skill and configure components
* [Hosted embed and widget](/sections/conversational-video-interface/magic-canvas/integrations/hosted): the no-code path
* [React SDK](/sections/conversational-video-interface/magic-canvas/integrations/cvi-ui-sdk): `` in your own app
* [Canvas interactions](/sections/conversational-video-interface/magic-canvas/api/interactions): webhooks, recording, and history
# Magic Canvas Quickstart
Source: https://docs.tavus.io/sections/conversational-video-interface/magic-canvas/quickstart
Attach the Magic Canvas skill to a PAL, render cards in the call, and receive every interaction on your webhook.
**Magic Canvas** lets a PAL show interactive cards, such as questions, calendars, and charts, during a conversation. The PAL's LLM decides when to show one; user input flows back to the conversation and your webhook.
Attaching the skill enables the available components with default settings:
```bash theme={null}
curl -X PUT https://tavusapi.com/v2/pals/{pal_id}/skills/magic_canvas \
-H "Content-Type: application/json" \
-H "x-api-key: " \
-d '{ "config": {} }'
```
`config.components` is a sparse overlay, not an allowlist; add an entry only to configure or disable a component:
```bash Disable charts theme={null}
curl -X PUT https://tavusapi.com/v2/pals/{pal_id}/skills/magic_canvas \
-H "Content-Type: application/json" \
-H "x-api-key: " \
-d '{ "config": { "components": { "chart": { "enabled": false } } } }'
```
Later components are enabled automatically on PALs with the skill attached; disable them the same way. See [Configuring your PAL](/sections/conversational-video-interface/magic-canvas/api/configuration) for all component settings and [when cards appear](/sections/conversational-video-interface/magic-canvas/api/configuration#when-cards-appear) to steer timing.
The skill has no effect on `echo` and speech-to-speech PALs.
Every video conversation with this PAL gets Canvas. Set `callback_url`; interactions arrive there in step 4:
```bash theme={null}
curl -X POST https://tavusapi.com/v2/conversations \
-H "Content-Type: application/json" \
-H "x-api-key: " \
-d '{
"face_id": "r79e1c033f",
"pal_id": "p5317866",
"callback_url": "https://yourapp.example.com/webhooks/tavus"
}'
```
Audio-only, text-chat, and Zoom/Teams/Meet (`meeting_url`) conversations never receive Canvas actions.
Conversation creation uses your API key; call it from your backend, never the browser.
With the Tavus-hosted embed or widget, Canvas renders automatically:
```html theme={null}
```
For React, add the `@tavus/cvi-ui` component:
```bash theme={null}
npx @tavus/cvi-ui@latest add magic-canvas
```
Mount it inside the same `CVIProvider` as your conversation UI:
```tsx theme={null}
import { CVIProvider } from "./components/cvi/components/cvi-provider";
import { Conversation } from "./components/cvi/components/conversation";
import { MagicCanvas } from "./components/cvi/components/magic-canvas";
console.log("user interacted:", event)}
/>
```
The default class sets `position: fixed` (full-viewport overlay). To keep cards inside your player, wrap both in a `position: relative` container and pass a `className` that sets `position: absolute !important`.
Each interaction arrives at your `callback_url` as a `canvas.interaction` event, fired once when first recorded; duplicate submissions and client retries never re-fire it:
```json canvas.interaction [expandable] theme={null}
{
"properties": {
"conversation_id": "c123456",
"interaction_id": "ci_call_abc123_submit_8f3d2a",
"tool_call_id": "call_abc123",
"component": "canvas.question",
"component_version": "v1",
"type": "submit",
"value": { "selected_option_ids": ["opt_2"], "skipped": false },
"metadata": {},
"created_at": "2026-06-09T21:14:03.412210"
},
"conversation_id": "c123456",
"message_type": "canvas",
"event_type": "canvas.interaction",
"timestamp": "2026-06-09T21:14:03.498Z"
}
```
Fetch the full history any time with your API key:
```bash theme={null}
curl https://tavusapi.com/v2/conversations/{conversation_id}/canvas/interactions \
-H "x-api-key: "
```
The response is `{ "data": [ ... ] }`, oldest first, with the same fields as the webhook's `properties`.
See [Canvas components](/sections/conversational-video-interface/magic-canvas/components) for the full component list and per-component reference. `scheduling_embed` needs a booking link (`provider` plus `scheduling_url`) configured before it activates:
```json theme={null}
"config": {
"components": {
"scheduling_embed": {
"provider": "calendly",
"scheduling_url": "https://calendly.com/your-team/30min"
}
}
}
```
Cards render in a sandboxed iframe on Tavus infrastructure that isolates styles and scripts in both directions. Your webhook receives `skip` and `dismiss` interactions in addition to `submit`.
# Memories
Source: https://docs.tavus.io/sections/conversational-video-interface/memories
Memories let PALs remember information across conversations, allowing participants to have personalized, flowing conversations across multiple sessions.
Memories are pieces of information that the PAL learns during a conversation. Once learned, these memories can be referenced and used by the PAL during subsequent conversations.
Developers are able to organize memories within `memory_stores` - a flexible tag-based system to track memories across conversations and participants into different buckets.
If a `memory_stores` value is provided in the conversation creation request, memories will automatically be created and associated to the tag provided.
When defining `memory_stores` values, we recommend incorporating static values that will not change with PAL updates, like PAL ID.
For example, using a PAL's name as part of your `memory_stores` values could result in memories being miscategorized if you were to change their name.
## Basic Example
For example, if a participant named Anna starts a conversation with the PAL (Charlie, with the PAL ID `p123`), we can specify `memory_stores=["anna_p123"]` in the conversation creation request.
By doing so, Charlie will:
* Remember what was mentioned in a conversation and form new memories with Anna.
* Reference memories from previous conversations that Charlie had with Anna in new conversations.
Example [conversation creation](https://docs.tavus.io/api-reference/conversations/create-conversation) request body:
```json theme={null}
{
"pal_id": "your_pal_id",
"face_id": "your_face_id",
"memory_stores": ["anna_p123"]
}
```
## Managing Memories Between Participants and Conversations
To prevent different PALs from mixing up information for the same participant, we generally recommend you to create separate `memory_stores` values for each user when they talk to different PALs.
For example,
* When Anna talks to Charlie (PAL ID of `p123`), you can use the `memory_stores` value of `["anna-p123"]`.
* when she talks with Gloria (PAL ID of `p456`), you can use the `memory_stores` value of `["anna-p456"]`.
The `memory_stores` system can be used flexibly to cover your use cases - they do not have to map 1:1 with your participants and instead can be designed for your unique use cases.
For example,
* If you were setting up an online classroom, you could use a `memory_stores` tag value of `"classroom-1"` so any participant of this group could reference and create new memories to enhance and deepen learning and connections.
* You can control whether you want PALs to share memory or not (and if so, which PALs) by passing them different `memory_stores` values.
## Delete a memory
You can delete a single memory via the API. Use the same `memory_store` value you used when creating the conversation, and the memory ID returned when the memory was created or listed.
```bash theme={null}
curl -X DELETE "https://tavusapi.com/v2/memories//" \
-H "x-api-key: YOUR_API_KEY"
```
Replace `` with your memory store identifier (e.g. `anna_p123`) and `` with the ID of the memory to delete.
# Mobile
Source: https://docs.tavus.io/sections/conversational-video-interface/mobile
Ship Tavus CVI on iOS, Android, React Native, and mobile web using Daily's mobile SDKs and the same conversation URLs as web.
Tavus **Conversational Video Interface (CVI)** runs on **Daily** rooms. After you [create a conversation](/api-reference/conversations/create-conversation), use the returned **`conversation_url`** wherever Daily's docs refer to a room URL. On **web**, you can embed with our [React component library](/sections/conversational-video-interface/component-library/overview) (`@tavus/cvi-ui`, built on `@daily-co/daily-js` / `@daily-co/daily-react`) or follow [Embed CVI](/sections/integrations/embedding-cvi). On **native mobile**, Daily's SDKs are the supported path today - you bring the UI and join the same Tavus-issued Daily room.
## React (web) vs React Native
* **React for the browser:** Use Tavus's [component library overview](/sections/conversational-video-interface/component-library/overview), plus [blocks](/sections/conversational-video-interface/component-library/blocks), [components](/sections/conversational-video-interface/component-library/components), and [hooks](/sections/conversational-video-interface/component-library/hooks) - see [Embed CVI](/sections/integrations/embedding-cvi) for full flows.
* **React Native:** Daily's [React Native SDK](https://docs.daily.co/reference/rn-daily-js) uses the same underlying model as `daily-js`; join with your **`conversation_url`** from Tavus.
## Android
Daily's native [Android SDK](https://docs.daily.co/reference/android) (Kotlin) is the usual integration surface for Tavus CVI on Android. Use their [Android quickstart](https://docs.daily.co/guides/products/mobile/android-quickstart) with your **`conversation_url`**.
## iOS
Daily's native [iOS SDK](https://docs.daily.co/reference/ios) (Swift) pairs the same way with Tavus. Follow their [iOS quickstart](https://docs.daily.co/guides/products/mobile/ios-quickstart) and pass the **`conversation_url`** from [Create Conversation](/api-reference/conversations/create-conversation).
## Flutter
For cross-platform apps, Daily's [Flutter SDK](https://docs.daily.co/reference/flutter) joins the same Daily rooms Tavus provisions.
## Mobile web
Users can join a Tavus conversation in **Chrome on Android** or **Safari on iOS** without a native SDK when a mobile-capable web experience is enough. Daily's [mobile web guide](https://docs.daily.co/guides/products/mobile) covers browser behavior and constraints.
## Further reading
Daily's [mobile intro guide](https://docs.daily.co/guides/products/mobile/intro) summarizes all native mobile SDKs in one place.
# What Is CVI?
Source: https://docs.tavus.io/sections/conversational-video-interface/overview-cvi
CVI enables real-time, human-like video interactions through configurable PALs.
Conversational Video Interface (CVI) is a framework for creating real-time multimodal video interactions with AI. It enables an AI agent to see, hear, and respond naturally, mirroring human conversation.
CVI is the world’s fastest interface of its kind. It maps a **face** (visual) and a **PAL** (behavior) onto your AI agent. With CVI, you can achieve low-latency utterance-to-utterance response: the full round-trip from when a participant speaks to when the PAL responds.
CVI provides a comprehensive solution, with the option to plug in your existing components as required.
## At a glance
Building with an AI coding agent or automation? Use
`https://docs.tavus.io/llms.txt` for the canonical page index,
`https://docs.tavus.io/llms-full.txt` for the full bundled docs export, and
`https://docs.tavus.io/openapi.yaml` for the HTTP API contract.
* **CVI** - Real-time multimodal video: the agent sees, hears, and responds; media runs over **WebRTC** (powered by Daily).
* **Latency** - Utterance-to-utterance round-trip is optimized for real-time use (participant speaks → PAL replies).
* **Three pillars** - **[PAL](/sections/conversational-video-interface/pal/what-is-a-pal)** (behavior, knowledge, and CVI layer pipeline); **[Face](/sections/faces/overview)** (visual likeness, **Phoenix**); **[Conversation](/sections/conversational-video-interface/conversation/overview)** (live session linking a PAL and its face).
* **Pipeline (in order)** - Perception (**Raven**) → Conversational Flow (**Sparrow**) → Speech recognition (STT) → Large language model (LLM) → Text-to-speech (TTS) → Realtime replica (**Phoenix**). **Raven** is visual perception; **Sparrow** handles turn-taking and interruptibility; **Phoenix** is the real-time visual face engine.
* **Where to configure** - Most layers are set on the **[PAL](/sections/conversational-video-interface/pal/overview)**.
## Key Concepts
CVI is built around three core concepts that work together to create real-time, humanlike interactions with an AI agent:
The **PAL** defines the agent’s behavior, tone, and knowledge. It also configures the CVI layer and pipeline.
The **Face** brings the PAL to life visually. It renders a photorealistic human-like avatar using **Phoenix**.
A **Conversation** is a real-time video session that connects a PAL and its face through a WebRTC connection.
## Key Features
CVI uses facial cues, body language, and real-time turn-taking to enable natural, human-like conversations.
Customize the Perception, STT, LLM and TTS layers to control identity, behavior, and responses.
Choose from over 100+ hyper-realistic stock faces or customize your own with human-like voice and expression.
Hold natural conversations in 42+ languages using the supported TTS engines.
Experience real-time interactions with low utterance-to-utterance latency and smooth turn-taking.
## Layers
The Conversational Video Interface (CVI) is built on a modular layer system, where each layer handles a specific part of the interaction. Together, they capture input, process it, and generate a real-time, human-like response.
Here’s how the layers work together:
Uses **Raven** to analyze user expressions, gaze, background, and screen content. This visual context helps the PAL understand and respond more naturally.
[Configure the Perception layer](/sections/conversational-video-interface/pal/perception)
Controls the natural dynamics of conversation, including turn-taking and interruptibility. Uses **Sparrow** for intelligent turn detection, enabling the PAL to decide when to speak and when to listen.
[Configure the Conversational Flow layer](/sections/conversational-video-interface/pal/conversational-flow)
This layer transcribes user speech in real time with lexical and semantic awareness.
[Configure the Speech Recognition (STT) layer](/sections/conversational-video-interface/pal/stt)
Processes the user's transcribed speech and visual input using a low-latency LLM. Tavus provides ultra-low latency optimized LLMs or lets you integrate your own.
[Configure the Large Language Model (LLM) layer](/sections/conversational-video-interface/pal/llm)
Converts the LLM response into speech using the supported TTS Engines (Cartesia **(Default)**, ElevenLabs, Azure).
[Configure the Text-to-Speech (TTS) layer](/sections/conversational-video-interface/pal/tts)
Delivers a high-quality, synchronized face using Tavus's real-time avatar engine (**Phoenix**).
[Face overview](/sections/faces/overview)
Most layers are configurable via the [PAL](/sections/conversational-video-interface/pal/overview).
## Getting Started
You can quickly create a conversation by using the PAL Maker or following the steps in the [API Conversation Quickstart](/sections/conversational-video-interface/quickstart/cvi-quickstart) guide.
If you use **Cursor**, **Copilot**, or another **AI coding agent**, use the copy-paste checklist on **[CVI App: AI Prompt](/sections/conversational-video-interface/quickstart/ai-prompt-cvi-quickstart)**.
For web apps, start with [CVI App Quickstart](/sections/conversational-video-interface/quickstart/build-first-app), then choose an embed path in [Embed CVI](/sections/integrations/embedding-cvi). React apps that want Tavus-provided UI should use the [`@tavus/cvi-ui` component library](/sections/conversational-video-interface/component-library/overview), including [blocks](/sections/conversational-video-interface/component-library/blocks), [components](/sections/conversational-video-interface/component-library/components), [hooks](/sections/conversational-video-interface/component-library/hooks), and [server helpers](/sections/conversational-video-interface/component-library/server).
# Conversational Flow
Source: https://docs.tavus.io/sections/conversational-video-interface/pal/conversational-flow
Learn how to configure the Conversational Flow layer to fine-tune turn-taking and interruption handling behavior.
The **Conversational Flow Layer** in Tavus gives you precise control over the natural dynamics of conversation. This layer allows you to customize how your PAL handles turn-taking and interruptions to create conversational experiences that match your specific use case.
## Understanding Conversational Flow
Conversational flow encompasses the subtle dynamics that make conversations feel natural:
* **Turn-taking**: How the PAL decides when to speak and when to listen
* **Interruptibility**: How easily the PAL can be interrupted by the user
All conversational flow parameters are optional. When the layer is omitted or any parameter is set, unspecified fields use the defaults below.
The PAL's greeting is always non-interruptible, regardless of `pal_interruptibility`. These settings only take effect after the greeting completes.
## Configuring the Conversational Flow Layer
If you're migrating from sparrow-0 (formerly called `smart_turn_detection` on the STT Layer) then check out the [migration guide here](/sections/troubleshooting#conversational-flow-vs-stt-relationship-and-migration).
Define the conversational flow layer under the `layers.conversational_flow` object. Below are the parameters available:
### 1. `turn_detection_model`
Specifies the model used for detecting conversational turns.
* **Options**:
* `sparrow-1` **(default)**: Advanced turn detection model - faster, more accurate, and more natural (recommended)
* `sparrow-0`: Legacy turn detection model (API-only, not actively supported)
* `timebased`: Simple time-based turn detection (API-only, not actively supported)
* **Default**: `sparrow-1`
```json theme={null}
"turn_detection_model": "sparrow-1"
```
**Sparrow-1 is recommended for all use cases** as it provides superior performance with faster response times, higher accuracy, and more natural conversational flow.
### 2. `turn_taking_patience`
Controls how eagerly the PAL claims conversational turns. This affects both response latency and the likelihood of interrupting during natural pauses.
* **Options**:
* `low`: Eager and quick to respond. May interrupt natural pauses. Best for rapid-fire exchanges or customer service scenarios where speed is prioritized.
* `medium` **(default)**: Balanced behavior. Waits for appropriate conversational cues before responding.
* `high`: Patient and waits for clear turn completion. Ideal for thoughtful conversations, interviews, or therapeutic contexts.
```json theme={null}
"turn_taking_patience": "medium"
```
**Use Cases:**
* `low`: Fast-paced customer support, quick information lookups, casual chat
* `medium`: General purpose conversations, sales calls, presentations
* `high`: Medical consultations, legal advice, counseling sessions
### 3. `pal_interruptibility`
Controls how sensitive the PAL is to user speech while the PAL is talking. Determines whether the PAL stops to listen or keeps speaking when interrupted.
**`replica_interruptibility`** is a legacy alias for this field. Existing requests keep working; use **`pal_interruptibility`** in new integrations.
* **Options**:
* `low`: Less interruptible. The PAL keeps talking through minor interruptions.
* `medium` **(default)**: Balanced sensitivity. Responds to clear interruption attempts.
* `high`: Highly sensitive. Stops easily when the user begins speaking, maximizing user control.
```json theme={null}
"pal_interruptibility": "high"
```
**Use Cases:**
* `low`: Educational content delivery, storytelling, guided onboarding
* `medium`: Standard conversations, interviews, consultations
* `high`: User-driven conversations, troubleshooting, interactive support
### 4. `voice_isolation`
Voice isolation separates speech from background noise in the participant's microphone audio. It is enabled by default for improved audio quality and can be disabled if needed.
* **Options**:
* `near` **(default)**: Separates speech from background noise for scenarios where the user is less than 1 meter away from the microphone.
* `off`: No voice isolation model is used. The raw audio is sent down the conversational pipeline.
```json theme={null}
"voice_isolation": "near"
```
### 5. `wake_phrase`
A specific phrase the PAL listens for before responding. When set, the PAL remains silent until it hears the wake phrase, similar to how voice assistants like Siri or Alexa work.
* **Type**: `string`
* **Default**: `None` (disabled)
```json theme={null}
"wake_phrase": "Hey Charlie"
```
**How wake phrases work:**
* The PAL stays silent and does not respond until it hears the specified wake phrase.
* The PAL still "hears" everything that is said. All user utterances are recorded in the transcript so the PAL has full context when it does respond.
* Once the wake phrase is detected, the PAL responds using the full conversation history, including anything said before the wake phrase was triggered.
Choose a wake phrase that is unique enough to avoid over-triggering. Avoid generic greetings like `"Hey"` or single common words, which can cause the PAL to respond unintentionally. Phrases with two or more distinctive words (for example, `"Hey Charlie"` or `"Okay Assistant"`) work best.
### 6. `sleep_phrase`
A specific phrase that puts the PAL back to sleep after it has been woken with the `wake_phrase`. When the PAL hears the sleep phrase, it stops responding and returns to the silent state, listening again for the `wake_phrase` before it will respond.
* **Type**: `string`
* **Default**: `None` (disabled)
```json theme={null}
"sleep_phrase": "Thanks Charlie"
```
**How sleep phrases work:**
* After being woken with the `wake_phrase`, the PAL keeps responding normally until it hears the `sleep_phrase`.
* Once the sleep phrase is detected, the PAL goes to sleep: it stops responding and waits for the `wake_phrase` again.
* While asleep, the PAL still "hears" everything that is said. All user utterances are recorded in the transcript so it has full context when it is woken again.
**Example interaction** with `wake_phrase` set to `"Hey Charlie"` and `sleep_phrase` set to `"Thanks Charlie"`:
```text theme={null}
You: "Hey Charlie, what's the weather like today?"
PAL: "Today's forecast is..."
You: "Great, and what about tomorrow?"
PAL: "Tomorrow will be..."
You: "Thanks Charlie, that's all"
PAL: [goes to sleep]
You: "Now let's talk about something else..."
PAL: [no reply]
```
As with the `wake_phrase`, choose a sleep phrase that is unique enough to avoid over-triggering. Phrases with two or more distinctive words work best.
### 7. `idle_engagement`
Controls whether the PAL proactively re-engages the user after a stretch of silence, and how eagerly.
* **Options**:
* `off` **(default)**: The PAL never breaks silence - it only speaks in response to user input.
* `patient`: The PAL re-engages after longer silences. Suited to tutors, coaches, or contemplative use cases where users may need time to think.
* `eager`: The PAL re-engages after shorter silences. Suited to SDR or sales-style conversations where keeping momentum matters.
```json theme={null}
"idle_engagement": "patient"
```
**Use Cases:**
* `off`: General conversational use cases where the user always drives the next turn
* `patient`: Tutoring, coaching, therapy, interviews
* `eager`: Outbound sales, SDR, qualification calls
`idle_engagement` is independent of `turn_taking_patience`. Turn-taking patience controls how quickly the PAL responds after the user finishes speaking; `idle_engagement` controls whether the PAL proactively breaks an extended silence.
## Default Behavior
When the layer is omitted or any parameter is set, unspecified fields use:
* `turn_detection_model`: `sparrow-1`
* `turn_taking_patience`: `medium`
* `pal_interruptibility`: `medium`
* `voice_isolation`: `near`
* `wake_phrase`: `None`
* `sleep_phrase`: `None`
* `idle_engagement`: `off`
## Example Configurations
The following example configurations demonstrate how to tune conversational timing and interruption behavior for different use cases. Use `turn_taking_patience` to bias how quickly the PAL responds after a user finishes speaking. Set it high when the PAL should avoid interrupting, and low when fast responses are preferred. Use `pal_interruptibility` to control how easily the PAL recalculates its response when interrupted; lower values are recommended for most experiences, with higher values reserved for cases where frequent, abrupt interruptions are desirable. Sparrow-1 dynamically handles turn-taking in all cases, with these settings acting as guiding biases rather than hard rules.
### Example 1: Customer Support Agent
Fast, responsive, and easily interruptible for customer-driven conversations:
```json theme={null}
{
"pal_name": "Support Agent",
"system_prompt": "You are a helpful customer support agent...",
"pipeline_mode": "full",
"default_face_id": "r90bbd427f71",
"layers": {
"conversational_flow": {
"turn_detection_model": "sparrow-1",
"turn_taking_patience": "low",
"pal_interruptibility": "medium",
"voice_isolation": "near"
}
}
}
```
### Example 2: Medical Consultation
Patient, thoughtful, with engaged listening for sensitive conversations:
```json theme={null}
{
"pal_name": "Medical Advisor",
"system_prompt": "You are a compassionate medical professional...",
"pipeline_mode": "full",
"default_face_id": "r90bbd427f71",
"layers": {
"conversational_flow": {
"turn_detection_model": "sparrow-1",
"turn_taking_patience": "high",
"pal_interruptibility": "verylow",
"voice_isolation": "near"
}
}
}
```
### Example 3: Educational Instructor
Delivers complete information with minimal interruption, and gently re-engages the user after long pauses for thought:
```json theme={null}
{
"pal_name": "Instructor",
"system_prompt": "You are an experienced educator teaching complex topics...",
"pipeline_mode": "full",
"default_face_id": "r90bbd427f71",
"layers": {
"conversational_flow": {
"turn_detection_model": "sparrow-1",
"turn_taking_patience": "medium",
"pal_interruptibility": "low",
"voice_isolation": "near",
"idle_engagement": "patient"
}
}
}
```
### Example 4: Minimal Configuration
Configure just one parameter - others will use defaults:
```json theme={null}
{
"pal_name": "Quick Chat",
"system_prompt": "You are a friendly conversational AI...",
"pipeline_mode": "full",
"default_face_id": "r90bbd427f71",
"layers": {
"conversational_flow": {
"turn_taking_patience": "low"
}
}
}
```
In this example, the system will automatically set:
* `turn_detection_model`: `sparrow-1`
* `pal_interruptibility`: `medium`
* `voice_isolation`: `near`
## Best Practices
### Match Flow to Use Case
Choose conversational flow settings that align with your application's purpose:
* **Speed-critical applications**: Use `low` turn-taking patience and `high` interruptibility
* **Thoughtful conversations**: Use `high` turn-taking patience
* **Important information delivery**: Use `low` interruptibility
* **User-controlled interactions**: Use `high` interruptibility
### Consider Cultural Context
Conversational norms vary across cultures. Some cultures prefer:
* More overlap and interruption (consider lower commitment, higher interruptibility)
* Clear turn-taking with pauses (consider higher patience, lower interruptibility)
### Test with Real Users
Conversational flow preferences can be subjective. Test your configuration with representative users to ensure it feels natural for your audience.
Refer to the Create PAL API for the complete API specification and additional PAL configuration options.
# Draft vs Live PALs
Source: https://docs.tavus.io/sections/conversational-video-interface/pal/draft-and-live
How the PAL Builder draft and the live PAL relate — and what that means for PATCH, GET, and publish calls against a PAL id.
Every PAL you edit in the [PAL Builder](https://maker.tavus.io/dev) has two rows behind a single `pal_id`: a **live** row that powers conversations, deployments, and public reads, and a **draft** row that the Builder writes into as you edit. Publishing copies the draft over the live row.
For most integrations you never need to think about this — you call [Create PAL](/api-reference/pals/create-pal) or [Patch PAL](/api-reference/pals/patch-pal) with the same `pal_id` and things work. This page exists because the API endpoints that read and write PALs expose the draft/live split when a Builder session is open, and you may want to opt into (or out of) that behavior explicitly.
## The model
A PAL created purely through the API — never opened in the PAL Builder — has no draft. All of the behavior on this page applies only once a Builder session has produced a draft row.
* **Live PAL** — the version of the PAL that runs when a conversation starts, when a deployment answers a call, or when a Google Meet or Zoom invite arrives. Public reads (`GET /v2/pals/{pal_id}`) return this row by default. This is the only row that is exposed to end users.
* **Builder draft** — a hidden copy of the PAL that the [PAL Builder](https://maker.tavus.io/dev) writes into while you edit. It shares its `pal_id` alias with live from the caller's perspective, but has its own internal row so the Builder can preview changes (including "Test" conversations) before publishing.
* **Publish** — copies the draft's content over the live row. Until you publish, live traffic keeps using the previously-published version.
## How PATCH picks a row
`PATCH /v2/pals/{pal_id}` **routes to the draft by default** when a PAL Builder draft exists for that PAL:
* If a draft exists, the patch lands on the draft. Live conversations continue to use the previously-published version until you publish.
* If no draft exists (the PAL has never been opened in the Builder), the patch lands on live, exactly as before.
* Pass `?target=live` to bypass the draft. Tavus writes to live **and resets the draft to match live**, so live becomes the single source of truth again. Any unpublished draft edits are discarded.
This default keeps API and UI edits consistent. The Builder reads the draft row, so a PATCH that skipped the draft would produce an editor showing the pre-patch state — the classic "my edit didn't show up" symptom. Routing to the draft by default means an API caller and a human editor never disagree about what "the current PAL" is.
Every PATCH response echoes the routing back so a client can log or branch on it:
```json theme={null}
{
"pal_id": "pcb7a34da5fe",
"edited_pal_id": "p5f1e8d2a934",
"edit_target": "draft",
"live_pal_id": "pcb7a34da5fe",
"draft_pal_id": "p5f1e8d2a934",
"publish_url": "/v2/pals/pcb7a34da5fe/publish",
"routing_message": "Edited the Builder draft (p5f1e8d2a934). The live PAL (pcb7a34da5fe) is unchanged and will not reflect these edits until you publish. To edit the live PAL directly, re-send with ?target=live (this discards unpublished draft edits and resyncs the draft from live)."
}
```
* `pal_id` is always the id the caller passed in the URL — safe to persist on the client.
* `edit_target` is `draft` or `live`.
* `draft_pal_id` is the underlying draft row. You do not usually need it — patching or publishing the live id automatically routes to the draft — but it's returned for tooling that wants to address the draft directly.
* `publish_url` is returned for draft-routed edits so the next API action is explicit.
## Google Meet / Zoom / `layers.conferencing` is live-owned
`layers.conferencing` (see [Google Meet / Zoom](/sections/conversational-video-interface/pal/meetings)) is deployment config. It provisions the PAL's `@tavusinvite.com` email address, and calendar invites keep arriving while a Builder draft is open. Because of that, conferencing is **always owned by the live PAL**:
* A PATCH that touches `layers.conferencing` routes to live **even without `?target=live`** and syncs the conferencing layer into the open draft. Draft content in other fields is preserved.
* `?target=draft` with a conferencing op is rejected with `400`.
* A single request cannot mix conferencing ops with edits to other fields. Split the request in two if you need to change both.
* Publishing a stale draft (`POST /v2/pals/{pal_id}/publish`) — one that predates a conferencing change made against live — preserves the live conferencing config. Publishing never wipes deployed meeting integrations.
## How GET picks a row
`GET /v2/pals/{pal_id}` **returns the live PAL by default**, even when a draft exists. This is the public API contract customers depend on, and it is unchanged.
* Add `?source=draft` to read the draft body without publishing it. The response `pal_id` still matches the id you queried, and the response adds `is_draft_view: true`, `has_unpublished_changes`, `live_pal_id`, `draft_pal_id`, `published_view_url`, `publish_url`, and a `routing_message` explaining that conversations still use the live version.
* If you queried a draft id directly (rare — you usually address the shared live id), add `?source=live` to read the live parent's body instead of the draft.
* On any draft view, `layers.conferencing` is still the live value, so a draft read never surfaces a stale conferencing layer.
## When to use each mode
Use the default (no `target`). Your edits land on the draft, the Builder shows them, and live traffic is unaffected until you publish (`POST /v2/pals/{pal_id}/publish`).
Pass `?target=live`. Live is updated and the retained draft is reset to match, so nothing about the PAL diverges from what your API call just wrote. Any unpublished draft edits (including ones made by a teammate in PAL Maker) are discarded, but the draft row and its id remain available for future Builder edits.
PATCH `layers.conferencing` on its own and either omit `target` or pass `?target=live`. It is always routed to live and synced into the draft. Explicit `?target=draft` returns `400`. Do not combine it with other field edits in the same request.
`GET /v2/pals/{pal_id}?source=draft`. Response body is the draft; `is_draft_view` is `true`. Live conversations are unchanged.
`POST /v2/pals/{pal_id}/publish`. The draft is copied onto the live row, and the response returns the published PAL with `status: "success"`. If the draft predates a conferencing change made against live, the live conferencing config is preserved.
## Migration notes
If you were patching PALs against the API before the PAL Builder draft was introduced, or your PAL has never been opened in the Builder, nothing changes: your patches still apply to live.
If your account uses PAL Maker **and** you patch the same PAL over the API, the default write target has changed — your edits now stack on top of any open Builder draft instead of jumping past it. To restore the previous behavior for a single call, pass `?target=live`.
# Large Language Model (LLM)
Source: https://docs.tavus.io/sections/conversational-video-interface/pal/llm
Learn how to use Tavus-optimized LLMs or integrate your own custom LLM.
The **LLM Layer** in Tavus enables your PAL to generate intelligent, context-aware responses. You can use Tavus-hosted models or connect your own OpenAI-compatible LLM.
Configure the LLM under **`layers.llm`** when you [Create PAL](/api-reference/pals/create-pal) or update a PAL. For how a PAL fits together, see [PAL overview](/sections/conversational-video-interface/pal/overview).
## Tavus-Hosted Models
### 1. `model`
Select one of the available models. **`tavus-gemma-4` is recommended as the default**; the table below helps you choose based on your priorities.
| Model | Speed | Intelligence | Naturalness | Best For |
| ------------------------- | ----- | ------------ | ----------- | ---------------------------------------- |
| `tavus-gemma-4` (default) | ⚡⚡⚡ | 🧠🧠 | 💬💬💬 | Low-latency dialogue and strong tool use |
| `tavus-gpt-5.6-sol` | ⚡ | 🧠🧠🧠 | 💬💬 | Strongest tool adherence, higher latency |
| `tavus-gpt-5.6-terra` | ⚡⚡ | 🧠🧠🧠 | 💬💬 | Near-Sol quality with lower latency |
| `tavus-gemini-2.5-flash` | ⚡⚡ | 🧠🧠 | 💬💬💬 | Latency + logical deduction |
| Model | Speed | Intelligence | Naturalness | Best For |
| ------------------------ | ----- | ------------ | ----------- | -------------------------------------- |
| `tavus-glm-4.7` | ⚡⚡ | 🧠🧠🧠 | 💬💬 | Agentic tool use, multi-step reasoning |
| `tavus-gpt-oss` | ⚡⚡⚡ | 🧠 | 💬 | Snappy, low-latency |
| `tavus-claude-haiku-4.5` | ⚡⚡ | 🧠🧠 | 💬💬 | Grounded, fewer hallucinations |
| `tavus-gpt-5.2` | ⚡⚡ | 🧠🧠 | 💬💬 | General use, latency less critical |
| `tavus-gemini-3-flash` | ⚡ | 🧠🧠🧠 | 💬💬💬 | Highest intelligence, lower speed |
| `tavus-gpt-4.1` | ⚡⚡ | 🧠🧠🧠 | 💬💬💬 | Long-context reasoning |
**Context Window Limit**
* Performance and intelligence are best when prompts are **limited to 5,000 tokens**. You may see degradations in speed and instruction following in the **15,000–20,000 token** range.
* Context limits vary by model; staying within 5k is recommended for optimal behavior.
**Tip**: 1 token ≈ 4 characters, so 5,000 tokens ≈ 20,000 characters (including spaces and punctuation).
```json theme={null}
"model": "tavus-gemma-4"
```
### 2. `tools`
Legacy field. Do **not** use for new integrations.
Inline LLM tools were historically defined here as OpenAI-style function objects on the PAL body. That approach still runs for existing PALs but is deprecated - it cannot use the tools registry features (`delivery`, `on_call`, `on_resolve`, API auth, or reuse across PALs).
Use the [Tools overview](/sections/conversational-video-interface/pal/tools) and [Tool Calling for LLM](/sections/conversational-video-interface/pal/llm-tool) instead. If you maintain a PAL that still sets `layers.llm.tools`, see [Legacy inline tool calling](/sections/troubleshooting#legacy-inline-tool-calling).
### 3. `speculative_inference`
When set to `true`, the LLM begins processing speech transcriptions before user input ends, improving responsiveness. **This is the default value**; you can set it to `false` to disable.
```json theme={null}
"speculative_inference": true
```
This field is optional. It defaults to `true` for better performance.
### 4. `extra_body`
Add parameters to customize the LLM request. For Tavus-hosted models, you can pass `temperature` and `top_p`:
```json theme={null}
"extra_body": {
"temperature": 0.7,
"top_p": 0.9
}
```
This field is optional.
### Example Configuration
```json theme={null}
{
"pal_name": "Health Coach",
"system_prompt": "You provide wellness tips and encouragement for people pursuing a healthy lifestyle.",
"pipeline_mode": "full",
"default_face_id": "r90bbd427f71",
"layers": {
"llm": {
"model": "tavus-gemma-4",
"speculative_inference": true,
"extra_body": {
"temperature": 0.7,
"top_p": 0.9
}
}
}
}
```
## Custom LLMs
### Prerequisites
To use your own OpenAI-compatible LLM, you'll need:
* Model name
* Base URL
* API key
Ensure your LLM:
* Streamable (i.e. via SSE)
* Uses the `/chat/completions` endpoint
### 1. `model`
Name of the custom model you want to use.
```json theme={null}
"model": "gpt-3.5-turbo"
```
### 2. `base_url`
Base URL of your LLM endpoint.
Do not include route extensions in the `base_url`.
```json theme={null}
"base_url": "https://your-llm.com/api/v1"
```
### 3. `api_key`
API key to authenticate with your LLM provider.
```json theme={null}
"api_key": "your-api-key"
```
`base_url` and `api_key` are required only when using a custom model.
### 4. `tools`
Legacy field. Do **not** use for new integrations.
Same inline shape as `layers.llm.tools` on Tavus-hosted models - deprecated in favor of the [tools registry](/sections/conversational-video-interface/pal/tools). See [Legacy inline tool calling](/sections/troubleshooting#legacy-inline-tool-calling) if you still patch tools on a custom-LLM PAL this way.
### 5. `speculative_inference`
When set to `true`, the LLM begins processing speech transcriptions before user input ends, improving responsiveness. **This is the default value**; you can set it to `false` to disable.
```json theme={null}
"speculative_inference": true
```
This field is optional. It defaults to `true` for better performance.
### 6. `headers`
Optional additional headers to include when making requests to your LLM. Use this for any extra headers your provider requires beyond the API key (which should be set via the `api_key` field).
```json theme={null}
"headers": {
"X-Organization-ID": "your-org-id",
"X-Request-Source": "tavus-cvi"
}
```
This field is optional, depending on your LLM provider's requirements.
### 7. `extra_body`
Add parameters to customize the LLM request. You can pass any parameters that your LLM provider supports:
```json theme={null}
"extra_body": {
"temperature": 0.5,
"top_p": 0.9,
"frequency_penalty": 0.5
}
```
This field is optional.
### 8. `default_query`
Add default query parameters that get appended to the base URL when making requests to the `/chat/completions` endpoint.
```json theme={null}
"default_query": {
"api-version": "2024-02-15-preview"
}
```
This field is optional. Useful for LLM providers that require query parameters for authentication or versioning.
### Example Configuration
```json theme={null}
{
"pal_name": "Storyteller",
"system_prompt": "You are a storyteller who entertains people of all ages.",
"pipeline_mode": "full",
"default_face_id": "r90bbd427f71",
"layers": {
"llm": {
"model": "gpt-4o",
"base_url": "https://your-azure-openai.openai.azure.com/openai/deployments/gpt-4o",
"api_key": "your-api-key",
"speculative_inference": true,
"default_query": {
"api-version": "2024-02-15-preview"
}
}
}
}
```
Refer to [Create PAL](/api-reference/pals/create-pal) for a full list of supported fields.
### Perception
When using the `raven-1` perception model with a custom LLM, your LLM will receive system messages containing visual context extracted from the user's video input. See [Perception](/sections/conversational-video-interface/pal/perception) for how perception is configured and what is sent to the model.
```json theme={null}
{
"role": "system",
"content": "........."
}
```
#### Disabled Perception model
If you disable the perception model, your LLM will not receive any special messages.
# Tool Calling for LLM
Source: https://docs.tavus.io/sections/conversational-video-interface/pal/llm-tool
Define reusable tools, attach them to any PAL, and deliver tool calls via app message or API call.
**LLM tool calling** lets the PAL trigger functions based on what the user says during a conversation. Tools are reusable objects: create them once, and attach them to any number of PALs. They dispatch via the channel you pick on the `delivery` field - see [Tool Delivery](/sections/conversational-video-interface/pal/llm-tool-delivery) and [Tool Authentication](/sections/conversational-video-interface/pal/llm-tool-auth).
This page documents the **tools registry** (`/v2/tools`). If your PAL still defines tools inline under `layers.llm.tools`, see [Legacy inline tool calling](/sections/troubleshooting#legacy-inline-tool-calling).
## Tool Object
### Top-Level Fields
| Field | Type | Required | Description |
| --------------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name` | string | ✅ | A unique identifier for the tool, scoped to your account. Must match OpenAI function naming rules (`^[a-zA-Z_][a-zA-Z0-9_]{0,63}$`). |
| `description` | string | ✅ | Natural language explanation of what the tool does. Used by the model to decide when to call it. |
| `parameters` | object | ❌ | JSON Schema object describing the tool's input arguments. Defaults to `{}` (no arguments). |
| `origin` | string | ❌ | One of `llm`, `vision`, `audio`. Defaults to `llm`. This page covers `llm`; see [Tool Calling for Perception](/sections/conversational-video-interface/pal/perception-tool) for `vision` / `audio`. |
| `on_call` | string | ❌ | what the PAL does **while** the tool is running. See [`on_call` reference](#on-call-reference). Defaults to `generate_filler` for `llm` tools; must be omitted for `vision` / `audio` tools. |
| `on_resolve` | string | ❌ | what the PAL does **after** the tool returns a result. See [`on_resolve` reference](#on-resolve-reference). Defaults to `fire_and_forget`. |
| `static_filler` | string | ❌ | The exact line the PAL speaks while the tool runs when `on_call` is `static_filler`. Required in that case. |
| `delivery` | object | ❌ | How the tool call is delivered. See [Tool Delivery](/sections/conversational-video-interface/pal/llm-tool-delivery). Defaults to `{"app_message": true}`. |
### `parameters`
Standard JSON Schema. Same shape as the OpenAI `function.parameters` object.
| Field | Type | Required | Description |
| ------------ | ---------------- | -------- | -------------------------------------------------------------------------- |
| `type` | string | ✅ | Always `"object"`. |
| `properties` | object | ✅ | Map of parameter name to its schema (`type`, `description`, `enum`, etc.). |
| `required` | array of strings | ❌ | Names of mandatory parameters. |
#### Examples
```json parameters theme={null}
{
"type": "object",
"properties": {}
}
```
```json parameters theme={null}
{
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The city to get the weather for, e.g. San Francisco"
}
},
"required": ["city"]
}
```
```json parameters theme={null}
{
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The city to get the weather for"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit"
}
},
"required": ["city"]
}
```
### `on_call` reference
`on_call` controls what the PAL does **while a tool is dispatched but has not yet returned**. Defaults to `generate_filler` for `llm` tools; set it explicitly if you want a different behavior.
| Value | Behavior |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `generate_filler` | The LLM produces a short filler line itself (a `response_to_user` argument is injected into the tool's schema). the PAL speaks it via TTS. |
| `static_filler` | The PAL speaks the configured `static_filler` string at dispatch time. The top-level `static_filler` field is required. |
| `silent` | The face stays silent while the tool runs. |
| `passthrough` | The tool spec is untouched. You manage the prompt and the schema. Use this when the LLM is already configured to preamble (i.e. the model naturally speaks a short line on its own before invoking a tool). |
#### Examples
```json theme={null}
{
"on_call": "generate_filler"
}
```
The LLM picks the filler line per-call (e.g. "Let me check that for you.").
```json theme={null}
{
"on_call": "static_filler",
"static_filler": "One moment - checking the weather now."
}
```
The PAL speaks the exact `static_filler` string every time the tool fires.
```json theme={null}
{
"on_call": "silent"
}
```
The face says nothing while the tool runs.
```json theme={null}
{
"on_call": "passthrough"
}
```
The tool spec is untouched. Use when your LLM is already configured to preamble before tool calls.
### `on_resolve` reference
`on_resolve` controls what the PAL does **with the tool's result once it returns**.
| Value | Behavior |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `generate_response` | The LLM regenerates a contextual reply using the tool's result. Most common choice; The PAL speaks a natural-language summary of the result. |
| `response_in_result` | The tool's response body is spoken **verbatim** via TTS - return `Content-Type: text/plain` with a single natural-language string. Use when the tool already returns a fully-formed natural-language reply. On timeout, non-2xx, or transport error, the tool call falls back to `generate_response` so the PAL still acknowledges the user. |
| `add_to_context` | The result lands silently in the conversation history as a system message. Nothing is spoken on the result-landing turn; the next user utterance's LLM call sees it. |
| `fire_and_forget` | The result is not awaited or processed. Used when the tool's side effect is what matters (e.g. logging a CRM event). The default when `on_resolve` is omitted. |
#### Examples
```json theme={null}
{
"on_resolve": "generate_response"
}
```
The LLM regenerates a contextual reply using the tool's result. Most common choice.
```json theme={null}
{
"on_resolve": "response_in_result"
}
```
The tool's response body is spoken verbatim via TTS. Return `Content-Type: text/plain` with a natural-language string.
```json theme={null}
{
"on_resolve": "add_to_context"
}
```
The result lands silently in conversation history; the next user turn's LLM call sees it.
```json theme={null}
{
"on_resolve": "fire_and_forget"
}
```
The result is not awaited or processed. Use for side-effect-only tools (e.g. logging a CRM event).
## Creating a Tool
```bash Create tool [expandable] theme={null}
curl --request POST \
--url https://tavusapi.com/v2/tools \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"name": "get_current_weather",
"description": "Get the current weather for a city.",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The city to get the weather for, e.g. San Francisco"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"]
}
},
"required": ["city"]
},
"origin": "llm",
"on_call": "generate_filler",
"on_resolve": "generate_response",
"delivery": {
"api": {
"url": "https://api.example.com/tools/get_weather",
"method": "POST",
"auth": { "type": "hmac", "secret": "whsec_long_random_string" },
"timeout": 20
}
}
}'
```
A successful create returns the full tool object including its `tool_id`:
```json Response theme={null}
{
"tool_id": "tabc123def456",
"owner_id": 12345,
"name": "get_current_weather",
"description": "Get the current weather for a city.",
"parameters": { "...": "..." },
"delivery": { "api": { "...": "..." } },
"is_system_tool": false,
"origin": "llm",
"on_call": "generate_filler",
"on_resolve": "generate_response",
"static_filler": null,
"created_at": "2026-05-20 14:22:01.123456",
"updated_at": "2026-05-20 14:22:01.123456"
}
```
The `tool_id` (prefix `t…`) is how you reference the tool when attaching it to a PAL or updating / deleting it.
## Attaching Tools to a PAL
A PAL only sees a tool if it is attached. Attach one or more tools in a single call:
```bash Attach tools to PAL theme={null}
curl --request POST \
--url https://tavusapi.com/v2/pals/{pal_id}/tools \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"tool_ids": ["tabc123def456", "tdef456abc789"]
}'
```
List the tools currently attached to a PAL:
```bash List PAL tools theme={null}
curl --request GET \
--url https://tavusapi.com/v2/pals/{pal_id}/tools \
--header 'x-api-key: '
```
Detach a single tool from a PAL:
```bash Detach tool from PAL theme={null}
curl --request DELETE \
--url https://tavusapi.com/v2/pals/{pal_id}/tools/{tool_id} \
--header 'x-api-key: '
```
Detaching removes the link between the PAL and the tool. The tool itself is untouched and still attached to any other PALs that use it.
## End-to-End Example
1. **Create the tool** at `/v2/tools` (see above). Save the returned `tool_id`.
2. **Attach it to a PAL** at `/v2/pals/{pal_id}/tools`.
3. **Start a conversation** with that PAL. The tool is now available to the face.
4. **Handle the tool call** in your application:
* If `delivery.app_message: true`, listen for [`conversation.tool_call`](/sections/event-schemas/conversation-toolcall) events in your frontend.
* If `delivery.api` is set, your endpoint receives the call. See [Tool Delivery](/sections/conversational-video-interface/pal/llm-tool-delivery).
5. **Return a result** (only meaningful when `on_resolve` is not `fire_and_forget`):
* App message delivery: send a `conversation.tool_result` event with the matching `tool_call_id`. See [Tool Delivery](/sections/conversational-video-interface/pal/llm-tool-delivery#app-message-delivery).
* API delivery: return a `2xx` response with the result in the response body.
## Errors
| Status | When |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `400` | Validation failure (bad `name`, missing `on_call` on an `llm` tool, both delivery channels set, invalid API URL, unknown enum value, etc.). |
| `400` | Attempted to attach a non-existent `tool_id`. |
| `409` | A tool with the same `name` already exists in your account. Names are unique per owner. |
| `404` | `tool_id` not found, or the authenticated account does not own it. |
Replace `` with your actual API key. You can generate one in the PAL Maker.
# Tool Authentication
Source: https://docs.tavus.io/sections/conversational-video-interface/pal/llm-tool-auth
How Tavus authenticates to your endpoint when a tool is delivered via an API call.
When a tool uses [API delivery](/sections/conversational-video-interface/pal/llm-tool-delivery), Tavus needs to know how to authenticate to the endpoint it's calling. Set `auth.type` on `delivery.api` to one of the values below. The credentials live in the tool config alongside the URL.
| Type | Use when | Adds |
| --------------------------- | ---------------------------- | ---------------------------------------- |
| `none` | Public API | nothing |
| `bearer` | Long-lived token | `Authorization: Bearer ` |
| `api_key` (header) | Custom-header API key | `: ` |
| `api_key` (query) | Key in URL | `?=` |
| `basic` | Username + password | `Authorization: Basic ` |
| `oauth2_client_credentials` | M2M OAuth | `Authorization: Bearer ` |
| `hmac` | Customer-controlled callback | `X-Tavus-Signature: ` |
## `none` - public APIs
```json theme={null}
"auth": { "type": "none" }
```
No auth header or query is added.
## `bearer` - Bearer token
Use when the API gives you a single long-lived token.
```json theme={null}
"auth": { "type": "bearer", "token": "sk_live_abc123..." }
```
Adds `Authorization: Bearer sk_live_abc123...` to the request.
## `api_key` - API key in a header or query string
```json In a custom header theme={null}
"auth": {
"type": "api_key",
"location": "header",
"name": "X-API-Key",
"value": "abc123..."
}
```
```json In the URL query string theme={null}
"auth": {
"type": "api_key",
"location": "query",
"name": "api_key",
"value": "abc123..."
}
```
`location` defaults to `header`. The named header/query is added to every request.
## `basic` - HTTP Basic auth
```json theme={null}
"auth": {
"type": "basic",
"username": "alice",
"password": "s3cret"
}
```
Adds `Authorization: Basic `.
## `oauth2_client_credentials` - machine-to-machine OAuth 2.0
For APIs that issue short-lived tokens via the OAuth2 `client_credentials` grant. Tavus exchanges your credentials for an access token, caches it per worker, and refreshes it automatically as it nears expiry. If the API returns `401` (e.g. the token was revoked out-of-band), Tavus drops the cached token, fetches a fresh one with the same credentials, and retries the request once.
```json theme={null}
"auth": {
"type": "oauth2_client_credentials",
"token_url": "https://accounts.example.com/oauth2/token",
"client_id": "abc123",
"client_secret": "secret_xyz",
"scope": "read:orders write:tickets"
}
```
| Field | Required | Description |
| --------------- | -------- | ----------------------------------------------------------------------------------------------------- |
| `token_url` | ✅ | HTTPS endpoint Tavus POSTs the `client_credentials` grant to. Validated the same way as the tool URL. |
| `client_id` | ✅ | Provided by the API. |
| `client_secret` | ✅ | Provided by the API. |
| `scope` | ❌ | OAuth scope string. Omit if the API doesn't use scopes. |
On the request itself Tavus adds `Authorization: Bearer `. User-delegated OAuth (the "Log in with..." flow with per-end-user refresh tokens) is **not** supported.
## `hmac` - signed Tavus-shape callback
HMAC (Hash-based Message Authentication Code) is a shared-secret signature: it proves the request was sent by someone who knows your secret and that the body was not altered in transit.
For customer-controlled callback endpoints. Tavus signs the request body with HMAC-SHA256 and sends the hex digest in `X-Tavus-Signature`. The body uses the [Tavus callback shape](/sections/conversational-video-interface/pal/llm-tool-delivery#tavus-callback-shape), not the templated/auto-routed shape from `body_template` or query auto-routing.
```json theme={null}
"auth": { "type": "hmac", "secret": "your_shared_secret" }
```
Use this when you control the receiving endpoint and want signature verification. For third-party APIs, use one of the other auth types instead. See [Verifying API signatures](/sections/conversational-video-interface/pal/llm-tool-delivery#verifying-api-signatures) for receiver-side code samples.
# Tool Delivery
Source: https://docs.tavus.io/sections/conversational-video-interface/pal/llm-tool-delivery
How tool calls reach your application - app messages to your frontend or HTTPS API calls to your backend.
This page applies to **both LLM tools and Perception tools**. Every tool dispatches via **exactly one** of two channels, picked per tool via the `delivery` field:
* **App message** (default) - Tavus emits a `conversation.tool_call` event (or `conversation.perception_tool_call` for perception tools) on the data channel; your frontend handles it.
* **API call** - Tavus makes an HTTPS request to a URL you configure, either a customer-controlled callback or a third-party API directly. Auth + body shape is configured per tool.
A tool with `delivery.app_message: true` and a `delivery.api` block at the same time is rejected. Setting both to disabled is also rejected. Pick exactly one.
## App message delivery
The default. Your client receives the tool call inline with the conversation events.
```json App message delivery (default) theme={null}
"delivery": { "app_message": true }
```
Each invocation arrives as a [`conversation.tool_call`](/sections/event-schemas/conversation-toolcall) event carrying a `tool_call_id`. To return a result, send a [`conversation.tool_result`](/sections/event-schemas/conversation-tool-result) event back with the **matching `tool_call_id`** - that's how Tavus pairs the result with the in-flight dispatch and applies the configured `on_resolve`.
```json conversation.tool_result (sent by your client) theme={null}
{
"message_type": "conversation",
"event_type": "conversation.tool_result",
"conversation_id": "",
"properties": {
"tool_call_id": "",
"output": "It is 72 degrees and sunny in San Francisco.",
"status": "success"
}
}
```
| Field | Type | Required | Description |
| -------------- | ---------------- | -------- | ---------------------------------------------------------------------------------------------------------------- |
| `tool_call_id` | string | ✅ | Must match the `tool_call_id` from the original `conversation.tool_call` event. |
| `output` | string \| object | ❌ | The tool result. Strings are passed through; objects are JSON-serialized. |
| `status` | string | ❌ | `"success"` (default) or `"error"`. On `error`, the PAL acknowledges the failure instead of speaking the result. |
If your client never sends a result, the dispatch eventually drops out of context. There's no hard timeout on this path - the PAL just won't have the data.
**App messages are capped at 4 KB.** `conversation.tool_call` events are delivered over the Interaction Protocol data channel, which enforces a 4 KB per-message limit. Most tool calls stay well under this, but a large argument set or payload can exceed it - the message is dropped in transit and the tool call never reaches your client, with no error surfaced. If a tool's arguments (or the results you send back) may be large, use [API delivery](#api-delivery) instead, which is not subject to this limit.
## API delivery
Tavus calls an HTTPS endpoint each time the tool fires. The exact request shape depends on whether you're hitting a **third-party API directly** (any `auth.type` other than `hmac`) or a **customer-controlled callback** (`auth.type: hmac`).
```json API delivery theme={null}
"delivery": {
"api": {
"url": "https://api.example.com/tools/get_weather",
"method": "POST",
"auth": { "type": "hmac", "secret": "whsec_long_random_string" },
"headers": { "X-Tenant": "acme" },
"timeout": 10
}
}
```
### `delivery.api` fields
| Field | Type | Required | Description |
| --------------- | ------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `url` | string | ✅ | HTTPS URL Tavus will call when the tool fires. Must be publicly reachable; private/loopback/link-local addresses are rejected. May contain `{placeholders}` in the path and query string - see [URL templating](#url-templating). The hostname must be static (no placeholders). |
| `method` | string | ❌ | One of `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`. Defaults to `POST`. |
| `headers` | object | ❌ | Extra request headers as a `{string: string}` map. Sent verbatim with every dispatch. |
| `timeout` | number | ❌ | Seconds Tavus will wait for your endpoint to respond, `0 < timeout <= 60`. Defaults to `10`. This is also the watchdog deadline: if the request hasn't returned by then, the tool call is marked `timeout`. |
| `auth` | object | ❌ | How Tavus authenticates **to your endpoint**. See [Tool Authentication](/sections/conversational-video-interface/pal/llm-tool-auth). Defaults to no auth headers. |
| `body_template` | object | ❌ | JSON object with `{placeholders}` in its string values. Renders to the request body and overrides the default auto-routing (see [Request body](#request-body)). Only valid with `POST` / `PUT` / `PATCH`; pairing it with `GET` / `HEAD` / `DELETE` is rejected. |
| `query_params` | object | ❌ | Static and templated query-string parameters as `{string: string}`. Values containing `{placeholders}` are interpolated from LLM arguments; plain strings pass through. |
| `content_type` | string | ❌ | Override the request `Content-Type` header (defaults to `application/json`). Useful for `application/x-www-form-urlencoded` and similar. |
## URL templating
`{placeholders}` in the URL path or query string are filled at request time from the LLM's tool arguments and URL-encoded. The hostname must be static.
### Reserved system placeholders
In addition to the LLM's tool arguments, Tavus injects a small set of system placeholders at request time. They're available everywhere placeholders are interpolated - URL, `query_params` values, and `body_template`. Useful for idempotency keys, correlation IDs in your logs, and Tavus-side tracing.
| Placeholder | Filled with |
| ------------------------- | ------------------------------------------------------------------- |
| `{tavus_conversation_id}` | The Tavus conversation ID (`c…`). |
| `{tavus_tool_call_id}` | Unique ID for this specific tool invocation. Stable across retries. |
| `{tavus_inference_id}` | The LLM turn (inference) that emitted the tool call. |
| `{tavus_turn_idx}` | Integer index of the conversational turn. |
| `{tavus_tool_name}` | The tool's `name` as registered at `/v2/tools`. |
The `tavus_` prefix is reserved. Tool `parameters.properties` cannot declare a property whose name starts with `tavus_` - the create / update tool API rejects it with `400`. Pick a different name (e.g. `customer_conversation_id`) if you need a similar field.
## Request body and query string
Only arguments declared in your tool's `parameters.properties` ride along. Body and query string are decided independently - you can combine them in any way.
### Body
The HTTP method decides whether a body is sent. `GET` / `HEAD` / `DELETE` never carry a body - `body_template` on those methods is rejected at validation time.
| Method | `body_template` | Body |
| ------------------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `GET` / `HEAD` / `DELETE` | not allowed | empty |
| `POST` / `PUT` / `PATCH` | unset | Declared arguments not consumed by URL placeholders become the **JSON body**. `Content-Type: application/json`. |
| `POST` / `PUT` / `PATCH` | set | Renders the template with arguments and uses that as the body. Use this when the API expects a nested or renamed shape. |
### Query string
`query_params` applies to **every** method - you can attach a query string to a POST as easily as to a GET.
| Method | `query_params` | Query string |
| ------------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------- |
| `GET` / `HEAD` / `DELETE` | unset | Declared arguments not consumed by URL placeholders auto-route here. |
| `GET` / `HEAD` / `DELETE` | set | Only the entries you list reach the query string. Auto-route is **skipped** so your named entries aren't duplicated. |
| `POST` / `PUT` / `PATCH` | unset | empty (body holds the args). |
| `POST` / `PUT` / `PATCH` | set | Only the entries you list reach the query string. Body is decided separately by `body_template` or auto-route. |
### Example - reshape flat tool args into a nested API body
Suppose the tool's `parameters` schema defines two flat fields:
```json Tool parameters (what the LLM emits) theme={null}
{
"type": "object",
"properties": {
"search_term": { "type": "string" },
"region": { "type": "string" }
}
}
```
But the API expects a nested shape - `query.text` and `filters.region`. Use `body_template` to remap:
```json delivery.api with body_template theme={null}
"delivery": {
"api": {
"url": "https://api.example.com/search",
"method": "POST",
"auth": { "type": "api_key", "location": "header", "name": "X-API-Key", "value": "..." },
"body_template": {
"query": { "text": "{search_term}" },
"filters": { "region": "{region}" }
}
}
}
```
When the LLM calls the tool with `{ "search_term": "pizza", "region": "tokyo" }`, Tavus sends:
```
POST https://api.example.com/search
X-API-Key: ...
Content-Type: application/json
{"query": {"text": "pizza"}, "filters": {"region": "tokyo"}}
```
`body_template` is a regular JSON object - write it as one, no escaping. Each `{placeholder}` inside a string value is replaced with the matching LLM argument. If a string is **exactly** one placeholder (`"{count}"`), the substituted value preserves its native type - so `"{count}"` with `count: 10` produces `10` (number), not `"10"` (string). Non-string values in the template (numbers, bools, `null`) pass through unchanged.
## Tavus callback shape
Used when `auth.type: hmac` is set. Tavus ignores `body_template` / `query_params` / URL templating in this mode and sends a fixed JSON envelope, signed with HMAC-SHA256.
```json Request body (POST/PUT/PATCH) theme={null}
{
"name": "get_current_weather",
"arguments": "{\"city\":\"San Francisco\",\"unit\":\"celsius\"}",
"tool_call_id": "call_abc123",
"conversation_id": "c123456789",
"inference_id": "inf_987654321",
"turn_idx": 4
}
```
| Field | Type | Description |
| ----------------- | ------- | ---------------------------------------------------------------------------------------- |
| `name` | string | The tool's `name`. |
| `arguments` | string | Tool arguments as a JSON-encoded string (the LLM's raw output). Parse this on your side. |
| `tool_call_id` | string | Unique ID for this specific invocation. |
| `conversation_id` | string | The Tavus conversation ID. |
| `inference_id` | string | The inference (LLM turn) that produced the call. |
| `turn_idx` | integer | Index of the conversational turn (groups events from the same turn). |
Request headers Tavus always sets in this mode:
| Header | Value |
| ------------------- | ------------------------------------------------------------------------------------------------------ |
| `Content-Type` | `application/json` |
| `X-Tavus-Signature` | HMAC-SHA256 hex digest of the request body. See [Verifying API signatures](#verifying-api-signatures). |
Any extra `delivery.api.headers` you configured are merged in.
## Response Tavus expects
The same response handling applies to both API modes:
| Status | Outcome |
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `2xx` | Treated as success. Response body is used as the tool result (see `on_resolve` for how it is consumed). |
| `5xx` | One retry after a short backoff. If the retry also fails, the call is marked `error`. |
| `401` (oauth2\_client\_credentials only) | One retry with a freshly fetched token. |
| Other non-2xx | No retry; marked `error`. |
| Timeout (no response within `delivery.api.timeout` seconds) | Marked `timeout`. |
| Connection error | One retry. If the retry also fails, marked `error`. |
When the status is `error` or `timeout` and `on_resolve` is not `fire_and_forget`, the PAL acknowledges the failure to the user (no result content is fed back to the LLM).
## Verifying API signatures
When you set `auth.type: "hmac"` with an `auth.secret`, Tavus signs the **exact request body bytes** with HMAC-SHA256 and sends the hex digest in `X-Tavus-Signature`. Verify it before trusting the payload.
The signing input is the raw body Tavus sends, which is canonical JSON with keys sorted alphabetically. Verify against the raw bytes you received, **not** a re-serialized version - any re-encoding can change byte order and break the signature.
```python Python (Flask) [expandable] theme={null}
import hmac
import hashlib
import json
import os
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["TAVUS_TOOL_SECRET"].encode("utf-8")
@app.post("/tools/get_weather")
def get_weather():
received_sig = request.headers.get("X-Tavus-Signature", "")
expected_sig = hmac.new(SECRET, request.get_data(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(received_sig, expected_sig):
abort(401)
body = request.get_json()
args = json.loads(body["arguments"])
# ... your business logic ...
return "It is 72 degrees and sunny in San Francisco.", 200
```
```javascript Node.js (Express) [expandable] theme={null}
import express from "express";
import crypto from "crypto";
const app = express();
const SECRET = process.env.TAVUS_TOOL_SECRET;
// IMPORTANT: capture the raw bytes; do not let middleware re-serialize.
app.use(express.raw({ type: "application/json" }));
app.post("/tools/get_weather", (req, res) => {
const received = req.header("X-Tavus-Signature") || "";
const expected = crypto
.createHmac("sha256", SECRET)
.update(req.body)
.digest("hex");
const ok =
received.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
if (!ok) return res.status(401).end();
const body = JSON.parse(req.body.toString("utf8"));
const args = JSON.parse(body.arguments);
// ... your business logic ...
res.type("text/plain").send("It is 72 degrees and sunny in San Francisco.");
});
app.listen(3000);
```
Replace `` with your actual API key. You can generate one in the PAL Maker.
# Google Meet / Zoom
Source: https://docs.tavus.io/sections/conversational-video-interface/pal/meetings
Give a PAL its own meeting identity so it can be invited to Google Meet or Zoom calls over a calendar invite and join automatically - including meetings that are already in progress.
The **conferencing layer** gives a PAL its own email identity so it can be invited to meetings just like a human teammate. Schedule it on a future calendar event, or invite it to a meeting that's already running. Google Meet and Zoom are both supported.
## How it works
When you set a `username` on the conferencing layer, Tavus provisions a real, invitable email address for the PAL (for example `ada@tavusinvite.com`). From there:
1. You (or anyone on the PAL's allowlist) **send a calendar invite** to that email, with a Google Meet or Zoom link attached.
2. Tavus receives the invite, validates the sender, and **replies `Accepted`** to the organizer.
3. For a **scheduled** meeting, the PAL joins the call about **one minute before the start time**. For a meeting **already in progress**, it joins shortly after the invite is accepted (see [Joining a meeting that's already in progress](#joining-a-meeting-thats-already-in-progress)).
4. The PAL participates with its face's likeness and voice.
## Prerequisites
* A PAL with a **`default_face_id`**. This is **required** whenever the conferencing layer is provided - the **face** supplies the likeness and voice the PAL uses in the meeting. A request that sets the conferencing layer without a default face is rejected with a `400`.
* A standard `full` pipeline PAL (system prompt, LLM, TTS, etc.) configured as you would for any CVI conversation.
## Configuring the conferencing layer
Add a `conferencing` object to `layers` when you [create a PAL](/api-reference/pals/create-pal):
```json theme={null}
{
"pal_name": "Anna",
"system_prompt": "You are Anna, a helpful meeting assistant who takes notes and answers questions.",
"pipeline_mode": "full",
"default_face_id": "r90bbd427f71",
"layers": {
"conferencing": {
"username": "anna",
"allowlist": ["alex@acme.com", ".*@acme\\.com"]
}
}
}
```
A successful create returns the derived email so you know what address to invite:
```json theme={null}
{
"pal_id": "pcb7a34da5fe",
"pal_name": "Anna",
"created_at": "2026-06-11T12:00:00Z",
"conferencing_email": "anna@tavusinvite.com"
}
```
### `username`
The local part of the PAL's meeting email. The full address is rendered as `@tavusinvite.com`.
Rules:
* **Length & characters**: must be **2+ characters**, start and end with an alphanumeric character, and may contain `.`, `_`, or `-` in between (pattern: `^[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9]$`).
* **Case-insensitive**: usernames are stored and matched in lowercase. `Anna` and `anna` are the same identity.
* **Reserved names**: usernames matching `botN` (e.g. `bot1`, `bot42`) are reserved for internal use and rejected.
* **Global uniqueness**: see [Username uniqueness & limitations](#username-uniqueness--limitations) below.
### `allowlist`
An optional list controlling **who is allowed to invite the PAL** to meetings. Each entry is either:
* an **exact email address** - e.g. `"alex@acme.com"`, or
* a **regular expression** matched against the sender's email - e.g. `".*@acme\\.com"` to allow anyone at `acme.com`.
Behavior:
* If `allowlist` is **empty or omitted**, any sender can invite the PAL.
* If `allowlist` is **set**, calendar invites from senders that don't match any entry are **rejected** (the PAL declines), and the invite never schedules a join.
* Google's forwarding address (`forwarding-noreply@google.com`) is always permitted internally; you don't need to add it.
Use the allowlist to keep a PAL's meeting identity private to your team or customers. Pair an exact-match list for known contacts with a domain regex (`.*@yourcompany\.com`) to cover everyone in your org.
### Checking availability before you commit
Before saving a username, you can check whether it's free with [Check Conferencing Username Availability](/api-reference/pals/check-conferencing-username):
```bash theme={null}
curl --request GET \
--url 'https://tavusapi.com/v2/pals/check-username?username=anna' \
--header 'x-api-key: '
```
```json theme={null}
{ "available": true }
```
If the name is taken or invalid, `available` is `false` with a `reason`:
```json theme={null}
{ "available": false, "reason": "Username is already taken." }
```
## Username uniqueness & limitations
All non-whitelabel PALs share a single email domain (`tavusinvite.com`), so the `username` is a **global namespace**:
* A username must be **unique across the entire `tavusinvite.com` domain**, not just within your account. If another PAL - including one owned by a different customer - already uses `anna`, you cannot also use `anna`; you'll get a `400` telling you to choose a different username.
* This is **first-come, first-served**. Pick a distinctive username (for example, prefix it with your product or company name like `acme-anna`) to avoid collisions and to reserve the identity you want.
* Renaming a PAL's username **frees the old name** and provisions the new one. Any calendar invites that were sent to the old address will no longer reach the PAL, so prefer choosing a stable username up front.
## Join a meeting via API
Instead of sending a calendar invite, you can have a PAL join an **existing Google Meet or Zoom call** by passing the meeting link when you [Create Conversation](/api-reference/conversations/create-conversation):
```bash theme={null}
curl --request POST \
--url https://tavusapi.com/v2/conversations \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"face_id": "r90bbd427f71",
"pal_id": "pcb7a34da5fe",
"meeting_url": "https://meet.google.com/xgq-epxn-ccp"
}'
```
The `meeting_url` can be a Google Meet URL (`https://meet.google.com/...`) or a Zoom URL (`https://.zoom.us/...`). The PAL begins joining **shortly after** the API returns a successful response.
Use this when you already have a live meeting link and want to dispatch the PAL programmatically (for example from your own scheduling UI or an ad hoc demo). The [calendar invite flow](#scheduling-calls-via-calendar-invites) below is better when you want the PAL to accept invites and join on a schedule.
This path does not use the conferencing [`allowlist`](#allowlist) - authorization is your API key.
## Scheduling calls via calendar invites
Once a PAL has a conferencing email, scheduling it into a meeting is exactly like inviting a coworker:
1. **Create a calendar event** and **attach a Google Meet or Zoom link** to it (in Google Calendar, use "Add Google Meet video conferencing"; for Zoom, paste the Zoom meeting link into the event details or use your Zoom calendar integration).
2. **Invite the PAL's email** (e.g. `anna@tavusinvite.com`) as a guest.
3. Send the invite. The PAL **replies `Accepted`** to the organizer once the invite is received and authorized.
4. The PAL **joins the call about one minute before the start time** and participates with its face's likeness and voice.
The invite **must include a Google Meet or Zoom link**. If a calendar invite has no supported meeting link, the PAL replies with a `Declined`, since there is nothing to join.
### Joining a meeting that's already in progress
You don't have to schedule the PAL ahead of time. If a meeting is **already running**, invite the PAL's conferencing email to that calendar event the same way you would for a future meeting - add the PAL's address (for example `anna@tavusinvite.com`, from its configured `username`) as a guest on the event that holds the active meeting link.
The PAL **accepts the invite** and **joins the call shortly after**, rather than waiting until one minute before a future start time. The same rules apply: the event must include a Google Meet or Zoom link, and the sender must pass the PAL's [`allowlist`](#allowlist) if one is set.
### Recurring meetings
Recurring calendar events are supported. The PAL will join **each occurrence** of the series at its scheduled time. Editing or cancelling occurrences is reflected automatically (see below).
### Updates, reschedules, and cancellations
The system stays in sync with changes you make in the calendar:
* **Reschedule (new time) or change the meeting link** → the join is automatically rescheduled to the new time/URL.
* **Other edits** (title, description, guest list) → metadata is updated without disrupting the scheduled join.
* **Cancel the event** → the scheduled join is cancelled. For a recurring series, cancelling a single occurrence cancels only that one; cancelling the series cancels all remaining occurrences.
### Allowlist enforcement at invite time
If the PAL has an [`allowlist`](#allowlist), the **invite's sender** (the calendar event organizer) must match it. Invites from senders not on the allowlist are silently rejected and no join is scheduled. With no allowlist, anyone who can email the PAL's address can schedule it.
### Supported meeting platforms
PALs can join **Google Meet** and **Zoom** calls. The invite must contain a Google Meet or Zoom link.
**Microsoft Teams support is coming soon.** Invites with Teams links are recognized, but the PAL cannot join those calls yet - only Google Meet and Zoom are live today.
## Modifying conferencing on an existing PAL
Use [Patch PAL](/api-reference/pals/patch-pal) to add, change, or remove conferencing settings with JSON Patch operations:
```bash theme={null}
# Give an existing PAL a meeting identity
curl --request PATCH \
--url https://tavusapi.com/v2/pals/{pal_id} \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '[
{ "op": "add", "path": "/layers/conferencing/username", "value": "anna" }
]'
# Update the allowlist
curl --request PATCH \
--url https://tavusapi.com/v2/pals/{pal_id} \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '[
{ "op": "replace", "path": "/layers/conferencing/allowlist", "value": ["alex@acme.com", ".*@acme\\.com"] }
]'
```
Changing the `username` changes the PAL's email identity. Calendar invites sent to the **previous** address will no longer reach the PAL, so re-invite the PAL using its new email after a rename.
## End-to-end example
```json theme={null}
// 1. Create a PAL with a meeting identity
{
"pal_name": "Anna",
"system_prompt": "You are Anna, a meeting assistant. Greet attendees, take notes, and answer questions about the agenda.",
"pipeline_mode": "full",
"default_face_id": "r90bbd427f71",
"layers": {
"conferencing": {
"username": "acme-anna",
"allowlist": [".*@acme\\.com"]
}
}
}
```
```json theme={null}
// Response - invite this address from any @acme.com calendar
{
"pal_id": "pcb7a34da5fe",
"pal_name": "Anna",
"conferencing_email": "acme-anna@tavusinvite.com"
}
```
Then, from an `@acme.com` account, create a calendar event with a Google Meet or Zoom link and invite `acme-anna@tavusinvite.com`. Anna replies `Accepted`, and joins the call automatically a minute before it begins.
See [Create PAL](/api-reference/pals/create-pal) and [Patch PAL](/api-reference/pals/patch-pal) for the full request schema.
# Objectives
Source: https://docs.tavus.io/sections/conversational-video-interface/pal/objectives
Objectives are goal-oriented instructions to define the desired outcomes and flow of your conversations.
Objectives work alongside your system prompt to provide a structured, flexible approach to guide conversations. They provide the most value during purposeful conversations that need to be tailored to specific processes, customer journeys, or workflows, while maintaining engaging and natural interactions.
For example, if you're creating a lead qualification PAL for sales, you can set objectives to gather contact information, understand budget requirements, and assess decision-making authority before scheduling a follow-up meeting.
Objectives can only be created using the [Create Objectives](/api-reference/objectives/create-objectives) API.
For a deep dive on best practices for structuring objectives and guardrails, see the [Objectives and Guardrails Prompting Guide](/sections/onboarding-guide/objectives).
When designing your objectives, it's helpful to keep a few things in mind:
* Plan your entire ideal workflow. This will help create a robust branching structure that successfully takes the participant from start to finish.
* Think through the possible answers a participant might give, and ensure the workflow covers these cases.
* Ensure your PAL's system prompt does not conflict with the objectives. For example, a system prompt, "You are a tutor," would not perform well with the objectives workflow of a sales associate.
## Attaching objectives to a PAL
To attach objectives to a PAL, you can either:
* Add them during [PAL creation](/api-reference/pals/create-pal) like this:
```sh theme={null}
curl --request POST \
--url https://tavusapi.com/v2/pals/ \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"system_prompt": "You are a lead qualification assistant.",
"objectives_id": "o12345"
}'
```
OR
* Add them by [editing the PAL](/api-reference/pals/patch-pal) like this:
```sh theme={null}
curl --request PATCH \
--url https://tavusapi.com/v2/pals/{pal_id} \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '[
{"op": "add", "path": "/objectives_id", "value": "o12345"}
]'
```
For the best results, try creating unique objectives for different conversation purposes or business outcomes.
For example, a customer onboarding PAL might use objectives focused on data collection, while a support PAL might use objectives focused on issue resolution.
## Parameters
### `objective_name`
A desciptive name for the objective.
Example: `"check_patient_status"`
This must be a string value without spaces.
### `objective_prompt`
A text prompt that explains what the goals of this objective are. The more detail you can provide, the better.
Example: `"Ask the patient if they are new or are returning."`
### `confirmation_mode`
This string value defines whether the LLM should determine whether this objective was completed or not.
* If set to `auto`, the LLM makes this decision.
* If set to `manual`, the participant must manually confirm that the objective was completed by the platform triggering an app message (`conversation.objective.pending`) and the participant having the ability to send one back called `conversation.objective.confirm`. This can include having the participant review the collected values for accuracy.
The default value of `confirmation_mode` is `auto`.
### `output_variables` (optional)
This is a list of string variables that should be collected as a result of the objective being successfully completed.
Example: `["patient_status", "patient_group"]`
### `modality`
This value represents whether a specific objective should be completed based on the participant's verbal or visual responses. Each individual objective can be visual or verbal (not both), but this can vary across objectives.
The default value for `modality` is `"verbal"`.
### `next_conditional_objectives`
This represents a mapping of objectives (identified by `objective_name`), to conditions that must be satisfied for that objective to be triggered after the completion of the current objective.
`next_conditional_objectives` and `next_required_objective` are mutually exclusive - you can use one or the other on a given objective, but not both.
Example:
```json theme={null}
{
"new_patient_intake_process": "If the patient has never been to the practice before",
"existing_patient_intake_process": "If the patient has been to the practice before"
}
```
### `next_required_objective`
The name of the next required objective (identified by `objective_name`) that will be activated once the current objective is completed. Use this to define a single next objective without conditions.
`next_required_objective` and `next_conditional_objectives` are mutually exclusive - you can use one or the other on a given objective, but not both.
Example: `"get_patient_name"`
### `callback_url` (optional)
A URL that you can send notifications to when a particular objective has been completed.
Example: `"https://your-server.com/objectives-webhook"`
When completed, the callback payload includes the `conversation_id`, the name of the objective, and any collected output variables:
```json theme={null}
{
"conversation_id": "",
"objective_name": "",
"output_variables": {
"": ""
}
}
```
# Overview
Source: https://docs.tavus.io/sections/conversational-video-interface/pal/overview
Define how your PAL behaves, responds, and speaks by configuring layers and pipeline modes.
A **PAL** (Personified Application Layer) contains the full configuration for its role in CVI: behavior, knowledge, objectives, guardrails, tools, and every pipeline layer. The **face** defines the on-screen likeness and voice; the PAL defines how it thinks, speaks, and acts.
**Terminology update:** Tavus now uses **PAL** (behavior, knowledge, and pipeline configuration) and **Face** (visual appearance and voice) in the API and docs. Legacy names **persona** and **replica** still work on existing endpoints and request fields (`/v2/personas`, `/v2/replicas`, `persona_id`, `replica_id`, and related aliases) for backward compatibility.
**At a glance**
* **PAL** - The agent's identity (name, behavior) plus **pipeline mode**, **default face**, **layers**, **documents** (Knowledge Base), **objectives**, and **guardrails**. Voice comes from the **face** by default; the TTS layer can override it.
* **Relationship to CVI** - PALs hold the settings that drive a real-time CVI session; see **[What Is CVI?](/sections/conversational-video-interface/overview-cvi)** for the full stack (WebRTC, layers, Phoenix, and latency characteristics on the default path).
* **Layers (order of guides below)** - Perception → STT → Conversational Flow → LLM → TTS; each has its own configuration page.
## PAL Customization Options
Each PAL includes configurable fields. Here's what you can customize:
* **PAL Name**: Display name shown when the PAL joins a call.
* **System Prompt**: Instructions sent to the language model to shape the PAL's tone, personality, and behavior.
* **Pipeline mode**: Controls which CVI pipeline layers are active and how input/output flows through the system. See [Pipeline modes](/sections/conversational-video-interface/quickstart/pipeline-modes) for how the full pipeline, Echo, integrations, and custom LLM paths differ.
* **Default face**: Sets the photorealistic face associated with the PAL (`default_face_id` on `POST /v2/pals`; required).
* **Layers**: Perception, STT, conversational flow, LLM, and TTS - each processes part of the interaction and can be tuned independently (see [Layers](#layers) below).
* **Documents**: A set of documents that the PAL has access to via the **Knowledge Base** (retrieval-augmented generation, RAG).
* **Objectives**: The goal-oriented instructions your PAL will adhere to throughout the conversation.
* **Guardrails**: Conversational boundaries that can be used to strictly enforce desired behavior.
* **AI disclosure** (`disclosure_type`, `verbal_disclosure`, `visual_disclosure`): Spoken and on-screen notice that the participant is talking to an AI. See [EU AI Act](/sections/onboarding-guide/eu-ai-act) for product guidance.
## Objectives & Guardrails
Provide your PAL with robust workflow management tools, curated to your use case
The sequence of goals your PAL will work to achieve throughout the conversation - for example, gathering a piece of information from the user.
Conversational boundaries that can be used to strictly enforce desired behavior.
## Layers
Explore our in-depth guides to customize each layer to fit your specific use case:
Defines how the PAL interprets visual input like facial expressions and gestures.
Transcribes user speech into text using the configured speech-to-text engine.
Controls turn-taking, interruption handling, and active listening behavior for natural conversations.
Generates PAL responses using a language model. Supports Tavus-hosted or custom LLMs.
Converts text responses into speech using Tavus or supported third-party TTS engines.
Gives the PAL an email identity so it can be invited to Google Meet or Zoom calls via a calendar invite and join automatically - including meetings already in progress.
# Perception
Source: https://docs.tavus.io/sections/conversational-video-interface/pal/perception
Learn how to configure the perception layer with Raven to enable real-time visual and audio understanding.
The **Perception Layer** in Tavus enhances an AI agent with real-time visual and audio understanding.
By using [Raven](/sections/models#raven%3A-perception-model), the AI agent becomes more context-aware, responsive, and capable of triggering actions based on visual and audio input.
## Configuring the Perception Layer
To configure the Perception Layer, define the following parameters within the `layers.perception` object:
### 1. `perception_model`
Specifies the perception model to use.
* **Options**:
* `raven-1` **(default and recommended)**: Real-time emotional understanding from user audio, more natural and human-like interactions, plus advanced visual perception.
* `off`: Disables the perception layer.
**Screen Share Feature**: When using Raven, screen share is enabled by default without additional configuration.
### Audio Perception
Raven-1 (the default) analyzes user tone and emotion in real-time. This context is automatically sent to the LLM alongside utterances, enabling more natural, empathetic responses. For example:
```
The user sounded sarcastic when they said this
Wow, I love Mondays.
```
Audio analysis tags are stripped from transcription callbacks.
Audio analysis output is limited to 32 tokens per utterance.
### `emotion_recognition`
Controls whether Raven-1 may infer emotion from **biometric signals** — the user's facial expression and tone of voice. Relevant to [EU AI Act](/sections/onboarding-guide/eu-ai-act) configuration: workplace and education deployments generally should not infer emotion from face or voice biometrics.
| Value | Behavior |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `full` | Raven-1 attaches biometric emotion analysis. |
| `limited` | Raven-1 does not attach any biometric-derived emotion. Other cues (spoken content, objects, activity) still flow through. |
| `auto` (default) | Follows the conversation's [`policy`](/api-reference/conversations/create-conversation) param — behaves like `limited` when `policy` is `eu`, otherwise `full`. |
```json theme={null}
"layers": {
"perception": {
"perception_model": "raven-1",
"emotion_recognition": "auto"
}
}
```
## Perception Analysis Queries
Raven supports three kinds of queries that differ by **when** they run and **how** they affect the call:
* **perception\_analysis\_queries** - Evaluated only at **end of call**. They do not change live behavior; they only shape the summary you get in the [Perception Analysis](/sections/event-schemas/conversation-perception-analysis) event sent to your [conversation callback](/sections/webhooks-and-callbacks#conversation-callbacks).
* **visual\_awareness\_queries** and **audio\_awareness\_queries** - Evaluated **throughout the call**. Their answers are passed to the LLM as context, so the PAL can react in real time. You receive this ongoing analysis in each user turn via the [Utterance event](/sections/event-schemas/conversation-utterance) as `user_visual_analysis` and `user_audio_analysis`.
Use **visual\_awareness\_queries** and **audio\_awareness\_queries** when you want the PAL to be aware of or focus on something specific during the conversation. Use **perception\_analysis\_queries** when you want your end-of-call summary to address specific points.
## Visual Perception Configuration
### 2. `visual_awareness_queries`
An array of custom queries that Raven continuously monitors in the visual stream.
```json theme={null}
"visual_awareness_queries": [
"Is the user wearing a bright outfit?"
]
```
Queries that Raven evaluates **continuously during the call** (on the order of every second). The answers are fed into the rolling visual context for the LLM, so the PAL can respond to what it "sees." This same context also supports the end-of-call summary. You can read the ongoing visual analysis for each user utterance in the [Utterance event](/sections/event-schemas/conversation-utterance) as **user\_visual\_analysis**.
**When to use:** when you want the PAL to pay attention to something visual in real time (e.g. expression, clothing, objects on screen).
**Example:**
```json theme={null}
"visual_awareness_queries": [
"What is the main expression on the user's face?",
"Is the user wearing a jacket?",
"Does the user appear distressed or uncomfortable?"
]
```
### 3. `perception_analysis_queries`
An array of custom queries that Raven processes at the end of the call to generate a visual analysis summary for the user.
Queries that are answered **once, at the end of the call**, by looking at what was observed over the whole conversation. They do not affect the call itself-only the content of the end-of-call summary. (Currently the summary is visual only; naming is kept general for future support.)
**When to use:** When you want the post-call report to answer specific questions (e.g. "Did the user ever have two people on screen?", "How often was the user looking at the screen?").
**Example:**
```json theme={null}
"perception_analysis_queries": [
"On a scale of 1-100, how often was the user looking at the screen?",
"Is there any indication that more than one person is present?"
]
```
The answers are delivered in a [Perception Analysis](/sections/event-schemas/conversation-perception-analysis) event. Example payload:
```json theme={null}
{
"properties": {
"analysis": "**User's Gaze Toward Screen:** The participant looked at the screen approximately 75% of the time.\n\n**Multiple People Present:** No indication of additional participants was detected during the call."
},
"conversation_id": "",
"event_type": "application.perception_analysis",
"timestamp": "2025-07-11T09:13:35.361736Z"
}
```
You do not need to set `visual_awareness_queries` in order to use `perception_analysis_queries`.
```json theme={null}
"perception_analysis_queries": [
"Is the user wearing multiple bright colors?",
"Is there any indication that more than one person is present?",
"On a scale of 1-100, how often was the user looking at the screen?"
]
```
Best practices for `visual_awareness_queries` and `perception_analysis_queries`:
* Use simple, focused prompts.
* Use queries that support your PAL's purpose.
All Raven API parameters (queries, prompts, tool definitions, etc.) have a **10,000 character limit** per entry. Entries exceeding this limit will cause an exception.
### 4. `visual_tool_prompt`
Tell Raven when and how to trigger tools based on what it sees.
```json theme={null}
"visual_tool_prompt":
"You have a tool to notify the system when a bright outfit is detected, named `notify_if_bright_outfit_shown`. You MUST use this tool when a bright outfit is detected."
```
### 5. `visual_tools`
Legacy inline perception tools. For new integrations, create vision tools at `/v2/tools` with `origin: "vision"` and attach them to the PAL - see [Tool Calling for Perception](/sections/conversational-video-interface/pal/perception-tool).
The field below defines OpenAI-style function objects **directly on the PAL**. Tavus still merges them at conversation start alongside any registry tools you attach, but inline tools cannot use registry-only settings (`delivery`, API auth, etc.).
```json theme={null}
"visual_tools": [
{
"type": "function",
"function": {
"name": "notify_if_bright_outfit_shown",
"description": "Use this function when a bright outfit is detected in the image with high confidence",
"parameters": {
"type": "object",
"properties": {
"outfit_color": {
"type": "string",
"description": "Best guess on what color of outfit it is"
}
},
"required": ["outfit_color"]
}
}
}
]
```
Legacy field names (`perception_tools`, `perception_tool_prompt`) still work - see [Migration from Legacy Perception to Raven-1](/sections/troubleshooting#migration-from-legacy-perception-to-raven-1). For the full legacy inline reference, see [Legacy inline tool calling](/sections/troubleshooting#legacy-inline-tool-calling).
## Audio Perception Configuration (Raven-1)
The following fields are available when using `raven-1` and enable custom audio-based perception capabilities.
### 6. `audio_awareness_queries`
An array of custom queries that Raven-1 continuously monitors in the audio stream. Use these to track specific audio patterns or user states.
Audio analysis output is limited to 32 tokens per query response.
```json theme={null}
"audio_awareness_queries": [
"Does the user sound frustrated or confused?",
"Is the user speaking quickly as if in a hurry?"
]
```
Queries that Raven-1 evaluates **continuously during the call** on the audio stream. The answers are passed to the LLM as context so the PAL can respond to tone and delivery. You can read the ongoing audio analysis for each user utterance in the [Utterance event](/sections/event-schemas/conversation-utterance) as **user\_audio\_analysis**. (There is no separate end-of-call summary for audio.)
**When to use:** when you want the PAL to react to how the user sounds (e.g. frustrated, confused, in a hurry).
**Example:**
```json theme={null}
"audio_awareness_queries": [
"Does the user sound frustrated or confused?",
"Is the user speaking quickly as if in a hurry?"
]
```
### 7. `audio_tool_prompt`
Tell Raven-1 when and how to trigger tools based on what it hears (beyond the automatic emotion analysis).
```json theme={null}
"audio_tool_prompt":
"You have a tool to escalate to a human agent when the user sounds very frustrated, named `escalate_to_human`. Use this tool when detecting sustained frustration."
```
### 8. `audio_tools`
Legacy inline perception tools. For new integrations, create audio tools at `/v2/tools` with `origin: "audio"` and attach them to the PAL - see [Tool Calling for Perception](/sections/conversational-video-interface/pal/perception-tool).
```json theme={null}
"audio_tools": [
{
"type": "function",
"function": {
"name": "escalate_to_human",
"description": "Escalate the conversation to a human agent when user frustration is detected",
"parameters": {
"type": "object",
"properties": {
"reason": {
"type": "string",
"description": "The reason for escalation"
}
},
"required": ["reason"]
}
}
}
]
```
Requires `perception_model: "raven-1"`. Legacy inline details: [Legacy inline tool calling](/sections/troubleshooting#legacy-inline-tool-calling).
## Example Configurations
The JSON below uses **legacy inline** `visual_tools` / `audio_tools` for illustration. New PALs should define tools in the [tools registry](/sections/conversational-video-interface/pal/tools) instead.
This example demonstrates a PAL that monitors for visual cues (bright outfits) and triggers a tool when detected.
```json theme={null}
{
"pal_name": "Fashion Advisor",
"system_prompt": "As a Fashion Advisor, you specialize in offering tailored fashion advice.",
"pipeline_mode": "full",
"default_face_id": "r90bbd427f71",
"layers": {
"perception": {
"perception_model": "raven-1",
"visual_awareness_queries": [
"Is the user wearing a bright outfit?"
],
"perception_analysis_queries": [
"Is the user wearing multiple bright colors?",
"On a scale of 1-100, how often was the user looking at the screen?"
],
"visual_tool_prompt": "You have a tool to notify the system when a bright outfit is detected, named `notify_if_bright_outfit_shown`. You MUST use this tool when a bright outfit is detected.",
"visual_tools": [
{
"type": "function",
"function": {
"name": "notify_if_bright_outfit_shown",
"description": "Use this function when a bright outfit is detected in the image with high confidence",
"parameters": {
"type": "object",
"properties": {
"outfit_color": {
"type": "string",
"description": "Best guess on what color of outfit it is"
}
},
"required": ["outfit_color"]
}
}
}
]
}
}
}
```
This example demonstrates a PAL that monitors user tone and escalates to a human agent when sustained frustration is detected.
```json theme={null}
{
"pal_name": "Support Agent",
"system_prompt": "You are a helpful customer support agent.",
"pipeline_mode": "full",
"default_face_id": "r90bbd427f71",
"layers": {
"perception": {
"perception_model": "raven-1",
"audio_awareness_queries": [
"Does the user sound frustrated or confused?",
"Is the user speaking quickly as if in a hurry?"
],
"audio_tool_prompt": "You have a tool to escalate to a human agent when the user sounds very frustrated, named `escalate_to_human`. Use this tool when detecting sustained frustration.",
"audio_tools": [
{
"type": "function",
"function": {
"name": "escalate_to_human",
"description": "Escalate the conversation to a human agent when user frustration is detected",
"parameters": {
"type": "object",
"properties": {
"reason": {
"type": "string",
"description": "The reason for escalation"
}
},
"required": ["reason"]
}
}
}
]
}
}
}
```
Please see the Create a PAL endpoint for more details.
# Tool Calling for Perception
Source: https://docs.tavus.io/sections/conversational-video-interface/pal/perception-tool
Define vision and audio tools that fire when Raven sees or hears something during a conversation.
**Perception tool calling** lets the PAL trigger functions based on **visual** or **audio** cues the perception model (Raven) detects during a conversation, in parallel with the main LLM turn. Tools are reusable objects: create them once, and attach them to any number of PALs. The only difference from LLM tools is `origin`.
This page documents the **tools registry** (`/v2/tools` with `origin: "vision"` or `"audio"`). If your PAL still embeds tools under `layers.perception.visual_tools` or `layers.perception.audio_tools`, see [Legacy inline tool calling](/sections/troubleshooting#legacy-inline-tool-calling).
Perception tool calling is only available with **Raven** (`perception_model: "raven-1"` on the PAL's `perception` layer).
## How Perception Tools Work
Perception runs as a **parallel step** alongside the conversational LLM. Raven analyses the audio and video streams continuously and fires a tool the moment it detects something matching one of the tool descriptions you defined.
There are two flavors, picked via the tool's `origin`:
* **Vision tools** (`origin: "vision"`) - triggered by what Raven **sees** in the video stream (e.g. an ID card, a bright outfit, a hat).
* **Audio tools** (`origin: "audio"`) - triggered by what Raven **hears** in the audio stream (e.g. sarcasm, sustained frustration).
Because perception runs in parallel, **the PAL keeps speaking and listening normally** while a perception tool dispatches. Perception tools are **fire-and-forget**: the PAL does not pause, fill, or react to the result on the conversational side.
## Defining a Perception Tool
The `name`, `description`, `parameters`, and `delivery` fields work the same way they do for LLM tools - see [Tool Calling for LLM](/sections/conversational-video-interface/pal/llm-tool#tool-object) for the full reference.
| Field | Type | Required | Description |
| ------------- | ------ | -------- | -------------------------------------------------------------------------------------- |
| `name` | string | ✅ | Unique identifier, scoped to your account. Must match `^[a-zA-Z_][a-zA-Z0-9_]{0,63}$`. |
| `description` | string | ✅ | What Raven should look or listen for. Be specific - this is what triggers the tool. |
| `parameters` | object | ❌ | JSON Schema for the arguments Raven extracts when the cue is detected. |
| `origin` | string | ✅ | `"vision"` or `"audio"`. |
| `delivery` | object | ❌ | Defaults to `{"app_message": true}`. API is also supported (same shape as LLM tools). |
You do **not** need to set `on_call`, `on_resolve`, or `static_filler` on a perception tool. Omit them and the API applies the only allowed values (`null`, `"fire_and_forget"`, `null` respectively). Passing any other value returns a 400.
## Vision Tool Example
```bash Create a vision tool [expandable] theme={null}
curl --request POST \
--url https://tavusapi.com/v2/tools \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"name": "notify_if_id_shown",
"description": "Trigger when a driver'\''s license or passport is clearly visible in the video stream with high confidence.",
"parameters": {
"type": "object",
"properties": {
"id_type": {
"type": "string",
"description": "Best guess on what type of ID it is"
}
},
"required": ["id_type"]
},
"origin": "vision"
}'
```
When Raven detects an ID in frame, your application receives a [`conversation.perception_tool_call`](/sections/event-schemas/conversation-perception-tool-call) event with `modality: "vision"`, the `name`, structured `arguments`, and a `frames` array of base64-encoded images that triggered the call.
## Audio Tool Example
```bash Create an audio tool [expandable] theme={null}
curl --request POST \
--url https://tavusapi.com/v2/tools \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"name": "notify_sarcasm_detected",
"description": "Trigger when the user'\''s tone or phrasing suggests sarcasm.",
"parameters": {
"type": "object",
"properties": {
"reason": {
"type": "string",
"description": "Why you detected sarcasm (e.g. what the user said)"
}
},
"required": ["reason"]
},
"origin": "audio"
}'
```
When Raven hears the cue, your application receives a [`conversation.perception_tool_call`](/sections/event-schemas/conversation-perception-tool-call) event with `modality: "audio"` and the structured `arguments`.
## Attaching to a PAL
Perception tools are attached the same way as LLM tools:
```bash Attach perception tools theme={null}
curl --request POST \
--url https://tavusapi.com/v2/pals/{pal_id}/tools \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"tool_ids": ["tabc123def456"]
}'
```
The same PAL can hold both LLM and perception tools. Make sure the PAL's `perception` layer has `perception_model: "raven-1"` for vision and audio tools to fire.
## Delivery
Perception tools use the same `delivery` field as LLM tools - see [Tool Delivery](/sections/conversational-video-interface/pal/llm-tool-delivery) and [Tool Authentication](/sections/conversational-video-interface/pal/llm-tool-auth). The only perception-specific bit: the app-message event is `conversation.perception_tool_call` (not `conversation.tool_call`).
Because perception tools are fire-and-forget, the response body your API returns is **not consumed** by the conversational LLM. A `2xx` is enough to acknowledge receipt; a non-2xx is logged but does not affect the conversation.
Replace `` with your actual API key. You can generate one in the PAL Maker.
# Post-Call Actions
Source: https://docs.tavus.io/sections/conversational-video-interface/pal/post-call-tool
Run a tool automatically after a conversation ends - Tavus fills its open arguments from the transcript and perception analysis and delivers the result for you.
**Post-call actions** run a tool automatically once a conversation ends. Unlike [LLM](/sections/conversational-video-interface/pal/llm-tool) and [perception](/sections/conversational-video-interface/pal/perception-tool) tool calling - which fire *during* the conversation and you handle in your own app - a post-call action runs *after* the call is over. Tavus fills its open arguments from the conversation transcript and [perception analysis](/sections/conversational-video-interface/pal/perception) and delivers the call to your endpoint for you.
LLM and perception tools are surfaced to you as events to run yourself during the call. A post-call action is the opposite: it runs **once, after the conversation ends**, and **Tavus delivers it for you** - no client-side handling required.
Use a post-call action when something should happen once, after the call, based on what was discussed - for example: post a summary to Slack, create a CRM note or support ticket, or trigger a follow-up email.
## How It Works
When a conversation ends, for each post-call action attached to the PAL:
1. Tavus reads the conversation transcript and perception analysis.
2. An AI step fills the action's **open** arguments from that context - the fields you describe.
3. Any **fixed** values you set are used exactly as provided.
4. Tavus delivers an HTTPS request to your configured webhook endpoint.
Each action runs **once per conversation**. It is never shown to the model during the live conversation, so it can't be triggered mid-call.
A post-call action needs a transcript to work from, so it won't run for a conversation with no dialogue.
## Defining a Post-Call Action
A post-call action is a [tool](/api-reference/tools/create-tool) you create with the Tools API, with three things set:
| Field | Type | Required | Description |
| -------------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `trigger_type` | string | ✅ | Set to `"post_call"` so the tool runs after the conversation ends (rather than the default `"in_call"`). |
| `origin` | string | - | Leave unset. `origin` is the live modality (`llm`, `vision`, `audio`) for in-call tools; post-call actions have no live modality. |
| `parameters` | object | ✅ | A JSON Schema object describing the action's arguments (see below). |
| `delivery` | object | ✅ | How the action is sent when it runs (see [Delivery](#delivery)). Uses the standard tool [`delivery`](/sections/conversational-video-interface/pal/llm-tool-delivery) field. |
### Parameters
Each parameter is either **fixed** or **AI-filled**:
| Kind | How to define it | Behavior |
| --------------- | --------------------------------------------------------- | --------------------------------------------------------------------------------- |
| **Fixed value** | Set a `const` on the parameter (e.g. a Slack channel ID). | Tavus uses it exactly as provided. The AI never changes it. |
| **AI-filled** | Leave the parameter open with a clear `description`. | Tavus writes it from the transcript and perception analysis when the action runs. |
**Best practices:**
* Pin destinations and identifiers (channels, recipient IDs, record IDs) as **fixed values** so the AI never has to guess them.
* Give every AI-filled field a precise `description` - it's the biggest lever on the quality of what gets written.
### Delivery
A post-call action uses the standard tool [`delivery`](/sections/conversational-video-interface/pal/llm-tool-delivery) field with **`delivery.api` only**. App-message delivery doesn't apply once the call is over.
Configure `delivery.api` with a URL, HTTP method, headers, `body_template`, and any auth. Tavus sends one HTTPS request to that endpoint when the action runs. See [Tool Delivery](/sections/conversational-video-interface/pal/llm-tool-delivery) for the request shape, URL templating, and response handling.
## Example Configuration
A post-call action that posts a one-line summary to your backend. `channel` is pinned with a `const`; `message` is written by the AI from the transcript and perception analysis, and `body_template` maps both onto the request body. See the [Tools API reference](/api-reference/tools/create-tool) for the full request and [Tool Delivery](/sections/conversational-video-interface/pal/llm-tool-delivery) for the `delivery` model.
```json Create a post-call action [expandable] theme={null}
{
"name": "post_call_summary",
"description": "After the call ends, post a one-sentence summary of the conversation.",
"trigger_type": "post_call",
"parameters": {
"type": "object",
"properties": {
"channel": {
"type": "string",
"const": "C0SUPPORT"
},
"message": {
"type": "string",
"description": "A concise, one-sentence summary of what the caller wanted and the outcome."
}
},
"required": ["channel", "message"]
},
"delivery": {
"api": {
"url": "https://your-app.example.com/hooks/call-summary",
"method": "POST",
"body_template": { "channel": "{channel}", "text": "{message}" }
}
}
}
```
## Observing Results
The `delivery.api.url` above is where the action *fires* - Tavus's outbound request carrying your `body_template`. To see **whether it actually ran and how it went**, Tavus emits an `application.post_call_action_executed` event for each action it runs (one per attached post-call action), independent of how your endpoint responded.
The event's `properties` record the outcome:
| Field | Description |
| ----------------------- | --------------------------------------------------- |
| `tool_id` / `tool_name` | Which action ran. |
| `status` | `success`, `error`, `timeout`, or `skipped`. |
| `request` | What Tavus sent: `url`, `method`, `body`. |
| `response` | What your endpoint returned: `http_status`, `body`. |
| `error` | Present only on failure - the error detail. |
You can read it two ways:
* **Webhook** - it's delivered to the conversation's [`callback_url`](/sections/webhooks-and-callbacks#conversation-callbacks), alongside the other conversation events. Branch on `event_type == "application.post_call_action_executed"`.
* **Pull** - it's also returned on the verbose [GET conversation](/api-reference/conversations/get-conversation) response under `events`, so you can reconcile after the fact without running a webhook server.
```json application.post_call_action_executed theme={null}
{
"properties": {
"tool_id": "",
"tool_name": "post_call_summary",
"status": "success",
"request": {
"url": "https://your-app.example.com/hooks/call-summary",
"method": "POST",
"body": "{\"channel\":\"C0SUPPORT\",\"text\":\"Caller asked about order status; resolved.\"}"
},
"response": { "http_status": 200, "body": "ok" }
},
"conversation_id": "",
"event_type": "application.post_call_action_executed",
"message_type": "application",
"timestamp": "2026-04-29T03:47:05Z"
}
```
## Attaching to a PAL
A post-call action runs for a PAL once you attach it, like any other tool - see [Attach Tools to a PAL](/api-reference/pal-tools/attach-tools-to-pal). Once attached, it runs after every conversation with that PAL.
Replace `` with your actual API key when calling the Tools API. You can generate one in the PAL Maker.
# Pronunciation Dictionaries
Source: https://docs.tavus.io/sections/conversational-video-interface/pal/pronunciation-dictionaries
Control how your PAL pronounces specific words, names, and terms during conversations.
Pronunciation dictionaries let you define custom pronunciation rules so your PAL says words exactly how you want. This is useful for brand names, technical terms, acronyms, and foreign words that TTS engines may mispronounce.
Tavus automatically syncs your dictionary to your TTS provider, so rules work regardless of which TTS engine your PAL uses.
## How it works
1. You create a pronunciation dictionary with a set of rules
2. Each rule maps a **text** (the word to match) to a **pronunciation** (how it should be spoken)
3. You attach the dictionary to a PAL via the `pronunciation_dictionary_id` field in the TTS layer
When you update a dictionary's rules, all PALs referencing it are automatically updated. When you delete a dictionary, it is cleanly removed from all linked PALs.
## Bring your own TTS API key
If you provide your own TTS API key, you can use Tavus pronunciation dictionaries the same way - just set `pronunciation_dictionary_id` on the TTS layer. Tavus will sync the dictionary rules to your provider account automatically.
## Rule types
Each rule requires a `type` that determines how the pronunciation is interpreted:
| Type | Description | Example |
| ------- | ------------------------------------------------------- | ----------------------- |
| `alias` | Replace the matched text with a different spoken phrase | `"Tavus"` → `"TAH-vus"` |
| `ipa` | Use IPA (International Phonetic Alphabet) notation | `"bayou"` → `"ˈbɑju"` |
### Alias rules
Alias rules perform simple text substitution. The TTS engine speaks the `pronunciation` value instead of the original `text`.
```json theme={null}
{
"text": "Tavus",
"pronunciation": "TAH-vus",
"type": "alias"
}
```
### IPA rules
IPA rules let you specify exact phonetic pronunciation. You can provide IPA in two formats:
* **Raw IPA**: Standard IPA string (e.g., `"hɛloʊ"`)
* **Pipe-delimited IPA**: Pre-tokenized phonemes separated by `|` (e.g., `"ˈ|b|ɑ|j|u"`)
```json theme={null}
{
"text": "bayou",
"pronunciation": "ˈ|b|ɑ|j|u",
"type": "ipa"
}
```
## Rule options
Each rule supports optional matching parameters:
| Parameter | Type | Default | Description |
| ----------------- | ------- | ------- | ---------------------------------- |
| `case_sensitive` | boolean | `false` | Whether matching is case-sensitive |
| `word_boundaries` | boolean | `true` | Whether to match only whole words |
`word_boundaries` is only applied by ElevenLabs. When syncing to Cartesia, this option is ignored and the rule is applied without word-boundary matching.
```json theme={null}
{
"text": "UN",
"pronunciation": "United Nations",
"type": "alias",
"case_sensitive": false,
"word_boundaries": false
}
```
## Attaching a dictionary to a PAL
Set `pronunciation_dictionary_id` in the TTS layer when creating or updating a PAL:
```json Create PAL theme={null}
{
"pal_name": "Sales Agent",
"system_prompt": "You are a helpful sales agent.",
"layers": {
"tts": {
"tts_engine": "cartesia",
"pronunciation_dictionary_id": "pd_abc123def456"
}
}
}
```
```json Patch PAL theme={null}
[
{
"op": "add",
"path": "/layers/tts/pronunciation_dictionary_id",
"value": "pd_abc123def456"
}
]
```
Each PAL supports one pronunciation dictionary at a time. Setting a new `pronunciation_dictionary_id` replaces the previous one. Setting it to an empty string removes the dictionary.
## Limits
| Limit | Value |
| ------------------------------ | -------------- |
| Text field max length | 200 characters |
| Pronunciation field max length | 500 characters |
| Dictionary name max length | 255 characters |
## API reference
* [Create pronunciation dictionary](/api-reference/pronunciation-dictionaries/create-pronunciation-dictionary)
* [Get pronunciation dictionary](/api-reference/pronunciation-dictionaries/get-pronunciation-dictionary)
* [List pronunciation dictionaries](/api-reference/pronunciation-dictionaries/list-pronunciation-dictionaries)
* [Update pronunciation dictionary](/api-reference/pronunciation-dictionaries/update-pronunciation-dictionary)
* [Delete pronunciation dictionary](/api-reference/pronunciation-dictionaries/delete-pronunciation-dictionary)
# Stock PALs
Source: https://docs.tavus.io/sections/conversational-video-interface/pal/stock-pals
Tavus offers pre-built PAL templates to help you get started quickly.
These template PALs match the presets in [PAL Maker](https://maker.tavus.io/dev/pals/create). Clone one in the portal or call them directly by `pal_id`:
To list every stock PAL your account can use, call the List PALs endpoint. For the full configuration (including system prompt), use Get PAL.
### Template PALs
Interactive table-read with immediate feedback and a post-call audition review
```text theme={null}
p735435f8c36
```
```shell theme={null}
curl --request POST \
--url https://tavusapi.com/v2/conversations \
-H "Content-Type: application/json" \
-H "x-api-key: " \
-d '{
"pal_id": "p735435f8c36"
}'
```
Qualify leads on a live call and sync results to your CRM.
```text theme={null}
p88b777355b2
```
```shell theme={null}
curl --request POST \
--url https://tavusapi.com/v2/conversations \
-H "Content-Type: application/json" \
-H "x-api-key: " \
-d '{
"pal_id": "p88b777355b2"
}'
```
Pre-visit intake flow with structured data capture.
```text theme={null}
pa5ad6596ef5
```
```shell theme={null}
curl --request POST \
--url https://tavusapi.com/v2/conversations \
-H "Content-Type: application/json" \
-H "x-api-key: " \
-d '{
"pal_id": "pa5ad6596ef5"
}'
```
Trivia coach and trainer, complete with quizzes
```text theme={null}
p89b602b1174
```
```shell theme={null}
curl --request POST \
--url https://tavusapi.com/v2/conversations \
-H "Content-Type: application/json" \
-H "x-api-key: " \
-d '{
"pal_id": "p89b602b1174"
}'
```
# Speech-to-Text (STT)
Source: https://docs.tavus.io/sections/conversational-video-interface/pal/stt
Configure the STT layer to select an STT model, improve transcription accuracy, and optimize for your target languages.
The STT layer transcribes participant speech in real time using automatic speech recognition (ASR). You can select a model optimized for your use case and language requirements.
## STT models
Select an STT model using the `stt_engine` parameter in the `layers.stt` object. The following models are available:
| Model | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `tavus-auto` **(default)** | Automatically selects the best STT model for the conversation's language. **Recommended for most use cases.** |
| `tavus-parakeet` | Highest throughput, lowest latency for English and European languages. |
| `tavus-soniox` | Purpose-built for Indian languages with broad multilingual coverage. |
| `tavus-whisper` | Broad multilingual coverage across all supported languages. |
| `tavus-deepgram-medical` | Domain-specific English STT optimized for clinical and healthcare vocabulary. English only. |
| `tavus-advanced` | **Deprecated.** Still active but not recommended for new integrations. |
`tavus-auto` is the default. Use it unless you have a specific language or domain requirement. It automatically routes to the best model for each conversation.
## Choosing the right model
A language is listed for a model only if both STT and TTS coverage are available.
| Category | Recommended model | Supported languages |
| ------------------ | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| General purpose | `tavus-auto` | [All 43 languages](/sections/conversational-video-interface/language-support) |
| Indic languages | `tavus-soniox` | Bengali, English, Gujarati, Hindi, Kannada, Malayalam, Marathi, Punjabi, Tamil, Telugu + broad support for all other languages |
| English + European | `tavus-parakeet` | Bulgarian, Croatian, Czech, Danish, Dutch, English, Finnish, French, German, Greek, Hungarian, Italian, Polish, Portuguese, Romanian, Russian, Slovak, Spanish, Swedish, Ukrainian |
| Broad multilingual | `tavus-whisper` or `tavus-soniox` | [All 43 languages](/sections/conversational-video-interface/language-support) |
| Medical (English) | `tavus-deepgram-medical` | English |
Using [Smart Language Detection](/sections/conversational-video-interface/language-support#smart-language-detection) requires either `tavus-auto`, `tavus-soniox`, or `tavus-whisper`.
## Configuring the STT layer
Define the STT layer under the `layers.stt` object.
### `stt_engine`
Set the STT model for transcription:
```json theme={null}
"stt": {
"stt_engine": "tavus-auto"
}
```
### `hotwords`
Use this to prioritize certain names or terms that are difficult to transcribe.
```json theme={null}
"hotwords": "Roey is the name of the person you're speaking with."
```
The above helps the model transcribe "Roey" correctly instead of "Rowie."
Use hotwords for proper nouns, brand names, or domain-specific language that standard STT engines might struggle with.
## Example configuration
Below is an example PAL with a configured STT layer using the recommended `tavus-auto` engine:
```json theme={null}
{
"pal_name": "Customer Service Agent",
"system_prompt": "You assist users by listening carefully and providing helpful answers.",
"pipeline_mode": "full",
"default_face_id": "r90bbd427f71",
"layers": {
"stt": {
"stt_engine": "tavus-auto",
"hotwords": "Roey is the name of the person you're speaking with."
}
}
}
```
Refer to the Create PAL API for a complete list of supported fields.
# Overview
Source: https://docs.tavus.io/sections/conversational-video-interface/pal/tools
How tool calling works on Tavus - LLM, perception, post-call, and system tools like end_call, plus delivery channels.
**Tool calling** lets a PAL trigger code while a conversation is happening - look something up, write to a CRM, hit a third-party API, or notify your frontend. Tools are reusable objects: create them once, and attach them to any number of PALs.
**Skills vs tools:** [Tools](/sections/conversational-video-interface/pal/tools) are functions **you** define and attach for the LLM or Raven to call. [Skills](/sections/conversational-video-interface/skills/overview) are Tavus-authored capabilities (search, presentation, Magic Canvas, etc.) you attach from the skill registry. A PAL can use both.
## Tool Types
| Type | When to use it | Pages |
| -------------------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| **LLM tool** | The conversational LLM should call a function based on what the **user says**. | [Tool Calling for LLM](/sections/conversational-video-interface/pal/llm-tool) |
| **Perception tool** | Raven should call a function based on what it **sees or hears** in the audio/video streams. | [Tool Calling for Perception](/sections/conversational-video-interface/pal/perception-tool) |
| **Post-call action** | You want work to run **once after the conversation ends**, using the transcript and perception analysis. | [Post-Call Actions](/sections/conversational-video-interface/pal/post-call-tool) |
| **System tool** | You need a built-in Tavus capability that is always available - today, hanging up with `end_call`. | [System Tools](#system-tools) |
## In-Call Tools
In-call tools fire during the conversation. Pick the type by setting `origin` on the tool: `"llm"`, `"vision"`, or `"audio"`. A single PAL can hold both LLM and perception tools.
| Type | Triggered by | Pages |
| ------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| **LLM tool** | What the **user says** - the conversational LLM decides to call the tool. | [Tool Calling for LLM](/sections/conversational-video-interface/pal/llm-tool) |
| **Perception tool** | What Raven **sees or hears** in the audio/video streams. | [Tool Calling for Perception](/sections/conversational-video-interface/pal/perception-tool) |
## Post-Call Actions
[Post-call actions](/sections/conversational-video-interface/pal/post-call-tool) run **once after the conversation ends**, with Tavus filling open arguments from the transcript and perception analysis and delivering the call for you. Set `trigger_type: "post_call"` (and omit `origin`) instead of using in-call `origin` values.
## System Tools
**System tools** are built-in tools Tavus provides. They are available on every conversation. You do not create them, attach them, update them, or delete them.
List them with [Get Tools](/api-reference/tools/get-tools) using `type=system` (or `type=all` to see system tools first, then your tools). System tools have `is_system_tool: true`, `owner_id: null`, and use their `name` as the `tool_id`.
| Tool | What it does |
| ---------- | ---------------------------------------------------------------------------- |
| `end_call` | Lets the PAL hang up when the conversation reaches a natural stopping point. |
### `end_call`
`end_call` is always available. The conversational LLM can invoke it when the call should end - for example after the participant says goodbye, after objectives are complete, or when your system prompt tells the PAL to wrap up.
Steer when the PAL uses it with the system prompt, objectives, or conversational context. You do not attach `end_call` to a PAL.
```bash theme={null}
curl "https://tavusapi.com/v2/tools?type=system" \
-H "x-api-key: "
```
## Delivery Channels
Every tool dispatches via **exactly one** channel:
* **App message** (default) - the call lands as a `conversation.tool_call` event your frontend handles.
* **API call** - Tavus makes an HTTPS request to a URL you configure - your own callback endpoint or a third-party API directly.
See [Tool Delivery](/sections/conversational-video-interface/pal/llm-tool-delivery) for the request shape, URL templating, and response handling. See [Tool Authentication](/sections/conversational-video-interface/pal/llm-tool-auth) for the auth types and how they map to outgoing headers.
## Quick Start
1. **Create the tool** at `/v2/tools` with a `name`, `description`, `parameters` JSON Schema, `origin`, and `delivery`.
2. **Attach it to a PAL** at `/v2/pals/{pal_id}/tools`.
3. **Start a conversation** with that PAL; the tool is now callable.
## Legacy Inline Tools
Older integrations define tools directly on the PAL (`layers.llm.tools`, `layers.perception.visual_tools`, `layers.perception.audio_tools`). That path still runs for existing PALs but is deprecated for new work. See [Legacy inline tool calling](/sections/troubleshooting#legacy-inline-tool-calling) for how it behaves and how to migrate.
# Text-to-Speech (TTS)
Source: https://docs.tavus.io/sections/conversational-video-interface/pal/tts
Discover how to integrate custom voices from third-party TTS engines for multilingual or localized speech output.
The TTS layer generates natural-sounding voice responses for your PAL. You can use Tavus's default engine or configure a third-party provider for a specific voice.
Set **`layers.tts`** when you [Create PAL](/api-reference/pals/create-pal) or update a PAL. For how a PAL fits together, see [PAL overview](/sections/conversational-video-interface/pal/overview). For languages and locale-oriented setup, see [Language support](/sections/conversational-video-interface/language-support).
## Configuring the TTS Layer
Define the TTS layer under the `layers.tts` object. The snippets below show only the **`tts`** object for readability; in a full PAL payload it is nested under **`layers`** (see [Example configuration](#example-configuration)).
Below are the parameters available:
### 1. `tts_engine`
Specifies the TTS engine.
* **Options**: `tavus-auto` **(default)**, `cartesia`, `elevenlabs`. Also `azure`, only as a fallback when your language is not otherwise supported.
`tavus-auto` is the default. Use it unless you need a specific provider or voice. It selects the best TTS engine and model for each conversation; the underlying provider may change over time. For deterministic behavior, set an explicit provider and model.
If you use `tavus-auto`, you do not need to specify any other parameters in the `tts` layer.
Use `azure` only if you need a language that is not supported by the default engines. Prefer `tavus-auto`, `cartesia`, or `elevenlabs` whenever your language is already covered. See [Additional language support via Azure](/sections/conversational-video-interface/language-support#additional-language-support-via-azure).
```json theme={null}
"tts": {
"tts_engine": "cartesia"
}
```
### 2. `api_key`
Authenticates requests to your selected third-party TTS provider. You can obtain an API key from one of the following:
For Cartesia and ElevenLabs, only required when using private (non-public) voices. If you are using Azure as a fallback for an unsupported language, an API key is **required**.
* Cartesia
* ElevenLabs - if using pronunciation dictionaries, the key must have the `pronunciation_dictionaries_write` scope (or full account access). See ElevenLabs API key scopes.
* Azure (fallback only) - required when using Azure. Use your own Azure Speech resource key; the resource must be in the **East US** region (Tavus synthesizes via `eastus`; a key from another region returns an authentication error). Any standard neural voice available in East US works; only Custom Neural Voices need to be deployed in your resource.
```json theme={null}
"tts": {
"api_key": "your-api-key"
}
```
### 3. `external_voice_id`
Specifies which voice to use with the selected TTS engine. To find supported voice IDs, refer to the provider’s documentation:
* Cartesia
* ElevenLabs
* Azure (fallback only; e.g. `en-US-JennyNeural`) - if using Azure, the voice determines the accent, not the language: any voice speaks the conversation's language, carrying that voice's own accent. For a natural result, choose a voice whose locale matches your target language, or use an Azure `*MultilingualNeural` voice.
You can use any publicly accessible custom voice from ElevenLabs or Cartesia without the provider's API key. If the custom voice is private, you still need to use the provider's API key.
```json theme={null}
"tts": {
"external_voice_id": "external-voice-id"
}
```
### 4. `tts_model_name`
Model name used by the TTS engine. Refer to:
* Cartesia
* ElevenLabs
`tts_model_name` is not supported when `tts_engine` is `azure`. If you are using Azure as a language fallback, omit this field.
```json theme={null}
"tts": {
"tts_model_name": "sonic-3"
}
```
### 5. `tts_emotion_control`
If set to `true`, enables emotion control in speech. **Defaults to `true`.**
```json theme={null}
"tts": {
"tts_emotion_control": true
}
```
### 6. `voice_settings`
Optional object for controlling speed, volume, and similar effects. **Which approach you use depends on your TTS engine and model:**
| Engine | Model | Approach |
| ---------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ElevenLabs | All models | `voice_settings` in PAL config |
| Cartesia | sonic-2 | `voice_settings` in PAL config |
| Cartesia | sonic-3 | **Either** `voice_settings` (global, set once per conversation) **or** prompt the LLM in `system_prompt` to output [Cartesia SSML tags](https://docs.cartesia.ai/build-with-cartesia/sonic-3/ssml-tags) for dynamic control. Not both. |
**Cartesia sonic-3:** If you use `voice_settings` for speed/volume, those settings apply globally for the whole conversation and you cannot use SSML tags for dynamic, per-phrase control. If you want dynamic control, omit `voice_settings` and have the LLM output SSML tags instead. See [Cartesia volume, speed, and emotion](https://docs.cartesia.ai/build-with-cartesia/sonic-3/volume-speed-emotion).
**ElevenLabs (all models):** Set parameters in the `voice_settings` object:
| Parameter | ElevenLabs |
| ------------------- | ----------------------------------------------------------- |
| `speed` | Range `0.7` to `1.2` (`0.7` = slowest, `1.2` = fastest) |
| `stability` | Range `0.0` to `1.0` (`0.0` = variable, `1.0` = stable) |
| `similarity_boost` | Range `0.0` to `1.0` (`0.0` = creative, `1.0` = original) |
| `style` | Range `0.0` to `1.0` (`0.0` = neutral, `1.0` = exaggerated) |
| `use_speaker_boost` | Boolean (enhances speaker similarity) |
See ElevenLabs Voice Settings for details.
**Cartesia sonic-2:** Use the `voice_settings` object (e.g. `speed`, `emotion`). SSML tags are not used for sonic-2.
**Cartesia sonic-3:** You can use **either** of these, but not both:
* **`voice_settings`** - We accept speed/volume params for sonic-3. They apply **globally**, set once per conversation. Use this when you want a single default speed and volume for the entire conversation. Using `voice_settings` prevents dynamic SSML control.
* **SSML in LLM output** - Omit `voice_settings` for speed/volume and instead add instructions to your `system_prompt` so the LLM outputs [Cartesia SSML tags](https://docs.cartesia.ai/build-with-cartesia/sonic-3/ssml-tags) in its responses. This gives you dynamic, per-phrase control. See [Cartesia volume, speed, and emotion](https://docs.cartesia.ai/build-with-cartesia/sonic-3/volume-speed-emotion).
Emotion control is separate; see [Emotion Control with Phoenix-4](/sections/conversational-video-interface/quickstart/emotional-expression).
**Example: system prompt for Cartesia sonic-3 (dynamic speed and volume)**
If you are **not** using `voice_settings` for sonic-3, add instructions like this to your `system_prompt` so the LLM outputs Cartesia SSML tags:
```
When you want to emphasize a word or phrase, use Cartesia SSML tags for speed and volume:
- To slow down: phrase
- To speed up: phrase
- To speak louder: phrase
- To speak more quietly: phrase
You can combine tags, e.g. important point.
Only use these tags when it improves clarity or emphasis; keep most of your response in plain text.
```
**Example: voice\_settings (ElevenLabs, Cartesia sonic-2, or Cartesia sonic-3 global)**
```json theme={null}
"tts": {
"voice_settings": {
"speed": 0.9
}
}
```
For sonic-3, this sets global speed once per conversation; for sonic-2 and ElevenLabs, it applies as configured.
## Example Configuration
Below is an example PAL with a fully configured TTS layer:
```json Cartesia theme={null}
{
"pal_name": "AI Presenter",
"system_prompt": "You are a friendly and informative video host.",
"pipeline_mode": "full",
"context": "You're delivering updates in a conversational tone.",
"default_face_id": "r90bbd427f71",
"layers": {
"tts": {
"tts_engine": "cartesia",
"api_key": "your-api-key",
"external_voice_id": "external-voice-id",
"tts_emotion_control": true,
"tts_model_name": "sonic-3"
}
}
}
```
```json ElevenLabs theme={null}
{
"pal_name": "Narrator",
"system_prompt": "You narrate long stories with clarity and consistency.",
"pipeline_mode": "full",
"context": "You're reading a fictional audiobook.",
"default_face_id": "r90bbd427f71",
"layers": {
"tts": {
"tts_engine": "elevenlabs",
"api_key": "your-api-key",
"external_voice_id": "elevenlabs-voice-id",
"voice_settings": {
"speed": 0.9
},
"tts_emotion_control": true,
"tts_model_name": "eleven_turbo_v2_5"
}
}
}
```
```json Azure (fallback only) theme={null}
{
"pal_name": "Azure fallback PAL",
"system_prompt": "You are a friendly host.",
"pipeline_mode": "full",
"default_face_id": "r90bbd427f71",
"layers": {
"tts": {
"tts_engine": "azure",
"api_key": "your-azure-speech-key",
"external_voice_id": "en-US-JennyNeural"
}
}
}
```
The Azure example above is only for cases where your target language is not supported by the default engines. Prefer Cartesia or ElevenLabs (or leave `tts_engine` unset for `tavus-auto`) whenever possible.
Refer to [Create PAL](/api-reference/pals/create-pal) for a complete list of supported fields.
# What Is a PAL?
Source: https://docs.tavus.io/sections/conversational-video-interface/pal/what-is-a-pal
A short definition of PAL, how it relates to Face and Conversation, and where to configure one.
A **PAL** (Personified Application Layer) is the behavior configuration for your AI agent in Conversational Video Interface (CVI). It defines how the agent thinks, speaks, and acts during a conversation.
The **Face** is separate: it is the on-screen likeness and default voice. A **Conversation** is the live session that puts a PAL and a Face together over WebRTC.
| Piece | What it is | What you configure |
| ---------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| **PAL** | Behavior, knowledge, tools, skills, and pipeline layers | System prompt, objectives, guardrails, LLM, perception, TTS overrides, tools, skills |
| **Face** | Visual appearance and default voice | Stock face or a trained face |
| **Conversation** | One live call | Which PAL and Face join, plus session options |
## Why the name matters
Docs and the API use **PAL** for the behavior object and **Face** for visual appearance and voice. Older docs and some endpoints still say **persona** and **replica**. Same ideas: PAL owns prompts, layers, tools, and skills; Face is the on-screen likeness and voice.
Prefer **PAL** and **Face** in new work. Legacy names still work on existing endpoints and request fields (`/v2/personas`, `/v2/replicas`, `persona_id`, `replica_id`, and related aliases) for backward compatibility.
## Where to go next
* [PAL Overview](/sections/conversational-video-interface/pal/overview) - fields, layers, and customization options
* [What Is CVI?](/sections/conversational-video-interface/overview-cvi) - how PAL, Face, and Conversation fit in the full pipeline
* [CVI Quickstart](/sections/conversational-video-interface/quickstart/cvi-quickstart) - create a PAL and start a conversation
# CVI App: AI Prompt
Source: https://docs.tavus.io/sections/conversational-video-interface/quickstart/ai-prompt-cvi-quickstart
Copy-paste checklist for Cursor, Copilot, or other AI coding agents to scaffold React (TypeScript, Vite) with Tavus CVI and @tavus/cvi-ui.
This page is a **single checklist** you paste into an AI coding agent so it scaffolds a **Vite + React (TypeScript)** app with **`@tavus/cvi-ui`**, creates conversations via the Tavus API, and wires **`CVIProvider`** and **`Conversation`**.
For how embedding fits in the product, see [Embed CVI](/sections/integrations/embedding-cvi). For UI primitives after `init` / `add`, see the [component library overview](/sections/conversational-video-interface/component-library/overview). API calls need a Tavus key from [Authentication](/api-reference/authentication); sessions use **`conversation_url`** from [Create Conversation](/api-reference/conversations/create-conversation).
## ✅ **System Prompt for AI: React (Vite) + Tavus CVI Integration**
**Purpose:**
Generate **React (TypeScript)** apps with Tavus CVI using **Vite**, following the official docs and GitHub examples (embed guide: `https://docs.tavus.io/sections/integrations/embedding-cvi`).
***
### ✅ **AI MUST ALWAYS DO THE FOLLOWING:**
#### **1. Setup React App Using Vite**
```bash theme={null}
npm create vite@latest my-tavus-app -- --template react-ts
cd my-tavus-app
npm install
```
***
#### **2. Install Tavus CVI UI Components**
```bash theme={null}
npx @tavus/cvi-ui@latest init
npx @tavus/cvi-ui@latest add conversation
```
✅ This creates:
```
src/components/cvi/components/
cvi-provider.tsx
conversation.tsx
```
***
#### **3. Wrap App with `CVIProvider`**
Update `src/App.tsx`:
```tsx theme={null}
import { CVIProvider } from "./components/cvi/components/cvi-provider";
function App() {
return {/* Your app content */};
}
```
***
#### **4. Create Conversation API Call**
Always use this pattern:
```ts theme={null}
const response = await fetch("https://tavusapi.com/v2/conversations", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": import.meta.env.VITE_TAVUS_API_KEY || "",
},
body: JSON.stringify({
face_id: import.meta.env.VITE_REPLICA_ID || "r90bbd427f71", // Default
pal_id: import.meta.env.VITE_PERSONA_ID || "pcb7a34da5fe", // Default
}),
});
const data = await response.json();
setConversationUrl(data.conversation_url);
```
✅ **Explain:** `conversation_url` from API response is used inside ``.
***
#### **5. Render `Conversation` Inside `CVIProvider`**
```tsx theme={null}
setConversationUrl(null)}
/>
```
***
#### **6. Add Environment Variable**
Create `.env` in the **`my-tavus-app`** folder (if not already there):
```
VITE_TAVUS_API_KEY=your_api_key_here
VITE_REPLICA_ID=r90bbd427f71 // Default face_id, can be overridden in .env
VITE_PERSONA_ID=pcb7a34da5fe // Default pal_id, can be overridden in .env
```
* **Important**: **DO NOT** create `.env` outside of the `my-tavus-app` folder. It must be inside your project directory where `src` exists.
Access via:
```ts theme={null}
import.meta.env.VITE_TAVUS_API_KEY
import.meta.env.VITE_REPLICA_ID
import.meta.env.VITE_PERSONA_ID
```
***
#### **7. Create `cvi-components.json` by Default**
Ensure that the `cvi-components.json` file is generated in the `my-tavus-app` folder with the following content:
```json theme={null}
{
"tsx": true
}
```
***
### ✅ **Expected AI Output Example**
**`src/App.tsx`:**
```tsx theme={null}
import React, { useState } from "react";
import { CVIProvider } from "./components/cvi/components/cvi-provider";
import { Conversation } from "./components/cvi/components/conversation";
const App: React.FC = () => {
const [conversationUrl, setConversationUrl] = useState(null);
const createConversation = async () => {
const response = await fetch("https://tavusapi.com/v2/conversations", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": import.meta.env.VITE_TAVUS_API_KEY || "",
},
body: JSON.stringify({
face_id: import.meta.env.VITE_REPLICA_ID || "r90bbd427f71",
pal_id: import.meta.env.VITE_PERSONA_ID || "pcb7a34da5fe",
}),
});
const data = await response.json();
setConversationUrl(data.conversation_url);
};
return (
Tavus CVI Integration (Vite)
{!conversationUrl ? (
) : (
setConversationUrl(null)}
/>
)}
);
};
export default App;
```
***
### ✅ **Checklist for AI Output**
* ✅ Uses **Vite + React TypeScript** (`npm create vite@latest --template react-ts`)
* ✅ Includes `npx @tavus/cvi-ui init` and `add conversation`
* ✅ Imports `CVIProvider` and `Conversation` from `src/components/cvi/components/`
* ✅ Uses correct API call with:
* `https://tavusapi.com/v2/conversations`
* Headers: `Content-Type`, `x-api-key`
* Body: `face_id` & `pal_id`
* ✅ Uses `import.meta.env.VITE_TAVUS_API_KEY`
* ✅ Renders `` inside ``
* ✅ Purple button is visible in both light and dark modes
* ✅ `.env` is created inside the correct project folder (`my-tavus-app`)
* ✅ `cvi-components.json` is created by default with `{ "tsx": true }`
***
### Keep things in mind:
* If you're already in the `my-tavus-app` folder, avoid running `cd my-tavus-app` again. Check your current folder before running commands.
* After running the necessary setup, remember to run `npm run dev` to start your app.
* Do **NOT** place the `.env` file outside of the project folder. It must reside within the `my-tavus-app` directory.
# CVI App Quickstart
Source: https://docs.tavus.io/sections/conversational-video-interface/quickstart/build-first-app
Create a server-authenticated Tavus CVI conversation and embed it in a web app with the returned conversation_url.
Use this page when you are building an app that creates and embeds Tavus CVI
conversations. If you only want to create a PAL and start your first
Tavus-hosted conversation, use [API Conversation Quickstart](/sections/conversational-video-interface/quickstart/cvi-quickstart).
This guide gets a new web app from an empty project to a working embedded Tavus conversation. The happy path is:
1. Keep `TAVUS_API_KEY` on your server.
2. Create a conversation with `POST /v2/conversations`.
3. Embed the returned `conversation_url` in an iframe.
4. End the conversation when the user leaves.
For API details, see [Create Conversation](/api-reference/conversations/create-conversation), [End Conversation](/api-reference/conversations/end-conversation), [Get PALs](/api-reference/pals/list-pals), and [Get Faces](/api-reference/faces/list-faces).
Live conversations can count toward billing and concurrency as soon as they are
created. Use `test_mode: true` while wiring automated tests or checking your
integration flow. In test mode, Tavus creates the conversation without the
PAL joining and returns it with `status: "ended"`.
## Prerequisites
* A Tavus API key from the PAL Maker.
* A usable `pal_id`, `face_id`, or both. The next section shows how to choose.
* Node.js 20+ for the TypeScript examples below.
## Choose a PAL and Face
For CVI, a **Face** defines the on-screen likeness and voice. A **PAL** defines behavior, knowledge, and pipeline configuration (prompt, layers, objectives, guardrails, tools, and more).
Use this decision tree:
* If you have a PAL with a `default_face_id`, create the conversation with just `pal_id`.
* If you have a PAL without a `default_face_id`, create the conversation with both `pal_id` and `face_id`.
* If you only want to test a Face and voice without a custom PAL, create the conversation with just `face_id`.
First-time builders should start with stock Tavus resources. List stock PALs and stock faces with:
```bash theme={null}
curl --request GET \
--url "https://tavusapi.com/v2/pals?pal_type=system" \
--header "x-api-key: $TAVUS_API_KEY"
curl --request GET \
--url "https://tavusapi.com/v2/faces?face_type=system&verbose=true" \
--header "x-api-key: $TAVUS_API_KEY"
```
In the PALs response, look for `pal_id` and `default_face_id`. In the faces response, look for `face_id`, `face_type`, and `model_name`.
`r90bbd427f71` is the stock Anna face ID used throughout these docs, and
`pcb7a34da5fe` is a stock Sales Development Rep PAL ID. They are stock
IDs, not placeholder strings, and are available for quickstart use.
For more details, see [Stock Faces](/sections/faces/stock-faces), [Get PALs](/api-reference/pals/list-pals), [Get Faces](/api-reference/faces/list-faces), and [Create Conversation](/api-reference/conversations/create-conversation).
## 1. Create the app
Create a Vite React app and install the small server dependencies used in this guide:
```bash theme={null}
npm create vite@latest tavus-first-call -- --template react-ts
cd tavus-first-call
npm install
npm install express cors dotenv
npm install -D tsx @types/express @types/cors
```
Add these scripts to `package.json`:
```json theme={null}
{
"scripts": {
"dev": "vite",
"server": "tsx server.ts",
"dev:all": "npm run server & npm run dev"
}
}
```
## 2. Keep your API key server-only
Create `.env.example`:
```bash theme={null}
TAVUS_API_KEY=tvsk_your_api_key_here
```
Copy it to `.env` locally and fill in your real key:
```bash theme={null}
cp .env.example .env
```
Never expose `TAVUS_API_KEY` in browser code, client-side environment
variables, mobile apps, or public repositories. The frontend should call your
backend route, and your backend should call Tavus.
## 3. Add backend routes
Create `server.ts` at the project root. The first route creates a conversation. The second route ends it when the user leaves or your test finishes.
```ts theme={null}
import "dotenv/config";
import cors from "cors";
import express from "express";
const app = express();
const port = 3001;
const tavusApiKey = process.env.TAVUS_API_KEY;
if (!tavusApiKey) {
throw new Error("Missing TAVUS_API_KEY in .env");
}
app.use(cors({ origin: "http://localhost:5173" }));
app.use(express.json());
type CreateConversationRequest = {
pal_id?: string;
face_id?: string;
conversation_name?: string;
test_mode?: boolean;
};
app.post("/api/conversations", async (req, res) => {
const {
pal_id,
face_id,
conversation_name = "My first Tavus video chat",
test_mode = false,
} = req.body as CreateConversationRequest;
if (!pal_id && !face_id) {
return res
.status(400)
.json({ error: "Provide pal_id, face_id, or both" });
}
const tavusResponse = await fetch("https://tavusapi.com/v2/conversations", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": tavusApiKey,
},
body: JSON.stringify({
...(pal_id ? { pal_id } : {}),
...(face_id ? { face_id } : {}),
conversation_name,
test_mode,
}),
});
const data = await tavusResponse.json();
if (!tavusResponse.ok) {
return res.status(tavusResponse.status).json(data);
}
return res.json(data);
});
app.post("/api/conversations/:conversationId/end", async (req, res) => {
const { conversationId } = req.params;
const tavusResponse = await fetch(
`https://tavusapi.com/v2/conversations/${conversationId}/end`,
{
method: "POST",
headers: {
"x-api-key": tavusApiKey,
},
}
);
if (tavusResponse.status === 204) {
return res.status(204).send();
}
const data = await tavusResponse.json().catch(() => ({}));
if (!tavusResponse.ok) {
return res.status(tavusResponse.status).json(data);
}
return res.json(data);
});
app.listen(port, () => {
console.log(`Tavus backend listening on http://localhost:${port}`);
});
```
## 4. Embed the conversation URL
Replace `src/App.tsx` with this frontend. It calls your backend, receives the Tavus `conversation_url`, and embeds it in an iframe. This quickstart uses an iframe because it is the fastest path to a working CVI app. For Tavus-provided React components, including the complete `CVIProvider` + `Conversation` + server-helper example, see the [`@tavus/cvi-ui` component library](/sections/conversational-video-interface/component-library/overview). For Daily JS/React or LiveKit guidance, see [Embed CVI](/sections/integrations/embedding-cvi).
```tsx theme={null}
import { useState } from "react";
type ConversationResponse = {
conversation_id: string;
conversation_name?: string;
conversation_url: string;
status: "active" | "ended";
callback_url?: string;
created_at?: string;
meeting_token?: string;
};
const API_BASE_URL = "http://localhost:3001";
export default function App() {
const [palId, setPalId] = useState("");
const [faceId, setFaceId] = useState("");
const [testMode, setTestMode] = useState(false);
const [conversation, setConversation] = useState(
null
);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
async function startConversation() {
setLoading(true);
setError(null);
try {
const response = await fetch(`${API_BASE_URL}/api/conversations`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
...(palId ? { pal_id: palId } : {}),
...(faceId ? { face_id: faceId } : {}),
test_mode: testMode,
}),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || "Failed to create conversation");
}
setConversation(data);
} catch (error) {
setError(error instanceof Error ? error.message : "Unknown error");
} finally {
setLoading(false);
}
}
async function endConversation() {
if (!conversation) return;
await fetch(
`${API_BASE_URL}/api/conversations/${conversation.conversation_id}/end`,
{ method: "POST" }
);
setConversation(null);
}
return (
My first Tavus video chat
{!conversation ? (
{error ?
{error}
: null}
) : (
)}
);
}
```
Run both servers:
```bash theme={null}
npm run dev:all
```
Open `http://localhost:5173`, enter your `pal_id`, and click **Start conversation**.
The iframe must include browser permissions in the `allow` attribute. At
minimum, include `camera` and `microphone`. `fullscreen`, `display-capture`,
and `autoplay` are recommended for the default Tavus/Daily in-call experience.
## Expected create response
`POST /v2/conversations` returns the join URL your app should embed:
```json theme={null}
{
"conversation_id": "c123456",
"conversation_name": "My first Tavus video chat",
"conversation_url": "https://tavus.daily.co/c123456",
"status": "active",
"callback_url": "",
"created_at": "2026-05-20T14:30:00.000000Z"
}
```
When `test_mode` is `true`, expect the same shape, but `status` is `ended` and the PAL does not join.
## Cleanup
End live conversations when the user leaves, when a test completes, or when your app no longer needs the room:
```bash theme={null}
curl --request POST \
--url https://tavusapi.com/v2/conversations//end \
--header "x-api-key: $TAVUS_API_KEY"
```
The sample app calls the same endpoint through your backend route:
```ts theme={null}
await fetch(
`http://localhost:3001/api/conversations/${conversationId}/end`,
{ method: "POST" }
);
```
## Errors and cleanup
For automated tests, scaffolding, and agent-generated validation, create conversations with `test_mode: true`. the PAL does not join, the response returns `status: "ended"`, and the conversation does not affect billing or concurrency.
For live conversations, call [End Conversation](/api-reference/conversations/end-conversation) when the user leaves or your app no longer needs the room. Use [Delete Conversation](/api-reference/conversations/delete-conversation) only when you want destructive data removal, not routine call cleanup.
If conversation creation fails:
* `400` usually means the request body is invalid. Check that you sent a valid `pal_id`, `face_id`, or both, and that customizations are in the expected location.
* `401` means the Tavus API key is missing or invalid. Keep `TAVUS_API_KEY` on the server and never send it from browser code.
* Quota or concurrency errors mean your app should stop creating live conversations, surface a retry/support path, and use `test_mode: true` for validation flows.
* For private rooms, join with the returned `meeting_token`. If a token is invalid or expired, create a new authenticated conversation instead of reusing the old token.
## Where to go next
* Use the [React component library](/sections/conversational-video-interface/component-library/overview) when you want Tavus CVI components instead of a plain iframe.
* Use [Embed CVI](/sections/integrations/embedding-cvi) for iframe, vanilla JavaScript, and Daily JS patterns.
* Use [customize the conversation UI](/sections/conversational-video-interface/quickstart/customize-conversation-ui) for Daily Prebuilt styling.
* Use [LiveKit Agent](/sections/integrations/livekit) only if you already run a LiveKit Agents pipeline and want Tavus as the avatar video layer. It is not the recommended path for most CVI apps because LiveKit only provides rendering, while Tavus's Full Pipeline includes perception, turn-taking, and rendering for complete conversational intelligence.
* Use [conversation customizations](/sections/conversational-video-interface/conversation/overview) for recording, language, participant limits, private rooms, backgrounds, captions, and timeouts.
* Point coding agents at [Agents & automation](/sections/agents-and-automation) for `llms.txt`, OpenAPI, Agent Skills, and MCP access.
# Conversation Recordings
Source: https://docs.tavus.io/sections/conversational-video-interface/quickstart/conversation-recordings
Store conversation recordings in your own S3, GCS, or Azure Blob storage. Federated identity - no secrets shared with Tavus.
The `recording_storage` config field works for **Amazon S3, Google Cloud Storage, and Azure Blob Storage** - pick a provider, configure a one-time trust relationship on your side, and pass us the resulting non-secret identifiers.
**No customer secrets are stored at Tavus.** Every supported path uses provider-native federated identity (IAM role assumption, GCP Workload Identity Federation, or Entra ID Federated Credentials). You configure trust on your side; we receive short-lived tokens at runtime.
Recordings are typically available in your bucket within seconds to a few minutes after the call ends, depending on call length and provider. Once the recording lands, Tavus fires `application.recording_ready` (with `storage_provider` and a fully-qualified `storage_uri`) to your `callback_url`. See [Webhooks and Callbacks](/sections/webhooks-and-callbacks#application-callbacks).
## Set up your storage
S3 is the fastest path - recordings are written directly into your bucket as they finalize. Works in every AWS region.
Configure the role's trust relationship with all three of the following - every field is **mandatory**:
* **Trusted AWS principal:** AWS account ID `291871421005`.
* **ExternalId:** `tavus`.
* **Max session duration: 12 hours (43200 seconds).** AWS roles default to 1 hour, but the recording service requests 12-hour sessions when assuming the role. A role with the default duration will fail validation at room creation with `unable to assume role with given parameters`.
**About the trusted AWS account.** Tavus's recording infrastructure is operated through Daily.co; AWS account ID `291871421005` belongs to them. The same account ID is documented in [Daily's S3 setup guide](https://docs.daily.co/guides/products/live-streaming-recording/storing-recordings-in-a-custom-s3-bucket) for customers running their own security review. The ExternalId `tavus` is Tavus's identifier with Daily, gating cross-account `sts:AssumeRole` per the [confused-deputy AWS pattern](https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html).
Permissions policy (scoped to your bucket):
```json theme={null}
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:ListBucketMultipartUploads",
"s3:AbortMultipartUpload",
"s3:ListBucketVersions",
"s3:ListBucket",
"s3:GetObjectVersion",
"s3:ListMultipartUploadParts"
],
"Resource": [
"arn:aws:s3:::your-bucket-name",
"arn:aws:s3:::your-bucket-name/*"
]
}]
}
```
```shell cURL {7-13} theme={null}
curl --request POST \
--url https://tavusapi.com/v2/conversations \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"properties": {
"auto_start_recording": true,
"recording_storage": {
"provider": "s3",
"bucket_name": "your-bucket-name",
"bucket_region": "us-east-1",
"assume_role_arn": "arn:aws:iam::123456789012:role/TavusRecordingWriter"
}
},
"face_id": "r5f0577fc829"
}'
```
The original setup using flat properties on `properties` (without the `recording_storage` object) continues to work. Existing integrations don't need to change.
```shell cURL {7-9} theme={null}
curl --request POST \
--url https://tavusapi.com/v2/conversations \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"properties": {
"enable_recording": true,
"recording_s3_bucket_name": "your-bucket-name",
"recording_s3_bucket_region": "us-east-1",
"aws_assume_role_arn": "arn:aws:iam::123456789012:role/TavusRecordingWriter"
},
"face_id": "r5f0577fc829"
}'
```
These map internally to `provider: "s3"`. New integrations should use `recording_storage` - it's the only way to access GCS, Azure, and unsupported S3 regions, and it's where new fields will be added.
```hcl theme={null}
resource "aws_s3_bucket" "recordings" {
bucket = "your-recording-bucket"
}
resource "aws_iam_role" "tavus_writer" {
name = "TavusRecordingWriter"
# The recording service requests 12-hour sessions; default 3600s will fail.
max_session_duration = 43200
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { AWS = "arn:aws:iam::291871421005:root" }
Action = "sts:AssumeRole"
Condition = {
StringEquals = { "sts:ExternalId" = "tavus" }
}
}]
})
}
resource "aws_iam_role_policy" "writer" {
name = "TavusRecordingWriter-s3-write"
role = aws_iam_role.tavus_writer.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = [
"s3:PutObject",
"s3:GetObject",
"s3:ListBucketMultipartUploads",
"s3:AbortMultipartUpload",
"s3:ListBucketVersions",
"s3:ListBucket",
"s3:GetObjectVersion",
"s3:ListMultipartUploadParts",
]
Resource = [
aws_s3_bucket.recordings.arn,
"${aws_s3_bucket.recordings.arn}/*",
]
}]
})
}
```
GCS uses Workload Identity Federation. Tavus exposes an OIDC issuer at `https://recording-copy.tavus.io`; you configure your GCP project to trust that issuer and bind it to a service account that has write access to your bucket.
**Scope your trust to your account.** The steps below bind trust using your Tavus Workspace ID as an attribute condition (`attribute.customer_id`). This ensures only recordings belonging to your account can authenticate to your GCP resources. Click your user profile on the [PAL Maker](https://maker.tavus.io/dev) to find your Workspace ID.
```bash theme={null}
PROJECT_ID="your-gcp-project"
gcloud iam workload-identity-pools create tavus-recording-pool \
--project="$PROJECT_ID" \
--location="global" \
--display-name="Tavus Recording Storage"
gcloud iam workload-identity-pools providers create-oidc tavus-worker \
--project="$PROJECT_ID" \
--location="global" \
--workload-identity-pool="tavus-recording-pool" \
--display-name="Tavus Worker OIDC" \
--issuer-uri="https://recording-copy.tavus.io" \
--attribute-mapping="google.subject=assertion.sub,attribute.customer_id=assertion.customer_id" \
--attribute-condition="attribute.customer_id == ''"
```
```bash theme={null}
BUCKET="your-recording-bucket"
SA_EMAIL="tavus-recording-writer@${PROJECT_ID}.iam.gserviceaccount.com"
gcloud iam service-accounts create tavus-recording-writer \
--project="$PROJECT_ID" \
--display-name="Tavus Recording Writer"
gsutil iam ch "serviceAccount:${SA_EMAIL}:objectCreator" "gs://${BUCKET}"
```
```bash theme={null}
PROJECT_NUMBER=$(gcloud projects describe "$PROJECT_ID" --format='value(projectNumber)')
# Replace with your Tavus Workspace ID (find in PAL Maker - click your user profile)
PRINCIPAL="principalSet://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/tavus-recording-pool/attribute.customer_id/"
gcloud iam service-accounts add-iam-policy-binding "$SA_EMAIL" \
--project="$PROJECT_ID" \
--role="roles/iam.workloadIdentityUser" \
--member="$PRINCIPAL"
```
```shell cURL {7-14} theme={null}
curl --request POST \
--url https://tavusapi.com/v2/conversations \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"face_id": "r90bbd427f71",
"properties": {
"auto_start_recording": true,
"recording_storage": {
"provider": "gcs",
"bucket_name": "your-recording-bucket",
"project_id": "your-gcp-project",
"workload_identity_provider": "projects/123456/locations/global/workloadIdentityPools/tavus-recording-pool/providers/tavus-worker",
"service_account_email": "tavus-recording-writer@your-gcp-project.iam.gserviceaccount.com"
}
}
}'
```
`workload_identity_provider` is the resource name **without** the `//iam.googleapis.com/` prefix - Tavus prepends it.
```hcl theme={null}
resource "google_iam_workload_identity_pool" "tavus" {
workload_identity_pool_id = "tavus-recording-pool"
display_name = "Tavus Recording Storage"
}
resource "google_iam_workload_identity_pool_provider" "tavus_worker" {
workload_identity_pool_id = google_iam_workload_identity_pool.tavus.workload_identity_pool_id
workload_identity_pool_provider_id = "tavus-worker"
attribute_mapping = {
"google.subject" = "assertion.sub"
"attribute.customer_id" = "assertion.customer_id"
}
# Replace with your Tavus Workspace ID (find in PAL Maker - click your user profile)
attribute_condition = "attribute.customer_id == ''"
oidc {
issuer_uri = "https://recording-copy.tavus.io"
}
}
resource "google_service_account" "tavus_writer" {
account_id = "tavus-recording-writer"
display_name = "Tavus Recording Writer"
}
resource "google_storage_bucket_iam_member" "writer" {
bucket = "your-recording-bucket"
role = "roles/storage.objectCreator"
member = "serviceAccount:${google_service_account.tavus_writer.email}"
}
resource "google_service_account_iam_member" "wif_user" {
service_account_id = google_service_account.tavus_writer.name
role = "roles/iam.workloadIdentityUser"
# Replace with your Tavus Workspace ID (find in PAL Maker - click your user profile)
member = "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.tavus.name}/attribute.customer_id/"
}
```
#### Customize the object key (optional)
By default, recordings land at `tavus//` (no file extension) in your bucket. Add a `key_template` field to your `recording_storage` config to override the destination key shape:
```json theme={null}
{
"recording_storage": {
"provider": "gcs",
"bucket_name": "your-bucket",
"workload_identity_provider": "...",
"service_account_email": "...",
"key_template": "recordings/{conversation_id}/{epoch_ms}.mp4"
}
}
```
Tokens substituted at copy time: `{conversation_id}` (Tavus conversation UUID) and `{epoch_ms}` (Daily epoch-ms timestamp, unique per recording instance). Allowed literal characters: `[0-9A-Za-z./_-{}]`. Max 512 characters. No leading slash, no `//`, no `..`. Invalid templates are rejected when your config is saved.
Common shapes:
| Goal | `key_template` | Resulting key |
| --------------------------------------- | ---------------------------------------------- | ------------------------------------ |
| Default (today's behavior) | (omit field) | `tavus//` |
| Add `.mp4` extension | `tavus/{conversation_id}/{epoch_ms}.mp4` | `tavus//.mp4` |
| Custom prefix | `my-org/recs/{conversation_id}/{epoch_ms}.mp4` | `my-org/recs//.mp4` |
| Flat layout (one file per conversation) | `my-org/recs/{conversation_id}.mp4` | `my-org/recs/.mp4` |
**Overwrite behavior for flat layouts.** A template without `{epoch_ms}` produces the same key for every recording instance on a given conversation. In normal Tavus CVI usage one conversation produces exactly one recording, so this is safe. If your integration calls `startRecording` / `stopRecording` multiple times on the same conversation, or you implement your own recording-error retry, later recordings will overwrite earlier ones in your bucket. Include `{epoch_ms}` in your template for a zero-collision guarantee.
**Permission scope.** If you scoped the service-account permission to a specific path prefix (rather than the whole bucket), update it to cover the prefix you choose in `key_template` before applying. Otherwise the recording will fail to deliver with `DESTINATION_AUTH_FAILED`.
Azure Blob uses Entra ID Federated Credentials. Tavus exposes an OIDC issuer at `https://recording-copy.tavus.io`; you create an App Registration on your side that trusts JWTs from this issuer.
**Scope your trust to your account.** The steps below set the federated credential `subject` to your Tavus Workspace ID. This ensures only recordings belonging to your account can authenticate to your Azure resources. Click your user profile on the [PAL Maker](https://maker.tavus.io/dev) to find your Workspace ID.
```bash theme={null}
TENANT_ID=""
SUBSCRIPTION=""
RESOURCE_GROUP="your-rg"
STORAGE_ACCOUNT="yourrecordingsaccount"
CONTAINER="conversation-recordings"
# 1. Create an App Registration
az ad app create --display-name "Tavus Recording Storage"
APP_ID=$(az ad app list --display-name "Tavus Recording Storage" --query "[0].appId" -o tsv)
# 2. Add the federated credential (this trusts JWTs from Tavus)
cat > federation.json <",
"audiences": ["api://AzureADTokenExchange"]
}
EOF
# Replace with your Tavus Workspace ID (find in PAL Maker - click your user profile)
az ad app federated-credential create --id "$APP_ID" --parameters federation.json
# 3. Create a service principal for the app
az ad sp create --id "$APP_ID"
SP_ID=$(az ad sp show --id "$APP_ID" --query id -o tsv)
```
```bash theme={null}
SCOPE="/subscriptions/${SUBSCRIPTION}/resourceGroups/${RESOURCE_GROUP}/providers/Microsoft.Storage/storageAccounts/${STORAGE_ACCOUNT}/blobServices/default/containers/${CONTAINER}"
az role assignment create \
--assignee-object-id "$SP_ID" \
--assignee-principal-type ServicePrincipal \
--role "Storage Blob Data Contributor" \
--scope "$SCOPE"
```
```shell cURL {7-14} theme={null}
curl --request POST \
--url https://tavusapi.com/v2/conversations \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"properties": {
"auto_start_recording": true,
"recording_storage": {
"provider": "azure_blob",
"storage_account": "yourrecordingsaccount",
"container": "conversation-recordings",
"tenant_id": "11111111-2222-3333-4444-555555555555",
"client_id": "66666666-7777-8888-9999-000000000000"
}
},
"face_id": "r5f0577fc829"
}'
```
**Subscription ID** - the `azurerm` provider needs an explicit subscription ID. Either set `export ARM_SUBSCRIPTION_ID=` before `terraform apply`, or set `subscription_id` in your `provider "azurerm"` block. Without this, `terraform plan` hangs without a clear error.
```hcl theme={null}
resource "azuread_application" "tavus" {
display_name = "Tavus Recording Storage"
}
resource "azuread_service_principal" "tavus" {
client_id = azuread_application.tavus.client_id
}
resource "azuread_application_federated_identity_credential" "tavus" {
application_id = azuread_application.tavus.id
display_name = "tavus-recording-copy"
description = "Tavus recording delivery"
audiences = ["api://AzureADTokenExchange"]
issuer = "https://recording-copy.tavus.io"
# Replace with your Tavus Workspace ID (find in PAL Maker - click your user profile)
subject = ""
}
resource "azurerm_role_assignment" "writer" {
scope = azurerm_storage_container.recordings.resource_manager_id
role_definition_name = "Storage Blob Data Contributor"
principal_id = azuread_service_principal.tavus.object_id
}
```
#### Customize the object key (optional)
By default, recordings land at `tavus//` (no file extension) in your container. Add a `key_template` field to your `recording_storage` config to override the destination key shape:
```json theme={null}
{
"recording_storage": {
"provider": "azure_blob",
"storage_account": "your-account",
"container": "your-container",
"tenant_id": "...",
"client_id": "...",
"key_template": "recordings/{conversation_id}/{epoch_ms}.mp4"
}
}
```
Tokens substituted at copy time: `{conversation_id}` (Tavus conversation UUID) and `{epoch_ms}` (Daily epoch-ms timestamp, unique per recording instance). Allowed literal characters: `[0-9A-Za-z./_-{}]`. Max 512 characters. No leading slash, no `//`, no `..`. Invalid templates are rejected when your config is saved.
Common shapes:
| Goal | `key_template` | Resulting blob name |
| --------------------------------------- | ---------------------------------------------- | ------------------------------------ |
| Default (today's behavior) | (omit field) | `tavus//` |
| Add `.mp4` extension | `tavus/{conversation_id}/{epoch_ms}.mp4` | `tavus//.mp4` |
| Custom prefix | `my-org/recs/{conversation_id}/{epoch_ms}.mp4` | `my-org/recs//.mp4` |
| Flat layout (one file per conversation) | `my-org/recs/{conversation_id}.mp4` | `my-org/recs/.mp4` |
**Overwrite behavior for flat layouts.** A template without `{epoch_ms}` produces the same blob name for every recording instance on a given conversation. In normal Tavus CVI usage one conversation produces exactly one recording, so this is safe. If your integration calls `startRecording` / `stopRecording` multiple times on the same conversation, or you implement your own recording-error retry, later recordings will overwrite earlier ones in your container. Include `{epoch_ms}` in your template for a zero-collision guarantee.
**Permission scope.** If you scoped the RBAC role assignment to a specific blob prefix (rather than the whole container), update it to cover the prefix you choose in `key_template` before applying. Otherwise the recording will fail to deliver with `DESTINATION_AUTH_FAILED`.
## Start recording
You can either start the recording yourself from your frontend, or have Tavus start it for you.
### Start it from your client
By default, recording does not start on its own - trigger it after the participant joins:
```javascript theme={null}
const call = Daily.createCallObject();
call.on('joined-meeting', () => {
call.startRecording();
});
```
### Start recording automatically
Set `auto_start_recording` and Tavus begins the recording as soon as the pal joins the call - no client-side code, and nothing to coordinate in your frontend:
```shell cURL {7} theme={null}
curl --request POST \
--url https://tavusapi.com/v2/conversations \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"properties": {
"auto_start_recording": true,
"recording_storage": {
"provider": "s3",
"bucket_name": "your-bucket-name",
"bucket_region": "us-east-1",
"assume_role_arn": "arn:aws:iam::123456789012:role/TavusRecordingWriter"
}
},
"face_id": "r5f0577fc829"
}'
```
**Use one or the other, not both.** With `auto_start_recording` set, the recording is already running by the time your client joins - calling `startRecording()` as well fails with `RecordingAlreadyExists` and surfaces a `recording-error` in your app.
**Requirements.** The request is rejected with a `400` if either of these isn't met:
* Pass `recording_storage`, so the recording has a destination to be delivered to.
* The conversation must use a Tavus-hosted room. Not supported alongside `daily_room`, `meeting_url`, or LiveKit - in those cases Tavus doesn't control the room, so start the recording from your own client.
**Recording begins about a second after the pal joins**, so it usually opens with a short lead-in before your user arrives. If the user never joins at all, you still receive a short recording and an `application.recording_ready` webhook for that conversation.
**Using echo mode?** Start sending audio once you receive Daily's `recording-started` event.
## Receive the recording
Once the recording lands in your destination, Tavus fires `application.recording_ready` to your `callback_url`:
```json theme={null}
{
"properties": {
"bucket_name": "",
"s3_key": "",
"duration": 1234,
"storage_provider": "gcs",
"storage_uri": "gs:///"
},
"conversation_id": "",
"event_type": "application.recording_ready",
"message_type": "application",
"timestamp": "2026-04-30T22:11:14Z"
}
```
The `s3_key` / `storage_uri` object key follows the pattern `tavus//` - a fixed `tavus/` prefix, the conversation UUID, and a Unix epoch-milliseconds timestamp assigned by the recording service when the recording starts. The key has no file extension by default; recordings are MP4 files regardless. The same key is reused across delivery retries for a given recording, so it is stable per recording.
GCS and Azure Blob accept an optional `key_template` on `recording_storage` to override the destination key shape - see the [Google Cloud Storage](#google-cloud-storage) or [Azure Blob Storage](#azure-blob-storage) setup section.
For GCS and Azure Blob, if delivery to your bucket exhausts retries (typically due to a misconfigured trust policy on your side), Tavus instead fires `application.recording_copy_failed` with `error_code` and `error_message`. The recording is retained in Tavus's recording infrastructure for \~30 days as a manual recovery window. See [event reference](/sections/webhooks-and-callbacks#application-callbacks).
## Verify your setup
After your first recording, check:
1. **`application.recording_ready` arrives** at your callback URL - typically within \~1 minute for an average call, longer for multi-hour recordings.
2. The `storage_uri` resolves - try opening it (or fetching it) from your cloud's CLI.
3. If you see `application.recording_copy_failed` instead, the `error_code` is your starting point: `DESTINATION_AUTH_FAILED` is almost always a trust-policy issue (verify the issuer URI, subject claim, or assume-role principal).
# Customize Conversation UI
Source: https://docs.tavus.io/sections/conversational-video-interface/quickstart/customize-conversation-ui
Experience a conversation in a custom Daily UI - styled to match your preference.
You can **customize your conversation interface** to match your style by updating Daily's Prebuilt UI.
Here’s an example showing how to customize the conversation UI by adding leave and fullscreen buttons, changing the language, and adjusting the UI color.
For more options, check the Daily theme configuration reference and Daily Call Properties.
### Customization Example Guide
In this example, we will use stock face ID ***r90bbd427f71*** (Anna) and stock PAL ID ***pcb7a34da5fe*** (Sales Development Rep).
Use the following request body example:
```sh theme={null}
curl --request POST \
--url https://tavusapi.com/v2/conversations \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"face_id": "r90bbd427f71",
"pal_id": "pcb7a34da5fe"
}'
```
Replace `` with your actual API key. You can generate one in the PAL Maker.
1. Make a new `index.html` file
2. Paste following code into the file, replace `DAILY_ROOM_URL` in the code with your own room URL from step above
```html {6-8,16-22} theme={null}
```
Start the application by opening the file in the browser.
# API Conversation Quickstart
Source: https://docs.tavus.io/sections/conversational-video-interface/quickstart/cvi-quickstart
Create your first PAL using the full pipeline and start a conversation in seconds.
Use this page when you want to create a PAL and start your first Tavus
conversation. If you are building a web app that creates conversations from a
backend and embeds the returned `conversation_url`, use [CVI App Quickstart](/sections/conversational-video-interface/quickstart/build-first-app).
Use the full pipeline to unlock the complete range of face capabilities - including perception and speech recognition.
In this example, we'll create an interviewer PAL with the following settings:
* A Phoenix-4 Pro face.
* `raven-1` as the perception model for visual and audio understanding.
* `sparrow-1` for natural turn-taking with high patience (ideal for interviews).
`r90bbd427f71` is the stock Anna Face ID used throughout these docs. It is
a stock ID, not a placeholder. A **Face** defines the on-screen likeness and voice; a **PAL** defines behavior, knowledge, and pipeline configuration.
Use the following request body example:
```shell cURL theme={null}
curl --request POST \
--url https://tavusapi.com/v2/pals \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"pal_name": "Interviewer",
"system_prompt": "As an Interviewer, you are a skilled professional who conducts thoughtful and structured interviews. Your aim is to ask insightful questions, listen carefully, and assess responses objectively to identify the best candidates.",
"pipeline_mode": "full",
"context": "You have a track record of conducting interviews that put candidates at ease, draw out their strengths, and help organizations make excellent hiring decisions.",
"default_face_id": "r90bbd427f71",
"layers": {
"perception": {
"perception_model": "raven-1"
},
"conversational_flow": {
"turn_detection_model": "sparrow-1",
"turn_taking_patience": "high",
"pal_interruptibility": "medium"
}
}
}'
```
Replace `` with your actual API key. You can generate one in the PAL Maker.
Tavus offers full layer customizations for your PAL. Please see the following for each layer configurations:
* [Large Language Model (LLM)](/sections/conversational-video-interface/pal/llm)
* [Perception](/sections/conversational-video-interface/pal/perception)
* [Text-to-Speech (TTS)](/sections/conversational-video-interface/pal/tts)
* [Speech-to-Text (STT)](/sections/conversational-video-interface/pal/stt)
* [Conversational Flow](/sections/conversational-video-interface/pal/conversational-flow)
Create a new conversation using your newly created `pal_id`:
```shell cURL theme={null}
curl --request POST \
--url https://tavusapi.com/v2/conversations \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"pal_id": "",
"conversation_name": "Interview User"
}'
```
* Replace `` with your actual API key.
* Replace `` with your newly created PAL ID.
To join the conversation, click the link in the `conversation_url` field from the response:
```json theme={null}
{
"conversation_id": "c477c9dd7aa6e4fe",
"conversation_name": "Interview User",
"conversation_url": "",
"status": "active",
"callback_url": "",
"created_at": "2025-05-13T06:42:58.291561Z"
}
```
Building an app around this flow? Follow [CVI App Quickstart](/sections/conversational-video-interface/quickstart/build-first-app). React apps that want Tavus-provided UI can use the [`@tavus/cvi-ui` component library](/sections/conversational-video-interface/component-library/overview) for `CVIProvider`, `Conversation`, hooks, and server helpers.
# Emotion Control with Phoenix-4
Source: https://docs.tavus.io/sections/conversational-video-interface/quickstart/emotional-expression
Unlock emotionally expressive facial movements and micro-expressions using Phoenix-4 faces.
## How It Works
Phoenix-4 faces can dynamically express emotions like happiness, sadness, anger, and more through lifelike facial expressions while speaking and listening.
For the most human-like results, emotional expression works best as part of a closed-loop system: **Phoenix-4** for expression, **Raven-1** for perception, and **Sparrow-1** for conversational flow. Each component informs the others.
Tavus handles the complex interactions behind the scenes - all of this powered by our state of the art models working seamlessly with any LLM. All of this available out of the box with default Tavus settings.
### Requirements
1. **Select a Phoenix-4 face** - All Phoenix-4 faces support emotional expression. Faces marked **Pro** in the [Stock Face Library](https://maker.tavus.io/dev/faces) are extra emotive. See [featured Pro faces here](/sections/faces/stock-faces#pro).
2. **Enable `tts_emotion_control`** - This is enabled by default for Phoenix-4 faces, so no action needed unless you've explicitly disabled it. See [TTS layer](/sections/conversational-video-interface/pal/tts) for details.
3. **Enable `speculative_inference`** - This is also enabled by default for all PALs, and again no action needed unless you've explicitly disabled it.
Pair with **Raven-1** as your perception model to enhance user emotion understanding. See [Perception](/sections/conversational-video-interface/pal/perception) for configuration.
For best results, use a Tavus-hosted model with strong instruction-following capabilities such as **`tavus-gemma-4`**.
### Guiding Emotional Delivery
You can further shape how the PAL expresses emotion through your `system_prompt`. For example:
* "Be enthusiastic when discussing new features"
* "Speak calmly and empathetically when the user is frustrated"
* "Show excitement when celebrating user achievements"
* "Respond with anger if the user interrupts you mid-sentence"
#### Example: Negotiation Sparring Partner
Here's an example system prompt designed to display a range of emotions:
> You are a tough but fair negotiation coach who helps users practice high-stakes conversations. When role-playing scenarios, embody the opposing party with conviction. If the user makes weak arguments or caves too easily, push back with frustration - they need to feel the pressure. When they fumble or seem lost, express concern and gently guide them. But when they land a strong point or hold their ground, show genuine satisfaction. Don't go easy on them. Real negotiations are uncomfortable, and you're here to prepare them for that.
This prompt naturally triggers **angry** responses when pushing back, **scared/concerned** reactions when the user struggles, and **content** acknowledgment when the user succeeds.
### Example PAL Configuration
```json theme={null}
{
"pal_name": "Hype Fitness Coach!",
"system_prompt": "You are an incredibly enthusiastic fitness coach who gets HYPED about every win, no matter how small. Crushed a workout? Let's GO! Drank enough water today? That's HUGE! Be wildly supportive and energetic. When users are struggling, dial it back - be warm, calm, and encouraging. But the moment they share any progress, bring the energy back up. You live for celebrating wins.",
"default_face_id": "r90bbd427f71"
}
```
You can learn more about [PAL Configuration here](/api-reference/pals/create-pal)
This minimal configuration works because `tts_emotion_control` and `speculative_inference` are enabled by default for Phoenix-4 faces.
## Echo Mode
When using [Echo Mode](/sections/conversational-video-interface/quickstart/echo-mode), you must manually insert emotion tags into your [text echos](/sections/event-schemas/conversation-echo).
**Valid emotion values:** `neutral`, `angry`, `excited`, `elated`, `content`, `sad`, `dejected`, `scared`, `contempt`, `disgusted`, `surprised`
```xml theme={null}
I'm so glad you asked about that!
```
```xml theme={null}
That's completely unacceptable.
```
```xml theme={null}
I'm sorry to hear that happened.
```
```xml theme={null}
I'm not sure we should go down that path...
```
# Pipeline Modes
Source: https://docs.tavus.io/sections/conversational-video-interface/quickstart/pipeline-modes
Run CVI with the full Tavus pipeline, Echo mode, or integrate via LiveKit and Pipecat.
## Use the Full Pipeline (Default & Recommended)
The default and recommended end-to-end configuration optimized for real-time conversation. All CVI layers are active and customizable.
* Low utterance-to-utterance latency with Tavus defaults (see [What Is CVI?](/sections/conversational-video-interface/overview-cvi))
* Best for natural humanlike interactions
## Alternate Modes
These modes are incompatible with Tavus's perception and speech recognition layers. For the lowest latency and the full multimodal stack (perception, turn-taking, and rendering together), we recommend the **full Tavus pipeline** above.
### Echo Mode
Tavus also supports an [Echo mode](/sections/conversational-video-interface/echo-mode) pipeline. It lets you send text or audio input directly to the PAL for playback, bypassing most of the CVI pipeline.
### Integration Modes
If you already run conversational AI on **LiveKit** or **Pipecat**, you can still use a Tavus face for synchronized avatar video - see the dedicated guides for setup and API details.
* **[LiveKit Agent](/sections/integrations/livekit)** - Tavus renders the Face in a LiveKit room alongside a LiveKit Agents voice assistant.
* **[Pipecat](/sections/integrations/pipecat)** - Tavus joins as a transport participant or supplies video via `TavusVideoService` while Pipecat runs the pipeline on Daily.
### Custom LLM / Bring Your Own Logic
Use this mode to integrate a custom LLM or a specialized backend for interpreting transcripts and generating responses.
* Adds latency due to external processing
* Does **not** require an actual LLM - any endpoint that returns a compatible chat completion format can be used
# Internet Search
Source: https://docs.tavus.io/sections/conversational-video-interface/skills/internet-search
Let your PAL answer questions with real-time web search.
The `internet_search` skill lets the PAL answer questions with up-to-date information from the web. With the skill attached, the PAL searches in real time during the conversation instead of being limited to its training data and your [Knowledge Base](/sections/conversational-video-interface/knowledge-base) documents.
## How it works
With the skill attached, the PAL runs a web search on every turn and the results are automatically injected into the conversation context so it can ground its response in the latest information.
## Configuration
None - `internet_search` is a pure on/off toggle. Attaching it enables real-time search on every conversation the PAL has.
## Attach
```bash theme={null}
curl --request PUT \
--url https://tavusapi.com/v2/pals/{pal_id}/skills/internet_search \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{}'
```
The skill is active on the PAL's next conversation. To turn it off, detach it:
```bash theme={null}
curl --request DELETE \
--url https://tavusapi.com/v2/pals/{pal_id}/skills/internet_search \
--header 'x-api-key: '
```
See [Skills Overview](/sections/conversational-video-interface/skills/overview) for how attachments work and the full [API reference](/api-reference/pal-skills/attach-skill-to-pal).
# Overview
Source: https://docs.tavus.io/sections/conversational-video-interface/skills/overview
Attach pre-built capabilities like internet search, presentations, and interactive UI to your PALs.
**Skills** are pre-built capabilities you can attach to a PAL. Each skill bundles the prompting, conversation configuration, and document wiring needed for a capability, so you can turn it on without building it yourself.
Skills are authored and maintained by Tavus. You choose which skills a PAL has from the skill registry; you don't write skill code.
**Skills vs tools:** [Skills](/sections/conversational-video-interface/skills/overview) are Tavus-authored capabilities you toggle on (`internet_search`, `presentation`, `magic_canvas`, etc.). [Tools](/sections/conversational-video-interface/pal/tools) are functions **you** define in the tools registry for the LLM or Raven to call during a conversation. A PAL can use both.
A skill is active as soon as it is attached to a PAL. To turn a skill off, detach it with [Detach Skill from PAL](/api-reference/pal-skills/detach-skill-from-pal).
## Available skills
Skill ID: `internet_search`. Real-time web search during a call. No configuration - a pure on/off toggle.
Skill ID: `presentation`. Walk participants through slide decks and images from your [Knowledge Base](/sections/conversational-video-interface/knowledge-base).
Skill ID: `magic_canvas`. Interactive UI during a call: questions, calendars, charts, and more. User responses flow back to the PAL and your backend.
List the current registry at any time with [List Skills](/api-reference/skills/list-skills). More skills will be added over time.
## Attaching a skill
Attach a skill to a PAL with a `PUT` to the PAL's skill collection. Certain skills require a `config` object - see each skill's page for its fields.
```bash theme={null}
curl --request PUT \
--url https://tavusapi.com/v2/pals/{pal_id}/skills/{skill_id} \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{}'
```
The attachment takes effect on the PAL's next conversation.
## Updating a skill's configuration
Use `PATCH` to merge changes into an existing attachment without resending the whole config. Fields you pass replace the existing values; fields you omit are preserved; fields set to `null` are removed:
```bash theme={null}
curl --request PATCH \
--url https://tavusapi.com/v2/pals/{pal_id}/skills/{skill_id} \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"config": {
"prompt": "Keep the walkthrough under five minutes."
}
}'
```
Use `PUT` on the same path to overwrite the entire configuration, or [Replace PAL Skills](/api-reference/pal-skills/replace-pal-skills) to set a PAL's full skill set in one call.
## API reference
| Endpoint | Description |
| ------------------------------------------------------------------------ | ---------------------------- |
| [List Skills](/api-reference/skills/list-skills) | All skills in the registry |
| [Get Skill](/api-reference/skills/get-skill) | Metadata for one skill |
| [List PAL Skills](/api-reference/pal-skills/list-pal-skills) | Skills attached to a PAL |
| [Get PAL Skill](/api-reference/pal-skills/get-pal-skill) | One attachment |
| [Attach Skill to PAL](/api-reference/pal-skills/attach-skill-to-pal) | Attach or overwrite |
| [Update PAL Skill](/api-reference/pal-skills/update-pal-skill) | Merge config changes |
| [Detach Skill from PAL](/api-reference/pal-skills/detach-skill-from-pal) | Remove an attachment |
| [Replace PAL Skills](/api-reference/pal-skills/replace-pal-skills) | Bulk-replace all attachments |
# Presentation
Source: https://docs.tavus.io/sections/conversational-video-interface/skills/presentation
Let your PAL walk participants through a slide deck from your Knowledge Base.
The `presentation` skill lets the PAL present one or more slide decks (pdf) and images from your [Knowledge Base](/sections/conversational-video-interface/knowledge-base). The documents are injected into the PAL's conversation context so the PAL can speak to their content during the call.
**Which documents can be presented**
Uploaded documents are prepared for presentation automatically - there is nothing to enable at upload time:
* **PDFs up to 50 pages** and **images** are presentable.
* **PDFs over 50 pages** are not presentable.
* **Websites and other file types** (e.g. Word documents) are not presentable.
## Adding a presentation skill to your PAL
Before you attach the presentation skill to your PAL, the documents must already exist in your Knowledge Base. See [Create Document](/api-reference/documents/create-document).
```bash theme={null}
curl --request PUT \
--url https://tavusapi.com/v2/pals/{pal_id}/skills/presentation \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"config": {
"document_ids": ["d1234567890", "d2468101214"],
"slides_trigger": "walk_the_deck",
"prompt": "Walk the participant through the Q4 roadmap deck, one slide at a time."
}
}'
```
## Configuration
| Field | Type | Required | Description |
| ---------------- | ---------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `document_ids` | array of strings | Yes | The documents you want the PAL to present. For many use cases this will be just one pdf slide deck. At least one is required. Document IDs are returned by the [Create Document](/api-reference/documents/create-document) API endpoint. Each document must be owned by you and presentable (see above). |
| `slides_trigger` | string | No | How the PAL decides to present a slide: show a relevant slide based on the conversation (`on_demand`) or let the walk-through of the slide deck drive the conversation flow (`walk_the_deck`). |
| `prompt` | string | No | Custom instructions for how the PAL should present the material. |
## Update the configuration
`PATCH` merges changes into the existing config - fields you omit are preserved:
```bash theme={null}
curl --request PATCH \
--url https://tavusapi.com/v2/pals/{pal_id}/skills/presentation \
--header 'Content-Type: application/json' \
--header 'x-api-key: ' \
--data '{
"config": {
"slides_trigger": "on_demand"
}
}'
```
See [Skills Overview](/sections/conversational-video-interface/skills/overview) for how attachments work and the full [API reference](/api-reference/pal-skills/attach-skill-to-pal).
## Displaying the presentation video track in your app
If you embed with the [`@tavus/cvi-ui`](/sections/conversational-video-interface/component-library/blocks) [`Conversation`](/sections/conversational-video-interface/component-library/blocks#conversation-block) block, face video and screen share switching is built in - skip this section unless you are building a custom Daily React layout.
When the PAL presents, the slide is **not** a separate participant in the call. It is published as a `screenVideo` track on the **same participant as the PAL's video** (its face video). Your frontend shows it by watching that participant's `screenVideo` track and rendering it when it becomes playable.
The examples below use [`@daily-co/daily-react`](https://docs.daily.co/reference/daily-react). All Daily components are client-side, so wrap your call UI in a `` and mark the components as client components. Import hooks and components from that package - for example `useParticipantIds`, `useParticipantProperty`, `DailyVideo`, and `DailyAudioTrack`.
The PAL is the remote participant with a playable video track. (A Tavus room can also contain a helper participant that has no video and only sends app-messages, so filter on a playable video track rather than taking the first remote participant.)
```tsx theme={null}
const videoIds = useParticipantIds({
filter: (p) => !p.local && p.tracks?.video?.state === "playable",
});
const remoteIds = useParticipantIds({ filter: "remote" });
const replicaId = videoIds[0] ?? remoteIds[0] ?? null;
```
The slide is published as a `screenVideo` track on that same participant. Subscribe to its state so you know when the PAL starts sharing:
```tsx theme={null}
const screenState = useParticipantProperty(replicaId ?? "", "tracks.screenVideo.state");
const screenSharing = screenState === "playable" || screenState === "loading";
```
`screenState` stays `off` until the PAL starts sharing, then transitions to `loading` and `playable`. Including `loading` lets your layout switch the moment the slide starts coming in instead of waiting for the first frame.
Show the slide when the PAL is sharing, and fall back to the face video otherwise. Both use the same `sessionId` - only the `type` differs (`screenVideo` for the slide, `video` for the face video):
```tsx theme={null}
{replicaId && screenSharing ? (
// The PAL is sharing - show the slide
) : replicaId ? (
// Not sharing - show the face video
) : (
Waiting for the PAL...
)}
{/* Render the PAL's audio independently so it plays in either layout */}
{replicaId && }
```
Use `objectFit: "contain"` for the slide so documents are never cropped, and `"cover"` for the face video. When the slide is the main view, a common pattern is to keep the face visible as a small picture-in-picture by rendering a second `` in a corner.
The `screenVideo` track is lazy: it does not exist until the PAL starts sharing. Don't block or error on a missing screen track when the call connects - just render the face video and switch to the slide once `screenVideo.state` becomes `playable`. If a slide isn't appearing, logging `screenState` makes it clear whether the track is never publishing or your frontend simply isn't rendering it.
## Troubleshooting
Documents must be fully processed for presentation before they can be attached. The API rejects the configuration with a `400` if any document is still processing or not presentable (too long, a website, or an unsupported file type). Wait for the document's processing to finish before attaching it.
# Embed
Source: https://docs.tavus.io/sections/deployments/embed
Render a Tavus conversation inline in your layout with the custom element - in plain HTML, React, or Next.js.
## Overview
`` renders a Tavus conversation inline, inside your own layout. It uses the same managed deployment flow as the [widget](/sections/deployments/widget), but instead of a floating launcher the experience lives where you put the element - a product page, a demo section, a dashboard panel.
The embed renders in a shadow DOM and adapts to its container: it maintains a `16:9` aspect ratio in wide containers and switches to a compact `9:16` portrait layout in narrow ones.
PALs with a [Magic Canvas](/sections/conversational-video-interface/magic-canvas/overview) skill render cards in the embed automatically. See [Hosted embed & widget](/sections/conversational-video-interface/magic-canvas/integrations/hosted) for Canvas-specific behavior.
## Installation
```html theme={null}
```
To pin an exact version instead of tracking the latest beta, use a versioned URL such as `https://unpkg.com/@tavus/embed@0.1.0`.
```bash theme={null}
npm install @tavus/embed@latest
```
Importing the package registers the `` custom element as a side effect. Use the tag directly in JSX:
```tsx theme={null}
import "@tavus/embed";
export function Support() {
return (
);
}
```
```bash theme={null}
npm install @tavus/embed@latest
```
The element touches browser APIs at registration time, so import it in a client component:
```tsx theme={null}
"use client";
import "@tavus/embed";
export function TavusEmbed() {
return (
);
}
```
## Attributes
| Attribute | Type | Description |
| ------------------------ | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `deployment-id` | string | The Tavus deployment ID. Fetches configuration from `/v2/deployments/:id/init`. |
| `conversational-context` | string | Extra context injected into the conversation at start (maps to `conversational_context`). Use it to pass what the deployment should know about the current user or page. |
| `custom-greeting` | string | Overrides the PAL's opening line for this conversation (maps to `custom_greeting`). |
| `memory-stores` | string | Comma-separated list of memory store IDs (maps to `memory_stores`). Pass a stable per-user identifier to give that user consistent, persistent memory across every conversation with this PAL. |
## Context and greeting
`conversational-context` and `custom-greeting` let you tailor a single conversation at start time:
```html theme={null}
```
`conversational-context` overrides the deployment's configured default context for this conversation; when omitted, the deployment default is used. `custom-greeting` sets the PAL's opening line. Empty or whitespace-only values are ignored, so an empty attribute behaves the same as not setting it.
## Persistent memory
`memory-stores` lets a returning user pick up where they left off. A store ID is a memory namespace: every conversation that uses the same store ID shares one bucket of memories, so the PAL can remember that user across sessions and devices. See [Memories](/sections/conversational-video-interface/memories) for how memory stores work on the API.
Store IDs are global to your account, **not** scoped to a deployment. A bare `user-12345` would share the same memory across every deployment that uses it. To keep memory specific to one deployment, combine the deployment ID with your user ID (similar to namespacing with PAL ID in the [Memories guide](/sections/conversational-video-interface/memories#basic-example)):
```html theme={null}
```
Pass multiple stores as a comma-separated list (`memory-stores="YOUR_DEPLOYMENT_ID-user-12345,team-acme"`). Empty or whitespace-only values are ignored.
In the [PAL Maker](https://maker.tavus.io/dev), open your deployment's **Settings** and use **Unique memory per visitor** (maps to `customization.conversation.unique_memory_tag`; on by default). When that toggle is on and you omit `memory-stores`, the widget or embed generates an anonymous per-browser ID (stored in `localStorage`, scoped to that deployment) and sends it as `memory_stores` on each conversation start. Memory persists for that visitor in the same browser, but not across devices or after storage is cleared. Set `memory-stores` to your own stable user ID when you want memory to follow a logged-in user everywhere. With the toggle off and no `memory-stores` attribute, no memory store is sent.
## Sizing
`` fills its parent: the host element is `width: 100%; height: 100%; max-height: 100vh; overflow: hidden`. Give it a parent with a definite size, or wrap it in a container with `aspect-ratio`:
```html theme={null}
```
If the parent has no resolved height - for example a chain of auto-height flex containers - the embed cannot size itself. Give the parent an explicit height or an `aspect-ratio`, or ensure the full chain (`html`, `body`, root element, container) resolves to a real height.
## Interacting from the page
The embed emits lifecycle events (`tavus:conversation-started`, `tavus:tool-call`, …) and exposes an imperative API for starting, ending, and messaging the conversation. See [Host communication](/sections/deployments/host-communication) for the full reference.
# Host Communication
Source: https://docs.tavus.io/sections/deployments/host-communication
Listen to deployment lifecycle events and send interactions to the conversation from your page, with plain DOM APIs or the TavusIntegration helper.
## Overview
Both `` and `` support two-way communication with the host page:
* **Element → page**: the element dispatches `CustomEvent`s (with `bubbles: true` and `composed: true`) for conversation lifecycle, tool calls, and protocol messages.
* **Page → element**: the element exposes an imperative API at `element.tavus` for starting, ending, and messaging the conversation.
CDN consumers use plain DOM APIs - no extra bundle needed. npm consumers can use the typed `TavusIntegration` helper exported from `@tavus/widget` and `@tavus/embed`.
## Events
Listen on the element itself, or on `document` (events bubble out of the shadow DOM):
```js theme={null}
const el = document.querySelector("tavus-embed");
el.addEventListener("tavus:conversation-started", (e) => {
console.log("started", e.detail.conversationId);
});
el.addEventListener("tavus:tool-call", (e) => {
console.log("tool call", e.detail.name, e.detail.arguments);
});
```
| Event | `detail` | Fires when |
| ---------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `tavus:conversation-started` | `{ conversationId }` | The visitor joins and the conversation connects. |
| `tavus:conversation-ended` | `{ conversationId }` | The conversation ends. |
| `tavus:state-change` | `{ state }` | The conversation state changes (connecting, connected, ended, …). |
| `tavus:mode-change` | `{ mode }` | The experience switches mode (for example between screens or modalities). |
| `tavus:error` | `{ code, message }` | The conversation hits an error. |
| `tavus:tool-call` | `{ name, arguments, seq?, turn_idx? }` | The PAL's LLM invokes a tool. `arguments` is a JSON string. |
| `tavus:protocol-message` | varies | Firehose: fires for every observable protocol event (utterances, perception, speaking state, …). Switch on `detail.event_type`. |
`tavus:protocol-message` carries the raw [Interaction Events](/sections/conversational-video-interface/interactions-protocols/overview) protocol - `detail.event_type` values such as `conversation.utterance`, `conversation.tool_call`, `conversation.started_speaking`, and `conversation.stopped_speaking` match the schemas documented there. For speaking state, check `detail.properties.role` (`"user"`, `"pal"`, or legacy `"replica"`). See [Started/Stopped Speaking Event](/sections/event-schemas/conversation-started-stopped-speaking).
Magic Canvas card taps are not emitted as `tavus:*` host events - they are delivered to your conversation webhook as [`canvas.interaction`](/sections/event-schemas/canvas-interaction) events. See [Canvas interactions](/sections/conversational-video-interface/magic-canvas/api/interactions).
## Imperative API
The element exposes its conversation controls on `element.tavus`:
```js theme={null}
const el = document.querySelector("tavus-widget");
await el.tavus.start(); // start the conversation
el.tavus.sendChat("Hello!"); // send a chat message as the visitor
el.tavus.sendMessage({ // send a typed protocol interaction
event_type: "conversation.echo",
properties: { text: "Read this aloud" },
});
await el.tavus.end(); // end the conversation
```
| Method | Description |
| -------------------------- | --------------------------------------------------------------------------------------- |
| `start()` | Starts the conversation, as if the visitor pressed the start button. Returns a promise. |
| `end()` | Ends the active conversation. Returns a promise. |
| `sendChat(text)` | Sends a chat message into the conversation as the visitor. |
| `sendMessage(interaction)` | Sends a typed protocol interaction. See [Interactions](#interactions). |
`element.tavus` is attached once the element has mounted and its configuration has loaded. Wait for the element to render (or for a `tavus:state-change` event) before calling into it.
## Interactions
`sendMessage` accepts the same interaction shapes as the [Interactions Protocol](/sections/conversational-video-interface/interactions-protocols/overview):
| `event_type` | `properties` | Effect |
| ------------------------------------ | ------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `conversation.echo` | `{ text }` | The PAL speaks the text verbatim. See [Echo](/sections/event-schemas/conversation-echo). |
| `conversation.respond` | `{ text }` | The text is sent to the LLM, which responds. See [Respond](/sections/event-schemas/conversation-respond). |
| `conversation.interrupt` | - | Interrupts the PAL mid-speech. See [Interrupt](/sections/event-schemas/conversation-interrupt). |
| `conversation.append_llm_context` | `{ context }` | Appends to the conversation's LLM context. See [Append Context](/sections/event-schemas/conversation-append-context). |
| `conversation.overwrite_llm_context` | `{ context }` | Replaces the conversation's LLM context. See [Overwrite Context](/sections/event-schemas/conversation-overwrite-context). |
## TavusIntegration helper (npm)
npm consumers get a typed wrapper around the element from the same package as the registration side effect:
```ts theme={null}
import "@tavus/embed";
import { TavusIntegration, type TavusInteraction } from "@tavus/embed";
// Pass the tag name to target: "tavus-embed" (default) or "tavus-widget".
const integration = new TavusIntegration("tavus-embed");
integration.on("tavus:conversation-started", (e) => {
console.log("started", e.detail.conversationId);
});
integration.on("tavus:protocol-message", (e) => {
console.log(e.detail);
});
const interaction: TavusInteraction = {
event_type: "conversation.respond",
properties: { text: "Tell me about pricing" },
};
integration.sendMessage(interaction);
// Plain strings are wrapped as conversation.respond for convenience.
integration.sendMessage("Tell me about pricing");
```
| Method | Description |
| ------------------------------------ | ------------------------------------------------------------------------------- |
| `on(event, handler)` | Subscribes to a typed deployment event. |
| `off(event, handler)` | Removes a previously added handler. |
| `sendMessage(interaction \| string)` | Sends a typed interaction. A plain string is wrapped as `conversation.respond`. |
The helper looks up the element by tag name in the DOM, so construct it (or make the first call) after the element exists on the page.
`TavusIntegration` is bundled into the ESM builds of `@tavus/widget` and `@tavus/embed` - no extra install. The CDN (IIFE) build stays a pure side-effect drop-in; CDN pages use the DOM APIs above instead.
# Landing Page
Source: https://docs.tavus.io/sections/deployments/landing-page
Share a standalone Tavus conversation with a link - fully hosted by Tavus, no website or embed code required.
## Overview
A **landing page** deployment is a shareable conversation experience that Tavus hosts for you on `*.maker.tavus.io`. Publish from the [PAL Maker](https://maker.tavus.io/dev), copy the link, and send it anywhere - email, social, ads, or a QR code. No script tags, no API keys, no backend.
It uses the same managed conversation flow as the [widget](/sections/deployments/widget) and [embed](/sections/deployments/embed): Tavus validates the deployment, creates the conversation server-side, and runs the call.
## What's on the page
You configure everything visually in PAL Maker.
**Hero** (landing-page channel settings):
| Setting | Description |
| ---------------------- | ------------------------------------------------------------------------ |
| Template | One of five layout variants for the hero section. |
| Logo | Optional brand mark shown on the page. |
| Title & description | Headline and supporting copy above the conversation. |
| Call-to-action buttons | Optional links (title + URL) in the hero or footer. |
| Background | Default gradient or a custom background image. |
| Access | `public` (anyone can start) or `auth_required` (visitors sign in first). |
**Conversation flow** (shared with widget and embed deployments):
| Screen | Description |
| ---------- | ---------------------------------------------------------- |
| Preview | Pre-call screen with title, description, and start button. |
| Haircheck | Camera and microphone check when video is enabled. |
| Call | The live conversation with your PAL. |
| After call | Closing screen with optional call-to-action buttons. |
The same [protections](/sections/deployments/overview#protecting-your-deployment) apply: call limits, optional deployment password, allowed-origin rules (Tavus-hosted pages are permitted automatically), and optional Cloudflare Turnstile bot protection.
## Getting started
1. In the PAL Maker, create a deployment and choose **Landing page** as the channel.
2. Pick your PAL, customize the hero and conversation screens, and set limits or access as needed.
3. Publish and copy the hosted link from PAL Maker.
Want the conversation inside your own site instead? Use the [widget](/sections/deployments/widget) or [embed](/sections/deployments/embed).
# Deployments Overview
Source: https://docs.tavus.io/sections/deployments/overview
Learn how Tavus Deployments let you ship a fully managed conversational AI experience with a single script tag - no backend or API key required in the browser.
## Overview
A **deployment** is a hosted, pre-configured Tavus experience that you publish from the PAL Maker and drop into any website. Tavus manages the full conversation lifecycle - validating the deployment, creating the conversation, and running the call - so your page never touches API keys or needs custom code.
Each deployment is published to one of three channels:
| Channel | What it is | Best for |
| ---------------- | ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| **Widget** | A floating launcher that expands into a conversation popup. Added with ``. | Adding an AI assistant to an existing site with one tag. |
| **Embed** | An inline conversation surface that lives inside your layout. Added with ``. | Demos, product pages, and experiences that should feel native to the page. |
| **Landing page** | A standalone conversation page fully hosted by Tavus. | Sharing a link - no website required. |
The widget and embed are Web Components served from a CDN. They render inside a shadow DOM, so your page styles and the Tavus UI never interfere with each other, and they work in any stack - plain HTML, React, Next.js, Vue, or anything else that renders HTML.
Deployments are configured visually in the [PAL Maker](https://maker.tavus.io/dev). Everything you set there - theme, modality, text, call limits - is fetched by the element at load time. No code changes are needed to update a live deployment.
## How it works
1. You create a deployment in the PAL Maker and choose a PAL, a channel, and customizations.
2. You add the script tag and custom element to your page with the deployment's ID.
3. On load, the element fetches its configuration from `/v2/deployments/:id/init` and renders the experience.
4. When a visitor starts a conversation, Tavus creates and manages it server-side - including usage limits and bot protection - and streams the call into the element.
Because the deployment ID is safe to expose publicly, there is no backend route to build and no key to protect. Abuse is handled server-side with origin restrictions, call limits, and bot protection - see [Protecting your deployment](#protecting-your-deployment).
PALs with a [Magic Canvas](/sections/conversational-video-interface/magic-canvas/overview) skill render cards automatically in hosted widget and embed deployments - no extra integration code on your page.
## Quick start
```html theme={null}
```
```html theme={null}
```
Replace `YOUR_DEPLOYMENT_ID` with the ID shown for your deployment in the PAL Maker.
## Attributes
Both elements accept the same attribute:
| Attribute | Type | Description |
| --------------- | ------ | ----------------------------------------------------------------------------------------------- |
| `deployment-id` | string | The Tavus deployment ID. The element fetches its configuration from `/v2/deployments/:id/init`. |
All customization - theme, modality, text, launcher appearance, call limits - is configured on the deployment in the [PAL Maker](https://maker.tavus.io/dev). The element picks up saved changes on the next page load, with no markup changes.
## Protecting your deployment
Your deployment ID is visible in your page's HTML, so deployments come with built-in protections - all configured in the PAL Maker, with no work needed on your page:
* **Allowed origins** - restrict a deployment to the origins (domains) you specify. The element only initializes on pages served from an allowed origin; requests from anywhere else are rejected, so copying your deployment ID onto another site does nothing.
* **Call limits** - cap daily and total conversations. When a limit is reached, or the PAL is busy, the element renders an unavailable screen whose title, description, and call-to-action are configurable per cause.
* **Bot protection** - public deployments can require a [Cloudflare Turnstile](https://www.cloudflare.com/products/turnstile/) challenge before a conversation starts, blocking automated abuse of your conversation quota. The element handles the challenge flow automatically and it stays invisible for most real visitors.
## Next steps
Add a floating conversation launcher to any page.
Render the conversation inline in your layout, including React and Next.js.
Share a hosted conversation link - no website required.
Listen to lifecycle events and send interactions from your page.
Management and browser endpoints are documented in the [Deployments API](/api-reference/deployments/create-deployment) reference.
## FAQs
No. The deployment ID is the only identifier the page needs, and it is safe to expose. Tavus creates and manages the conversation server-side. If you want full programmatic control over conversations instead, use the [CVI APIs and embedding paths](/sections/integrations/embedding-cvi).
The ID itself grants nothing sensitive, and you can lock it down further. Restrict the deployment to your origins so it only works on your domains, set daily and total call limits, and enable bot protection to require a Cloudflare Turnstile challenge before each conversation. See [Protecting your deployment](#protecting-your-deployment).
No. The widget and embed render inside an open shadow DOM with their own bundled styles. Page styles do not leak in, and Tavus styles do not leak out.
Use a deployment when you want a managed, configurable experience with no backend work. Use [direct CVI embedding](/sections/integrations/embedding-cvi) when you need full control over conversation creation, the call UI, or room state.
# Widget
Source: https://docs.tavus.io/sections/deployments/widget
Add a floating Tavus conversation launcher to any website with the custom element.
## Overview
`` renders a floating launcher that expands into a full Tavus conversation popup. It is the fastest way to put a deployment on a page: one script tag, one element, no backend.
The widget validates the deployment, creates the conversation, and manages the entire call lifecycle. It renders in a shadow DOM, so it is isolated from your page styles.
## Installation
Add the element and the script tag anywhere in your HTML:
```html theme={null}
```
To pin an exact version instead of tracking the latest beta, use a versioned URL such as `https://unpkg.com/@tavus/widget@0.1.0`.
```bash theme={null}
npm install @tavus/widget@latest
```
Importing the package registers the `` custom element as a side effect:
```ts theme={null}
import "@tavus/widget";
```
After that you can use the tag anywhere in your app's markup - including JSX:
```tsx theme={null}
```
## Attributes
| Attribute | Type | Description |
| ------------------------ | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `deployment-id` | string | The Tavus deployment ID. Fetches configuration from `/v2/deployments/:id/init`. |
| `conversational-context` | string | Extra context injected into the conversation at start (maps to `conversational_context`). Use it to pass what the deployment should know about the current user or page. |
| `custom-greeting` | string | Overrides the PAL's opening line for this conversation (maps to `custom_greeting`). |
| `memory-stores` | string | Comma-separated list of memory store IDs (maps to `memory_stores`). Pass a stable per-user identifier to give that user consistent, persistent memory across every conversation with this PAL. |
## Context and greeting
`conversational-context` and `custom-greeting` let you tailor a single conversation at start time:
```html theme={null}
```
`conversational-context` overrides the deployment's configured default context for this conversation; when omitted, the deployment default is used. `custom-greeting` sets the PAL's opening line. Empty or whitespace-only values are ignored, so an empty attribute behaves the same as not setting it.
## Persistent memory
`memory-stores` lets a returning user pick up where they left off. A store ID is a memory namespace: every conversation that uses the same store ID shares one bucket of memories, so the PAL can remember that user across sessions and devices. See [Memories](/sections/conversational-video-interface/memories) for how memory stores work on the API.
Store IDs are global to your account, **not** scoped to a deployment. A bare `user-12345` would share the same memory across every deployment that uses it. To keep memory specific to one deployment, combine the deployment ID with your user ID (similar to namespacing with PAL ID in the [Memories guide](/sections/conversational-video-interface/memories#basic-example)):
```html theme={null}
```
Pass multiple stores as a comma-separated list (`memory-stores="YOUR_DEPLOYMENT_ID-user-12345,team-acme"`). Empty or whitespace-only values are ignored.
In the [PAL Maker](https://maker.tavus.io/dev), open your deployment's **Settings** and use **Unique memory per visitor** (maps to `customization.conversation.unique_memory_tag`; on by default). When that toggle is on and you omit `memory-stores`, the widget or embed generates an anonymous per-browser ID (stored in `localStorage`, scoped to that deployment) and sends it as `memory_stores` on each conversation start. Memory persists for that visitor in the same browser, but not across devices or after storage is cleared. Set `memory-stores` to your own stable user ID when you want memory to follow a logged-in user everywhere. With the toggle off and no `memory-stores` attribute, no memory store is sent.
## Appearance
The launcher's look and placement are part of the deployment's configuration in the [PAL Maker](https://maker.tavus.io/dev) - no markup changes required:
| Setting | Values | Description |
| ---------- | ----------------------------------------------------------------------- | -------------------------------------------------------------- |
| Variant | `tiny`, `compact`, `full` | Size and detail level of the collapsed launcher. |
| Position | `bottom-right`, `bottom-left`, `top-right`, `top-left`, `top`, `bottom` | Where the launcher floats on the page. |
| Expandable | `default`, `always`, `start-expanded` | How and when the launcher expands into the conversation popup. |
| Avatar | `orb`, `image` | The launcher's avatar treatment. |
## Interacting from the page
The widget emits lifecycle events (`tavus:conversation-started`, `tavus:tool-call`, …) and exposes an imperative API for starting, ending, and messaging the conversation. See [Host communication](/sections/deployments/host-communication) for the full reference.
The widget positions itself relative to the viewport, so it can be placed anywhere in the document - typically just before the closing `