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

# Drop in an Orbit widget with one HTML tag

> Register the Orbit Elements (<orbit-chat>, <orbit-verify>, <orbit-video-room>, <orbit-click-to-call>, <orbit-preference-center>) from a self-hosted script tag and wire their events — no JavaScript build pipeline required.

# Drop in an Orbit widget with one HTML tag

**Orbit Elements** is the declarative surface of the [Web SDK](/sdks/web): five Custom Elements — `<orbit-chat>`, `<orbit-verify>`, `<orbit-video-room>`, `<orbit-click-to-call>`, and `<orbit-preference-center>` — that mount the SDK's prebuilt widgets from plain HTML attributes. Paste a tag, and the element boots the same engine its imperative `OrbitChat.init({...})`-style counterpart runs — the elements are a layer on top of those engines, not a second implementation.

Use Orbit Elements when the page has no JavaScript build pipeline — a CMS page, a Webflow site, a hand-written HTML template, an email-marketing landing page — and writing a `<script type="module">` bootstrap is one build step too many. If you already import `@devotel-orbit/web` through a bundler, the imperative classes in the [Web SDK reference](/sdks/web) give the same widgets with more control.

Two facts stay true across every element:

* **The browser never carries an Orbit API key.** A `widget-id` is a public identifier; every `token`, and the `<orbit-verify>` `base-url` proxy, is minted or supplied by your backend with your secret key (`dv_live_sk_…`). The elements inherit each engine's authentication model unchanged.
* **Attributes are read once, when the tag connects.** Changing an attribute after the element has mounted has no effect — remove and re-append the tag to re-mount with new values.

## 1. Install and register the elements

There is no Devotel-hosted CDN for the elements bundle — copy it out of the npm package and serve it from your own static host.

```bash theme={null}
npm install @devotel-orbit/web
```

Then copy `node_modules/@devotel-orbit/web/dist/orbit-elements.iife.js` wherever your site serves static assets, and load it on the page:

```html theme={null}
<script src="/your-static-host/orbit-elements.iife.js"></script>
```

The IIFE bundle registers all five tags on load — no further JavaScript is required. If you already bundle the SDK, register the same five tags with one call instead (the call is idempotent, and a no-op outside a browser, so it is safe in SSR):

```ts theme={null}
import { defineOrbitElements } from '@devotel-orbit/web/elements'

defineOrbitElements()
```

## 2. Element matrix

The five elements, their required attributes, and the native events they re-dispatch:

| Element                     | What it mounts                                               | Required attributes   | Events it emits                                                                    |
| --------------------------- | ------------------------------------------------------------ | --------------------- | ---------------------------------------------------------------------------------- |
| `<orbit-chat>`              | The native chat launcher (tenant-scoped Inbox chat)          | `widget-id`           | — (config carrier; the widget self-mounts its own launcher)                        |
| `<orbit-verify>`            | The prebuilt verification UI (silent one-tap → OTP fallback) | `to`, `base-url`      | `orbit-state`, `orbit-verified`, `orbit-failed`, `orbit-error`                     |
| `<orbit-video-room>`        | The prebuilt multi-party video room                          | `token`, `server-url` | `orbit-state`, `orbit-participant-joined`, `orbit-participant-left`, `orbit-error` |
| `<orbit-click-to-call>`     | The CRM click-to-dial iframe bridge                          | `embed-url`, `token`  | `orbit-ready`, `orbit-call-state`, `orbit-error`, `orbit-expired`                  |
| `<orbit-preference-center>` | The hosted subscriber preference page, framed inline         | `src`                 | — (the framed page is a complete UI)                                               |

### `<orbit-chat>`

Mounts the tenant-scoped native chat widget — the same launcher the dashboard's **Settings → Channels → Native chat → Install** snippet resolves to. The widget owns a fixed-position launcher on `document.body`; the element itself paints nothing — it is a config carrier that mounts the widget when it connects and destroys it when it disconnects.

