Skip to content

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
<script src="https://app.subsidia.protypa.fr/embed/v1.js"
data-widget="pk_live_XXXXXXXXXXXXXXXXXXXXXXXX"
data-api="https://api.subsidia.protypa.fr" 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

RouteWhat it does
GET /engine/widgetsLists the workspace widgets with this month's use. Works with an API key (scope reflex).
POST /engine/widgetsCreates 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/:idChanges it, including enabled. The kind cannot change.
DELETE /engine/widgets/:idDeletes 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

RouteBehaviour
GET /embed/:publicId/configName, greeting, theme. Nothing else.
POST /embed/:publicId/chatServer-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/submitBody { 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.

Related