Skip to main content

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.
The widget is a website embed, separate from outbound phone calls. Same agent technology, different surface: here a visitor starts the conversation from your page.

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

Open Agents → Deployments

Click Website, choose the agent visitors should meet, and give the deployment a name.
2

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

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

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

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

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

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

Set who can embed it (optional)

Add the allowed origins for your site — see Allowed origins below. Leave it empty while testing.
9

Copy the embed snippet

Each widget has a one-line <script> snippet. Copy it from the widget card.

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:

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

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 that fills a section of the page. Switching between them reloads the demo with that style applied.
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.

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

Give the assistant knowledge

The widget answers from your workspace’s 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, 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. Your public Knowledge does one more thing: Erdo distils it into a short brief the assistant carries with it, so the everyday questions — what you do, where you are, when you’re open — come back instantly instead of waiting on a lookup. The brief refreshes on its own whenever your public Knowledge changes, with nothing to re-publish or re-configure. You can read the current brief any time in the Public Knowledge panel on the Agents page. It’s an overview, not a replacement: specifics like prices, dates and availability, and anything the visitor asks to see, still come from a live search of the entries themselves.

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). 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 books the meeting. The visitor gets the calendar invite by email from your calendar provider when they shared an address; a visitor who declines to give one still gets the meeting booked, with the time confirmed right there. 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: 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 Meetings and leads 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:
1

Their details

Name and phone — the same details the assistant would otherwise collect in conversation. Email is asked for too: it is required when your calendar provider cannot book without an invitee (Calendly, Cal.com) and optional otherwise (Google Calendar), so a visitor who only wants to leave a number can still book.
2

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

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 — or, for a visitor who booked with only a phone number, the confirmed time alone.
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 when they shared an address (see the provider table above). Whichever way a visitor books, the meeting shows up in one place on your calendar.
The same booking flow can be laid out inline on a page rather than inside the widget panel — that’s what a follow-up page uses to let a captured lead pick a time straight from your autoreply email. It runs on this widget’s scheduling settings, so connecting a calendar once serves both.

Leads from your widget

A widget produces leads two ways, and both land in the same table. Everyone who fills in the booking form is 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. And every visitor who gives the assistant their details in conversation — a name, an email, a phone number, in a voice, text or video chat — is saved as a lead too, whether or not booking is enabled at all. Each lead is one row. It carries the contact details (first and last name, email, phone), the page they were on, campaign parameters (utm_*, gclid, fbclid), and a source column naming which surface it came from: widget_booking_form for the form, widget_conversation for details given in conversation. A booked column flips to true — with the meeting’s start time — when they confirm a slot in the form, and equally when the assistant books the meeting for them mid-conversation. That makes the follow-up queries trivial: “who started booking this week but didn’t finish?” is the rows where booked is false, and “which visitors left their details talking to the assistant?” is the rows where source is widget_conversation. Rows are keyed on the person, not on the touch, so the same visitor doesn’t appear twice however many times they come back. A later touch fills in what an earlier one left blank rather than overwriting it — a phone number given in conversation joins the row their form submission created, and the details they typed stay as they typed them. The person is identified by their email address where there is one and by their phone number otherwise, so a visitor who leaves an email once and only a number the next time can still land as two rows. Where the leads go is a deployment setting (Agents → Deployments → the widget → Save leads), resolved in this order:
  • A dataset you pick. Choose any dataset you already collect leads in — for example the one your Erdo landing page’s lead form writes to — and every lead this widget produces goes there.
  • Your organization’s leads dataset, when you have exactly one. An Erdo landing page creates one, so a customer running pages and a widget gets both streams in one table without configuring anything.
  • A “Widget leads” dataset Erdo creates for you, when neither of the above resolves — the first time a lead arrives, complete with a filter that keeps test traffic out of your counts. If your organization has several leads datasets and the widget pins none, leads land here and the deployment settings say so, so you can pin the right one.
