API authentication: keys and tokens
For developers, in code: the widget key and the Origin check on the runtime; the management key, the session and the identity token on the management API; what each failure returns.
The runtime: widget key plus Origin
Every runtime request carries the site’s widget key (wk_…), and the server checks two
things: that the key exists, is a widget key and is active; and, when the key lists allowed
origins, that the request’s Origin header is on the list.
| Where the key goes | Endpoints |
|---|---|
the X-Widget-Key header |
POST /message, GET /widget/config, POST /tts, GET /conversation |
in the frame or body as widget_key |
WS /ws (every frame that starts a turn), WS /ws/stt (the first frame), POST /followed, POST /handoff, POST /handoff/opened |
| Failure | HTTP | Socket | Detail |
|---|---|---|---|
| no key, or no such key | 401 |
error frame unauthorized, then close 1008 |
Missing widget key / Invalid widget key |
| the key is inactive | 403 |
forbidden, close 1008 |
Widget key is inactive |
| the Origin is not allowed | 403 |
forbidden, close 1008 |
Origin not allowed |
A key with no allowed origins accepts any origin (only an old key can be like that: the panel
always sets at least the site’s own origin). The panel’s own origin is trusted
platform-wide so its preview works without being listed. CORS reflects any Origin and answers
preflight, but CORS is not the check — the key and the list are.
The key is public by design (it ships in every page’s HTML), so the origin list is the boundary.
A non-browser client can forge an Origin; the runtime is built with that in mind — see
Overview. Keep a separate key per environment and list only the origins
each needs.
The management API: three ways in
| Method | Header | Who it acts as |
|---|---|---|
| Management key | Authorization: Api-Key <key> |
the owner who created the key — every request is scoped to that owner’s sites |
| session | the panel’s own cookie, with CSRF on writes | the signed-in owner; what the panel uses |
| identity token | Authorization: Bearer <token> |
the user the token was minted for — the panel’s own co-pilot relays this one; it is not for customers |
A management key is created on the panel’s API keys screen, shown once, and stored as a
hash; it cannot be read back. Rotate it from the same screen. Api-Key is compared
case-insensitively, and the header must be exactly two parts.
Every authentication failure on the management API answers 403 with
{"detail": "…"} — Authentication credentials were not provided., Invalid API key.,
Invalid Api-Key header. — not 401. A valid key reaching a row that is not the owner’s gets a
404, never a 403: another owner’s objects do not exist for you.
Two other 403s look alike and are not auth: a plan gate (Your plan (Starter) does not include custom tools and events. Upgrade to Growth to add them.) and a plan cap (Source limit reached for your plan (500/500). Upgrade to add more.). Both are a bare {"detail": "…"}, with no code field.
Owner scoping
Everything in the management API is filtered by the owner the credential resolves to. A ?site=
narrows a list to one of the owner’s sites; a well-formed id that is not theirs is a 404
on insights, overview, analytics and content gaps, and an empty list on a resource list — never
someone else’s data.
The identity token, for the curious
The panel mints a 15-minute HMAC token for the signed-in owner (GET /api/widget-identity/,
session only), hands it to Shukai.setUser, and the co-pilot’s tools send it back as
Bearer {{user}} — the same owner scoping as the panel. Your own app does the same thing with its
own tokens, which your own API validates; the panel’s token format is not something you use.
User identity.