> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://developer.jelliu.co/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://developer.jelliu.co/_mcp/server.

# Web chat widget

The web chat widget adds a floating chat button to your site. Visitors who open it talk to one of your Jelliu agents, by text or, if you enable it, by voice.

## Two credentials, not interchangeable

Every widget has two identifiers. Keep them apart:

| Credential             | What it is                                                                       | Where it goes                                                                                          |
| ---------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| **Widget ID** (a UUID) | **Public.** Identifies the widget. Safe to put in your page.                     | In the embed script URL. The widget sends it as the `x-widget-id` header or the `wid` query parameter. |
| **Widget API key**     | **Secret.** For calling the public widget endpoints directly from your own code. | Only in the `x-widget-api-key` header, and only from a server or app you control. Never in a URL.      |

The widget API key is shown **once**, as `plaintext_api_key` in the response that creates the widget. Jelliu stores only a keyed hash of it. The standard embed does not need it.

The widget API key is not a workspace API key (`jl_...`), and neither one belongs in a web page. The only credential the embed snippet uses is the public widget ID.

## Embed the widget

#### Create the widget

Create it in the dashboard under **Widgets**, or with the API (a `write` key is enough):

```bash
curl -sS -X POST "https://api.jelliu.co/api/widgets" \
  -H "Authorization: Bearer $JELLIU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "7c1d9f30-5e2a-4a7e-9c8f-91a0b1c2d3e4",
    "name": "Website chat",
    "allowed_origins": ["https://www.example.com"],
    "branding": {
      "primary_color": "#2563EB",
      "position": "bottom-right",
      "title": "Chat with us"
    }
  }'
```

The widget's `id` in the response is your widget ID.

#### Add the script to your site

Paste this before the closing `body` tag of every page where the chat should appear, replacing `WIDGET_ID`:

```html
<script src="https://api.jelliu.co/widget-embed/WIDGET_ID/embed.js" async></script>
```

This is the same snippet the dashboard gives you. The script is public and cached by browsers for up to 5 minutes, so branding changes can take that long to show up.

#### Check the allowed origins

Load your page and open the chat. If it does not answer, make sure the page's origin is in the widget's `allowed_origins` (see below).

### What the script does

* Adds a round launcher button in the corner set by `branding.position`, in your `primary_color`.
* Loads the chat panel in an iframe the first time a visitor opens it, so pages where nobody chats only load the small loader.
* Shows a side panel on desktop and a full-screen panel on screens up to 520 px wide.
* Allows the chat panel to use the microphone, which voice conversations need. Browsers still ask the visitor before granting it.
* Runs once per page, even if the snippet is included twice.
* On Shopify storefronts, forwards the signed-in customer's email, name and ID, and the shop domain, so conversations are linked to the right contact.

## Allowed origins

A widget answers only on the sites listed in `allowed_origins`. The chat panel reports the origin of the page that embeds it, and Jelliu compares it with the list:

* The **scheme** and **port** must match exactly.
* The **hostname** must match, except that one leading `www.` is ignored: `https://example.com` also covers `https://www.example.com`, and the reverse.
* Other subdomains need their own entry. `https://shop.example.com` is not covered by `https://example.com`.
* `*` is never accepted. At least one origin is required.

Requests from a page that is not allowed receive `403` with code `FORBIDDEN` and the message `Origin not allowed`.

## Widget settings

**`agent_id`** `string (uuid)` — required

The agent that answers visitors.

---

**`name`** `string` — required

1 to 200 characters.

---

**`allowed_origins`** `string[]` — required

Full origins such as `https://www.example.com`. At least one.

---

**`branding`** `object`

`primary_color` (required inside `branding`, hex `#RRGGBB`), `position` (`bottom-right` or `bottom-left`), `title` (up to 100 characters), `subtitle` (up to 120), `logo_url`, `quick_replies` (up to 4 short replies), `agent_names` (up to 8 display names), `agent_avatars` (up to 8 HTTPS image URLs aligned with `agent_names`), and `i18n` (per-language `greeting` and `quick_replies`, keyed by language such as `es` or `en`).

---

**`greeting_message`** `string`

Up to 2000 characters.

---

**`voice_enabled`** `boolean` — default: false

Let visitors talk to the agent by voice.

---

**`rate_limit_rpm`** `integer` — default: 30

Requests per minute allowed for this widget.

---

Update a widget with `PUT /api/widgets/{id}` (send only the fields to change) and remove it with `DELETE /api/widgets/{id}`.

## Limits

Public widget traffic is limited at several levels:

| Level                  | Limit                                                                                      |
| ---------------------- | ------------------------------------------------------------------------------------------ |
| Per visitor IP         | 60 requests per minute                                                                     |
| Per widget             | `rate_limit_rpm` (30 per minute by default)                                                |
| Per workspace          | 300 requests per minute across all widgets                                                 |
| Per workspace, per day | A daily cap on visitor messages; once reached, messages are refused until the next UTC day |

Exceeding a limit returns `429` with code `RATE_LIMIT_EXCEEDED`.

## Calling the widget endpoints directly

The standard embed handles everything. If you build your own chat interface, the widget endpoints are served from `https://api.jelliu.co`:

| Method | Path              | Body                                                                                                                                |
| ------ | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `POST` | `/widget/init`    | `visitor_id` (required), `metadata` (optional object). Returns `201` with a `session_id`.                                           |
| `POST` | `/widget/message` | `session_id` (uuid), `message` (1 to 4000 characters). An optional attachment can be sent as a multipart `file` field, up to 10 MB. |

Authenticate with the `x-widget-api-key` header and send `x-widget-parent-origin` set to one of the widget's allowed origins. Keep the widget API key on a server you control.