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

# React Frontend SDK

> Connect your React application to Layercode agents and build web and mobile voice AI applications.

<Icon icon="github" size={18} color="black" /> [layercode-react-sdk](https://github.com/layercodedev/layercode-react-sdk).

## useLayercodeAgent Hook

The `useLayercodeAgent` hook provides a simple way to connect your React app to a Layercode agent, handling audio streaming, playback, and real-time communication.

<RequestExample>
  ```typescript useLayercodeAgent Hook theme={null}
  import { useEffect } from "react";
  import { useLayercodeAgent } from "@layercode/react-sdk";

  // Connect to a Layercode agent
  const {
    // Methods
    connect,
    disconnect,
    triggerUserTurnStarted,
    triggerUserTurnFinished,
    sendClientResponseText,
    sendClientResponseData,
    setAudioInput,

    // State
    status,
    audioInput,
    userAudioAmplitude,
    agentAudioAmplitude,
  } = useLayercodeAgent({
    agentId: "your-agent-id",
    authorizeSessionEndpoint: "/api/authorize",
    conversationId: "optional-conversation-id", // optional
    metadata: { userId: "user-123" }, // optional
    onAudioInputChanged: (enabled) => {
      console.log("Microphone enabled?", enabled);
    },
    onAgentSpeakingChange: (isSpeaking) => console.log("Agent speaking:", isSpeaking),
    onUserSpeakingChange: (isSpeaking) => console.log("User speaking:", isSpeaking),
    onConnect: ({ conversationId, config }) => {
      console.log("Connected to agent", conversationId);
      console.log("Agent config", config);
    },
    onDisconnect: () => console.log("Disconnected from agent"),
    onError: (error) => console.error("Agent error:", error),
    onDataMessage: (data) => console.log("Received data:", data),
  });

  useEffect(() => {
    connect();

    return () => {
      disconnect();
    };
  }, [connect, disconnect]);
  ```
</RequestExample>

Call `connect()` to start the session; call `disconnect()` to cleanly close it when the component unmounts or you no longer need the agent connection.

## Hook Options

<ParamField path="agentId" type="string" required>
  The ID of your Layercode agent.
</ParamField>

<ParamField path="authorizeSessionEndpoint" type="string" required>
  Your backend endpoint that authorizes a client session and returns a `client_session_key` and `conversation_id`.
</ParamField>

<ParamField path="conversationId" type="string">
  The conversation ID to resume a previous conversation (optional).
</ParamField>

<ParamField path="metadata" type="object">
  Any metadata included here will be passed along to your backend with all Layercode webhooks for this session.
</ParamField>

<ParamField path="audioInput" type="boolean">
  Whether the browser microphone should be initialized immediately. Defaults to `true`. Set to `false` to keep the SDK in text-only mode until you explicitly enable audio input.
</ParamField>

<ParamField path="audioOutput" type="boolean">
  Whether agent audio should start playing immediately. Defaults to `true`. Set to `false` to suppress speaker playback until you explicitly enable it (the agent connection still runs in the background).
</ParamField>

<ParamField path="enableAmplitudeMonitoring" type="boolean">
  Whether microphone and speaker amplitude monitoring should run. Defaults to `true`. Disable this when you start with `audioInput: false` to avoid unnecessary audio processing. When disabled, amplitude readings remain `0`.
</ParamField>

<ParamField path="enableVAD" type="boolean">
  Whether to initialize the client-side VAD model. Defaults to `true`. Set to `false` to skip loading the VAD model, reducing CPU/memory usage at the cost of local speech detection.
</ParamField>

<ParamField path="onAudioInputChanged" type="function">
  Callback fired whenever the audio input state toggles. Receives a boolean (`true` when the mic is active).
</ParamField>

<ParamField path="onAudioOutputChanged" type="function">
  Callback fired whenever the audio output state toggles. Receives a boolean (`true` when the agent audio is audible).
</ParamField>

<ParamField path="onConnect" type="function">
  Callback when the connection is established. Receives `{ conversationId: string | null; config?: AgentConfig }`.
  Use `config` to inspect the effective agent configuration returned from `authorizeSessionEndpoint`.
</ParamField>

<ParamField path="onDisconnect" type="function">
  Callback when the connection is closed.
</ParamField>

<ParamField path="onError" type="function">
  Callback when an error occurs. Receives an `Error` object.
</ParamField>

<ParamField path="onDataMessage" type="function">
  Callback for custom data messages from the server (see `response.data` events from your backend).
</ParamField>

<ParamField path="onUserSpeakingChange" type="function">
  Callback when VAD detects that the user started or stopped speaking. Receives a boolean.
</ParamField>

<ParamField path="onAgentSpeakingChange" type="function">
  Callback when the agent starts or stops speaking. Receives a boolean.
</ParamField>

<ParamField path="onMuteStateChange" type="function">
  Callback when `mute()`/`unmute()` are invoked. Receives a boolean for the new muted state.
</ParamField>

### Client-side VAD (enableVAD)

When `enableVAD` is set to `false`, the client skips loading and initializing the VAD (Voice Activity Detection) model. This reduces client-side CPU/memory usage, but the client will not detect speech locally.

**When to use it**

Only disable client-side VAD when your transcription model includes built-in turn-taking (for example, Deepgram Flux) and you rely entirely on server-side speech detection. Disabling VAD otherwise removes local speech detection.

* **Performance optimization**: Reduce bundle size and CPU usage on low-powered devices.
* **Custom VAD**: You implement your own speech detection logic.

**Behavior**

* **When true (default)**: The VAD model loads on `connect()`, detects speech locally, and sends `vad_start`/`vad_end` events.
* **When false**: The VAD model is not loaded, local speech detection is disabled, and you avoid the \~2MB VAD bundle overhead.

```tsx theme={null}
const { connect, disconnect } = useLayercodeAgent({
  agentId: "your-agent-id",
  authorizeSessionEndpoint: "/api/authorize",
  enableVAD: false, // Disable client-side VAD
});
```

## Return Values

The `useLayercodeAgent` hook returns an object with the following properties:

### State

<ParamField path="status" type="string">
  The connection status. One of `"initializing"`, `"disconnected"`, `"connecting"`, `"connected"`, or `"error"`. The hook begins in `"initializing"` before the client reports a lifecycle status.
</ParamField>

<ParamField path="userAudioAmplitude" type="number">
  Real-time amplitude of the user's microphone input (0-1). Useful for animating UI when the user is speaking.
</ParamField>

<ParamField path="agentAudioAmplitude" type="number">
  Real-time amplitude of the agent's audio output (0-1). Useful for animating UI when the agent is speaking.
</ParamField>

<ParamField path="audioInput" type="boolean">
  Current audio input state. `false` means the SDK has not requested microphone access.
</ParamField>

<ParamField path="audioOutput" type="boolean">
  Current audio output state. `false` means agent audio playback is muted locally.
</ParamField>

<ParamField path="userSpeaking" type="boolean">
  Whether the user is currently detected as speaking by VAD.
</ParamField>

<ParamField path="agentSpeaking" type="boolean">
  Whether the agent is currently speaking (based on active audio playback).
</ParamField>

<ParamField path="isMuted" type="boolean">
  Whether the local microphone stream is muted.
</ParamField>

<ParamField path="conversationId" type="string">
  The conversation ID the hook is currently connected to, or `null` when none is active. Useful for resuming or persisting sessions.
</ParamField>

### Turn-taking (Push-to-Talk)

Layercode supports both automatic and push-to-talk turn-taking. For push-to-talk, use these methods to signal when the user starts and stops speaking:

Automatic versus push-to-talk behavior is configured on the backend via `AgentConfig.transcription.trigger` and `AgentConfig.transcription.can_interrupt`. Push-to-talk skips the VAD model because you drive turn boundaries manually.

<ParamField path="triggerUserTurnStarted" type="function">
  **triggerUserTurnStarted(): void** Signals that the user has started speaking (for [push-to-talk mode](/explanations/turn-taking#push-to-talk-mode)). Interrupts any agent audio
  playback.
</ParamField>

<ParamField path="triggerUserTurnFinished" type="function">
  **triggerUserTurnFinished(): void** Signals that the user has finished speaking (for [push-to-talk mode](/explanations/turn-taking#push-to-talk-mode)).
</ParamField>

### Audio Input Controls

<ParamField path="setAudioInput" type="function">
  **setAudioInput(next: boolean | ((prev: boolean) => boolean)): void** Enables or disables microphone capture without recreating the client. Use this to defer the browser permission prompt until the user switches into voice mode.
</ParamField>

### Agent Audio Output Controls

<ParamField path="setAudioOutput" type="function">
  **setAudioOutput(next: boolean | ((prev: boolean) => boolean)): void** Enables or disables local playback of agent audio without disconnecting from the session.
</ParamField>

> Need to hide agent audio temporarily (for example, when playing other media in your UI)? Toggle `setAudioOutput(false)` while the session stays active, and re-enable with `setAudioOutput(true)` once you want to hear the agent again. Combine this with the `audioOutput` state or the `onAudioOutputChanged` callback to drive your UI controls.

### Audio / Mic Controls

<ParamField path="mute" type="function">
  **mute(): void** Stops sending microphone audio to the server while keeping the connection active.
</ParamField>

<ParamField path="unmute" type="function">
  **unmute(): void** Resumes sending microphone audio after a local mute.
</ParamField>

### Text messages

Use this method when the user submits a chat-style message instead of speaking.

<ParamField path="sendClientResponseText" type="function">
  **sendClientResponseText(text: string): void** Sends a `client.response.text` to the server and interrupts any agent audio playback. The server emits `user.transcript` and manages turn boundaries; the client does not send `trigger.turn.end` here.
</ParamField>

<ParamField path="sendClientResponseData" type="function">
  **sendClientResponseData(payload: Record\<string, any>): void** Sends a JSON-serializable `payload` to your agent backend without affecting the current turn. The data surfaces as a `data` webhook event. See docs page: [Send JSON data from the client](/how-tos/send-json-data).
</ParamField>

## Notes & Best Practices

* The hook manages microphone access, audio streaming, and playback automatically.
* Start text-first experiences with `audioInput: false` and call `setAudioInput(true)` when the user explicitly opts into voice. Pair this with `enableAmplitudeMonitoring: false` to skip microphone metering until voice is enabled.
* The `metadata` option allows you to set custom data which is then passed to your backend webhooks for this session (useful for user/session tracking).
* The `conversationId` can be used to resume a previous conversation, or omitted to start a new one. The hook will report `null` until it connects.

## Authorizing Sessions

To connect a client (browser) to your Layercode voice agent, you must first authorize the session. The SDK will automatically send a POST request to the path (or url if your backend is on a different domain) passed in the `authorizeSessionEndpoint` option. In this endpoint, you will need to call the Layercode REST API to generate a `client_session_key` and `conversation_id` (if it's a new conversation).

<Info>If your backend is on a different domain, set `authorizeSessionEndpoint` to the full URL (e.g., `https://your-backend.com/api/authorize`).</Info>

**Why is this required?**
Your Layercode API key should never be exposed to the frontend. Instead, your backend acts as a secure proxy: it receives the frontend's request, then calls the Layercode authorization API using your secret API key, and finally returns the `client_session_key` to the frontend.

This also allows you to authenticate your user, and set any additional metadata that you want passed to your backend webhook.

**How it works:**

1. **Frontend:**
   The SDK automatically sends a POST request to your `authorizeSessionEndpoint` with a request body.

2. **Your Backend:**
   Your backend receives this request, then makes a POST request to the Layercode REST API `/v1/agents/web/authorize_session` endpoint, including your `LAYERCODE_API_KEY` as a Bearer token in the headers.

3. **Layercode:**
   Layercode responds with a `client_session_key` (and a `conversation_id`), which your backend returns to the frontend.

4. **Frontend:**
   The SDK uses the `client_session_key` to establish a secure WebSocket connection to Layercode.

**Example backend authorization endpoint code:**

<CodeGroup>
  ```ts Next.js app/api/authorize/route.ts [expandable] theme={null}
  export const dynamic = "force-dynamic";
  import { NextResponse } from "next/server";

  export const POST = async (request: Request) => {
    // Here you could do any user authorization checks you need for your app
    const endpoint = "https://api.layercode.com/v1/agents/web/authorize_session";
    const apiKey = process.env.LAYERCODE_API_KEY;
    if (!apiKey) {
      throw new Error("LAYERCODE_API_KEY is not set.");
    }
    const requestBody = await request.json();
    if (!requestBody || !requestBody.agent_id) {
      throw new Error("Missing agent_id in request body.");
    }
    try {
      const response = await fetch(endpoint, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${apiKey}`,
        },
        body: JSON.stringify(requestBody),
      });
      if (!response.ok) {
        const text = await response.text();
        throw new Error(text || response.statusText);
      }
      return NextResponse.json(await response.json());
    } catch (error: any) {
      console.log("Layercode authorize session response error:", error.message);
      return NextResponse.json({ error: error.message }, { status: 500 });
    }
  };
  ```

  ```ts Hono theme={null}
  import { Context } from 'hono';
  import { env } from 'cloudflare:workers';

  export const onRequestPost = async (c: Context) => {
    try {
      const response = await fetch("https://api.layercode.com/v1/agents/web/authorize_session", {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${env.LAYERCODE_API_KEY}`,
        },
        body: JSON.stringify({ agent_id: "your-agent-id", conversation_id: null }),
      });
      if (!response.ok) {
        console.log('response not ok', response.statusText);
        return c.json({ error: response.statusText });
      }
      const data: { client_session_key: string; conversation_id: string; config?: Record<string, any> } = await response.json();
      return c.json(data);
    } catch (error) {
      return c.json({ error: error });
    }
  };
  ```

  ```ts ExpressJS theme={null}
  import type { RequestHandler } from 'express';

  export const onRequestPost: RequestHandler = async (req, res) => {
    try {
      const response = await fetch("https://api.layercode.com/v1/agents/web/authorize_session", {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${process.env.LAYERCODE_API_KEY}`,
        },
        body: JSON.stringify({ agent_id: "your-agent-id", conversation_id: null }),
      });
      if (!response.ok) {
        console.log('response not ok', response.statusText);
        return res.status(500).json({ error: response.statusText });
      }
      const data: { client_session_key: string; conversation_id: string; config?: Record<string, any> } = await response.json();
      res.json(data);
    } catch (error) {
      res.status(500).json({ error: (error as Error).message });
    }
  };
  ```

  ```python Python theme={null}
  import os
  import httpx
  from fastapi.responses import JSONResponse


  @app.post("/authorize")
  async def authorize_endpoint(request: Request):
      api_key = os.getenv("LAYERCODE_API_KEY")
      if not api_key:
          return JSONResponse({"error": "LAYERCODE_API_KEY is not set."}, status_code=500)
      try:
          body = await request.json()
      except Exception:
          return JSONResponse({"error": "Invalid JSON body."}, status_code=400)
      if not body or not body.get("agent_id"):
          return JSONResponse({"error": "Missing agent_id in request body."}, status_code=400)
      endpoint = "https://api.layercode.com/v1/agents/web/authorize_session"
      try:
          async with httpx.AsyncClient() as client:
              response = await client.post(
                  endpoint,
                  headers={
                      "Content-Type": "application/json",
                      "Authorization": f"Bearer {api_key}",
                  },
                  json=body,
              )
          if response.status_code != 200:
              return JSONResponse({"error": response.text}, status_code=500)
          return JSONResponse(response.json())
      except Exception as error:
          print("Layercode authorize session response error:", str(error))
          return JSONResponse({"error": str(error)}, status_code=500)
  ```
</CodeGroup>

### Custom Authorization

`useLayercodeAgent` exposes the same `authorizeSessionRequest` option as the vanilla SDK. Provide this function to inject custom headers, cookies, or a different HTTP client when calling your backend.

```tsx theme={null}
import { useMemo } from "react";
import { useLayercodeAgent } from "@layercode/react-sdk";

