> ## Documentation Index
> Fetch the complete documentation index at: https://docs.slng.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Embed in website

> Add a browser voice session with your agent to a web page using LiveKit and a small backend proxy.

You can let visitors talk to your SLNG voice agent directly in the browser. You need a small backend to create the web session (this keeps your API key off the client) and a frontend that connects to LiveKit for real-time audio.

## Prerequisites

* A configured agent and its agent ID. If you do not have one, see [Create an agent](/guides/get-started/create-a-project/create-agent).
* An SLNG API key. See [How to set up](/guides/get-started/quickstart#create-your-api-key).
* A backend you can deploy server-side code to (Node.js, Deno, Python, etc.)
* A frontend project with React (the examples below use React, but the LiveKit client SDK works with any framework)

## How it works

```mermaid theme={null}
sequenceDiagram
    participant Browser
    participant Backend as Your backend
    participant SLNG as SLNG API
    participant LK as LiveKit room

    Browser->>Backend: POST /api/session
    Backend->>SLNG: POST /v1/agents/{id}/web-sessions
    SLNG-->>Backend: livekit_url + livekit_token
    Backend-->>Browser: livekit_url + livekit_token
    Browser->>LK: Connect (token)
    Browser->>LK: Publish mic audio
    LK-->>Browser: Agent audio + transcripts
```

1. The browser asks your backend to start a session.
2. Your backend calls the SLNG web-sessions endpoint and forwards the LiveKit credentials back.
3. The browser connects to the LiveKit room, publishes the mic, and plays the agent's audio.

## Build the integration

<Steps>
  <Step title="Create a backend endpoint">
    Your backend proxies the SLNG API so the API key never reaches the browser.

    <Note>
      Never call the SLNG API directly from client-side code. Your `SLNG_API_KEY`
      must stay server-side.
    </Note>

    The only call you need is:

    ```
    POST https://api.agents.slng.ai/v1/agents/{agent_id}/web-sessions
    ```

    <CodeGroup>
      ```javascript Node.js (Express) theme={null}
      import express from "express";

      const app = express();
      app.use(express.json());

      const SLNG_API_KEY = process.env.SLNG_API_KEY;
      const SLNG_AGENT_ID = process.env.SLNG_AGENT_ID;

      app.post("/api/session", async (req, res) => {
        const response = await fetch(
          `https://api.agents.slng.ai/v1/agents/${SLNG_AGENT_ID}/web-sessions`,
          {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
              Authorization: `Bearer ${SLNG_API_KEY}`,
            },
            body: JSON.stringify({
              arguments: {},
              ...(req.body.participant_name
                ? { participant_name: req.body.participant_name }
                : {}),
            }),
          }
        );

        const data = await response.json();
        if (!response.ok) {
          return res.status(response.status).json({ error: "SLNG API error", details: data });
        }
        res.json(data);
      });

      app.listen(3001);
      ```

      ```python Python (FastAPI) theme={null}
      import os
      import httpx
      from fastapi import FastAPI
      from pydantic import BaseModel

      app = FastAPI()

      SLNG_API_KEY = os.environ["SLNG_API_KEY"]
      SLNG_AGENT_ID = os.environ["SLNG_AGENT_ID"]

      class SessionRequest(BaseModel):
          participant_name: str | None = None

      @app.post("/api/session")
      async def create_session(body: SessionRequest):
          async with httpx.AsyncClient() as client:
              payload = {"arguments": {}}
              if body.participant_name:
                  payload["participant_name"] = body.participant_name

              r = await client.post(
                  f"https://api.agents.slng.ai/v1/agents/{SLNG_AGENT_ID}/web-sessions",
                  headers={
                      "Content-Type": "application/json",
                      "Authorization": f"Bearer {SLNG_API_KEY}",
                  },
                  json=payload,
              )
          return r.json()
      ```
    </CodeGroup>

    The response includes the fields you need for the frontend:

    ```json Response theme={null}
    {
      "call_id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
      "room_name": "web-session-8f3a2b",
      "livekit_url": "wss://...",
      "livekit_token": "...",
      "max_session_seconds": 300,
      "message": "Web session created successfully"
    }
    ```
  </Step>

  <Step title="Install the LiveKit client SDK">
    `livekit-client` is LiveKit's browser SDK. It opens the WebRTC connection to the room, publishes your microphone, subscribes to the agent's audio, and delivers transcripts over a data channel. It ships TypeScript types and runs in any framework or in plain JavaScript.

    ```bash theme={null}
    npm install livekit-client
    ```

    This guide uses a few of its primitives:

    | Export                           | What it does                                                                                                      |
    | -------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
    | `Room`                           | The connection to a LiveKit room. You create one, connect with the session token, then read its state and events. |
    | `RoomEvent`                      | The events you subscribe to, such as `TrackSubscribed`, `DataReceived`, and `ActiveSpeakersChanged`.              |
    | `createLocalAudioTrack`          | Captures the microphone and returns a track you publish to the room.                                              |
    | `RemoteTrack`, `LocalAudioTrack` | The track objects you attach to the DOM, mute, or stop.                                                           |

    The SDK needs a secure context, so serve your page over HTTPS or `localhost` for the browser to allow microphone capture and WebRTC. For the full API, see the [LiveKit JS client reference](https://docs.livekit.io/reference/client-sdk-js/).
  </Step>

  <Step title="Connect to the LiveKit room">
    Call your backend to get a session, then connect to the room:

    ```tsx theme={null}
    import { Room, RoomEvent, createLocalAudioTrack } from "livekit-client";

    async function startSession() {
      // 1. Get session credentials from your backend
      const res = await fetch("/api/session", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ participant_name: "visitor" }),
      });
      const session = await res.json();

      // 2. Create and connect to the LiveKit room
      const room = new Room({ adaptiveStream: true, dynacast: true });
      await room.connect(session.livekit_url, session.livekit_token);

      // 3. Publish your microphone
      const micTrack = await createLocalAudioTrack();
      await room.localParticipant.publishTrack(micTrack);

      return { room, micTrack, session };
    }
    ```

    <Tip>
      The browser will prompt the user for microphone access on `createLocalAudioTrack()`.
      If your page is not served over HTTPS, most browsers will block the request.
    </Tip>
  </Step>

  <Step title="Play the agent's audio">
    Attach the agent's remote audio track to the DOM so the browser plays it:

    ```tsx theme={null}
    room.on(RoomEvent.TrackSubscribed, (track) => {
      if (track.kind !== "audio") return;
      const el = track.attach();
      el.autoplay = true;
      document.body.appendChild(el);
    });

    room.on(RoomEvent.TrackUnsubscribed, (track) => {
      track.detach().forEach((el) => el.remove());
    });
    ```
  </Step>

  <Step title="Show live transcripts">
    Transcript updates arrive over a LiveKit data channel on the `slng.transcript.v1` topic:

    ```tsx theme={null}
    const TRANSCRIPT_TOPIC = "slng.transcript.v1";

    room.on(RoomEvent.DataReceived, (payload, _participant, _kind, topic) => {
      if (topic !== TRANSCRIPT_TOPIC) return;

      const msg = JSON.parse(new TextDecoder().decode(payload));

      if (msg.type === "conversation_item_added" && msg.item) {
        const { id, role, content, created_at } = msg.item;
        const text = Array.isArray(content) ? content.join("\n") : String(content);
        // Append { id, role, content: text, created_at } to your transcript state
      }
    });
    ```

    Each transcript item has:

    | Field        | Description                                       |
    | ------------ | ------------------------------------------------- |
    | `id`         | Unique message ID (use to deduplicate)            |
    | `role`       | `"user"` or `"assistant"`                         |
    | `content`    | The transcribed text (string or array of strings) |
    | `created_at` | Timestamp                                         |
  </Step>

  <Step title="Add mute and disconnect controls">
    ```tsx theme={null}
    // Toggle mute
    async function toggleMute(micTrack, muted) {
      if (muted) {
        await micTrack.unmute();
      } else {
        await micTrack.mute();
      }
      return !muted;
    }

    // End the session
    async function disconnect(room, micTrack) {
      micTrack.stop();
      await room.disconnect();
    }
    ```
  </Step>

  <Step title="Detect who is speaking">
    The active-speakers event tells you when the agent is talking, so you can drive a visual indicator or avatar animation:

    ```tsx theme={null}
    room.on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
      const agentIsSpeaking = speakers.some(
        (p) => p.identity !== room.localParticipant.identity
      );
      // Update your UI based on agentIsSpeaking
    });
    ```
  </Step>
</Steps>

## Optional: Add a visual persona

A voice-only interface gives users no visual cue about what the agent is doing. An animated persona such as an orb, waveform, or avatar makes the experience feel more responsive. Two ready-made libraries work well here:

<CardGroup cols={2}>
  <Card title="Vercel AI SDK Persona" href="https://elements.ai-sdk.dev/components/persona">
    A React component with built-in states: `idle`, `listening`, `speaking`, `thinking`. Drop it in and map LiveKit events to states.
  </Card>

  <Card title="ElevenLabs Conversational UI" href="https://ui.elevenlabs.io/docs/components">
    Orb and avatar components designed for voice interfaces, with audio-reactive animations.
  </Card>
</CardGroup>

To wire either library up, map your session and LiveKit events to persona states:

```tsx theme={null}
// Derive a persona state from your session + LiveKit events
function getPersonaState({ status, muted, agentIsSpeaking }) {
  if (status === "connecting") return "idle";
  if (status === "ended") return "idle";
  if (agentIsSpeaking) return "speaking";
  if (muted) return "thinking";
  return "listening";
}