| Attribute       | Required | Description                                              |
| --------------- | -------- | -------------------------------------------------------- |
| `widget-id`     | Yes      | Public widget id (today: your organization id, `org_…`). |
| `position`      | No       | `bottom-right` (default) or `bottom-left`.               |
| `primary-color` | No       | Accent color as a `#RGB` or `#RRGGBB` hex string.        |
| `greeting`      | No       | Greeting copy shown in the opened panel.                 |
| `locale`        | No       | Locale hint forwarded on the session bootstrap.          |
| `api-base`      | No       | API origin override for self-hosted deployments.         |

This element re-dispatches no events — drive it with the imperative widget API (see [section 5](#5-events-and-programmatic-interop)) when you need unread-badge or open/close hooks.

### `<orbit-verify>`

Paints the prebuilt verification UI — silent one-tap (operator-verified number authentication) with an OTP form as fallback — into the element itself. `base-url` must point at **your** backend proxy that fronts Orbit's authenticated `/verify` routes with your secret key; the element's network calls go to that proxy, and the browser still holds no Orbit key.

| Attribute             | Required | Description                                                                               |
| --------------------- | -------- | ----------------------------------------------------------------------------------------- |
| `to`                  | Yes      | Destination being verified — E.164 phone or email address.                                |
| `base-url`            | Yes      | Origin + path of your verify proxy.                                                       |
| `channel`             | No       | OTP fallback channel: `sms` (default), `whatsapp`, `email`, `voice`, `viber`, `telegram`. |
| `code-length`         | No       | Digits in the OTP. Default 6.                                                             |
| `max-attempts`        | No       | Code-entry attempts before the flow fails. Default 5.                                     |
| `disable-silent-auth` | No       | Presence flag — set it to skip silent one-tap and start on the OTP form.                  |
| `silent-auth-token`   | No       | Device-bound operator token, forwarded to silent auth.                                    |

| Event            | Fires when                       | `event.detail`                        |
| ---------------- | -------------------------------- | ------------------------------------- |
| `orbit-state`    | The flow changes step            | `{ state }` — the flow's current step |
| `orbit-verified` | The destination verifies         | The verification result object        |
| `orbit-failed`   | Attempts are exhausted           | `{ reason }`                          |
| `orbit-error`    | A proxy or transport call throws | `{ message }`                         |

### `<orbit-video-room>`

Paints the multi-party video room — participant tile grid, self-view, and the control bar — into the element, which is itself the container. Give the element an explicit height (a wrapper `div` or CSS on the tag) or the tiles collapse to zero. Both `token` and `server-url` come from your backend: minted via `POST /api/v1/video/rooms/:id/join` for authenticated participants, or via the anonymous `POST /widget/video-session` endpoint for public pages (the join response carries the token and the media URL). The full token-minting walkthrough is in the [video-call button guide](/guides/embed-video-consultation-button).

| Attribute                 | Required | Description                                                                 |
| ------------------------- | -------- | --------------------------------------------------------------------------- |
| `token`                   | Yes      | Join token minted server-side.                                              |
| `server-url`              | Yes      | Media server URL from the join response.                                    |
| `display-name`            | No       | Participant label shown in the room.                                        |
| `start-with-camera`       | No       | `"false"` joins camera-off. Default on.                                     |
| `start-with-microphone`   | No       | `"false"` joins muted. Default on.                                          |
| `enable-screen-share`     | No       | `"false"` hides the screen-share control. Default on.                       |
| `enable-device-selection` | No       | `"false"` hides the device picker. Default on.                              |
| `auto-join`               | No       | `"false"` waits — no join until you call the engine's `join()`. Default on. |

| Event                      | Fires when                       | `event.detail`                                                              |
| -------------------------- | -------------------------------- | --------------------------------------------------------------------------- |
| `orbit-state`              | Connection state changes         | `{ state }` — `idle`, `connecting`, `connected`, `disconnected`, or `error` |
| `orbit-participant-joined` | A remote participant connects    | `{ identity }`                                                              |
| `orbit-participant-left`   | A remote participant disconnects | `{ identity }`                                                              |
| `orbit-error`              | The room fails to join or drops  | `{ message }`                                                               |

### `<orbit-click-to-call>`

Mounts the click-to-dial iframe bridge — the surface a CRM sidebar embeds for click-to-call from a record page. `token` is minted server-side via `POST /api/v1/voice/cti-embed/session`. When `to` is set, the element dials that number as soon as the bridge iframe reports ready; omit `to` to show a bare surface and drive `dial()` from your own button (see [section 5](#5-events-and-programmatic-interop)). Outbound dialing rides the same voice path your tenant's calls already terminate on — the element adds no new carrier.

| Attribute   | Required | Description                                                                             |
| ----------- | -------- | --------------------------------------------------------------------------------------- |
| `embed-url` | Yes      | Full URL of the CTI iframe — `https://orbit.devotel.io/cti-embed`.                      |
| `token`     | Yes      | CTI embed JWT minted by your backend.                                                   |
| `to`        | No       | E.164 number to auto-dial once the bridge is ready.                                     |
| `width`     | No       | Iframe width. Default `320px`.                                                          |
| `height`    | No       | Iframe height. Default `560px`.                                                         |
| `allow`     | No       | Iframe `allow` override — defaults to `microphone`; add `camera` for video-call embeds. |

| Event              | Fires when                                     | `event.detail`                                                                           |
| ------------------ | ---------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `orbit-ready`      | The iframe bridge handshake completes          | The ready payload (protocol version)                                                     |
| `orbit-call-state` | The call moves between states                  | The call event — `dialing`, `ringing`, `answered`, `ended`, or `failed` with its dial id |
| `orbit-error`      | The bridge reports an error                    | The error payload                                                                        |
| `orbit-expired`    | The token's expiry passes before the handshake | The expiry payload                                                                       |

### `<orbit-preference-center>`

Frames the hosted, per-contact preference page — the token-signed opt-in/opt-out page every high-volume sender already links from its email footer — inline on your own site instead of as a full-page link. Unlike the other elements there is no engine class behind this one: the hosted page is the complete UI, so the element is a thin, validated iframe wrapper. The page, the signed-link model, and the compliance surfaces an opt-out writes are documented in the [preference center guide](/guides/preference-center-opt-out-page).

| Attribute | Required | Description                                                                                                                                                                                                      |
| --------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `src`     | Yes      | The signed preference-center URL — `https://app.orbit.devotel.io/en/preferences?token=…`. Only absolute `http:`/`https:` URLs are accepted; anything else is rejected with a console warning and nothing mounts. |
| `width`   | No       | Iframe width. Default `100%`.                                                                                                                                                                                    |
| `height`  | No       | Iframe height. Default `720px`.                                                                                                                                                                                  |
| `title`   | No       | Iframe accessible title. Default `Communication preferences`.                                                                                                                                                    |

## 3. Quickstarts — one page, five tags

A complete page that registers the bundle and mounts every element. Each value tagged `YOUR_…` is minted or supplied by your backend — none of them is an API key.

```html theme={null}
<!doctype html>
<html>
  <body>
    <!-- Register the five elements — nothing else to import. -->
    <script src="/your-static-host/orbit-elements.iife.js"></script>

    <!-- Chat launcher (tenant-scoped native chat). -->
    <orbit-chat widget-id="org_xxx" position="bottom-left" primary-color="#0EA5A4"></orbit-chat>

    <!-- Verification flow (base-url = YOUR backend proxy). -->
    <orbit-verify to="+14155550100" channel="sms" base-url="/api/orbit-verify"></orbit-verify>

    <!-- Video room — give the tag a height. -->
    <div style="height: 600px">
      <orbit-video-room token="YOUR_JOIN_TOKEN" server-url="wss://media.orbit.devotel.io"></orbit-video-room>
    </div>

    <!-- Click-to-call — omit `to` to defer the dial. -->
    <orbit-click-to-call embed-url="https://orbit.devotel.io/cti-embed" token="YOUR_CTI_EMBED_JWT" to="+14155550100"></orbit-click-to-call>

    <!-- Hosted preference page, framed inline. -->
    <orbit-preference-center src="https://app.orbit.devotel.io/en/preferences?token=YOUR_SIGNED_TOKEN"></orbit-preference-center>
  </body>
</html>
```

Per-element minimal snippets, for a page that mounts one at a time:

**`<orbit-chat>`** — the launcher appears in the corner once the tag connects:

```html theme={null}
<orbit-chat widget-id="org_xxx"></orbit-chat>
```

**`<orbit-verify>`** — start on silent one-tap, fall back to an SMS code entry:

```html theme={null}
<orbit-verify to="+14155550100" channel="sms" base-url="/api/orbit-verify"></orbit-verify>
```

**`<orbit-video-room>`** — camera-off, muted entry for a lobby page:

```html theme={null}
<div style="height: 600px">
  <orbit-video-room
    token="YOUR_JOIN_TOKEN"
    server-url="wss://media.orbit.devotel.io"
    display-name="Jordan"
    start-with-camera="false"
    start-with-microphone="false"
  ></orbit-video-room>
</div>
```

**`<orbit-click-to-call>`** — bare surface; a button you own triggers the dial:

```html theme={null}
<orbit-click-to-call embed-url="https://orbit.devotel.io/cti-embed" token="YOUR_CTI_EMBED_JWT"></orbit-click-to-call>
<button id="dial">Call support</button>
<script>
  document.getElementById('dial').addEventListener('click', () => {
    document.querySelector('orbit-click-to-call').dial('+14155550100')
  })
</script>
```

**`<orbit-preference-center>`** — embed the footer-link page inside your account area:

```html theme={null}
<orbit-preference-center src="https://app.orbit.devotel.io/en/preferences?token=YOUR_SIGNED_TOKEN"></orbit-preference-center>
```

## 4. Theming

Each element keeps its engine's theming mechanism — there is no elements-level token system to learn.

* **Hex-color attribute.** `<orbit-chat>` accepts `primary-color="#0EA5A4"` (a `#RGB` / `#RRGGBB` hex value; anything else falls back to the color configured on your tenant's native-chat channel). The element forwards the attribute to the same config field the imperative class sets.
* **`data-orbit-*` CSS hooks.** The video room paints its tiles and controls with a `data-orbit-video-room` attribute on every element it renders, and the verify form does the same with `data-orbit-verify` hooks — so a plain stylesheet on your page can restyle either without touching shadow roots:

```css theme={null}
[data-orbit-video-room="controls"] { background: rgba(0, 0, 0, 0.7); }
```

* **Plain CSS on the light-dom hosts.** The elements are light-DOM custom elements — no shadow root — so your page's existing stylesheet reaches them directly. The most useful case is sizing: `<orbit-video-room>` needs an explicit height, and both iframe-backed elements (`<orbit-click-to-call>`, `<orbit-preference-center>`) take `width`/`height` attributes that become iframe dimensions.

## 5. Events and programmatic interop

The elements re-dispatch their engine's lifecycle events as native, bubbling `CustomEvent`s, so a page with no import statement reacts with a plain `addEventListener`:

```html theme={null}
<script>
  const verify = document.querySelector('orbit-verify')
  verify.addEventListener('orbit-verified', (e) => {
    window.location.href = '/welcome'
  })
  verify.addEventListener('orbit-failed', (e) => {
    showSupportLink(e.detail.reason)
  })

  document.querySelector('orbit-video-room').addEventListener('orbit-state', (e) => {
    if (e.detail.state === 'connected') hideLobbySpinner()
  })

  const cti = document.querySelector('orbit-click-to-call')
  cti.addEventListener('orbit-call-state', (e) => logCallToCrm(e.detail))
  cti.addEventListener('orbit-expired', () => refreshCtiToken())
</script>
```

Because the events bubble, a single listener on `document.body` catches every element's events — useful when a page mounts several and centralizes analytics.

For anything beyond events, drop to the imperative classes from the same package — the elements and the classes are two surfaces over one engine, so the [Web SDK reference](/sdks/web) is the authority for the full option and method list:

| Element                 | Imperative class              | Import                      |
| ----------------------- | ----------------------------- | --------------------------- |
| `<orbit-chat>`          | `OrbitWidget.init(config)`    | `@devotel-orbit/web/widget` |
| `<orbit-verify>`        | `OrbitVerify.init(config)`    | `@devotel-orbit/web`        |
| `<orbit-video-room>`    | `OrbitVideoRoom.init(config)` | `@devotel-orbit/web`        |
| `<orbit-click-to-call>` | `OrbitCTI.init(config)`       | `@devotel-orbit/web`        |

`<orbit-click-to-call>` also exposes one method on the element itself — `dial(number, crmContext)` — so a host page can trigger a call from its own button without importing the class. Calling it before the element mounts logs a warning and does nothing.

## 6. Troubleshooting

**The tag renders nothing and the console shows a warning.** Each element warns and refuses to mount when a required attribute is missing or malformed: `<orbit-chat>` logs the missing `widget-id`; `<orbit-verify>` logs the missing `to`/`base-url`; `<orbit-video-room>` and `<orbit-click-to-call>` log the missing token pair; `<orbit-preference-center>` logs that `src` must be an absolute `http(s)` URL. Open the browser console — the warning names the attribute.

**The tag is still an unknown element (`HTMLUnknownElement`).** The registration script never ran. With the IIFE bundle, check the `<script src="…orbit-elements.iife.js">` actually 200s (view-source and click the URL); with the ESM path, check `defineOrbitElements()` was called. A tag that upgrades silently but mounts nothing usually means the attribute typo above instead.

**A Content-Security-Policy blocks the widget.** The page needs CSP room for: (1) `script-src` covering wherever you host `orbit-elements.iife.js` (self-host keeps this to your own origin), (2) `frame-src` for the two iframe-backed elements' origins — `https://orbit.devotel.io` for the CTI embed and `https://app.orbit.devotel.io` for the preference center — and (3) `connect-src` for `https://api.orbit.devotel.io` plus your own verify-proxy origin for `<orbit-verify>`. The video room additionally opens a WebSocket to the media URL you pass in `server-url` — allow its host in `connect-src`.

**The `<script>` tag placement matters less than load order.** Elements mount when they connect to the document; if the registration script loads after the browser parses your tags, the tags upgrade then and mount normally. The failure mode to avoid is the reverse — a page that injects tags dynamically *without* the bundle having loaded — because nothing ever upgrades them. Load the bundle in the page `<head>` or right after `<body>` opens and any placement of the tags themselves works.

**`<orbit-video-room>` shows a collapsed blank strip.** The element is the video container; with no height the tiles have nowhere to paint. Wrap it in a `<div style="height: 600px">` or set a height on the tag itself.

**`<orbit-preference-center>` frames a blank or broken page.** Two causes, in order of frequency: (1) the `token` on the `src` is expired or was signed for a different contact — mint a fresh per-contact link (30-day expiry, scoped to exactly one contact); (2) the tenant changed the preference page's locale path and the `src` points at a removed URL — copy the current link out of the dashboard's preference-center settings rather than freezing it in a template.

**`<orbit-click-to-call>` fires `orbit-expired` immediately.** The CTI embed JWT has a short lifetime; mint it at page-render time on your backend rather than baking it into a static page, and re-fetch when the event fires.

## See also

* [Web SDK reference](/sdks/web) — the imperative classes behind every element, with the full option tables.
* [Work through the SDK catalog](/guides/developer-sdks-catalog) — pick SDK surfaces per language and copy install commands from the dashboard.
* [Add a "video call us" button to your website](/guides/embed-video-consultation-button) — the join-token minting flow `<orbit-video-room>` depends on.
* [Preference center: the public opt-in/opt-out page](/guides/preference-center-opt-out-page) — the hosted page behind `<orbit-preference-center>` and the compliance surfaces its opt-outs write.
* [Native chat channel configuration](/guides/native-chat-widget-channel-config) — the console surface whose settings `<orbit-chat>` inherits.
