{"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 optionalexpected_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 thework_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 itsid,widget_id,agent_id,handoff(waitingoroperator),revision,held_by_you,messagesandexpires_at.POST /v1/widget-live-chats/{chat_id}?agent_id=...(sessions:write) withactionandexpected_revision(therevisionyou read):claima waiting chat,replywithtext(up to 500 characters) to the chat you hold,reclaima chat another member holds (the owner, or any member of the desk, so a chat is never stranded) orresumeto return it to the agent. Answers the chat. A changedrevision, or an action the chat’s state does not allow, answers409 conflict; another agent’s chat, or one that has ended, answers404 not_found.
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).