> ## 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.

# Vanilla JS Frontend SDK

> API reference for the Layercode Vanilla JS Frontend SDK.

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

## LayercodeClient

The `LayercodeClient` is the core client for all JavaScript frontend SDKs, providing audio recording, playback, and real-time communication with the Layercode agent.

<RequestExample>
  ```javascript theme={null}
  import LayercodeClient from "https://cdn.jsdelivr.net/npm/@layercode/js-sdk@latest/dist/layercode-js-sdk.esm.js";

  window.layercode = new LayercodeClient({
    agentId: "your-agent-id",
    conversationId: "your-conversation-id", // optional
    authorizeSessionEndpoint: "/api/authorize",
    metadata: { userId: "123" }, // optional

    // Connection lifecycle
    onConnect: ({ conversationId, config }) => {
      console.log("connected", conversationId);
      console.log("agent config", config);
    },
    onDisconnect: () => console.log("disconnected"),
    onError: (err) => console.error("error", err),
    onStatusChange: (status) => console.log("status", status),

    // Messaging & events
    onMessage: (msg) => console.log("message", msg),           // non-audio messages
    onDataMessage: (msg) => console.log("data message", msg),  // response.data messages

    // Audio meters & speaking state
    onUserAmplitudeChange: (amp) => console.log("user amplitude", amp),
    onAgentAmplitudeChange: (amp) => console.log("agent amplitude", amp),
    audioInputChanged: (enabled) => console.log("mic enabled", enabled),
    onUserIsSpeakingChange: (speaking) => console.log("user speaking?", speaking),
    onAgentSpeakingChange: (speaking) => console.log("agent speaking?", speaking),

    // Device & mute state
    onDeviceSwitched: (id) => console.log("active input device", id),
    onDevicesChanged: (list) => console.log("devices changed", list),
    onMuteStateChange: (muted) => console.log("muted?", muted),

    enableAmplitudeMonitoring: false, // skip metering while audio is disabled
  });

  window.layercode.connect();
  ```
</RequestExample>

### Usage Example

Call `connect()` to start the session once the user is ready, and invoke `disconnect()` when you want to tear the connection down.

## Constructor Options

<ParamField path="options" type="object" required>
  Options for the LayercodeClient.
</ParamField>

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

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

<ParamField path="options.authorizeSessionEndpoint" type="string" required>
  The endpoint to authorize the session. It must return a JSON object:
  <code>{`{ client_session_key: string, conversation_id: string, config?: AgentConfig }`}</code>.
</ParamField>

<ParamField path="options.metadata" type="object">
  Optional metadata to send with the session authorization request.
</ParamField>

<ParamField path="options.audioInput" type="boolean">
  Whether microphone capture should start immediately. Defaults to <code>true</code>. Set to <code>false</code> to initialize the client in text-only mode and defer the permission prompt.
</ParamField>

<ParamField path="options.audioOutput" type="boolean">
  Whether agent audio should play through the browser immediately. Defaults to <code>true</code>. Set to <code>false</code> to keep the connection active while holding back speaker playback until you opt in.
</ParamField>

<ParamField path="options.enableAmplitudeMonitoring" type="boolean">
  Whether microphone and speaker amplitude monitoring should run. Defaults to <code>true</code>. Disable this when <code>audioInput</code> starts as <code>false</code> to avoid unnecessary audio processing.
</ParamField>

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

<ParamField path="options.vadResumeDelay" type="number">
  Milliseconds before resuming agent audio after a temporary pause due to a false interruption. Defaults to <code>500</code>.
</ParamField>

<ParamField path="options.audioInputChanged" type="function">
  Callback triggered when the audio input state changes. Receives a boolean.
</ParamField>

<ParamField path="options.audioOutputChanged" type="function">
  Callback triggered when the audio output state changes. Receives a boolean indicating whether the agent audio is audible.
</ParamField>

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

<ParamField path="options.onDisconnect" type="function">
  Callback when the client disconnects.
</ParamField>

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

<ParamField path="options.onMessage" type="function">
  Callback for all non-audio messages from the server (excludes <code>response.audio</code>).
</ParamField>

<ParamField path="options.onDataMessage" type="function">
  Callback for custom data messages from the server (typically <code>response.data</code> events).
