Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.tavus.io/llms.txt

Use this file to discover all available pages before exploring further.

Use this page when you are building an app that creates and embeds Tavus CVI conversations. If you only want to create a persona and start your first Tavus-hosted conversation, use API Conversation 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, End Conversation, Get Personas, and Get Replicas.
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 replica joining and returns it with status: "ended".

Prerequisites

  • A Tavus API key from the Developer Portal.
  • A usable persona_id, replica_id, or both. The next section shows how to choose.
  • Node.js 20+ for the TypeScript examples below.

Choose a persona and replica

For CVI, a replica is the face and voice in the call. A persona is the behavior, prompt, context, and conversational configuration. Use this decision tree:
  • If you have a persona with a default_replica_id, create the conversation with just persona_id.
  • If you have a persona without a default_replica_id, create the conversation with both persona_id and replica_id.
  • If you only want to test a face and voice without a custom persona, create the conversation with just replica_id.
First-time builders should start with stock Tavus resources. List stock personas and stock replicas with:
curl --request GET \
  --url "https://tavusapi.com/v2/personas?persona_type=system" \
  --header "x-api-key: $TAVUS_API_KEY"

curl --request GET \
  --url "https://tavusapi.com/v2/replicas?replica_type=system&verbose=true" \
  --header "x-api-key: $TAVUS_API_KEY"
In the personas response, look for persona_id and default_replica_id. In the replicas response, look for replica_id, replica_type, and model_name.
r90bbd427f71 is the stock Anna replica ID used throughout these docs, and pcb7a34da5fe is a stock Sales Development Rep persona ID. They are stock IDs, not placeholder strings, and are available for quickstart use.
For more details, see Stock Replicas, Get Personas, Get Replicas, and Create Conversation.

1. Create the app

Create a Vite React app and install the small server dependencies used in this guide:
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:
{
  "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:
TAVUS_API_KEY=tvsk_your_api_key_here
Copy it to .env locally and fill in your real key:
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.
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 = {
  persona_id?: string;
  replica_id?: string;
  conversation_name?: string;
  test_mode?: boolean;
};

app.post("/api/conversations", async (req, res) => {
  const {
    persona_id,
    replica_id,
    conversation_name = "My first Tavus video chat",
    test_mode = false,
  } = req.body as CreateConversationRequest;

  if (!persona_id && !replica_id) {
    return res
      .status(400)
      .json({ error: "Provide persona_id, replica_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({
      ...(persona_id ? { persona_id } : {}),
      ...(replica_id ? { replica_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. For Daily JS/React or LiveKit guidance, see Embed CVI.
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 [personaId, setPersonaId] = useState("");
  const [replicaId, setReplicaId] = useState("");
  const [testMode, setTestMode] = useState(false);
  const [conversation, setConversation] = useState<ConversationResponse | null>(
    null
  );
  const [error, setError] = useState<string | null>(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({
          ...(personaId ? { persona_id: personaId } : {}),
          ...(replicaId ? { replica_id: replicaId } : {}),
          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 (
    <main style={{ maxWidth: 960, margin: "40px auto", fontFamily: "system-ui" }}>
      <h1>My first Tavus video chat</h1>

      {!conversation ? (
        <section style={{ display: "grid", gap: 12 }}>
          <label>
            Persona ID, optional if you provide a replica ID
            <input
              value={personaId}
              onChange={(event) => setPersonaId(event.target.value)}
              placeholder="p... or use pcb7a34da5fe"
              style={{ display: "block", width: "100%" }}
            />
          </label>

          <label>
            Replica ID, optional if your persona has a default replica
            <input
              value={replicaId}
              onChange={(event) => setReplicaId(event.target.value)}
              placeholder="r... or use r90bbd427f71"
              style={{ display: "block", width: "100%" }}
            />
          </label>

          <label>
            <input
              type="checkbox"
              checked={testMode}
              onChange={(event) => setTestMode(event.target.checked)}
            />
            Create in test mode
          </label>

          <button
            onClick={startConversation}
            disabled={loading || (!personaId && !replicaId)}
          >
            {loading ? "Creating..." : "Start conversation"}
          </button>

          {error ? <p role="alert">{error}</p> : null}
        </section>
      ) : (
        <section style={{ display: "grid", gap: 12 }}>
          <button onClick={endConversation}>End conversation</button>
          <iframe
            title="Tavus conversation"
            src={conversation.conversation_url}
            allow="camera; microphone; fullscreen; display-capture; autoplay"
            style={{
              width: "100%",
              height: 640,
              border: "1px solid #ddd",
              borderRadius: 12,
            }}
          />
        </section>
      )}
    </main>
  );
}
Run both servers:
npm run dev:all
Open http://localhost:5173, enter your persona_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:
{
  "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 replica does not join.

Cleanup

End live conversations when the user leaves, when a test completes, or when your app no longer needs the room:
curl --request POST \
  --url https://tavusapi.com/v2/conversations/<conversation_id>/end \
  --header "x-api-key: $TAVUS_API_KEY"
The sample app calls the same endpoint through your backend route:
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 replica does not join, the response returns status: "ended", and the conversation does not affect billing or concurrency. For live conversations, call End Conversation when the user leaves or your app no longer needs the room. Use 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 persona_id, replica_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 when you want Tavus CVI components instead of a plain iframe.
  • Use Embed CVI for iframe, vanilla JavaScript, and Daily JS patterns.
  • Use customize the conversation UI for Daily Prebuilt styling.
  • Use LiveKit Agent 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 for recording, language, participant limits, private rooms, backgrounds, captions, and timeouts.
  • Point coding agents at Agents & automation for llms.txt, OpenAPI, Agent Skills, and MCP access.