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

# Add to Website

> Add your Ringlyra voice agent to any website so visitors can talk to it.

### How to add it

Add your voice agent to any website using the Configure Widget dialog in your agent's settings.

Step 1: Open the agent settings by clicking the gear icon in the top-right of the agent editor.

<img src="https://mintcdn.com/ringlyra/tgVXsYGwUs81-_3r/images/open-settings.png?fit=max&auto=format&n=tgVXsYGwUs81-_3r&q=85&s=42ef5f84cf5f8af56cd59f1b49371d66" alt="Open agent settings" width="2880" height="1557" data-path="images/open-settings.png" />

Step 2: Scroll to the **Add to Website** section and click **Configure Widget**.

<img src="https://mintcdn.com/ringlyra/tgVXsYGwUs81-_3r/images/add-to-website.png?fit=max&auto=format&n=tgVXsYGwUs81-_3r&q=85&s=388ee7d1b1d69325c30504cfed222704" alt="Go to Add to Website" width="2850" height="1558" data-path="images/add-to-website.png" />

Step 3: Enable embedding, add your website's domain to **Allowed Domains**, choose **Floating Widget**, **Inline Component**, or **Headless (Bring Your Own UI)**, customize the button (position, color, text) if applicable, and click **Save Configurations**.

<img src="https://mintcdn.com/ringlyra/tgVXsYGwUs81-_3r/images/save-configurations.png?fit=max&auto=format&n=tgVXsYGwUs81-_3r&q=85&s=e4e80e169f97adba45cc0177cd754fd5" alt="Save configurations" width="1974" height="1534" data-path="images/save-configurations.png" />

Step 4: Copy the generated embed code and paste it into your web page to test your agent.

<img src="https://mintcdn.com/ringlyra/tgVXsYGwUs81-_3r/images/copy-deployment-code.png?fit=max&auto=format&n=tgVXsYGwUs81-_3r&q=85&s=975dc207e03fbc1d62b5136a9e7d048c" alt="Copy deployment code" width="2880" height="1537" data-path="images/copy-deployment-code.png" />

## Embed modes

| Mode                 | What it renders                                                                               | When to use                                                                                       |
| -------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| **Floating Widget**  | A pill-shaped CTA button anchored to a corner of the page.                                    | You want a turn-key chat-bubble experience that doesn't disturb your existing layout.             |
| **Inline Component** | A panel rendered inside a `<div id="ringlyra-inline-container">` that you place in your page. | You want the agent embedded in a specific section (landing-page hero, support tab, etc.).         |
| **Headless**         | No UI. Only the audio pipeline plus a JavaScript API on `window.RinglyraWidget`.              | You want full control over the UI — your own buttons, design system, framework state, animations. |

## Prerequisites

These apply to all three modes:

* Serve your page over **HTTPS** or from `http://localhost`. Browsers refuse microphone access on plain HTTP origins or `file://`.
* If you set **Allowed Domains** in the dashboard, include your test origin (e.g. `localhost`) — otherwise the widget's config and signaling requests are rejected. Leave the list empty to allow all domains.
* The embed snippet you copy from the dashboard is a single `<script>` tag that loads `ringlyra-widget.js` **asynchronously**. The widget auto-initializes once it loads and exposes `window.RinglyraWidget`. Code that registers callbacks must wait for the widget to be available.

## Floating Widget

<img src="https://mintcdn.com/ringlyra/tgVXsYGwUs81-_3r/images/floating-widget-example.png?fit=max&auto=format&n=tgVXsYGwUs81-_3r&q=85&s=632d87ba85537fe60a87cd642463b19a" alt="Floating widget shown in the corner of a host page" width="2880" height="1555" data-path="images/floating-widget-example.png" />

Renders a pill-shaped button (microphone icon + text) anchored to a corner of the page. Clicking it starts a call; clicking again ends it. The button auto-updates its label and color across the call lifecycle: configured text → "Connecting…" → "End Call" → "Retry" on failure.

