# Embeddable widget

> Put a Subsidia chat bubble or a form on a website with one script tag: a public id, allowed domains, a monthly ceiling, no secret key in the page.

An `sk_` API key can never live in a web page. A **widget** is a *publishable* identity instead: a public id (`pk_live_...`, not a secret), the domains it may be loaded from, a monthly ceiling and its own rate limit. It comes in two kinds:

- **`agent`**: a chat bubble answering from one dedicated agent and its knowledge sources, streamed, with the sources it used. The agent has **no tool at all** (no mail, no outside call, no web) and nothing is remembered about the visitor.
- **`form`**: a form whose submission starts a Reflex flow through its **webhook trigger**. The trigger's secret is never exposed to the page.

## Add it to a page

**One script tag**

_HTML_

```html
<script src="https://app.subsidia.protypa.fr/embed/v1.js"
        data-widget="pk_live_XXXXXXXXXXXXXXXXXXXXXXXX"
        data-api="https://api.subsidia.protypa.fr" async></script>
```

_Form, inline_

```html
<div id="contact"></div>
<script src="https://app.subsidia.protypa.fr/embed/v1.js"
        data-widget="pk_live_XXXXXXXXXXXXXXXXXXXXXXXX"
        data-api="https://api.subsidia.protypa.fr"
        data-target="#contact" async></script>
```

The script has no dependency, renders in a Shadow DOM (your page's CSS cannot reach it and it cannot restyle your page), is keyboard accessible (Esc closes the chat) and works at phone width. Without `data-api` it calls the origin it was loaded from. Create and edit widgets in the console (**Console > Widgets**) or through the API below.

## Manage widgets

| Route | What it does |
| --- | --- |
| `GET /engine/widgets` | Lists the workspace widgets with this month's use. Works with an API key (scope `reflex`). |
| `POST /engine/widgets` | Creates one. Signed-in person only. `kind`, `name`, `agentId` or `flowId`, `allowedOrigins`, `monthlyQuestionLimit`, `ratePerMinutePerIp` (default 10), `greeting`, `theme` (`color`, `title`, `position`, and for a form `fields`: `[{ name, label, type: text|email|textarea, required }]`). |
| `PATCH /engine/widgets/:id` | Changes it, including `enabled`. The kind cannot change. |
| `DELETE /engine/widgets/:id` | Deletes it; the public id stops answering at once. |

An **agent widget requires an agent limited to chosen documents or dossiers**. This is enforced by the server: an agent that can read the whole workspace is refused at creation and at every edit (`400`), because anyone who reaches the page can question it. Build an agent for this use, give it only the sources you accept to see quoted publicly, and the widget answers without any tool.

`allowedOrigins` are exact origins: `https://host[:port]`, no wildcard, no path, 20 at most. `http` is accepted for `localhost` only. The agent and the flow must belong to the workspace; a form needs an **active flow with an enabled webhook trigger**, otherwise submissions are refused with `409`.

## What the page calls

| Route | Behaviour |
| --- | --- |
| `GET /embed/:publicId/config` | Name, greeting, theme. Nothing else. |
| `POST /embed/:publicId/chat` | Server-Sent Events: `token` deltas, then `done` (`sources`, `proofId`) or `error`. Body `{ message, history }`: the page keeps the conversation, the server keeps the last 10 turns and 8 000 characters at most. A message holds 2 000 characters. |
| `POST /embed/:publicId/submit` | Body `{ fields, eventId? }`: 50 flat fields, 32 KB at most. Starts the flow like any trigger, with the same concurrency and active-hours rules (`202 { started }`, or `409` when nothing started). |

Refusals carry a `code`: `origin_not_allowed`, `origin_required`, `wrong_kind` (a chat widget refuses submissions and the reverse), `WIDGET_BUDGET_EXCEEDED` (`402`, monthly ceiling), `WORKSPACE_LIMIT` (`402`, the workspace has no questions left), `no_webhook_trigger` / `flow_inactive` (`409`), `429` when the per-IP rate is reached. A disabled widget answers `404`, exactly like an unknown id.

An answer counts as one question of the workspace, like any chat. The widget's own ceiling counts its audited `embed.chat` (or `embed.submit`) lines since the 1st of the month, so it can lag a burst by a few seconds: a guard rail against a runaway page, not a meter.

> **WARNING: Origin is not authentication**
> The browser sets `Origin`, and anything that is not a browser (a script, `curl`) can set it to whatever it likes. The check protects a visitor's browser from a third-party page using your widget; it does **not** stop someone who calls the endpoint directly. The real protection is the perimeter you give the widget:
> 
> - a **dedicated agent**, with only the documents you would accept to see quoted publicly;
> - **no tool**, so a hostile visitor cannot make it send, call or fetch anything;
> - a **monthly ceiling** and a **rate limit per IP**, so a leaked public id costs at most the ceiling;
> - the ability to disable or delete the widget instantly.
> 
> Every chat and submission is written to the access journal with the caller's IP (never the message).
