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

# Embed a voice, video, or capture widget on your site

> Add Orbit's embeddable web widgets — an AI voice-agent button, a prebuilt video room, and on-site sign-up forms — to any page, and control them with one public key plus the origin allowlist.

# Embed a voice, video, or capture widget on your site

Orbit ships a family of **embeddable web widgets** — drop-in surfaces a
visitor on *your* site can use without leaving the page. One install
pattern covers four widgets, because they all share the same public,
Origin-gated mount model:

* **Native-chat web widget** — the omnichannel chat surface (with an
  optional conversation flow: pre-chat form, FAQ deflection, routing,
  AI handoff).
* **AI voice-agent widget** — a "Talk to AI" button that opens a browser
  WebRTC voice call straight to your AI agent.
* **Prebuilt video room** — the `OrbitVideoRoom` / `<orbit-video-room>`
  drop-in video-call UI (a live room on your own page).
* **Sign-up forms** — an embeddable list-growth form (inline, popup,
  flyout, exit-intent) you publish from a no-code builder.

Everything on this page is **inbound, browser-originated media or
capture.** No widget here wires an outbound voice or SMS leg — a visitor
starts the session from their browser, and the call or form lives on
Orbit's own media + capture path.

## The shared mount model — public key + origin allowlist

Every widget below mounts with two public values, and both are safe to
put in a plain HTML page:

* **`public_key`** — your widget's public key (today the tenant id you
  see in **Settings → Native Chat**). This is a public identifier, not a
  secret.
* **Your site's origin** — added to the widget's origin allowlist under
  **Settings → Native Chat → Privacy**, or switch on **Allow embedding
  from any domain**.

The public mint routes (`POST /widget/session`,
`POST /widget/voice-agent-session`, `POST /widget/video-session`) are
anonymous and Origin-gated: they read the browser's `Origin` header and
only mint a session when that origin is in your allowlist. Your **API
key never touches the browser** — only the short-lived session token the
widget needs.

<Note>
  All the widgets read the *same* origin allowlist and the same widget
  config row, so you configure privacy once and it applies to voice, video,
  chat, and signup formats alike.
</Note>

## 2. Choose the widget and its config surface

### Native-chat widget + conversation flow

The chat widget is driven by an optional **conversation flow** you
author in the no-code builder (Settings → Native Chat → Flow). The flow
is the chain of decisions the widget runs on first contact:

| Flow section           | What it controls                                                                                                                                                           |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Pre-chat form**      | Collect fields before the conversation starts (text, email, phone, select, textarea).                                                                                      |
| **Business hours**     | What happens after hours: show an away message, hide the launcher, or collect the visitor's email.                                                                         |
| **FAQ deflections**    | Inline answers the widget surfaces before paging a human, scored by keyword match.                                                                                         |
| **Routing rules**      | Keyword or page-path conditions that resolve to AI agent, human inbox, a specific FAQ, or collect-email.                                                                   |
| **AI handoff**         | When to hand a conversation to your AI agent — immediately, on a keyword, or after a deflection miss.                                                                      |
| **Proactive triggers** | Behavioral auto-invites (time on page, scroll depth, exit intent, repeat visit, cart value) that open the chat or pop a targeted message, with per-visitor frequency caps. |

The flow is published to the widget as part of the session bootstrap; a
missing or partially-written flow degrades to the plain
config-bootstrapped behaviour, so a bad publish never breaks the public
widget.

### AI voice-agent widget (`OrbitVoiceAgent`)

The voice widget is the browser's front door to your **AI voice agent** —
a "Talk to AI" button the visitor presses to hold a live WebRTC voice
conversation with the agent you bind. You configure it in **Settings →
Channels → Voice Agent**; the install snippet generator reads the same
config.

| Config key           | What it does                                                                                    | Default        |
| -------------------- | ----------------------------------------------------------------------------------------------- | -------------- |
| `enabled`            | Master switch; off until you bind an agent and enable.                                          | `false`        |
| `agent_id`           | Which AI voice agent the widget connects visitors to.                                           | unbound        |
| `primary_color`      | Widget accent colour (CSS hex).                                                                 | `#6366f1`      |
| `position`           | Floating launcher position: `bottom-right` or `bottom-left`.                                    | `bottom-right` |
| `theme`              | `light` or `dark`.                                                                              | `light`        |
| `button_label`       | Launcher button text.                                                                           | "Talk to AI"   |
| `business_hours`     | Optional weekly schedule + timezone; outside these hours the widget can defer.                  | none           |
| `ai_disclosure_text` | The disclosure shown so the visitor knows they're speaking with an AI before the call connects. | preset         |
| `consent_required`   | Whether the visitor must affirmatively consent before the call starts.                          | `true`         |
| `consent_text`       | The consent-checkbox copy shown with the button.                                                | preset         |