// Update on active-speaker changes (Step 7)
room.on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
  const agentIsSpeaking = speakers.some(
    (p) => p.identity !== room.localParticipant.identity
  );
  setPersonaState(getPersonaState({ status, muted, agentIsSpeaking }));
});
```

## Putting it all together

A minimal React component with all the steps above wired together:

```tsx VoiceSession.tsx expandable theme={null}
import { useEffect, useRef, useState, useCallback } from "react";
import {
  Room,
  RoomEvent,
  createLocalAudioTrack,
  type LocalAudioTrack,
  type RemoteTrack,
} from "livekit-client";

const TRANSCRIPT_TOPIC = "slng.transcript.v1";

interface SessionData {
  livekit_url: string;
  livekit_token: string;
  call_id: string;
  max_session_seconds: number;
}

interface TranscriptItem {
  id: string;
  role: "user" | "assistant";
  content: string;
}

export default function VoiceSession() {
  const [status, setStatus] = useState<"idle" | "connecting" | "active" | "ended">("idle");
  const [muted, setMuted] = useState(false);
  const [transcript, setTranscript] = useState<TranscriptItem[]>([]);
  const roomRef = useRef<Room | null>(null);
  const micRef = useRef<LocalAudioTrack | null>(null);

  const start = useCallback(async () => {
    setStatus("connecting");

    // Get session from your backend
    const res = await fetch("/api/session", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({}),
    });
    const session: SessionData = await res.json();

    const room = new Room({ adaptiveStream: true, dynacast: true });
    roomRef.current = room;

    // Play agent audio
    room.on(RoomEvent.TrackSubscribed, (track: RemoteTrack) => {
      if (track.kind !== "audio") return;
      const el = track.attach() as HTMLAudioElement;
      el.autoplay = true;
      document.body.appendChild(el);
    });

    room.on(RoomEvent.TrackUnsubscribed, (track: RemoteTrack) => {
      track.detach().forEach((el) => el.remove());
    });

    // Transcript
    room.on(
      RoomEvent.DataReceived,
      (payload: Uint8Array, _p: unknown, _k: unknown, topic?: string) => {
        if (topic !== TRANSCRIPT_TOPIC) return;
        try {
          const msg = JSON.parse(new TextDecoder().decode(payload));
          if (msg.type === "conversation_item_added" && msg.item) {
            const { id, role, content } = msg.item;
            const text = Array.isArray(content) ? content.join("\n") : String(content);
            setTranscript((prev) =>
              prev.find((t) => t.id === id) ? prev : [...prev, { id, role, content: text }]
            );
          }
        } catch {
          // ignore malformed messages
        }
      }
    );

    room.on(RoomEvent.Disconnected, () => setStatus("ended"));

    await room.connect(session.livekit_url, session.livekit_token);

    const micTrack = await createLocalAudioTrack();
    micRef.current = micTrack;
    await room.localParticipant.publishTrack(micTrack);

    setStatus("active");
  }, []);

  const toggleMute = async () => {
    if (!micRef.current) return;
    muted ? await micRef.current.unmute() : await micRef.current.mute();
    setMuted((m) => !m);
  };

  const disconnect = useCallback(async () => {
    micRef.current?.stop();
    micRef.current = null;
    await roomRef.current?.disconnect();
    roomRef.current = null;
    setStatus("ended");
  }, []);

  // Cleanup on unmount
  useEffect(() => {
    return () => {
      micRef.current?.stop();
      roomRef.current?.disconnect();
    };
  }, []);

  return (
    <div>
      {status === "idle" && <button onClick={start}>Start conversation</button>}
      {status === "connecting" && <p>Connecting…</p>}

      {status === "active" && (
        <div>
          <button onClick={toggleMute}>{muted ? "Unmute" : "Mute"}</button>
          <button onClick={disconnect}>End call</button>
        </div>
      )}

      {status === "ended" && <p>Session ended.</p>}

      <ul>
        {transcript.map((item) => (
          <li key={item.id}>
            <strong>{item.role}:</strong> {item.content}
          </li>
        ))}
      </ul>
    </div>
  );
}
```

## Next steps

* Configure your agent's prompt and tools in [Create an agent](/guides/get-started/create-a-project/create-agent).
* Add phone call support with [Telephony](/guides/agents/telephony/overview).
* Watch your sessions in [Observability and usage](/guides/usage-and-billing).
* Add a visual persona with [Vercel AI SDK](https://elements.ai-sdk.dev/components/persona) or [ElevenLabs UI](https://ui.elevenlabs.io/docs/components).