</ParamField>

<ParamField path="options.onUserAmplitudeChange" type="function">
  Callback for changes in the user's microphone amplitude (number, 0–1).
</ParamField>

<ParamField path="options.onAgentAmplitudeChange" type="function">
  Callback for changes in the agent's audio amplitude (number, 0–1).
</ParamField>

<ParamField path="options.onStatusChange" type="function">
  Callback when the client's status changes. Receives a string: <code>"disconnected" | "connecting" | "connected" | "error"</code>.
</ParamField>

<ParamField path="options.onUserIsSpeakingChange" type="function">
  Callback when the SDK detects user speech start/stop. Receives a boolean.
</ParamField>

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

<ParamField path="options.onMuteStateChange" type="function">
  Callback when the client is muted or unmuted. Receives a boolean representing the new muted state.
</ParamField>

<ParamField path="options.onDeviceSwitched" type="function">
  Callback when the active input device changes in the browser. Receives the active deviceId.
</ParamField>

<ParamField path="options.onDevicesChanged" type="function">
  Callback when available input devices change (hot-plug).
  Receives <code>{`Array<MediaDeviceInfo & { default: boolean }>`}</code>.
</ParamField>

### Client-side VAD (enableVAD)

When <code>enableVAD</code> is set to <code>false</code>, 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 <code>connect()</code>, detects speech locally, and sends <code>vad\_start</code>/<code>vad\_end</code> events.
* **When false**: The VAD model is not loaded, local speech detection is disabled, and you avoid the \~2MB VAD bundle overhead.

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

### Methods

<ParamField path="connect" type="function" required>
  **connect(): Promise\<void>** Connects to the Layercode agent, authorizes the session, and starts audio capture and playback.
</ParamField>

<ParamField path="disconnect" type="function" required>
  **disconnect(): Promise\<void>** Disconnects from the Layercode agent, stops audio capture and playback, and closes the WebSocket.
</ParamField>

<ParamField path="setAudioInput" type="function">
  **setAudioInput(state: boolean): Promise\<void>** Enables or disables microphone capture without tearing down the WebSocket. Pass <code>true</code> when the user opts into voice mode; pass <code>false</code> to drop back to text-only mode.
</ParamField>

<ParamField path="setAudioOutput" type="function">
  **setAudioOutput(state: boolean): Promise\<void>** Enables or disables local agent playback without interrupting the active session.
</ParamField>

> Tip: Call `setAudioOutput(false)` to silence agent audio while another clip plays in your UI, then restore playback with `setAudioOutput(true)` when you're ready. Pair this with the `audioOutputChanged` callback to keep toggles in sync.

<ParamField path="setInputDevice" type="function">
  **setInputDevice(deviceId: string): Promise\<void>** Switches the microphone input. Pass <code>'default'</code> (or an empty string) to use the system default device.
</ParamField>

<ParamField path="listDevices" type="function">
  **{`listDevices(): Promise<Array<MediaDeviceInfo & { default: boolean }>>`}** Returns available input devices, marking the default one.
</ParamField>

<ParamField path="getStream" type="function">
  **getStream(): MediaStream | null** Returns the active microphone <code>MediaStream</code>, or <code>null</code> if not initialized.
</ParamField>

<ParamField path="mute" type="function">
  **mute(): void** Stop sending mic audio to the server without tearing down the stream/connection.
</ParamField>

<ParamField path="unmute" type="function">
  **unmute(): void** Resume sending mic audio to the server.
</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:

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

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

### Text messages

<ParamField path="sendClientResponseText" type="function">
  **sendClientResponseText(text: string): Promise\<void>** Ends the active user turn, interrupts agent playback, and forwards <code>text</code> to the agent. The server will emit <code>user.transcript</code> before the agent responds, keeping UI components in sync.
</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>

## Events & Callbacks

### Properties

<ParamField path="status" type="string">
  Connection status: <code>"disconnected" | "connecting" | "connected" | "error"</code>.
</ParamField>

<ParamField path="audioInputEnabled" type="boolean">
  Indicates whether microphone capture is currently enabled.
</ParamField>

<ParamField path="audioOutputEnabled" type="boolean">
  Indicates whether agent audio playback is currently enabled for this client.