The voice widget never selects a phone carrier and never routes an
outbound PSTN leg — it's an **inbound, visitor-initiated WebRTC call**
bridged to your AI agent on Orbit's own media stack.

### Prebuilt video room (`OrbitVideoRoom` / `<orbit-video-room>`)

The video drop-in paints a complete conferencing surface — participant
tiles, control bar, screen share, device picker — from a single element.
The fastest path is the **Video Rooms → Embed** dialog in the dashboard:
open a live room, choose the options (camera on join, screen share,
device picker), and copy the snippet. The dialog gives you three shapes:

* **Element** — the declarative `<orbit-video-room>` tag:
  ```html theme={null}
  <div style="height:600px">
    <orbit-video-room
      token="&lt;server-minted join token&gt;"
      server-url="wss://media.orbit.devotel.io"
      start-with-camera="true"
      enable-screen-share="true"
    ></orbit-video-room>
  </div>
  ```
* **JavaScript** — the imperative `OrbitVideoRoom.init()`:
  ```js theme={null}
  import { OrbitVideoRoom } from "@devotel-orbit/web";
  const room = OrbitVideoRoom.init({
    container: document.getElementById("orbit-video"),
    token: "<server-minted join token>",
    serverUrl: "wss://media.orbit.devotel.io",
    startWithCamera: true,
    enableScreenShare: true,
  });
  ```
* **Server** — the token-mint call your backend makes (never in the
  browser).

For a *site visitor* joining a room (no dashboard access), the browser
mints its token through the public `POST /widget/video-session` mount
route — same public-key + origin-allowlist model as the chat and voice
widgets, so a paste-in-page snippet needs no API key. See the full
video-token flow in [Using a video room access
token](/guides/video-room-access-tokens).

## 3. The minimal HTML embed

Whichever widget you mount, the pattern is the same: a container element,
the Web SDK module, and `init()` with the public values. Here's the
voice agent end to end:

```html theme={null}
<div id="orbit-voice-agent"></div>
<script type="module">
  import { OrbitVoiceAgent } from "@devotel-orbit/web";

  const agent = OrbitVoiceAgent.init({
    publicKey: "your_widget_public_key",
    agentId: "agent_support",
    container: document.getElementById("orbit-voice-agent"),
  });
  agent.on("state", (s) => console.log("voice-agent:", s));
</script>
```

The chat and video surfaces follow the same shape — a container +
`publicKey` (chat) or a minted token + `serverUrl` (video) +
`init(container, …)`. Mount the widget **on a click** (not on page load)
so an idle page costs nothing.

## 4. Web-call context — the screen-pop half

When a visitor starts a browser voice or chat call, the widget collects
**where the call came from** and posts it along: the page URL, page
title, referrer, and an optional bag of host-supplied attributes (plan
tier, cart value, account id, …).

That context rides with the session as the participant's metadata, so
the agent runtime that answers — and, on a handoff, the human agent's
screen-pop — knows *which page* the visitor was on and *who* they are,
instead of a cold "anonymous web visitor."

Because the context arrives from an anonymous, public browser surface,
it's strictly bounded before it touches a room token:

* URLs must be `http(s)` — any other scheme is dropped.
* Page title, URLs, and attribute values are length-capped.
* The attribute map is count- and size-capped, and the serialized blob
  is hard-capped so it always fits the media token's metadata budget.
* A malformed or oversized field is **dropped, never fatal** — a bad
  context attribute can't block the visitor from starting their call.

On the SDK side you pass host attributes via `metadata` and control
auto-collection with `collectPageContext` (set `false` on a
privacy-sensitive embed that should send only an explicit bag, or
nothing).

## 5. Sign-up forms — list growth on your own page

The sign-up form is a **capture surface** (not a chat routing aid): a
tenant-authored form with display rules, A/B variants, field→contact
mappings, and a consent block, that grows your own contacts and lists.
You build it in the no-code builder and it publishes to the same widget
surface as the chat.

