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

# Add to Website

> Add your agent to any website. Visitors can type, call, or do both in one conversation.

Add your agent to any website with one script tag. Visitors can type to it, call it, or do both in the same conversation.

## How the widget works

* **One widget does chat and calls.** Visitors type in the text box or tap the call button.
* **The agent remembers the conversation.** A call picks up where the chat left off. A chat after a call knows what was said on the call.
* **Visitors can type during a call.** If the agent is talking, it stops. Then it answers out loud, and the answer also shows as a text message.
* **Nothing starts until the visitor acts.** Opening the widget starts nothing, and it never asks for the microphone. The chat starts when the visitor clicks into the text box, and the agent's greeting shows as the first message.

What a visitor sees:

1. **The launcher.** A pill-shaped button with the Bananaflow logo and your label.
2. **The open view.** A large logo with a glowing ring and a call button in its center. Below it are your welcome line and a text box.
3. **The chat.** Messages appear under a small logo at the top left. A round call button sits next to the text box.
4. **A call.** The ring glows while the agent speaks, and a status shows "Listening…" or "Speaking…". An end call button takes the place of the call button.
5. **After a call.** A line such as "Call ended · 1:23" marks the call in the chat. The visitor can keep typing.

How greetings work:

* **A call from the open view:** the agent greets the visitor out loud, like any call.
* **A call after chatting:** the agent says one short line to pick up the conversation. It does not greet again.
* **A chat after a call:** the chat picks up from the call, with no greeting.
* **After the agent ends the conversation** at an [End Call](/voice-agent/end-call) node, the next chat or call starts fresh, with the normal greeting.

A few more details:

* On phones (screens under 480 pixels wide), the widget fills the screen.
* There is no End chat button. A chat ends after a stretch with no activity, or when your page calls `end()`. The stretch is 30 minutes unless you change **Chat Inactivity Timeout**. The widget then shows "Conversation ended." and a **Start new chat** button.
* A chat lasts up to 1 hour. After that, the visitor can start a new one.
* Reloading the page starts a new conversation. History is not kept across page loads.

<Note>
  **Already using the widget?** You don't need to change your page. The same embed snippet now loads this widget. Voice widgets and chat widgets are now one widget, so the old **Widget Type** setting no longer applies.
</Note>

## Before you start

