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

# Website Widget (concierge)

> Embed an AI concierge on your website — visitors can talk, chat, or video with it, grounded in your content. Configure its modes, voice, and branding from the dashboard, let it book meetings on your calendar, and review every conversation.

# Website Widget

Add a floating assistant to your website so visitors can **talk, chat, or
video** with an AI concierge — ask questions, get guided, and hear answers
grounded in your own content. You create and configure the widget in Erdo, then
drop one line of code on your site. Everything runs through Erdo; your site never
loads a third-party SDK or holds any keys.

<Info>
  The widget is a **website embed**, separate from [outbound phone
  calls](/voice). Same agent technology, different surface: here a visitor starts
  the conversation from your page.
</Info>

## Create a website deployment

Everything below — modes, voice, and branding — is configured in **Agents →
Deployments**, so the embed snippet stays a single line and you never have to
hand-edit code to change how the widget looks or sounds. A deployment is attached
to a durable Erdo agent: edit the agent once to change its identity and
instructions everywhere it is deployed. Website visitors can search only
knowledge your organization has explicitly marked **Public**.

<Steps>
  <Step title="Open Agents → Deployments">
    Click **Website**, choose the agent visitors should meet, and give the
    deployment a name.
  </Step>

  <Step title="Choose its modes">
    Pick any combination of **Voice** (talk out loud), **Chat** (type messages),
    and **Video** (an on-screen avatar). With more than one selected, visitors get
    a switcher to move between them.
  </Step>

  <Step title="Pick a voice">
    Search the voice library and **preview** candidates, then select one — or leave
    it on the default. This is the spoken voice for the voice mode.
  </Step>

  <Step title="Pick a video avatar (video mode only)">
    When **Video** is enabled, choose the on-screen avatar visitors see. The picker
    shows each available avatar as a thumbnail that plays a short preview on hover —
    select one, or leave it on the default avatar.
  </Step>

  <Step title="Configure the welcome and rules">
    Set its greeting and any channel-specific compliance guardrails. What the
    concierge does and how it behaves come from the attached agent's Instructions.
  </Step>

  <Step title="Brand it">
    Set the accent colour and the header / button / intro copy. A **live preview**
    in the dialog shows your changes before you save — see [Match your
    brand](#match-your-brand).
  </Step>

  <Step title="Let visitors book meetings (optional)">
    Connect a calendar and enable meeting scheduling so the assistant can offer
    real open slots and book meetings during the conversation — see [Book meetings
    on your calendar](#book-meetings-on-your-calendar).
  </Step>

  <Step title="Set who can embed it (optional)">
    Add the **allowed origins** for your site — see [Allowed
    origins](#allowed-origins) below. Leave it empty while testing.
  </Step>

  <Step title="Copy the embed snippet">
    Each widget has a one-line `<script>` snippet. Copy it from the widget card.
  </Step>
</Steps>

## Widget settings

Every setting lives on the widget itself, so changing any of them never touches
the snippet on your site — the embedded widget picks the new configuration up
automatically. The full set, and what each controls:

| Setting                | What it controls                                                                                                                                                                                    | Default                         |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- |
| **Name**               | The widget's name in your dashboard — also how you refer to it when asking Erdo about its conversations.                                                                                            | —                               |
| **Modes**              | Which of **Voice**, **Chat**, and **Video** the widget offers. More than one gives visitors a switcher; see [Conversation modes](#conversation-modes--voice-text-and-video).                        | Voice only                      |
| **Greeting**           | The first thing the assistant says when a conversation starts. Translated automatically for each extra language you configure.                                                                      | "Hi! How can I help you today?" |
| **Agent**              | The durable agent whose identity and instructions answer every mode. Website knowledge is limited to approved **Public** records.                                                                   | Required                        |
| **Agent instructions** | What the concierge helps with, its identity and tone. Edit these on the attached agent, not on the deployment.                                                                                      | From the agent                  |
| **Guardrails**         | Non-negotiable rules the assistant must always follow (compliance wording, topics to avoid).                                                                                                        | Empty                           |
| **Voice**              | The spoken voice, picked from the voice library with preview.                                                                                                                                       | Account default voice           |
| **Languages**          | A primary language plus extra languages — up to 10 in total, each with its own voice. See [Language & translation](#language--translation).                                                         | Single language (English)       |
| **Video avatar**       | The on-screen avatar for the video mode.                                                                                                                                                            | Default avatar                  |
| **Branding**           | Accent colour, header title, button labels, intro line, launcher copy, and launcher face. Copy fields take up to 200 characters; colours are hex values. See [Match your brand](#match-your-brand). | Built-in look                   |
| **Meeting scheduling** | Whether visitors can book meetings, on which connected calendar — Google Calendar, Calendly, or Cal.com. See [Book meetings on your calendar](#book-meetings-on-your-calendar).                     | Off                             |
| **Allowed origins**    | Which websites may embed the widget, up to 50 origins. Empty allows any site. See [Allowed origins](#allowed-origins).                                                                              | Empty — any site                |
| **Daily session cap**  | How many conversations the widget will start per day, across all modes. `0` means unlimited. See [Usage limits](#usage-limits).                                                                     | 500                             |
| **Enabled**            | Turn the widget off without removing the snippet — a disabled widget stops starting conversations until you enable it again.                                                                        | Enabled                         |

## Embed it on your site

Paste the snippet once into your site's HTML, just before the closing
`</body>` tag (or in your CMS / theme's "custom code" / footer area), on every
page where you want the assistant:

```html theme={null}
<script
  src="https://app.erdo.ai/voice-widget.js"
  data-erdo-voice-widget="vw_your_widget_key"
  async
></script>
```

It adds a floating launcher at the bottom of the page — a compact card with the
concierge's animated **orb** (or a **face**, if you pick one) above a
call-to-action — and nothing else on your page changes. Visitors tap it to
start. The widget's modes, voice, and branding all come from its settings — the
snippet itself never needs to change when you tweak them.

To keep the corner unobtrusive, the card **folds into a small pill** once the
visitor starts reading — by default when they've scrolled 10% of the way down
the page (tune it with `data-erdo-minimize-scroll`, below), or after 5 seconds
on a page they don't scroll, whichever comes first. The card
morphs into the pill in one continuous motion — just the orb, the
call-to-action, and the mode it offers. Hovering the pill brings the full card
back; moving away folds it again. Tapping either the pill or the card opens the
widget exactly the same way, and the fold respects a visitor's *reduce motion*
setting (it snaps between states instead of animating).

By default the launcher shows the plain animated orb. In the widget's
**Appearance** settings you can give the concierge a face instead — pick one of
a set of preset faces. A **video** widget is the one exception: it always shows
its selected avatar's own photo, so the launcher matches the avatar visitors are
about to meet. Whatever you choose appears inside the conversation too — in the
orb a visitor taps to start a call and while the call is live — so the concierge
stays one consistent presence.

<Tip>
  In the dashboard, each widget has an **embed builder** ("Customize" next to the
  snippet). It generates this snippet for you and, if you want per-page tweaks, adds
  the `data-erdo-*` overrides below — hide the launcher, set a language, or override
  copy/colours — with a live preview. Leave a field blank to inherit the widget's
  saved setting.
</Tip>

<Info>
  Pages you build **inside Erdo** (the "make me a page" flow) that embed a widget
  work automatically — Erdo's own page hosts are always allowed, so you don't need
  to add them to the allowed origins.
</Info>

## Preview your widget on a demo page

Before you drop the snippet on your own site — or when you just want to show a
colleague — each website deployment card in **Agents → Deployments** has a **Preview**
button. It opens a public demo page in a new tab: a placeholder website for a
fictional real-estate development with **your** widget embedded on it, running
exactly as it would on a real page. The widget pulls its modes, voice, and
branding from its settings and grounds its answers in the demo site's content,
so you can start a conversation and ask about the development's prices, viewing
hours, or buying process to hear how the concierge behaves in context.

The demo page is a **shareable, no-login link** — anyone you send it to can open
it and try the widget without an Erdo account (the same `vw_` key that's designed
for public embedding). It's the quickest way to get sign-off before you touch your
own site's code.

The page has a **Floating / Inline panel** toggle at the top so you can see both
embed styles: the default floating launcher in the bottom corner, and the
always-open [inline panel](#embed-it-inline-as-a-panel-no-floating-bubble) that
fills a section of the page. Switching between them reloads the demo with that
style applied.

<Info>
  The demo works even while your **allowed origins** are locked down to your own
  site — Erdo's own hosts are always allowed, so the demo page never needs to be in
  the list. If the widget is **disabled**, the demo page still loads the placeholder
  site but shows a banner instead of the widget; enable the widget to try it.
</Info>

## Conversation modes — voice, text, and video

One widget, one snippet — the same assistant can **talk** (voice), **chat**
(text), or appear as a **video** agent. You choose which modes a widget offers;
all three are answered by the **same** assistant, grounded in the same Knowledge,
so the experience is consistent however a visitor reaches out.

| Mode      | What the visitor does                              | Needs               |
| --------- | -------------------------------------------------- | ------------------- |
| **Voice** | Taps and talks; hears a spoken reply               | Microphone          |
| **Text**  | Types a message; reads replies (with images/cards) | Nothing             |
| **Video** | Speaks with an on-screen avatar in real time       | Microphone + camera |

When a widget offers more than one mode, visitors see a small **Talk / Chat /
Video** switcher; with a single mode they just get that experience. Permissions
are only requested when a visitor actually starts that mode — a voice- or
text-only widget never prompts for the camera. For the **Video** mode you can also
pick which on-screen **avatar** appears (see the avatar step above); leave it on the
default to use Erdo's standard avatar.

<Info>
  **Your existing snippet doesn't change.** Turning on text or video for a widget is
  a setting on the widget itself — the `<script>` tag you've already embedded picks
  the new modes up automatically. Widgets default to **voice only**, so nothing
  changes until you opt in.
</Info>

## Give the assistant knowledge

The widget answers from your workspace's [Knowledge](/knowledge): publish the
facts it may use, attach photos and videos to them, and the assistant both
answers visitors' questions and **shows** them what it's describing. You never
wire content to a specific widget — every widget in the organization draws from
the same published Knowledge.

### What the assistant can answer from

The widget talks to anonymous visitors on the open web, so it deliberately does
**not** see everything in your workspace. It answers only from Knowledge that is
both:

1. **Public** — the entry's visibility is set to *Public*, opting it into
   external surfaces. Every new Knowledge entry starts as *Workspace*
   (organization-internal), so nothing is exposed by default.
2. **Approved** — making an entry public approves a draft in the same step, so
   publishing is all you need; entries approved through the review flow also
   qualify once made public.

An entry that is approved but still *Workspace* is invisible to the widget — if
the assistant keeps saying it doesn't have information you know you've added,
this is almost always why.

To publish content, either ask Erdo in chat (for example *"make the property
gallery and FAQ Knowledge public"*) or open [Knowledge](/knowledge), select the
entry, and change its visibility to **Public**. Only publish customer-facing
facts you're happy to tell any visitor — products, pricing, opening hours,
locations, policies, FAQs, photo galleries. Internal notes and anything
sensitive should stay *Workspace*.

### Showing photos and videos

In every mode — talking, typing, or on a video call — the assistant can **show**
what it's describing: a photo of the lobby, a residence, a product — pulled
straight from your Knowledge. To give it something to show, attach the images
or videos to a Knowledge entry (a photo gallery, a product sheet) and publish
it like any other entry. The assistant finds the best-matching media and
displays it in the widget; you don't configure anything per widget, and it only
ever shows media attached to Knowledge that is public and approved (see
[What the assistant can answer from](#what-the-assistant-can-answer-from)). On a
video call, the avatar shrinks aside so the photos take the stage while it keeps
talking.

In the compact floating panel these appear as cards in the conversation. When the
widget is **expanded** to fullscreen (or embedded in a page tab), it opens a
two-pane **showcase**: the conversation stays on the left while a large image
**portal** fills the right, with a row of **thumbnails** to click through and any
matching **video** playable inline. Each item can carry a short caption and a
highlight (for example a price). Tap the main image to view it full-size.

## Book meetings on your calendar

A widget can let visitors **book a meeting with your team during the
conversation** — in any mode. A visitor asks about a demo, the assistant checks
your live calendar availability, offers real open slots, collects the visitor's
name and email, and books the meeting. The visitor gets the calendar invite by
email from your calendar provider; nothing else is needed from them.

Booking works the same way whether the visitor is talking, typing, or on a video
call, and it always lands on **your organization's** connected calendar. Three
calendar providers are supported, each with its own way of describing what gets
booked:

| Provider            | What you pick                                        | How availability works                                                                                 |
| ------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| **Google Calendar** | A calendar to book on, plus a default meeting length | Free/busy on that calendar — any open slot can be offered                                              |
| **Calendly**        | One of your event types                              | Your Calendly availability rules for that event type (creating invitees requires a paid Calendly plan) |
| **Cal.com**         | One of your event types                              | Your Cal.com availability rules for that event type                                                    |

To set it up:

1. Connect the calendar provider in **Settings → Connectors** (once, for the
   organization).
2. Enable scheduling on the widget and pick the provider — plus the event type
   (Calendly / Cal.com) or the calendar and default meeting length (Google). Two
   ways: ask Erdo in chat (*"let visitors book meetings through the Acme widget
   using our Cal.com"*), or edit the deployment in **Agents → Deployments** (the
   *Meeting scheduling* section).

The assistant only ever offers times it just read from your calendar — if it
can't read availability (for example the connection was revoked), it says so and
suggests another way to get in touch rather than guessing. Times are offered in
the visitor's own timezone.

### The "Book a call" form

Some visitors would rather pick a time straight away than talk it through. So
whenever meeting scheduling is enabled, the widget also shows a **Book a call**
option they can tap directly — a *"Prefer to schedule? Book a call"* line on the
launcher card, and a calendar button next to the chat input once the widget is
open. Either one drops the visitor into a short booking form, without starting a
call first. (When scheduling is off, neither appears — nothing about the widget
changes.)

The form is three quick steps, all in the visitor's own language and timezone:

<Steps>
  <Step title="Their details">
    Name and email (a phone number is optional) — the same details the assistant
    would otherwise collect in conversation.
  </Step>

  <Step title="Pick a time">
    A strip of the next two weeks of days, each showing the real open slots read
    live from your calendar, in the visitor's timezone. Days your schedule has no
    openings for — weekends, days off, fully booked days — are left out entirely,
    so what's offered always follows your calendar settings, and there's a retry
    if availability can't be reached.
  </Step>

  <Step title="Confirmation">
    A confirmation that the meeting is booked, with the day and time and a note
    that the calendar invite is on its way by email.
  </Step>
</Steps>

The booking lands on **your organization's** connected calendar exactly like a
booking made in conversation — same providers, same live availability, same
email invite to the visitor (see the provider table above). Whichever way a
visitor books, the meeting shows up in one place on your calendar.

### Booking leads

Everyone who fills in the booking form is also **saved as a lead** — the moment
they enter their name and email and continue to the time picker, before they've
even chosen a slot. So a visitor who starts booking but doesn't finish still
becomes a contact you can follow up with.

Each lead is one row in a dataset: email (the row's key — the same person never
appears twice), first and last name, phone if given, the page they were on,
campaign parameters (`utm_*`, `gclid`, `fbclid`), and a **`booked`** column that
flips to true — with the meeting's start time — once they confirm a slot. That
makes the follow-up queries trivial: *"who started booking this week but didn't
finish?"* is just the rows where `booked` is false.

Where the leads go is a widget setting (**Settings → Voice Widgets → Edit →
Meeting scheduling → Save booking leads**):

* **Automatic** (the default) — Erdo creates a **"Widget leads"** dataset for
  your organization the first time a lead arrives, complete with a filter that
  keeps test traffic out of your counts.
* **An existing dataset** — pick any dataset you already collect leads in (for
  example the one your Erdo landing page's lead form writes to). Widget leads
  merge in by email, so a visitor who uses both the page form and the booking
  form still ends up as one row.

Lead saving is on by default whenever meeting booking is enabled; untick **Save
booking leads** to turn it off.

## Conversation transcripts & follow-up

Every widget conversation — voice, text, or video — is **saved in your
workspace**, so a visitor question is never lost when the tab closes. Each
conversation keeps its full transcript along with the mode it used, the page it
started on, when it happened, how long it lasted, and a short auto-generated
**summary** of what the visitor wanted.

You review them by asking Erdo in chat:

* **List conversations** — *"what did visitors ask the Acme widget this
  week?"*, *"show me yesterday's widget conversations"*. Erdo lists them newest
  first with their summaries, so you can scan what visitors are asking without
  opening each one.
* **Read one back** — *"pull up the conversation where someone asked about
  enterprise pricing"*, or point at one from the list. Erdo reads back the full
  exchange, exactly as the visitor and the assistant said it.

Because the transcripts are ordinary workspace data for Erdo, you can go beyond
listing: ask it to summarize the week's themes, count how many visitors asked
about a topic, or draft follow-up emails from what was said.

The assistant also **asks visitors for their name and the best way to reach
them** — naturally, once it understands what they're looking for, and whether or
not meeting booking is enabled. It never withholds an answer to get contact
details and drops the subject if the visitor declines. Details a visitor shares
are part of the saved transcript, so following up is as simple as asking Erdo
*"which widget visitors left contact details this week?"*.

Transcripts are visible to your workspace members through Erdo — treat them like
any other customer communication.

### Read conversations programmatically

The same list-and-read surface is available over the [REST API](/api/overview)
and to MCP clients, so a scheduled automation or an external dashboard can pull
conversations without going through chat. Both are org-scoped to the caller's
credentials and identify a widget by its **name or public key** (`vw_…`), never
an internal id:

* **List** — `GET /v1/voice/widget-conversations?widget=Acme%20Concierge` (or a
  `POST` with a JSON body) returns each conversation's session id, mode, timing,
  duration, and summary, newest first and paginated. MCP tool:
  `erdo_voice_widget_conversation_list`.
* **Read one** — `POST /v1/voice/widget-conversations/get` with `{"widget": "…",
  "session_id": "…"}` returns the rendered transcript, the raw transcript, and —
  for voice conversations — a structured **`turns`** array. MCP tool:
  `erdo_voice_widget_conversation_get`.

Each entry in `turns` is one assistant turn that drove the language model, with
the fields ops teams watch for latency and cost monitoring:

| Field                                                | Meaning                                                                                                                                                                                                                                         |
| ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ttfb_seconds`                                       | The model's time-to-first-byte for that turn — the latency a visitor feels before the assistant starts answering.                                                                                                                               |
| `input_tokens`, `cache_read_tokens`, `output_tokens` | Token usage for the turn, summed across the models that served it, so you can track spend.                                                                                                                                                      |
| `models`                                             | Which model(s) answered the turn.                                                                                                                                                                                                               |
| `rescued` / `llm_override`                           | Set when the model cascade **failed over** to a backup model because the configured primary erred or timed out — `llm_override` names the model that stepped in. A run of `rescued` turns means the primary model was unhealthy at that moment. |
| `after_tool_result`                                  | The turn followed a tool result the model had to re-read (for example a calendar lookup), so a higher `ttfb_seconds` there is expected rather than a regression.                                                                                |

A typical use is an hourly automation that reads recent conversations and
appends the per-turn latency rows to a dataset, so you can chart TTFB and
fail-over rate over time. Text and video conversations carry no model metrics, so
their `turns` array is empty.

## Measure widget activity in Google Analytics

If Erdo has set up a **Google Analytics** property for your organization — the
same property your Erdo-built landing pages report to — your widget sends its
activity there automatically. You don't add a tag or a tracking id: the embed
already knows your property, so every conversation shows up in Google Analytics
alongside the rest of your site's traffic.

<Info>
  Analytics only flow when Erdo has provisioned a Google Analytics property for
  your organization. If your org has no property yet, the first widget load
  provisions one automatically — the same way your landing pages do — so there is
  nothing to configure.
</Info>

The widget reports these events. Every one also carries the widget's public key
(`widget_id`, the `vw_…` value) and its name (`widget_name`), so you can tell
your widgets apart when you have more than one:

| Event                                 | Fires when                                                                                                                                           | Details it carries                                                                                                                 |
| ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `voice_widget_open`                   | A visitor opens the widget — taps the launcher or your own button                                                                                    | —                                                                                                                                  |
| `voice_widget_call_start`             | A conversation begins: a voice or video call connects, or the visitor sends their first chat message                                                 | `modality`: `voice`, `video`, or `text`                                                                                            |
| `voice_widget_call_end`               | The conversation ends by any route — the visitor hangs up, closes the widget, or an error stops it; a chat "ends" when the visitor closes the widget | `modality`, and `duration_seconds` (how long it lasted)                                                                            |
| `voice_widget_message`                | Each chat message a visitor types                                                                                                                    | `modality`: `text`                                                                                                                 |
| `voice_widget_book_open`              | A visitor opens the "Book a call" form                                                                                                               | `source`: `launcher` (the *Book a call* link on the closed launcher card) or `expanded` (the calendar icon inside the open widget) |
| `voice_widget_book_details_completed` | The visitor fills in their name and email and continues to the time picker                                                                           | —                                                                                                                                  |
| `voice_widget_book_confirmed`         | The meeting is booked on your calendar                                                                                                               | —                                                                                                                                  |

### Find widget activity in GA4

Every widget event name starts with `voice_widget_`, so in **Reports →
Engagement → Events** you can filter on that prefix to see just the widget's
activity, and open any event to break it down by its details — `modality` to
split voice from chat from video, `duration_seconds` to see how long calls run,
`widget_name` to compare widgets. Because these land in your own property, you
report on them exactly like any other event: build an exploration, add them to a
funnel, or chart `voice_widget_call_start` over time.

### Turn widget visitors into a Google Ads audience

The reason the events go to Google Analytics rather than staying inside Erdo is
that GA4 is where audiences are built and shared to ads. In **Admin → Audiences**
you can define an audience from any widget event — for example *visitors who
triggered `voice_widget_call_start` in the last 30 days* — and, once your GA4
property is linked to your Google Ads account (**Admin → Product links → Google
Ads**), that audience becomes available in Google Ads for remarketing. So a
visitor who started a conversation but didn't convert can be followed up with an
ad, with no extra tag on your site.

<Info>
  The widget deliberately carries **no** Google Ads tag of its own — everything
  flows through your Google Analytics property, and audiences reach Google Ads
  through the standard GA4 ↔ Ads link. One property, one place to segment.
</Info>

### Your widget events as an Erdo dataset

The same events also land in a **"Widget events" dataset** in your Erdo
workspace, created automatically the first time your widget loads. Each row is
one event with its detail (`modality`, `duration_seconds`, the booking form's
`source`), the page it happened
on, the visitor's referrer and campaign parameters (`utm_*`, `gclid`), and
enriched dimensions Erdo stamps on arrival — device type, browser, language, and
country. A default *Exclude test traffic* filter keeps your own testing out of
reports.

Because it's a regular dataset, you can just ask Erdo about it in chat — *"how
many widget conversations did we have last week, by page?"*, *"average call
duration by campaign?"* — chart it on a dashboard, or query it in SQL. On pages
Erdo built for you, widget rows share a session key with the page's own event
rows, so a single query gives the full journey: page view → widget opened →
conversation → lead. On pages Erdo also runs session analytics, widget events
appear on the same session timeline as the page recording.

### Turn analytics off for a page

If you don't want a particular embed to report anything, add
`data-erdo-tracking="off"` to the script tag. The widget still works exactly as
before; it just sends no analytics events from that page.

```html theme={null}
<script
  src="https://app.erdo.ai/voice-widget.js"
  data-erdo-voice-widget="vw_your_widget_key"
  data-erdo-tracking="off"
  async
></script>
```

<Info>
  Analytics come from **Erdo's** widget embed. A page that embeds a voice provider
  directly, rather than through the Erdo snippet, runs none of Erdo's code and so
  sends none of these events.
</Info>

## Match your brand

Set the widget's accent colour and copy in **Agents → Deployments** — the
dialog shows a **live preview** as you type, and the branding is saved on the
widget, so the same one-line snippet always reflects your latest look:

| Setting                       | What it does                                                                                                                                                                                                                                              |
| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Accent colour**             | Your brand colour — drives the orb, the launcher glow, the active mode tab, focus rings, chat bubbles and the send button.                                                                                                                                |
| **Header title**              | Text in the panel header.                                                                                                                                                                                                                                 |
| **Start / End button labels** | The call button wording.                                                                                                                                                                                                                                  |
| **Intro line**                | Short line shown before the conversation starts.                                                                                                                                                                                                          |
| **Launcher face**             | The face shown on the launcher and in the in-conversation orb — choose from a set of preset faces, or leave it on the default: the plain animated orb with no face. **Video** widgets ignore this and follow their selected avatar's photo automatically. |

The widget runs in a sandboxed frame, so its inside is styled by these settings,
not your page's CSS. To target the widget's **container** (position, size,
z-index) from your own CSS, it has a stable id/class: `#erdo-voice-widget` /
`.erdo-voice-widget`.

### Per-page overrides (advanced)

If a single widget appears on pages that need different branding, you can
override any branding field for one page with a `data-erdo-*` attribute on the
embed script. A present attribute wins over the widget's saved setting; anything
you omit falls back to the saved value.

```html theme={null}
<script
  src="https://app.erdo.ai/voice-widget.js"
  data-erdo-voice-widget="vw_your_widget_key"
  data-erdo-accent="#215834"
  data-erdo-bg="#163b24"
  data-erdo-text="#f7f1e6"
  data-erdo-title="Talk to our team"
  data-erdo-start-label="Start"
  async
></script>
```

| Attribute                                       | Overrides                                                                                                                                                                                                                                                                                                                                                      |
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `data-erdo-accent`                              | Your brand colour (hex) — drives the orb, launcher glow, active mode tab, focus rings, chat bubbles and the send button.                                                                                                                                                                                                                                       |
| `data-erdo-bg`                                  | Panel background (hex). Set a dark value to run the widget dark.                                                                                                                                                                                                                                                                                               |
| `data-erdo-text`                                | Main text colour (hex).                                                                                                                                                                                                                                                                                                                                        |
| `data-erdo-muted`                               | Secondary / status text colour (hex).                                                                                                                                                                                                                                                                                                                          |
| `data-erdo-border`                              | Card and control border colour (hex).                                                                                                                                                                                                                                                                                                                          |
| `data-erdo-title`                               | Header text.                                                                                                                                                                                                                                                                                                                                                   |
| `data-erdo-start-label` / `data-erdo-end-label` | Call button labels.                                                                                                                                                                                                                                                                                                                                            |
| `data-erdo-intro`                               | Short greeting line shown before the call starts.                                                                                                                                                                                                                                                                                                              |
| `data-erdo-cta`                                 | Launcher headline. Set it to suit the page — e.g. `Ask about this home`, `Ask about this building`, `Ask about our menu`. Defaults to `Ask a question`.                                                                                                                                                                                                        |
| `data-erdo-tagline`                             | Launcher sub-line. Defaults to `Voice AI · replies instantly`.                                                                                                                                                                                                                                                                                                 |
| `data-erdo-face`                                | Path to a preset face image (e.g. `/agents/Agent1.jpeg`) to show on the launcher and in the orb for this page, or `none` for the plain orb with no face. Overrides the widget's configured face — and, because a per-page face always wins, it also overrides a video widget's avatar-derived face on that page. Leave it off to inherit the widget's setting. |
| `data-erdo-launcher`                            | Set to `none` to hide the built-in launcher. The widget mounts hidden and you open it from your own button or script (see below).                                                                                                                                                                                                                              |
| `data-erdo-minimize-scroll`                     | How far down the page (percent, `1`–`100`) a visitor scrolls before the launcher card folds into the compact pill. Defaults to `10`.                                                                                                                                                                                                                           |
| `data-erdo-embed`                               | Set to `panel` to embed the widget inline as an always-open panel that fills a container, instead of a floating launcher (see [Embed it inline as a panel](#embed-it-inline-as-a-panel-no-floating-bubble)).                                                                                                                                                   |
| `data-erdo-target`                              | With `data-erdo-embed="panel"`, a CSS selector for the container to mount the panel into (e.g. `#concierge`). Defaults to the embed script's parent element.                                                                                                                                                                                                   |
| `data-erdo-lang`                                | Language as an ISO 639-1 code (e.g. `es`, `pt`) — translates the widget's own chrome and makes the voice/video assistant speak that language. If omitted, the widget inherits the page's `<html lang>`. See [Language & translation](#language--translation).                                                                                                  |

Together, `data-erdo-bg` / `text` / `muted` / `border` (plus `accent`) let you run
the widget in a fully dark or branded palette. Colours must be hex; anything else
is ignored and the default is kept.

<Warning>
  Style only the **container box** (position, size, z-index, shadow) with your
  CSS. Never apply a colour `filter` — `hue-rotate`, `invert`, `sepia`,
  `grayscale` — to the widget element to recolour it. A filter on the iframe
  re-tints its **entire** rendered surface, including the photos the assistant
  shows during a call, so a blue sky turns orange and green turns purple. Use the
  `data-erdo-*` colour attributes above instead — they restyle the chrome without
  ever touching the images.
</Warning>

## Language & translation

The widget speaks your visitor's language on two levels: its own **chrome** (buttons,
status, hints, placeholders) and the **assistant's replies**.

**It follows the page automatically.** If your page declares a language —
`<html lang="es">`, `<html lang="pt-BR">` — the widget picks it up with no extra
configuration: the chrome is translated, and both the **voice** and **video** avatar
conversations speak that language. Set `data-erdo-lang` on the embed to override the
page's language for the widget specifically:

```html theme={null}
<script
  src="https://app.erdo.ai/voice-widget.js"
  data-erdo-voice-widget="vw_your_widget_key"
  data-erdo-lang="pt"
  async
></script>
```

**Your own copy always wins.** Any text you customized — in the widget settings or via
a `data-erdo-*` attribute (title, intro, launcher CTA/tagline, button labels) — is shown
exactly as you wrote it and is never auto-translated. Only the copy you leave at its
default gets the built-in translation. So a single widget can serve a localized page in
that language out of the box, while still honouring any wording you set yourself.

Translations currently ship for a starter set of languages; any language not yet
translated falls back to English per string (a partial translation never shows a blank).

**Give each language its own voice.** In the widget's settings, under **Voice &
languages**, you set a primary language and can add extra languages — up to 10 in
total — each with its own voice picked from the voice library. The agent then answers
every configured language in that language's voice (so Spanish sounds like a native
Spanish speaker, not an English voice reading Spanish), and your greeting is translated
into each language automatically. This is one agent serving all of them: a
`data-erdo-lang` page (or a visitor who switches language) is met in the right voice
with no per-page setup. Leave the extra languages empty and the widget stays
single-language.

## Open it from your own button, script, or agent

By default the widget shows a floating launcher. You can also drive it yourself —
useful for a custom "Talk to us" button, a help menu, or an in-page assistant.

**Declaratively** — add a `data-erdo-voice-*` attribute to any element; clicking it
controls the widget (no JavaScript needed):

```html theme={null}
<button data-erdo-voice-open>Talk to us</button>
<button data-erdo-voice-expand>Open fullscreen</button>
```

**Programmatically** — call the global API from any script:

```js theme={null}
window.ErdoVoiceWidget.open();      // open the panel and start the call
window.ErdoVoiceWidget.close();     // close it
window.ErdoVoiceWidget.toggle();    // open if closed, close if open
window.ErdoVoiceWidget.expand();    // open fullscreen (roomier, larger photos)
window.ErdoVoiceWidget.collapse();  // back from fullscreen to the panel
```

Opening the widget — whether the visitor taps the launcher or you call `open()` /
`toggle()` / `expand()` — goes straight into the call: on a voice widget it asks for
the microphone and connects, so there's no second tap on the orb. If the visitor
declines the microphone prompt, the call ends and the widget stays open on its
maximized screen, where they can type a question in chat or tap the orb to try the
call again. (Because a browser only starts audio in response to a real click, call
`open()` from within a click handler so the microphone prompt appears reliably.)

Calls made before the widget finishes loading are queued and run once it's ready.
Visitors can also expand to fullscreen with the ⤢ button in the widget header,
and tap any photo to view it full-size.

### Hide the built-in launcher and use your own call-to-action

Add `data-erdo-launcher="none"` to the embed to suppress the floating pill entirely.
The widget mounts hidden — no launcher, and nothing that intercepts clicks — so your
page owns the call-to-action and opens the widget whenever you choose:

```html theme={null}
<script
  src="https://app.erdo.ai/voice-widget.js"
  data-erdo-voice-widget="vw_your_widget_key"
  data-erdo-launcher="none"
  async
></script>

<!-- Your own CTA — a hero button, a nav link, anything -->
<button data-erdo-voice-open>Chat with our team</button>
```

Or open it from a script — for example on scroll, after a delay, or from your own UI:

```js theme={null}
window.ErdoVoiceWidget.open();
```

**Keep your CTA in sync.** The widget can be closed by the visitor too (the ✕, the
Escape key, or clicking outside it). Listen for state changes so your button always
reflects whether the widget is open:

```js theme={null}
document.addEventListener('erdo-voice:state', function (e) {
  // e.detail.open is true when the widget is open, false when closed
  myButton.classList.toggle('is-active', e.detail.open);
});

// Or register a callback / read the current state directly:
window.ErdoVoiceWidget.onState(function (open) { /* ... */ });
window.ErdoVoiceWidget.isOpen; // boolean
```

### Embed it inline as a panel (no floating bubble)

Add `data-erdo-embed="panel"` to mount the widget **inline, as an always-open panel that
fills a container** — a concierge section, a sidebar, a modal you own — instead of a
floating launcher. Point it at the container with `data-erdo-target="#your-element"`
(or, with no target, it mounts into the embed script's own parent element):

```html theme={null}
<!-- The container owns the panel's size; the widget fills it. Give it a height. -->
<div id="concierge-panel" style="min-height: 560px"></div>

<script
  src="https://app.erdo.ai/voice-widget.js"
  data-erdo-voice-widget="vw_your_widget_key"
  data-erdo-embed="panel"
  data-erdo-target="#concierge-panel"
  data-erdo-accent="#A67C1B"
  data-erdo-title="Aurelia Concierge"
  async
></script>
```

The panel is always open, has no launcher pill, and never takes over the viewport — the
container controls its size (the iframe fills its width and height, so give the container
a height). The panel shows its own **Start call** button on an orb: voice **and** video
both start on a tap, never automatically — so the panel can sit in the page without
grabbing the microphone on load. Every appearance attribute above still applies, including
the dark panel chrome (`data-erdo-bg` / `-text` / `-muted` / `-border`) if you want it to
read as part of the section rather than a light box.

## Allowed origins

**Allowed origins** restrict which websites may embed this widget, so someone
can't copy your snippet onto their own site and run up your usage. Leave the list
**empty to allow any site** (handy while testing); add origins to lock it down —
up to 50 per widget.

The rules — the widget shows an error and won't start on a site that doesn't
match:

* **Scheme + domain only.** Enter the full origin including `https://`, with no
  path: `https://acme.com`, not `acme.com` and not `https://acme.com/contact`.
* **No wildcards.** `*.acme.com` is not supported.
* **Matching is exact, and subdomains count as different sites.**
  `https://acme.com` does **not** cover `https://www.acme.com` or
  `https://shop.acme.com` — add each one you actually use, on its own line.
* Matching ignores case and a trailing `/`.

<Warning>
  The most common mistake: adding `https://acme.com` but serving your site from
  `https://www.acme.com` (or vice-versa). They're different origins — list every
  hostname your visitors actually load.
</Warning>

**Example** — a site served from both the apex and `www`:

```
https://acme.com
https://www.acme.com
```

## Usage limits

Each widget has a **daily session cap** (how many conversations it will start per
day) to keep usage predictable — **500 per day** unless you change it, and `0`
means unlimited. Every mode counts toward the same cap — a voice call, a text
chat, and a video session each use one conversation. When the cap is reached, the
widget tells visitors it's unavailable until the next day. Raise or lower it in
the widget's settings.

## Troubleshooting

| You see                                                     | What to do                                                                                                                                                            |
| ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| "This site is not allowed to embed this voice widget"       | The page's origin isn't in **Allowed origins**. Add the exact origin (with `https://`, and the right subdomain — see above), or empty the list to allow any site.     |
| The mic button doesn't appear                               | Confirm the `<script>` snippet is on the page and the `data-erdo-voice-widget` key matches your widget.                                                               |
| "Reached its daily limit"                                   | The daily session cap is used up for today; raise it in the widget's settings if needed.                                                                              |
| The assistant says it doesn't have information you've added | The Knowledge entry is probably still *Workspace* — set its visibility to **Public** (see [What the assistant can answer from](#what-the-assistant-can-answer-from)). |
| Nothing happens for any widget                              | Voice may not be enabled for your organization — contact your Erdo admin.                                                                                             |