Configure **Button Text**, **Button Color**, and **Position** (top/bottom + left/right) from the dashboard.

The host page writes no JavaScript — pasting the embed snippet is the entire integration. If you want to subscribe to call lifecycle events (e.g. analytics), see [Lifecycle callbacks](#lifecycle-callbacks-all-modes) below

## Inline Component

<img src="https://mintcdn.com/ringlyra/tgVXsYGwUs81-_3r/images/inline-widget-example.png?fit=max&auto=format&n=tgVXsYGwUs81-_3r&q=85&s=30b2ebf31d4b170d3da3ef9d0ded2930" alt="Inline widget rendered inside a page section" width="2844" height="1555" data-path="images/inline-widget-example.png" />

Renders a panel (status icon + status text + CTA button) inside a `<div>` you place in your page. Status changes update the panel in place.

Configure **Button Text**, **Button Color**, and **Call to Action Text** from the dashboard.

### Plain HTML

Place a container `<div>` where you want the widget to render. The widget auto-attaches to it.

```html theme={null}
<!-- Paste the ringlyra embed snippet from the dashboard somewhere on the page -->
<div id="ringlyra-inline-container"></div>
```

### React

Because React mounts after the widget script may have already loaded, integrate via `initInline` on first mount and `refresh` on remount. Poll for `window.RinglyraWidget` to handle the async script load.

```tsx theme={null}
import { useEffect } from 'react';

declare global {
  interface Window {
    RinglyraWidget?: {
      initInline: (options: { container: HTMLElement }) => void;
      refresh: () => void;
      getState: () => { isInitialized: boolean };
    };
  }
}

export function Assistant() {
  useEffect(() => {
    let retries = 0;
    const tryInit = () => {
      const container = document.getElementById('ringlyra-inline-container');
      if (window.RinglyraWidget && container) {
        const { isInitialized } = window.RinglyraWidget.getState();
        if (isInitialized) window.RinglyraWidget.refresh();
        else window.RinglyraWidget.initInline({ container });
      } else if (retries++ < 50) {
        setTimeout(tryInit, 100);
      }
    };
    tryInit();
  }, []);

  return <div id="ringlyra-inline-container" />;
}
```

## Headless Mode

<img src="https://mintcdn.com/ringlyra/tgVXsYGwUs81-_3r/images/headless-widget-example.png?fit=max&auto=format&n=tgVXsYGwUs81-_3r&q=85&s=253ce7f5290b94e61939e7c4178979a2" alt="Headless widget driven by host-page UI" width="2842" height="1548" data-path="images/headless-widget-example.png" />

In Headless mode the widget injects no UI of its own. You render whatever buttons, banners, or in-call indicators you want, and call the JavaScript API to start and end calls.

### JavaScript API

| Method / Callback                              | Description                                                                                                                            |
| ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `window.RinglyraWidget.start()`                | Begin a voice call. Must be called from inside a user-gesture handler (e.g. `click`) so the browser grants microphone access.          |
| `window.RinglyraWidget.end()`                  | End the active call.                                                                                                                   |
| `window.RinglyraWidget.onCallStart(cb)`        | Fires when `start()` is invoked (status `connecting`). No payload.                                                                     |
| `window.RinglyraWidget.onCallConnected(cb)`    | Fires when the WebRTC connection is established. Payload: `{ agentId, workflowRunId, token }`.                                         |
| `window.RinglyraWidget.onCallDisconnected(cb)` | Fires only if the call had connected, when teardown runs. Payload: `{ agentId, workflowRunId, token, durationSeconds }`.               |
| `window.RinglyraWidget.onCallEnd(cb)`          | Fires whenever the call session is torn down (including failed-to-connect attempts). No payload.                                       |
| `window.RinglyraWidget.onStatusChange(cb)`     | Fires on every status change. Callback receives `(status, text, subtext)`. Status values: `idle`, `connecting`, `connected`, `failed`. |
| `window.RinglyraWidget.onError(cb)`            | Fires on errors (mic permission denied, server error, etc.). Callback receives an `Error` object.                                      |

All `on*` setters are single-listener — calling the same one again replaces the previous handler.

<Note>
  **About timing.** The widget script loads asynchronously, so `window.RinglyraWidget` may not exist at the moment your inline `<script>` first runs. The examples below assume `window.RinglyraWidget` is already available when registration runs. To guarantee that:

  * **Vanilla JS:** wrap your registration code in `window.addEventListener('load', () => { /* register here */ })`.
  * **React:** inside `useEffect`, register immediately if `document.readyState === 'complete'`, otherwise add a one-time `window.load` listener that registers on fire.
  * **Click handlers** that call `start()` / `end()` don't need a guard — by the time a user clicks, the widget has long since loaded.
</Note>

### Vanilla JS

```html theme={null}
<button id="talk-btn">Talk to AI</button>

<script>
  let callStatus = 'idle';
  const btn = document.getElementById('talk-btn');

  function render() {
    btn.textContent =
      callStatus === 'connected' ? 'End Call'
      : callStatus === 'connecting' ? 'Connecting…'
      : callStatus === 'failed' ? 'Retry'
      : 'Talk to AI';
  }

  window.RinglyraWidget.onStatusChange((status) => {
    callStatus = status;
    render();
  });

  window.RinglyraWidget.onError((err) => {
    console.error('Ringlyra error:', err.message);
  });

  btn.addEventListener('click', () => {
    if (callStatus === 'connected' || callStatus === 'connecting') {
      window.RinglyraWidget.end();
    } else {
      window.RinglyraWidget.start();
    }
  });
</script>
```

### React + TypeScript

```tsx theme={null}
import { useEffect, useState } from 'react';

type CallStatus = 'idle' | 'connecting' | 'connected' | 'failed';

declare global {
  interface Window {
    RinglyraWidget: {
      start: () => void;
      end: () => void;
      onStatusChange: (cb: (status: CallStatus, text?: string, subtext?: string) => void) => void;
      onError: (cb: (err: Error) => void) => void;
    };
  }
}

export function TalkButton() {
  const [status, setStatus] = useState<CallStatus>('idle');

  useEffect(() => {
    window.RinglyraWidget.onStatusChange((s) => setStatus(s));
    window.RinglyraWidget.onError((err) => console.error('Ringlyra error:', err.message));
  }, []);

  const isLive = status === 'connected' || status === 'connecting';
  const label = { idle: 'Talk to AI', connecting: 'Connecting…', connected: 'End Call', failed: 'Retry' }[status];

  return (
    <button onClick={() => (isLive ? window.RinglyraWidget.end() : window.RinglyraWidget.start())}>
      {label}
    </button>
  );
}
```

<Note>
  `start()` must run inside a real user-gesture handler (`click`, `touchend`, etc.). Browsers refuse to grant microphone access to scripts that request it outside of one — calling `start()` from a `setTimeout` or on page load will fail with a permission error.
</Note>

## Lifecycle callbacks (all modes)

The `on*` callbacks in the [Headless JavaScript API](#javascript-api) work in **all three embed modes**, not just Headless. Use them for analytics or to trigger UI in the host page even when the widget is rendering its own UI (Floating or Inline).

```js theme={null}
window.RinglyraWidget.onCallConnected(({ agentId, workflowRunId }) => {
  analytics.track('voice_call_started', { agentId, workflowRunId });
});

window.RinglyraWidget.onCallDisconnected(({ workflowRunId, durationSeconds }) => {
  analytics.track('voice_call_ended', { workflowRunId, durationSeconds });
});
```

`onCallConnected` and `onCallDisconnected` only fire when the call actually establishes a media connection — failed-to-connect attempts (e.g. denied mic, network failure) don't trigger them, so analytics stay clean.
