---
title: "Embed & share"
description: "Put your agent on your own website with the two-script widget embed, or share it as a hosted chat page — anonymous or signed-in, always on your allowed domains."
---

## Embed on your site

Any deployed agent can live on your own website as a chat widget. The widget runs in an iframe on guuey's origin, so it never touches your page's cookies, storage, or scripts — the only thing you add to your site is two `<script>` tags.

You need two things: your agent's **app id** (shown in Studio and the console, and printed by `guuey apps list`) and your site's **domain** on the app's allowed list.

### 1. Allow your site's domain

The widget is embeddable only on domains you've explicitly allowed. Until your site's origin is on the app's allowed domains, the browser refuses the frame and the widget stays blank — this is deliberate, and it applies to every app from the moment it's created.

Add the origin your pages are served from (scheme + host, e.g. `https://www.example.com`):

- **Studio** — open your agent's **Share** drawer and add it under **Allowed origins**, right below the embed snippet.
- **Console** — open your app, go to the **Embed** tab, and add it under **Allowed origins**.
- **CLI** — `guuey apps update <appId> --domains https://www.example.com` — the flag sets the app's **complete** comma-separated allowlist (it replaces what was stored; an empty value clears it), so include every origin the app should allow.

Then verify it — `guuey apps check <appId> --origin https://www.example.com` (the app id is optional inside a project with a configured `app-id`) sends the same CORS preflight your visitor's browser will send to the live agent endpoint and prints the verdict, so you're checking the live allowlist rather than what you remember configuring.

**One list, two readers.** The same allowlist is read by two checks with different rules, and the widget's is the stricter one. The widget's frame check (a `frame-ancestors` policy) matches a bare domain such as `example.com` to that exact host only — `www.example.com` is not covered — and grants no implicit localhost. Your agent's endpoint, which the widget calls once it has mounted, is looser with the same entries: a bare domain covers `https://` on the apex and every subdomain, and `http://localhost` is always accepted. `guuey apps check` exercises the endpoint's check, so it can pass for a `www` origin on a bare-domain entry while the frame still refuses. For embeds, list explicit origins (`https://www.example.com`, and `http://localhost:3000` for local development) — an entry that satisfies the frame check satisfies the endpoint too, as long as you write origins the way a browser serializes them: no explicit `:443`/`:80`, which browsers omit and the endpoint matches byte-for-byte.

This allowlist controls where the widget may be _embedded_. To serve the
agent's own chat page from a hostname you own instead, see
[Your own domain](/domains/).

### 2. Paste the snippet

Copy the snippet from your agent's **Share** drawer in Studio (under _Embed on your website_, next to the allowed-origins editor — both steps live in one place there) or from the console's **Embed** tab — both render it with your real app id. It looks like this:

<!-- prettier-ignore -->
```html
<!-- guuey widget. Add this site to the app's allowed domains or the embed is refused. -->
<script>
  window.guuey = window.guuey || function(){(guuey.q=guuey.q||[]).push(arguments)};
  guuey("init", {
    app: "app_abc123",
  });
</script>
<script src="https://widget.guuey.com/v1.js" async></script>
```

Paste it anywhere in your HTML — before `</body>` is typical. No SDK, no build step, no framework required.

Keep the snippet exactly as generated. The first line registers a small queue so calls made before the loader finishes downloading are replayed when it arrives — that's what makes the `async` load safe. Some code formatters try to re-wrap the one-line shim; keep it byte-identical to the generated snippet so it always diffs clean against what Studio and the console emit.

The loader is a permanent contract: paste it once and it keeps working. New capabilities ship behind the frame, on guuey's release cadence, without you touching your page.

One serving caveat: if your app's 7-day free trial ends without a plan, the embedded widget shows visitors a clear notice that the agent is paused instead of a chat — see [Plans & billing](/plans-and-billing/).

### 3. Customize the launcher

Options go in the `guuey("init", …)` call:

| Option      | Values               | Default   | What it does                                                 |
| ----------- | -------------------- | --------- | ------------------------------------------------------------ |
| `app`       | string               | required  | Your app id.                                                 |
| `theme`     | `"light"` / `"dark"` | `"light"` | `"dark"` paints the chat panel dark, from the first frame.   |
| `launcher`  | string               | `"Chat"`  | Accessible label for the launcher button and the chat frame. |
| `color`     | CSS color            | `#111`    | Launcher bubble background.                                  |
| `iconColor` | CSS color            | `#fff`    | Launcher icon color.                                         |
| `position`  | `"left"` / `"right"` | `"right"` | Which side of the viewport the widget docks to.              |

`theme` only picks the light or dark variant. The panel's full look — colors, typography, shape — follows the **chat theme** set on your app's Design page in the platform console; the widget picks up changes without you touching the snippet. See [Chat theming](/chat-theming/).

You can also open and close the panel from your own UI:

```js
guuey("open");
guuey("close");
```

Unknown or invalid values are ignored with a console warning — the widget never throws into your page.

### Rich, interactive answers