Lead saving is on by default; untick Save leads to turn both surfaces off. (The setting used to read “Save booking leads” and covered the form only.) A conversation lead is not instant: the details are read out of the conversation after it ends, so the row appears within roughly 15 to 30 minutes of the visitor finishing. A text chat has no hang-up, so it counts as finished once it has been idle for 30 minutes. Form leads are unaffected — they are written as the visitor continues to the time picker. Test conversations are never saved as leads: a page loaded with ?erdo_test=1 (the marker QA and Erdo’s automated checks use) and the conversations Erdo runs against a widget you have designated for its own checks are read like any other conversation but never written as a lead.

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. What a visitor shares is more than transcript text: shortly after the conversation ends Erdo reads the name, email and phone back out of it and saves them as a lead in your leads dataset, marked source = widget_conversation, and a meeting the assistant booked during the conversation marks that same lead as booked. So following up is a query over your leads, not a trawl through transcripts. See Leads from your widget for where those rows land and how to turn the behaviour off. 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 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 where a widget is named it is named by its name or public key (vw_…), never an internal id:
  • ListGET /v1/voice/widget-conversations (or a POST with a JSON body) returns each conversation’s session id, mode, timing, duration, summary, the topics the visitor asked about, and the lead it produced, newest first and paginated. It covers every widget in your organization unless you pass ?widget=Acme%20Concierge to narrow it to one, so a dashboard never has to enumerate widgets first — each row names its own with widget (the public key) and widget_name. Bound the period with since and until (RFC3339; since inclusive, until exclusive, so two consecutive months never double-count a conversation), and pass canonical_lead_id to read one lead’s conversations (see below); filters combine. total is how many conversations match the filter at the moment of that request, regardless of paging — the number to put on a tile. It is live: a conversation that starts while you page through can change it, so a multi-page read is finished when a response has no next_cursor, not when the rows you have collected reach total. When a response includes next_cursor, pass it back as cursor to read the next stable page with the same filters; this avoids skips when a new conversation starts during a multi-page scan. MCP tool: erdo_voice_widget_conversation_list.
  • Read onePOST /v1/voice/widget-conversations/get with {"session_id": "…"} returns the conversation’s topics, a transcript_turns array ([{role, text, at}] — the conversation ready to display, whatever shape it was captured in), the rendered transcript text, the raw transcript, the same lead fields the list carries, and — for voice conversations — a structured turns array of model metrics. The session id identifies the conversation on its own, so widget is optional; a session belonging to another organization is never returned. MCP tool: erdo_voice_widget_conversation_get.
Topics are one to three short lowercase labels for what the visitor asked about — pricing, floor plans, booking a tour — written by the same summarizer that writes the summary, so they appear a few minutes after a conversation ends rather than the moment it does. topics is null while a conversation has not been analysed yet and [] once it has been analysed and nothing stood out. The distinction is worth keeping if you are counting: folding the two together reports conversations nobody has read yet as conversations about nothing. The lead a conversation produced rides the same rows. contact is the {first_name, last_name, email, phone} the summarizer read out of the conversation, lead_status is created | existing | disabled | refused | test, and lead_saved_at is when that outcome was recorded. refused means the details could not be tied to a single lead — they matched more than one, or the email and phone pointed at different leads, or the conversation left nothing in the columns your leads dataset identifies people by — so no row was written and the conversation was closed out rather than retried. A visitor nobody has met before is not refused: their details match no existing lead, so a new one is written and the conversation reads created. See lead identity. Both absences follow the same discipline as topics: contact is null while a conversation has not been analysed and an object with blank fields once it has been analysed and nobody left a way to be reached, and lead_status is an empty string while the lead writer has not reached the conversation rather than meaning no lead. When your leads dataset uses lead identity, each conversation also names the lead it became: canonical_lead_id is that lead’s permanent UUID and lead_reference is the same id in the 22-character form a link or a text message carries. Join on it when you show a lead’s conversations — the email or phone a visitor typed is only evidence, and the same person can come back with a different one. Pass either form as canonical_lead_id on the list to read one lead’s conversations across every widget; it narrows your organization’s conversations and never reaches anyone else’s. The same filter works on phone calls and text conversations. Both fields are empty while the lead has not been written yet, when the leads dataset is not enrolled in lead identity, and on conversations recorded before conversations carried the lead’s identity — those keep empty fields rather than being matched to a lead after the fact. From the CLI:
Each entry in turns is one assistant turn that drove the language model, with the fields ops teams watch for latency and cost monitoring: A typical use is a refresh configured on the target dataset that reads recent conversations and upserts the per-turn latency rows, so you can chart TTFB and fail-over rate over time. Keep the schedule and source recipe on that dataset’s refresh configuration rather than creating a second standalone automation for the same feed. Text and video conversations carry no model metrics, so their turns array is empty.