</ParamField>

<ParamField path="userSpeaking" type="boolean">
  Whether the voice activity detector currently hears the user speaking.
</ParamField>

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

<ParamField path="userAudioAmplitude" type="number">
  User mic amplitude (0–1). Non-zero only when amplitude monitoring is enabled.
</ParamField>

<ParamField path="agentAudioAmplitude" type="number">
  Agent playback amplitude (0–1). Non-zero only when amplitude monitoring is enabled.
</ParamField>

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

<ParamField path="conversationId" type="string">
  The conversation ID the client is currently attached to, if any.
</ParamField>

### Agent Config (from authorizeSessionEndpoint)

On `onConnect`, you receive `{ conversationId, config }`. Relevant fields:

```ts theme={null}
type AgentConfig = {
  transcription: {
    trigger: 'push_to_talk' | 'automatic';
    can_interrupt: boolean;
  };
  vad?: {
    enabled?: boolean;
    gate_audio?: boolean;            // default: true
    buffer_frames?: number;          // default: 10
    model?: string;                  // default: 'v5'
    positive_speech_threshold?: number; // default: 0.15
    negative_speech_threshold?: number; // default: 0.05
    redemption_frames?: number;      // default: 4
    min_speech_frames?: number;      // default: 2
    pre_speech_pad_frames?: number;  // default: 0
    frame_samples?: number;          // default: 512
  };
};
```

When `transcription.trigger === 'automatic'` and `vad.enabled !== false`, the SDK initializes MicVAD and gates mic audio accordingly.
`gate_audio` controls whether audio is sent only while speaking; `buffer_frames` controls the “pre-speech” buffer flushed when speech starts.

### Notes & Best Practices

* The SDK manages microphone access, audio streaming, and playback automatically.
* Use `audioInput: false` plus `setAudioInput(true)` to defer the browser permission prompt until the user explicitly switches to voice. Disable amplitude monitoring at the same time to avoid unnecessary processing.
* The `metadata` option allows you to set custom data which is then passed to your backend webhook (useful for user/session tracking).
* The `conversationId` can be used to resume a previous conversation, or omitted to start a new one.

### Small device example

```ts theme={null}
await layercode.setInputDevice('default');
const devices = await layercode.listDevices();
const usb = devices.find(d => /USB/i.test(d.label));
if (usb) await layercode.setInputDevice(usb.deviceId);
```

## 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

Use the optional `authorizeSessionRequest` function when you need to control how authorization credentials are exchanged with your backend (for example, to add custom headers or reuse an existing HTTP client).

```ts Custom Authorization example theme={null}
import LayercodeClient from "@layercode/js-sdk";

const client = new LayercodeClient({
  agentId: "agent_123",
  authorizeSessionEndpoint: "/api/authorize",
  authorizeSessionRequest: 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;
  },
});
```

If you omit `authorizeSessionRequest`, the client falls back to a standard `fetch` call that POSTs the JSON body to `authorizeSessionEndpoint`.

#### Request payload

* `agent_id` – ID of the agent to connect.
* `metadata` – metadata supplied when instantiating the client.
* `sdk_version` – version string of the JavaScript SDK.
* `conversation_id` – present only when reconnecting to an existing conversation.

## Troubleshooting

### AudioWorklet InvalidStateError on first connect

Browsers freeze a freshly created `AudioContext` until the user interacts with the page. If your app calls `connect()` during component initialization, `audioWorklet.addModule()` pauses, the worklet never registers, and the player throws `InvalidStateError: AudioWorklet does not have a valid AudioWorkletGlobalScope` as soon as audio starts streaming.

To avoid the race:

* Gate the first `connect()` behind a user gesture (click, tap, key press)—for example, a “Start voice agent” button.
* Await `connect()` inside that handler; keep any teardown logic (like `disconnect()`) in lifecycle cleanup.
* Expect this to show up most during rapid local reloads or with React Strict Mode double-mounting. In real usage, the user normally clicks before the agent activates, so reconnects after the first gesture are safe.

Once the audio worklet loads successfully the first time, subsequent reconnects can happen automatically within the same session. Just ensure the initial call happens after a gesture.