When your agent presents an interactive surface — a generative UI card or canvas — the open panel expands to a near-fullscreen overlay on your page, and returns to the docked panel when the surface is done. The widget never opens itself over your page: only a panel the visitor has already opened can expand. Cards can also carry follow-up action buttons — tapping one stages text into the composer for the visitor to edit and send; it never sends on its own.

### Who the visitor is

**Anonymous (the default).** Visitors chat without signing in. The widget keeps a per-browser identity in its own storage so a returning visitor continues their conversation; continuity is best-effort (strict privacy modes may reset it). If your agent is set to **require sign-in to chat**, anonymous embeds can't chat — use identified embeds instead.

**Identified (your own sign-in).** If your site has its own logged-in users, the widget can know who they are — giving each visitor durable history tied to their account with you. This mode is configured per app in the console's **Embed** tab, and takes three steps:

1. **Enrol your app once** and store the secret it prints (shown once — treat it like a database password):

   ```bash
   guuey widget keys create <appId> --audience <your-audience>
   ```

2. **Add a token endpoint to your backend** with [`@guuey/widget-auth`](https://www.npmjs.com/package/@guuey/widget-auth) — a zero-dependency package that mints a short-lived token for the currently signed-in user:

   ```ts
   import { signUserToken } from "@guuey/widget-auth";

   const { token } = await signUserToken(
     { userId: session.userId, name: session.name, email: session.email },
     { appId: process.env.GUUEY_APP_ID!, appSecret: process.env.GUUEY_APP_SECRET! }
   );
   // Return the raw token string from your endpoint.
   ```

3. **Point the widget at it.** The console's Embed tab generates the identified snippet, which adds an `identity` block:

   ```js
   identity: {
     getToken: (reason) =>
       fetch("https://www.example.com/api/guuey-token" + "?reason=" + reason).then((r) => {
         if (!r.ok) throw new Error("token endpoint failed: " + r.status);
         return r.text();
       }),
   },
   ```

   `getToken` is called with a `reason`: `"initial"` on first need, `"expired"` when a token has stopped working. If your endpoint caches tokens, always mint a fresh one on `"expired"`.

An app configured for identified embeds fails loudly, never silently: if the identity setup is incomplete, the widget shows a clear notice instead of mounting a chat that would drop your visitors into anonymous threads.

The same token endpoint also signs users into a surface you [build yourself](/surface-identity/) on the chat kit or the client SDK — same token, same identity, so a visitor is one person across the widget and your own page.

## Share links

Every agent can be shared as a hosted chat page — no website required. Open your agent in Studio and choose **Share**.

The link points at guuey's agent client:

```
https://app.guuey.com/agent/<app-id>
```

Anyone who opens it gets a full chat with your agent on the web (native mobile apps are on the way).

### Two ways to share

- **Unlisted link** — the agent stays off the public Discover directory (the same surface [Portal](/portal/) presents as its **Explore** tab) and is reachable only by its direct link. Available for any agent.
- **List in Discover** — the agent is also listed publicly in the Discover directory, where anyone can find it. Studio only offers public listing for read-only agents — ones with no connected accounts that could modify data (a Studio-side check, not a platform rule); if your agent gains a connected account later, you'll be prompted to switch it back to unlisted.

**Stop sharing** removes the agent from Discover; the direct link and the agent's page keep working for any live agent, and resuming later restores the same listing. To actually restrict who can chat, turn on **Require sign-in to chat** (below) — that is the access gate, not the listing.

### Require sign-in to chat

Separate from how the agent is listed, you choose whether visitors must sign in before chatting. This is the real access gate — enforced by the agent itself, and it applies to the share link even when the agent is unlisted. Changes take effect within about 15 seconds.

### Listing details

Optionally add a description and category in the Share panel. These appear on Discover cards and the agent's page once the listing is public and published; they change nothing for unlisted agents.

The agent's **icon** — along with the social-share image and accent color — is app branding, not listing metadata: set it in the console under **Agent Settings → General → Branding** or with `guuey apps update --brand-icon-url`, and it applies whether or not the agent is listed. The welcome line belongs to the standalone page instead — **Agent Settings → General → Standalone page**, or `guuey apps update --welcome-copy`. See [Design & distribution](/console-design/).

## Troubleshooting

**The widget stays blank or never mounts.** Almost always the page's origin is missing from the app's allowed domains — the browser refuses the frame by design. Add the exact origin your pages are served from (scheme + host, no path) and verify with `guuey apps check <appId> --origin https://www.example.com`; a `www` / apex mismatch is the classic miss.

**Visitors see a notice instead of a chat (identified embeds).** An app configured for identified embeds refuses to mount an anonymous chat when the identity setup is incomplete. Check that your token endpoint is reachable from the page and returns the raw token string, and that it mints a fresh token when `getToken` is called with `"expired"`.

**Visitors see that the agent is paused.** The app's free trial ended without a plan (or the app was paused). Choosing a plan resumes it automatically within a few minutes — see [Plans & billing](/plans-and-billing/).

**A share link stopped working.** **Stop sharing** only delists the agent from Discover — the direct link keeps working for any live agent, so a dead link means the app was deleted, not unlisted (a paused agent's page still loads and says it is paused — see the bullet above). To restrict who can chat, use **Require sign-in to chat**.