Each form is made of:

| Piece          | What it does                                                                                                                                                                                                                              |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Fields**     | The inputs (text, email, phone, select, textarea, checkbox, hidden). Each field maps to a canonical contact attribute (email, phone, first/last/full name, company), a custom field key, or stays as form metadata.                       |
| **Consent**    | An optional explicit opt-in checkbox, over email or SMS. When SMS + double opt-in is set, the captured number is handed to your double-opt-in flow before any send — the form only *records* consent intent, it never selects a provider. |
| **Display**    | How and when it shows: inline embed, popup, flyout, exit-intent, or banner; position (center, bottom, top); delay; display conditions (URL match, time on page, scroll depth, exit intent, repeat visit). All conditions AND together.    |
| **Frequency**  | Per-visitor impression cap + cooldown so a popup isn't re-shown on every page view (default once per visitor, 7-day cooldown).                                                                                                            |
| **Variants**   | One to four A/B variants with relative weights — the runtime assigns each visitor deterministically.                                                                                                                                      |
| **On success** | Show a message, or redirect the visitor to a URL.                                                                                                                                                                                         |

Every form has a `version` the runtime can refuse if it doesn't
recognise it, and the public surface only ever serves **enabled**
forms — a disabled draft is withheld from the visitor's browser.

## 6. Transcripts + agent trust policy

### Chat transcript

A visitor can email themselves a copy of a chat conversation
(`POST /widget/conversations/:id/transcript`). The email is optional —
when omitted, the transcript goes to the address the visitor already
supplied; it's only required if they're still anonymous. This is the
same "email me a copy" touch every chat widget ships, rendered by a
pure, side-effect-free renderer.

### Agent trust policy

This is the tenant-facing control for **which foreign AI agents you
trust** when a browser-resident agent (via Web Bot Auth / an AP2
directory) drives your widget. You list the foreign agent directories
you choose to trust by exact HTTPS directory URL, and what to grant a
verified request:

* **`allow`** — grant access at the surface's normal posture.
* **`deny`** — revoke a previously-trusted directory without deleting
  the row.
* **`rate`** — allow, but apply that directory's hourly rate limit.

Nothing is trusted by default: an empty list means every foreign
signature is denied, exactly as it was before this control existed — the
policy can only ever *grant* trust, and it defaults to closed. You edit
it via `PUT /api/v1/settings/native-chat/agent-trust-policy`.

## 7. Guardrails for MCP / WebMCP agents

Opening the widget as an in-page tool surface a browser-resident AI
agent can drive (WebMCP / agent action calling) is hardened by a set of
**independent write guardrails** that run before any action executes:

1. **Rate lane** — a per-visitor-session, per-hour sliding-window cap on
   agent-invoked actions (on top of the normal IP-keyed widget rate
   limit).
2. **Per-session caps** — cumulative ceilings on actions and
   preview-cost "spend" for a single session.
3. **Audit** — a structured line for every agent-invoked tool call,
   allowed or denied.
4. **Agent-identity attestation** — verify + classify the calling
   agent (Web Bot Auth signature and/or a scoped agent token) so you can
   allow, deny, or rate by identity.
5. **Reputation (KYA)** — a per-tenant behavioural score that can
   *tighten* — never loosen — the trust decision, downgrading an
   allowlisted agent that starts misbehaving.
6. **Compliance** — an agent-scheduled callback is a future outbound
   voice leg, so it runs through **your** tenant-configured quiet-hours
   / dialing window. Per the compliance posture this is default-open and
   tenant-owned: with no channel opted into quiet hours the gate allows;
   when you've configured a window and the visitor is outside it, the
   request is rejected with a reason.

Guardrails only ever **reject or attenuate** — they never originate a
new outbound termination path. A callback they allow is queued and
dialed by the existing dispatch worker over the platform's normal voice
termination.

## Where to go next

* [Video meetings](/guides/video-meetings) and [video room access
  tokens](/guides/video-room-access-tokens) — the token flow the video
  embed's Server tab runs.
* [Add a "video call us" button](/guides/embed-video-consultation-button)
  — a full walkthrough of one video-consultation embed with waiting
  room + retention.
* [Web SDK](/sdks/web) — the `OrbitVideoRoom`, `OrbitVoiceAgent`, and
  `OrbitChat` surfaces with the full event map.