* **Use HTTPS.** Serve your page over HTTPS, or from `http://localhost` while you test. Browsers block the microphone on plain `http://` pages, so calls fail there.
* **Allow your domains.** If you fill in **Allowed Domains**, add every domain the widget runs on, including `localhost` for tests. Requests from other domains are refused. Leave the list empty to allow all domains.
* **The script loads in the background.** The snippet loads `bananaflow-widget.js` asynchronously. Code that uses `window.BananaflowWidget` must wait for it. See [Wait for the widget to load](#wait-for-the-widget-to-load).
* **Strict security headers?** If your site sends a Content Security Policy, see [Content Security Policy](#content-security-policy).

## How to add it

Step 1: In the agent editor, click the gear icon at the top right to open the agent settings.

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

Step 3: Turn on **Enable Embedding** and add your domain under **Allowed Domains**. Pick an **Embed Mode**, set the look and the text, and click **Save Configurations**. Each setting is described in the table below.

Step 4: Click **Copy Code** and paste the embed code into your page's HTML.

Changes you save later show up on your site the next time a page loads. You don't need to paste the code again.

### Settings

| Setting | What it does |
| - | - |
| **Enable Embedding** | Turns the widget on or off for this agent. |
| **Allowed Domains** | The sites that may load the widget. Leave it empty to allow all sites. |
| **Embed Mode** | Floating, inline, or headless. See [Embed modes](#embed-modes). |
| **Launcher Label** | The text on the pill button. Floating mode only. |
| **Welcome Line** | The line under the logo in the open view. |
| **Accent Color** | The color of the call button, the send icon, and the focus ring. Black (`#111111`) by default. |
| **Position** | The corner the pill sits in. Floating mode only. |
| **Chat Inactivity Timeout** | How long a chat can sit idle before it ends. 30 minutes by default. When the chat ends, its webhooks run. |
| **Widget Text** | Every word the widget shows. See [Change the widget text](#change-the-widget-text). |

## Embed modes

| Mode | What it shows | Use it when |
| - | - | - |
| **Floating Widget** | A pill button in a corner of the page. It opens the widget card. | You want the agent on your pages without changing your layout. |
| **Inline Component** | The widget card, inside a container you place on the page. | You want the agent in one spot, like a hero section or a support tab. |
| **Headless** | Nothing. Your page draws its own buttons and messages. | You want full control over the look. |

In every mode, the widget handles the microphone, the call audio, and the chat. The [JavaScript API](#javascript-api) and its callbacks work in all three modes.

## Floating widget

A pill button sits in a corner of the page. It shows the Bananaflow logo and your **Launcher Label**.

* Clicking the pill opens the widget card.
* The minimize button at the top right closes the card, and so does the Escape key. The conversation keeps going, and reopening the card shows it again.
* Minimizing during a call keeps the call going. The pill glows and shows how long the call has run.

Set **Launcher Label**, **Welcome Line**, **Accent Color**, and **Position** in the dialog. Pasting the snippet is the whole job. You don't need any other code.

## Inline component

The widget card fills a container you place on your page. There is no pill and no minimize button. Give the container a height, since the card fills it.

Set **Welcome Line** and **Accent Color** in the dialog, plus any [widget text](#change-the-widget-text) you want to change.

### Plain HTML

Place the container where you want the widget. The widget finds it on its own.

```html theme={null}
<!-- Paste the Bananaflow embed snippet somewhere on the page -->
<div id="bananaflow-inline-container" style="height: 600px"></div>
```

### React

In React, the container can mount after the widget script has loaded. If the widget hasn't started yet, call `initInline` with the container. If it has, call `refresh` to draw the card in the new container. This example uses the declaration from [TypeScript](#typescript).

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

export function Assistant() {
  useEffect(() => {
    const mount = () => {
      const widget = window.BananaflowWidget;
      const container = document.getElementById('bananaflow-inline-container');
      if (!widget || !container) return;
      if (widget.getState().isInitialized) widget.refresh();
      else widget.initInline({ container });
    };
    if (window.BananaflowWidget) {
      mount();
      return;
    }
    const script = document.getElementById('bananaflow-widget');
    script?.addEventListener('load', mount, { once: true });
    return () => script?.removeEventListener('load', mount);
  }, []);

  return <div id="bananaflow-inline-container" style={{ height: 600 }} />;
}
```

## Headless mode

In headless mode the widget shows nothing. Your page draws its own buttons and messages, and drives the conversation with the [JavaScript API](#javascript-api). The widget still handles the microphone, the call audio, and the chat.

This example has a transcript, a text box, a send button, and a call button:

```html theme={null}
<div id="transcript"></div>
<input id="chat-input" placeholder="Send a message…" />
<button id="send-btn">Send</button>
<button id="call-btn">Call</button>

<script>
  let callStatus = 'idle';

  function addLine(who, text) {
    const line = document.createElement('p');
    line.textContent = who + ': ' + text;
    document.getElementById('transcript').appendChild(line);
  }

  function withBananaflowWidget(callback) {
    if (window.BananaflowWidget) {
      callback(window.BananaflowWidget);
      return;
    }
    const script = document.getElementById('bananaflow-widget');
    if (!script) {
      console.error('Bananaflow embed script not found');
      return;
    }
    script.addEventListener('load', () => {
      if (window.BananaflowWidget) callback(window.BananaflowWidget);
    }, { once: true });
  }

  withBananaflowWidget((widget) => {
    // Agent messages, from the chat and from calls
    widget.onMessage((text) => addLine('Agent', text));

    widget.onStatusChange((status) => {
      callStatus = status; // idle | connecting | connected | failed
      const live = status === 'connected' || status === 'connecting';
      document.getElementById('call-btn').textContent = live ? 'End call' : 'Call';
    });

    // The chat starts, and the agent greets, when the box first gets focus
    document.getElementById('chat-input').addEventListener('focus', () => {
      widget.startChat();
    }, { once: true });

    // Goes into the call while one is live, otherwise into the chat
    document.getElementById('send-btn').addEventListener('click', async () => {
      const input = document.getElementById('chat-input');
      addLine('You', input.value);
      const transcript = await widget.sendMessage(input.value);
      if (transcript !== null) input.value = '';
    });

    // Start calls from a click: browsers only let a page play sound
    // after the visitor interacts with it
    document.getElementById('call-btn').addEventListener('click', () => {
      if (callStatus === 'connected' || callStatus === 'connecting') {
        widget.end(); // ends only the call
      } else {
        widget.startCall();
      }
    });
  });
</script>
```

### React call button

A call button that follows the call status. It uses the declaration from [TypeScript](#typescript).

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

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

  useEffect(() => {
    const register = () => window.BananaflowWidget?.onStatusChange(setStatus);
    if (window.BananaflowWidget) {
      register();
      return;
    }
    const script = document.getElementById('bananaflow-widget');
    script?.addEventListener('load', register, { once: true });
    return () => script?.removeEventListener('load', register);
  }, []);

  const live = status === 'connected' || status === 'connecting';
  return (
    <button onClick={() => (live ? window.BananaflowWidget?.end() : window.BananaflowWidget?.startCall())}>
      {live ? 'End call' : 'Call'}
    </button>
  );
}
```

<Note>
  Call `startCall()` from a click or tap handler. Browsers only let a page play sound after the visitor interacts with it. A call started from a timer or on page load can fail.
</Note>

## JavaScript API

The widget adds `window.BananaflowWidget` to your page. It works in every embed mode, not just headless. Use it to drive your own interface, or to send widget events to your analytics.

### Wait for the widget to load

The snippet loads the widget script asynchronously, so `window.BananaflowWidget` may not exist yet when your code runs. This helper runs your code once the widget is ready:

```js theme={null}
function withBananaflowWidget(callback) {
  if (window.BananaflowWidget) {
    callback(window.BananaflowWidget);
    return;
  }
  const script = document.getElementById('bananaflow-widget');
  if (!script) {
    console.error('Bananaflow embed script not found');
    return;
  }
  script.addEventListener('load', () => {
    if (window.BananaflowWidget) callback(window.BananaflowWidget);
  }, { once: true });
}
```

### Methods

| Method | What it does |
| - | - |
| `start()` | Opens the widget, ready to type or call: the big call button and the message box. It never asks for the microphone, and it starts neither a chat nor a call. |
| `startChat()` | Starts the chat. The agent's greeting arrives through `onMessage`. If the visitor already called, the chat picks up from the call instead, with no greeting. |
| `startCall()` | Starts a call. Call it from a click or tap. If the visitor has been chatting, the call continues the same conversation. |
| `sendMessage(text)` | Sends the visitor's message. While a call is live, it goes into the call: the agent answers out loud, and the answer also arrives through `onMessage`. Otherwise it goes into the chat. Returns a Promise that resolves to `null` if the message was not sent. |
| `end()` | Ends the call if one is live. Otherwise it ends the chat, and the chat's webhooks run. |
| `stop()` | Kept so pages built for the older widget still work. Use `end()` in new code. |
| `retry()` | Tries the last failed action again. |
| `setContext(vars)` | Adds visitor details for the conversation. See [Pass context to the agent](#pass-context-to-the-agent). |
| `getContext()` | Returns the visitor details collected so far. |
| `getMessages()` | Returns the conversation so far, with chat messages and call lines together. |
| `getState()` | Returns the widget's current state. |
| `initInline({ container })` | Puts the widget card in `container`. For pages that add the container later, such as React apps. |
| `refresh()` | Draws the inline card again, for example after your app mounts its container again. |
| `isInlineMode()` | Returns `true` in inline mode. |
| `isChatMode()` | Always returns `true`. Every widget can chat. |

### Callbacks

| Callback | When it fires |
| - | - |
| `onMessage(cb)` | For each new message from the agent, in the chat and during calls. Receives `(text, turn)`. |
| `onChatStateChange(cb)` | When the chat state changes. States: `idle`, `starting`, `ready`, `waiting` (the agent is replying), `ended`, `expired`, `error`. |
| `onStatusChange(cb)` | When the call status changes. Receives `(status, text, subtext)`. The status is `idle`, `connecting`, `connected`, or `failed`. |
| `onCallStart(cb)` | When a call starts connecting. No payload. |
| `onCallConnected(cb)` | When a call connects. Payload: `{ agentId, workflowRunId, token }`. |
| `onCallDisconnected(cb)` | When a connected call ends. Payload: `{ agentId, workflowRunId, token, durationSeconds }`. |
| `onCallEnd(cb)` | When a call ends. It does not fire when a call fails to start, such as when the microphone is blocked. `onError` reports those. No payload. |
| `onError(cb)` | On errors, such as a blocked microphone or a server error. Receives an `Error`. |

All callbacks fire in every embed mode, including `onStatusChange`. Each callback function keeps one listener. Calling it again replaces the one before.

For example, to send calls to your analytics:

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

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

`onCallConnected` and `onCallDisconnected` fire only for calls that connect. A blocked microphone or a network failure fires neither, so your numbers stay clean.

### TypeScript

`window.BananaflowWidget` has no published types. Declare the parts you use. The React examples on this page use this declaration:

```ts theme={null}
// bananaflow-widget.d.ts
export {};

declare global {
  type BananaflowCallStatus = 'idle' | 'connecting' | 'connected' | 'failed';

  interface Window {
    BananaflowWidget?: {
      startCall: () => void;
      end: () => void;
      onStatusChange: (cb: (status: BananaflowCallStatus) => void) => void;
      initInline: (options: { container: HTMLElement }) => void;
      refresh: () => void;
      getState: () => { isInitialized: boolean };
    };
  }
}
```

## Pass context to the agent

Your page often knows something about the visitor: their name, their plan, the page they are on. Pass it to the agent, and the agent can use it from the first message.

The snippet you copy has a `data-bananaflow-context` attribute. It holds a JSON object of visitor details. Here is the part of the snippet that sets it. Keep the `js.src` value from your own snippet, since it holds your embed token.

```html theme={null}
<script>
  (function(d, s, id) {
    var js, fjs = d.getElementsByTagName(s)[0];
    if (d.getElementById(id)) return;
    js = d.createElement(s);
    js.id = id;
    js.src = '<dashboard-generated widget URL>';
    js.setAttribute('data-bananaflow-context', JSON.stringify({
      page_url: window.location.href,
      today: new Date().toISOString().slice(0, 10)
    }));
    js.async = true;
    fjs.parentNode.insertBefore(js, fjs);
  }(document, 'script', 'bananaflow-widget'));
</script>
```

The snippet builds the object in JavaScript when the page loads, so it can hold anything your page knows. Replace the object inside `JSON.stringify(...)`, for example:

```js theme={null}
{
  customer_name: currentUser.firstName,
  plan: currentUser.plan,
  cart: { items: cart.length, total: cart.total }
}
```

Each key is then available in any node prompt as `{{initial_context.<name>}}`:

```text theme={null}
Greet {{initial_context.customer_name | there}} and mention their {{initial_context.plan}} plan.
```

Values can be text, numbers, `true` or `false`, or nested objects. The agent gets them in chats and in calls. Each run records them, so you can see what the agent was given.

<Warning>
  Key names can't contain dots, spaces, pipes (`|`), or braces (`{` and `}`). Those characters mean something in template expressions. A key with one of them is dropped, and the conversation still starts.
</Warning>

### Update context after the page loads

The attribute is set once, when the page loads. That doesn't fit a single-page app, where the visitor logs in or fills a cart later. For details you learn later, call `setContext()`:

```js theme={null}
window.BananaflowWidget.setContext({
  customer_name: user.firstName,
  plan: user.plan
});
```

Each call merges into the details already collected, so you can add details as you learn them. Send a name again to correct it. `getContext()` returns the current set.

The agent reads the context once, when the conversation starts. Chats and calls later in the same conversation keep that context. So call `setContext()` before the conversation starts. Details you set after that apply to the next new conversation, such as one started with **Start new chat**.

<Note>
  The widget script loads asynchronously. Call `setContext()` inside the helper from [Wait for the widget to load](#wait-for-the-widget-to-load), or from a later event such as a click.
</Note>

Use `data-bananaflow-context` for what the page knows when it loads, and `setContext()` for what it learns later. The two merge. If both set the same name, `setContext()` wins.

<Warning>
  Context comes from the page, so a visitor can read it and change it. Never pass secrets. Don't let it decide what the agent will do or share. Treat `plan: "pro"` as a hint for wording, not as proof. For data the agent must trust, pass an ID such as `customer_id`. Then let Bananaflow fetch the real details from your API with [Pre-Call Data Fetch](/voice-agent/pre-call-data-fetch).
</Warning>

Limits for each conversation: up to 50 details, names up to 64 characters, and 8 KB in total. Text longer than 2,000 characters is cut short. An object or list longer than 2,000 characters is dropped. Anything else past a limit is dropped, and the conversation still starts. A few names are reserved and ignored, such as `provider`, `call_id`, and `workflow_run_id`.

## Change the widget text

You can change every word the widget shows, so it can speak your site's language. Open **Widget Text** in the Configure Widget dialog and fill in the fields you want to change. Leave a field blank to keep the default. The **Launcher Label** and **Welcome Line** are set with the other settings.

The key column shows each text's name in the widget's saved settings.

**Text box and buttons**

| Key | Default | Where it shows |
| - | - | - |
| `chatInputPlaceholder` | Send a message… | Placeholder in the text box. |
| `sendMessageLabel` | Send message | Label of the send button. Screen readers read it. |
| `startCallLabel` | Start call | Label of the call button. Screen readers read it. |
| `closeChatLabel` | Minimize | Label of the minimize button. Screen readers read it. |
| `backToOrbLabel` | Back | Label of the back button on the small logo, which returns to the big logo. Screen readers read it. |
| `showMessagesLabel` | Show messages | Label of the chat button that shows the messages again. Screen readers read it. |

**During a call**

| Key | Default | Where it shows |
| - | - | - |
| `voiceConnectingText` | Connecting… | While a call connects. |
| `callWaitingText` | Waiting for your last call to finish… | A new call waits while the last one is still ending. |
| `agentSpeakingText` | Speaking… | Call status while the agent talks. |
| `agentListeningText` | Listening… | Call status while the agent listens. |
| `voiceEndCallText` | End call | Label of the end call button. |
| `voiceCallEndedTitle` | Call ended | The line that marks a call in the chat, next to its length. |

**Problems**

| Key | Default | When it shows |
| - | - | - |
| `micPermissionDeniedText` | Microphone access is blocked. Allow it in your browser settings to call. | The visitor blocked the microphone. |
| `micNotFoundText` | No microphone found. | The device has no microphone. |
| `voiceConnectionFailedTitle` | Couldn't connect the call | A call failed to connect. |
| `voiceConnectionFailedSubtext` | Check your microphone and connection, then try again. | Under the text above. |
| `voiceConnectionLostTitle` | Call disconnected | A call dropped. |
| `voiceConnectionLostSubtext` | The connection dropped. | Under the text above. |
| `messageFailedText` | Message not sent. | A message could not be sent. It goes back into the text box. |
| `chatRetryText` | Retry | The button to try again after a chat error. |
| `agentUnavailableText` | The agent is unavailable right now. Please try again later. | The agent can't take a conversation right now. |

**End of a conversation**

| Key | Default | When it shows |
| - | - | - |
| `conversationEndedText` | Conversation ended. | After the chat ends. |
| `messageLimitReachedText` | Message limit reached | The chat reached its message limit and ended. |
| `startNewChatText` | Start new chat | The button to start over after the chat ends. |

<Note>
  Did you translate the older widget? Check this list again. New texts show in English until you fill them in. Texts for parts the widget no longer has, such as the End chat button, are no longer used.
</Note>

## See conversations in your dashboard

Each chat and each call is its own [run](/core-concepts/calls-and-runs). To see them, open your agent, click the three-dot menu at the top right, and choose **View Runs**.

When one visitor chats and calls, their runs are linked:

* **The chat run holds the whole conversation.** It shows the typed messages and the lines from each call. Each call shows as a line such as "Voice call · 2:31" that links to the call's run.
* **Each call run** has its own recording and transcript.
* **Links connect the runs.** Runs show **Continued from run #** and **Continued in run #** links. Follow them to move through the conversation.
* **Messages typed during a call** belong to that call's run.

Each run does its own follow-up work:

* **Webhooks and QA run once per run.** A call's [webhooks](/voice-agent/webhook) and [QA](/voice-agent/qa) run when the call ends. The chat's webhooks and QA run when the chat ends.
* A visitor who only chats, or only calls, makes one run, as before.
* If your embed token has a usage limit, each chat and each call counts once. A visitor who chats and then calls counts twice. A call blocked by the microphone never starts, so it doesn't count.

## Content Security Policy

If your site sends a `Content-Security-Policy` header, add these sources to it:

| Directive | Add | Why |
| - | - | - |
| `script-src` | `https://app.bananaflow.ai` | Loads the widget script. |
| `connect-src` | `https://app.bananaflow.ai wss://app.bananaflow.ai` | Chat messages go over HTTPS, and calls connect over a secure WebSocket. |
| `img-src` | `https://app.bananaflow.ai` | Shows the Bananaflow logo. |
| `style-src` | `'unsafe-inline'` | Only if you support Safari older than 16.4. Newer browsers don't need it. |

If your policy leaves out one of these directives, the browser uses `default-src` for it. Add the sources there instead.

The embed snippet itself is an inline script. If your policy blocks inline scripts, add your nonce to the snippet's `<script>` tag.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.