Widget API

Widget API

The browser-facing API the <pleiades-chat> widget uses. You only need this page to build your own client or to debug the widget. All paths are under /v1/widget.

Authentication

HeaderUsed byMeaning
x-publishable-key: pk_...POST /visitors onlyThe channel's public key.
x-visitor-tokenevery other endpointThe token returned by POST /visitors.
Originall (set by the browser)Must be one of the channel's allowed origins.
content-type: application/jsonPOST with a body

The flow: get a visitor token once with the key, then send only the token. The token is stored hashed on the server and has a sliding lifetime (24 hours by default, extended on every use). A request with a missing, unknown or expired token gets 401; the client should then create a new visitor.

None of these endpoints need the transport HARNESS_API_KEY; that key is for the server API .

CORS

OPTIONS /v1/widget/* answers 204 with these headers when Origin belongs to a web channel, and an empty 403 otherwise:

HeaderValue
access-control-allow-originthe request Origin
access-control-allow-methodsGET, POST, OPTIONS
access-control-allow-headerscontent-type, x-visitor-token, x-publishable-key
access-control-max-age600

Other responses carry access-control-allow-origin: <Origin> and vary: Origin when the origin is known. When the origin is not allowed for the channel the server answers 403 without CORS headers, so a browser reports a network error and hides the reason.

Endpoints

POST /v1/widget/visitors

Creates an anonymous visitor.

curl -i -X POST https://agent.example.com/v1/widget/visitors \
  -H "Origin: https://shop.example" \
  -H "x-publishable-key: pk_your_channel_key"
sh

201:

{ "visitorToken": "…", "expiresAt": 1790000000000 }
json

expiresAt is a Unix timestamp in milliseconds.

POST /v1/widget/threads

Starts a conversation for the visitor.

curl -X POST https://agent.example.com/v1/widget/threads \
  -H "Origin: https://shop.example" -H "x-visitor-token: $TOKEN"
sh

201: { "threadId": "…" }.

GET /v1/widget/threads/:id/messages

The visitor's recent history in a thread (the last 10 messages by default), oldest first. 404 if the thread is not the visitor's.

200:

{
  "items": [
    { "id": "41", "role": "user", "content": "Find a double room", "createdAt": 1790000000000 },
    {
      "id": "42", "role": "assistant", "content": "Two rooms are free.", "createdAt": 1790000004000,
      "steps": [{ "id": "search_rooms", "phase": "done", "text": "Looking for rooms…" }]
    }
  ]
}
json

steps is present on a reply that used tools.

POST /v1/widget/messages

Sends a message and streams the reply as NDJSON (see Stream ).

Body fieldTypeRequiredNotes
threadIdstringyesA thread of this visitor.
textstringyesAt most WIDGET_MAX_TEXT_CHARS (default 2000), else 413.
pagestringnoThe visitor's pathname + search, at most 512 characters.
customerContextobjectnoFlat map of string, number or boolean values. Its JSON may not exceed the same character limit as text, else 413.
curl -N -X POST https://agent.example.com/v1/widget/messages \
  -H "Origin: https://shop.example" -H "x-visitor-token: $TOKEN" \
  -H "content-type: application/json" \
  -d '{"threadId":"'"$THREAD"'","text":"book a double room for Friday","page":"/rooms"}'
sh

POST /v1/widget/tools

Registers the tool catalog of the page for a thread. The widget calls it when the panel opens and again when the catalog changes. See Tools .

Body fieldTypeNotes
threadIdstringA thread of this visitor.
webmcpToolsarrayEach item: name (string), optional description (string), optional inputSchema { type: "object", properties?, required? }.

The serialized webmcpTools may not exceed WIDGET_MAX_WEBMCP_TOOLS_CHARS (default 20,000), else 413. Fields outside the schema are dropped. An empty array clears the thread's catalog. Response: 204, no body.

curl -X POST https://agent.example.com/v1/widget/tools \
  -H "Origin: https://shop.example" -H "x-visitor-token: $TOKEN" \
  -H "content-type: application/json" \
  -d '{"threadId":"'"$THREAD"'","webmcpTools":[{"name":"set_pizza_size","description":"Set the pizza size.","inputSchema":{"type":"object","properties":{"size":{"type":"string","enum":["small","medium","large"]}},"required":["size"]}}]}'
sh

POST /v1/widget/tool-results

Answers a tool_call line and resumes the run. The response is a new NDJSON stream with the same line types.

Body fieldTypeRequiredNotes
threadIdstringyes
callIdstringyesThe callId of the tool_call line being answered.
toolstringyesThe tool of that line.
resultanyyesWhat the tool returned. MCP {content:[{text}]}, a string or JSON; its text is kept (4000 characters).
isErrorbooleannoDefault false.
pagestringnoWhere the visitor is now (a tool may have navigated).

Stream

POST /messages and POST /tool-results answer with content-type: application/x-ndjson: one JSON object per line. The body is gzip-compressed (content-encoding: gzip) when the request sends accept-encoding: gzip.

typeFieldsTerminal?
deltatextno. A chunk of the reply text; append it.
stepid, phase (running, done, failed), textno. Progress in the visitor's language. A later step with the same id replaces the earlier one.
tool_calltool, arguments, callIdends this stream. Run the tool in the browser and call POST /tool-results.
doneelapsedMs; optional inputTokens, outputTokens, totalInputTokens, totalOutputTokensyes. The goal was reached.
errormessage; optional code, hintyes.

An error with code: "task_failed" means the server worked but the site could not do the task. hint is the site's own last tool answer (at most 400 characters), usually advice on what to try. An error without a code is a fault on the server side. A dropped connection never arrives as a line; the client sees it itself.

Example:

{"type":"step","id":"search_rooms","phase":"running","text":"Looking for rooms…"}
{"type":"tool_call","tool":"search_rooms","arguments":{"checkIn":"2026-11-06","checkOut":"2026-11-08","guests":2},"callId":"6f1c…"}
null

The client runs search_rooms, then posts the result and reads the next stream:

curl -N -X POST https://agent.example.com/v1/widget/tool-results \
  -H "Origin: https://shop.example" -H "x-visitor-token: $TOKEN" \
  -H "content-type: application/json" \
  -d '{"threadId":"'"$THREAD"'","callId":"6f1c…","tool":"search_rooms","result":{"content":[{"type":"text","text":"- r12: double, $90/night"}]}}'
sh
{"type":"step","id":"search_rooms","phase":"done","text":"Found rooms"}
{"type":"delta","text":"Room r12 is free."}
{"type":"done","elapsedMs":2310}
null

(The step texts above are illustrative; real texts depend on the tool and the visitor's language.)

A run can issue several tool_call lines one after another, each in its own stream, before done or error.

Error statuses

Error responses have an empty body.

StatusWhereMeaning
401all but /visitorsMissing, unknown or expired x-visitor-token. Create a new visitor.
401/visitorsUnknown publishable key.
403allOrigin not allowed, channel disabled, visitor not allowed (access mode or blocked), or IP blocked.
404thread routesThe thread does not exist or belongs to another visitor.
413/messages, /tools, anyText, customerContext or catalog over its limit; or Content-Length over WIDGET_MAX_BODY_BYTES (default 131072) on any widget route.
422POST routesBody does not match the schema (Elysia validation).
429see belowRate limit.

Rate limits

Fixed windows, in memory, per server instance. Defaults, each set by an environment variable of alpha-orchestrator:

LimitApplies toDefaultVariable
Messages per visitor token per minutePOST /messages, POST /tools10WIDGET_MESSAGES_PER_MINUTE
Messages per client IP per minutesame30WIDGET_IP_MESSAGES_PER_MINUTE
Tool results per visitor token per minutePOST /tool-results120WIDGET_TOOL_RESULTS_PER_MINUTE
Threads per visitor token per hourPOST /threads30WIDGET_THREADS_PER_HOUR
New visitors per client IP per hourPOST /visitors20WIDGET_VISITORS_PER_HOUR_PER_IP

POST /tools counts against the message limits, not the tool-result limit. The tool-result limit is separate because one request can fan out into many browser round trips (clearing a 12-line cart is 13).

The client IP comes from the socket by default (CLIENT_IP_SOURCE, default socket). Behind a proxy the operator chooses another source and how many proxy hops to trust with TRUSTED_PROXY_HOPS (default 1).