export function AgentWidget() {
  const authorizeSessionRequest = useMemo(
    () => async ({ url, body }) => {
      const response = await fetch(url, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "X-Custom-Header": "my-header",
        },
        body: JSON.stringify(body),
      });

      if (!response.ok) {
        throw new Error(`Authorization failed: ${response.statusText}`);
      }

      return response;
    },
    [],
  );

  const { connect, disconnect, status } = useLayercodeAgent({
    agentId: "agent_123",
    authorizeSessionEndpoint: "https://example.com/authorize-session",
    authorizeSessionRequest,
  });

  // .. Render your voice agent here
}
```

If `authorizeSessionRequest` is not supplied, the hook defaults to a standard `fetch` call that POSTs the JSON body to `authorizeSessionEndpoint` using `Content-Type: application/json`.

#### Request payload

The SDK sends these fields in the authorization request body:

* `agent_id` – ID of the agent to connect.
* `metadata` – metadata supplied when creating the hook.
* `sdk_version` – version string of the React SDK (for example, `"2.2.1"`).
* `conversation_id` – present only when reconnecting to an existing conversation.

#### Response payload

Your endpoint must respond with JSON containing these fields:

```json response theme={null}
{
  "client_session_key": "cs_abc123",
  "conversation_id": "conv_456"
}
```

## Troubleshooting

### AudioWorklet InvalidStateError on first connect

If the browser complains that `AudioWorklet does not have a valid AudioWorkletGlobalScope`, the initial `connect()` probably ran before the user interacted with the page. Browsers block new `AudioContext` instances (and therefore `audioWorklet.addModule()`) until they detect a click, tap, or key press.

To resolve it:

* Trigger the first `connect()` from a user gesture—e.g., a “Start voice agent” button that awaits `connect()`.
* Keep `disconnect()` inside an effect cleanup so teardown still runs automatically.
* Expect the issue mostly during rapid development reloads or in React Strict Mode, where components mount twice. In production, users typically gesture before launching the agent, so reconnects after the initial gesture are safe.

Once the worklet loads successfully, you can auto-reconnect during the same session without another gesture. The requirement only applies to the very first call.
