Skip to main content
A widget puts an agent on a business’s own website: a public voice demo any visitor can start, and a web chat that runs through it (the chat can be switched on only while the demo is). A widget always runs its own agent’s published conversation. Widgets are created and configured in the Sempera App; the API reads them, reads their chat threads, and manages the custom components a widget shows. Every route here answers in the compatibility envelope {"data": ..., "errors": null}, except that an agent the credential cannot read, named by agent_id (or agentId) on GET /v1/widgets and GET /v1/widget/threads, answers 404 in the standard error envelope (Errors and limits), like one that never existed.

Widgets and threads

A widget reads id, agent_id, label, allowed_domains, config (its brand settings and the public demo’s demo_enabled, demo_daily_sessions and demo_max_seconds), sessions_last_24h and created_at. pathway_id, agent_prompt, messages_per_minute and updated_at are always null on this read: a widget speaks for an agent, and its per-minute message limit (10 by default, 1 to 120, set in the App) is not returned here. A visitor over it is answered 429 with retry_after_s. A thread reads id, widget_id, agent_id, created_at, visitor_id and origin. This historical projection does not expose the bounded live conversation: messages, ended_at and live_agent_handoff_at remain null. An invalid limit, cursor or sort answers 400.

Editing together

Widgets are saved in the Sempera App, and no route in this API saves one. Two owners can open the same widget and both save. Without a guard the later save would win and the earlier one would be lost without a word, so the App’s save takes an optional expected_updated_at, the updated_at the screen read. The widget is saved only if it still has that updated_at, and the check and the save are one step, so of two saves that carry the same value exactly one is applied. If another owner saved first, the save is refused with 409 and code: "stale_widget". Nothing is written and nothing is recorded in the audit log. The answer carries the widget now on file, with its updated_at, so the App can show what changed and the owner can discard the edit or save it over the newer version. A value that is not a time with a zone is 400 with field: "expected_updated_at". A widget of another workspace, or one deleted meanwhile, is answered as a widget that does not exist and names no updated_at. A save without the field behaves as it always did: the last save wins. GET /v1/widgets leaves updated_at null, so a script that only reads widgets here has no token to send and nothing to trip over.

Custom components

A custom component is a frame from your own website that the web chat shows when the conversation reaches one step of the agent’s published conversation: a booking calendar, a product card. The frame’s URL receives values the conversation has collected, as query parameters. Writes need the owner role: a widget speaks in the business’s name to visitors nobody has met.
All seven fields are required on create. A component reads those fields plus id, org_id, created_at and updated_at. Each step shows at most one component per widget, and a widget holds at most 20. When the chat reaches the step, the visitor’s browser loads iframe_url with each listed variable the conversation has a value for appended as a query parameter, replacing a same-named parameter already in the URL. Text is cut at 500 characters, true and false are written as words, and a value that is missing, a list or an object is left out.

Live chats

An owner can let a typed visitor ask for a person: the widget’s “Let visitors ask for a person” setting in the App, off by default. While it is off the visitor sees no button and an ask is refused. A visitor who asks waits with the agent paused; they can go back to the agent at any time, and after 5 minutes with nobody claiming the chat it returns to the agent by itself. These two routes are the desk. They answer in the standard envelope (Errors and limits), need the work_callback capability, and see only chats on the agent named by agent_id.
  • GET /v1/widget-live-chats?agent_id=... (sessions:write, operator role: the desk shows live visitors’ words) answers {"chats": [...], "lifetime": "chat"}: each waiting or person-held chat with its id, widget_id, agent_id, handoff (waiting or operator), revision, held_by_you, messages and expires_at.
  • POST /v1/widget-live-chats/{chat_id}?agent_id=... (sessions:write) with action and expected_revision (the revision you read): claim a waiting chat, reply with text (up to 500 characters) to the chat you hold, reclaim a chat another member holds (the owner, or any member of the desk, so a chat is never stranded) or resume to return it to the agent. Answers the chat. A changed revision, or an action the chat’s state does not allow, answers 409 conflict; another agent’s chat, or one that has ended, answers 404 not_found.
Live chats keep the chat’s own lifetime and message limits. Their state is stored with the admitted session, isolated by workspace, and shared across servers. A server restart does not reset the five-minute wait or lose the person holding a chat. Expiry clears the conversation content; the bounded end reason remains briefly for visitor polling. What a person said is handed to the agent as conversation, never as instructions. The widget’s “Every message” webhook still fires for each visitor message while a person holds the chat, with an empty agent_reply and handoff set to waiting or operator; every other message event carries handoff: "agent". The “End of conversation” webhook’s messages list each line with role visitor, agent or operator. Webhooks remain best-effort background delivery, not a durable delivery queue.

Refusals

A refusal these routes make is {"data": null, "errors": [{"error", "message"}]}: bad_request (400) for a field that breaks a rule above, not_found (404) for a widget or component that is not this workspace’s, and conflict (409) for a second component on the same step or a 21st on one widget. A missing credential, scope or role gets the standard error envelope (Errors and limits).