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. Every conversation flows through a public path that carries no sign-in cookie at all, identified only by an embed key.
An embed key is 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, and 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 — the scheme, host, and port it was served from — against your allowlist.
Enable the widget
Turning the widget on for the first time mints the agent's embed key. Set the allowed origins in the same step — they are the widget's real access control.
- platformctl
- curl
- Console
platformctl agents embed enable my-agent \
--allow-origin https://yoursite.com \
--allow-origin https://www.yoursite.com \
--title "Ask us anything" \
--greeting "Hi! How can I help?"
You should see the saved configuration, embed_key included:
accent_color -
allowed_origins https://yoursite.com, https://www.yoursite.com
embed_key emb_xxxxxxxxxxxxxxxxxxxxxxxx
enabled true
greeting Hi! How can I help?
launcher_text -
position right
subtitle -
title Ask us anything
updated_at 2026-08-15T10:22:04Z
A - means the field is unset — the widget falls back to its built-in default.
--allow-origin is repeatable. --subtitle, --accent-color, --launcher-text, and --position left|right complete the appearance set.
Run it again later to change one thing: the command reads the current configuration first and only changes the flags you actually passed, so embed enable on its own turns the widget back on without blanking a title set from the console.
export CAI_API=https://api.codyhill.dev
export CAI_TOKEN="<your session token or API key>"
curl -s -X PUT "$CAI_API/v1/agents/my-agent/embed" \
-H "Authorization: Bearer $CAI_TOKEN" -H 'content-type: application/json' \
-d '{"enabled": true,
"allowed_origins": ["https://yoursite.com", "https://www.yoursite.com"],
"title": "Ask us anything",
"greeting": "Hi! How can I help?",
"position": "right"}'
You should see the saved configuration, including the minted key:
{
"enabled": true,
"embed_key": "emb_xxxxxxxxxxxxxxxxxxxxxxxx",
"allowed_origins": ["https://yoursite.com", "https://www.yoursite.com"],
"title": "Ask us anything",
"greeting": "Hi! How can I help?",
"position": "right"
}
The first PUT for an agent mints its key, whether or not that PUT sets "enabled": true. GET the same path to read the configuration back — a never-configured agent answers 200 with {"enabled":false,"embed_key":"","allowed_origins":[],"position":"right"} rather than a 404.
If you belong to more than one project, add ?project=<slug> to the URL.
enabledEvery field other than enabled is replace-semantics: a field you leave out is cleared, not kept. A tidy-looking {"enabled": true} therefore wipes your title, your greeting, and — the one that matters — your entire allowlist. Read the current config, change what you need, and send the whole object back. The CLI and the console do that merge for you.
- 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.
Write each origin as 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 on the end is rejected with "<value>" is not a valid origin: it must look like https://example.com (scheme + host, no path), and the console 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 snippet is the same however you enabled the widget — the Embed tab builds it for you, and from the CLI or the API you drop the embed_key into it yourself. 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 pulls in no other libraries, and it renders inside a Shadow DOM — a sealed-off corner of the page with its own styles. Your site's CSS cannot break the widget, and the widget 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 lasting anonymous identity. The widget stores a session id in a cookie, and the transcript in the browser's local storage — a small store the browser keeps per site. The conversation therefore resumes across page loads. The transcript keeps the last 50 messages.
- Each message waits up to 70 seconds for a reply. That is deliberately generous: an idle agent runs zero copies of itself, so the first message after a quiet spell has to wait for one to start up. That wait is called a cold start.
You can watch these conversations from the agent's Users & Sessions view in the console.
Customize per page
You can override the look on one page without changing what you saved in the console. Put the overrides on the script tag. When the same setting exists in two places, the server-saved value wins over the page override, and the page override wins over the built-in default.
| 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
Two verbs are worth separating before you use either. Rotating mints a new key and kills the old one, keeping everything else. Removing deletes the key, the appearance settings, and the domain list together — and enabling the widget again after that mints a different key.
- platformctl
- curl
- Console
platformctl agents embed get my-agent # read the config, key included
platformctl agents embed rotate-key my-agent # new key, everything else kept
platformctl agents embed disable my-agent # remove the widget entirely
rotate-key prints the new key on a line of its own, because it is what has to go into your snippet right now:
new embed key: emb_yyyyyyyyyyyyyyyyyyyyyyyy
update the widget snippet on every page that embeds my-agent - the previous key no longer works
embed disable is a removal, not a toggleDespite the name, platformctl agents embed disable deletes the whole configuration, key included — it is the CLI equivalent of the console's Remove widget, not its Disable. To turn the widget off and keep the key, use the console, or PUT the configuration back with "enabled": false.
# Read the current configuration
curl -s "$CAI_API/v1/agents/my-agent/embed" \
-H "Authorization: Bearer $CAI_TOKEN"
# Mint a new key, keeping the appearance and the allowlist
curl -s -X POST "$CAI_API/v1/agents/my-agent/embed/rotate-key" \
-H "Authorization: Bearer $CAI_TOKEN"
# Remove the widget entirely — key, appearance, and domain list
curl -s -X DELETE "$CAI_API/v1/agents/my-agent/embed" \
-H "Authorization: Bearer $CAI_TOKEN"
rotate-key returns the full configuration with the new embed_key. Rotating an agent that has no widget configured returns 404 no embed widget is configured for this agent yet. DELETE returns 204.
To turn the widget off without losing anything, PUT the configuration back with "enabled": false — and remember to send the whole object, per the warning above.
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.
Rotations, removals, and configuration changes are all recorded in the project's audit log (agent.embed.update, agent.embed.rotate-key, agent.embed.delete).
The public paths the widget itself uses are not on this management surface at all. The console serves GET /embed/{key}/config and POST /embed/{key}/invoke on console.codyhill.dev, anonymously and cross-origin, and forwards them to the platform API. Those two are the only calls a visitor's browser ever makes.
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 key that is wrong, disabled, or rotated away renders nothing, by design. It logs 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