Embedded chat widget
This page shows you how to add a floating chat launcher — like an Intercom bubble — to your own website, wired to one of your agents. Visitors chat anonymously; you control the look and the list of sites allowed to embed it.
How it works
The widget is a single script tag. It renders a launcher button in the corner of your page, and every conversation flows through a public, cookie-less path identified only by an embed key — a public identifier shaped `emb_` plus exactly 24 lowercase letters and digits. Think of it as an app id, not a secret: it grants access to chat with one agent, nothing else.
The widget makes exactly two kinds of network calls: one to fetch its configuration (title, colors, greeting) and one per message. No sign-in cookie or token ever travels on this path — the key in the URL is the only identity, and the server checks the visiting page's origin against your allowlist.
Enable the widget
- In the console, open your agent and go to the Embed tab.
- Click Enable chat widget. The first save mints the agent's embed key.
- Fill in the appearance form — title, subtitle, greeting, brand color (a 6-digit hex code), launcher text, and corner (left or right). A live preview updates as you type.
- Under Allowed domains, enter one origin per line and Save.
An origin is scheme://host with an optional port — for example https://yoursite.com. The most common mistake is pasting a full page URL: an origin with a path is rejected, and the page names the offending line before the server ever sees it.
If you leave the allowed-origins list empty, any website can embed your widget and chat with your agent. List your real sites before you ship the snippet.
Paste the snippet
The Embed tab shows a copy-paste snippet. Paste it just before </body> on your site:
<script async src="https://console.codyhill.dev/embed/widget.js"
data-embed-key="emb_xxxxxxxxxxxxxxxxxxxxxxxx"></script>
That's the whole integration. The widget is zero-dependency and renders inside a Shadow DOM, so your site's CSS cannot break it and it cannot break your site. Pasting the snippet twice still renders only one widget.
What visitors experience
- A floating launcher opens a chat panel. The greeting shows first (default:
Hi! How can I help?). - Each visitor gets a persistent anonymous identity: a session id in a cookie plus the transcript in the browser's local storage, so the conversation resumes across page loads. The transcript keeps the last 50 messages.
- Each message waits up to 70 seconds for a reply — sized so that an agent that has scaled to zero has time to cold-start.
You can watch these conversations from the agent's Users & Sessions view in the console.
Customize per page
Server-saved settings win over page overrides, which win over defaults. Overrides go on the script tag:
| Attribute | Overrides |
|---|---|
data-title | Panel title (default Chat) |
data-subtitle | Panel subtitle |
data-greeting | First message shown |
data-accent-color | Brand color (default #f9743a) |
data-launcher-text | Text next to the launcher |
data-position | left or right (default right) |
You can also set the same keys on a window.crusoeChatSettings object before the script loads. For programmatic control, the widget exposes a tiny API:
window.CrusoeChat.open() window.CrusoeChat.close()
window.CrusoeChat.toggle() window.CrusoeChat.reset()
reset() forgets the visitor's identity and transcript — useful behind a "start over" button.
Manage the key
The Embed tab gives you three controls beyond appearance:
- Enable/Disable — turn the widget off without losing your configuration. A disabled key renders nothing on visitors' pages.
- Rotate key — mints a new key and kills the old one instantly.
- Remove widget — deletes the key, the appearance settings, and the domain list entirely.
The old key stops working the moment you rotate. Every site still carrying the old snippet goes dark until someone pastes the new one. Rotate when a key is abused or leaked into the wrong hands — and update your sites right after.
Both rotations and configuration changes are recorded in the project's audit log (agent.embed.update, agent.embed.rotate-key, agent.embed.delete).
Managing it from the API
For automation, the same controls live on the agent engine API (reachable via $CAI_API — the platform has no public API hostname yet). All calls need authentication and the project id:
export CAI_API=http://localhost:8080 # or the endpoint your admin gave you
export TOK=... # session token or API key
export PROJ=... # project id
# Read the current embed config
curl -s "$CAI_API/v1/agents/my-agent/embed?project=$PROJ" \
-H "Authorization: Bearer $TOK"
You should see (for a never-configured agent):
{"enabled":false,"embed_key":"","allowed_origins":[],"position":"right"}
PUT /v1/agents/{name}/embedwith that body shape saves the configuration — the first PUT for an agent mints its embed key, whether or not that PUT sets"enabled": true.POST /v1/agents/{name}/embed/rotate-keyreturns the new key.DELETE /v1/agents/{name}/embedremoves everything.
The public paths the widget itself uses are served by the console: GET /embed/{key}/config and POST /embed/{key}/invoke on console.codyhill.dev.
Limits
| Limit | Value |
|---|---|
| Embed key shape | emb_ + exactly 24 lowercase letters/digits (anything else: invalid embed key) |
| Message body | 1 MiB |
| Reply wait | Widget waits 70 s; the server allows 90 s |
| Config fetch | 15 s |
| Stored transcript | Last 50 messages, in the visitor's browser |
Troubleshooting
- The widget doesn't appear at all. A bad, disabled, or rotated-away key renders nothing by design, logging a single warning in the browser's developer console. Check the key in your snippet against the Embed tab.
- "invalid embed key". The key in the snippet doesn't match the required shape — usually a truncated copy-paste.
- Works on one site but not another. The failing site's origin is missing from the allowlist, or was entered with a path. Enter
https://hostonly.
Next steps
- Console overview — where the Embed tab lives
- Agent invoke — what happens to each message server-side
- Security overview — why this path is anonymous by design