Turn a widget on or off from your own system

The Enabled setting is reachable over the REST API and to MCP clients as well as from the dashboard, so a billing system, an onboarding flow, or a scheduled automation can quiet a widget without anyone opening Erdo or editing the site it is embedded on. It is the widget’s counterpart to disabling a lead-capture pipeline: together they stop a page collecting details and stop the assistant asking for them.
  • Set the statusPOST /v1/voice/widgets/{widget}/status with {"status": "disabled"} stops the widget starting conversations, and {"status": "active"} puts it back. {widget} is the widget’s name or public key (vw_…), url-encoded — the same handles the conversation endpoints take. MCP tool: erdo_set_voice_widget_status. Any other word is rejected rather than interpreted, so a mistyped status can never leave a widget serving when you meant to stop it.
Nothing else about the widget changes. The greeting, agent, branding, allowed origins, languages, and scheduling settings are all left exactly as they were, so re-enabling brings back the widget visitors saw before rather than a default one. The snippet stays on the site untouched — a disabled widget simply doesn’t open, and the page is otherwise unaffected. The response describes the widget as it now stands: Changing a widget’s status needs admin access to the organization, the same as changing it in the dashboard. A scoped API key needs resources:write — the same capability that lets it disable a capture pipeline, because stopping the capture and stopping the assistant that feeds it are two halves of one decision.

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

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

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

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: 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.
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.
Style only the container box (position, size, z-index, shadow) with your CSS. Never apply a colour filterhue-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.

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:
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. It switches mid-conversation, too. On a multi-language widget the assistant detects the language the visitor is actually speaking: start in English, answer in English; switch to Spanish — or simply ask for Spanish — and the assistant follows, voice and all, without restarting the conversation. Detection only ever switches between the languages you configured, so the assistant never wanders into a language you haven’t given a voice.

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):
Programmatically — call the global API from any script:
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:
Or open it from a script — for example on scroll, after a delay, or from your own UI:
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:

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):
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 /.
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.
Example — a site served from both the apex and www:

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.

Use your own ElevenLabs account

By default the widget runs on Erdo’s own voice account and each conversation counts against your Erdo credits. If you’d rather run it on your ElevenLabs account — so the usage bills to you and the agents live in a workspace you control — connect ElevenLabs from Data → Connectors and the widget uses it automatically. There is nothing to switch on. There are two ways to connect it, and both work:
  • Paste an API key. Choose ElevenLabs, paste a key from your ElevenLabs profile, and save.
  • Connect the app. Choose the ElevenLabs app and authorize it.
If you have both, the API key you entered is the one the widget uses. That is deliberate: a key you have just typed in is your current statement of which credential to use, whereas an app connection made months ago can still look healthy while holding a key that has since been revoked or rotated. So when a credential stops working, entering a new key is always enough — you don’t have to find and remove the old connection first.
Changing accounts doesn’t move anything. Agents created on Erdo’s account stay there; a widget built after you connect uses yours. If you disconnect, new conversations fall back to Erdo’s account and your credits.

Troubleshooting