Public chat and embed
These routes support anonymous public chat and website embed sessions. A browser or your own backend calls them directly against your Radioso host (https://api.radioso.ai, https://api-us.radioso.ai, or your self-hosted origin) using the public launch token embedded in your site. Personal and service API credentials are not public-launch credentials.
How it works
- Enable public chat or website embed in general settings.
- Create a public chat session from the launch token.
- Send the returned
publicSessionTokenasx-radioso-public-sessionon chat and history requests. - Store the returned
resumeTokenif the visitor should resume the same history after the public session expires. - Preserve the server-set anonymous session cookie across requests when available.
The launch token identifies the public surface, and the public session identifies the visitor. The resume token is the only supported way to request the same visitor session in a later session exchange.
Create a public chat session
Use this route before sending chat messages:
POST /api/v1/public/chat/{token}/sessionsFor a public link:
{
"channel": "anonymous_link"
}For an Enterprise website embed, call the route from an origin admitted by the website embed origin setting:
{
"channel": "website_embed"
}Browser clients must keep using that same origin on later chat, history, and
feedback requests. Radioso checks the signed public session against the current
website embed origin setting each time. Removing an origin from the list stops
existing browser sessions from sending new messages. If the list contains *,
any origin is admitted. If the list is empty, no origin is admitted.
To resume an existing visitor session, include the previous response’s resumeToken:
{
"channel": "website_embed",
"resumeToken": "previous-resume-token"
}The response includes publicChatToken, publicSessionId, publicSessionToken, resumeToken, workspaceName, assistantBootstrapActive, expiresAt, and resumeExpiresAt. Internally, publicSessionId is the chat session boundary for conversation continuity.
chatSessionId and anonymousSessionId are accepted as client hints but are not trusted as resume credentials. Use resumeToken to resume and the returned publicSessionToken for chat requests.
Bootstrap greeting response
When you request a new conversation with startConversation: true, a bootstrap greeting is returned if the agent has one configured. The greeting text is in answer; suggestions carries the authored chips when the greeting uses Exact words. Each suggestion has a stable id, and a click submits its text as a normal user message.
{
"bootstrapGreetingId": "5f2c1c2e-8d0a-4d0e-9d61-7c7a4b3a1f10",
"answer": "Welcome to our support team! How can we help?",
"suggestions": [
{ "id": "chip-faq", "text": "Browse FAQs", "kind": "authored", "action": { "kind": "ask_followup" } },
{ "id": "chip-schedule", "text": "Schedule a call", "kind": "authored", "action": { "kind": "ask_followup" } }
]
}An Exact words greeting is delivered verbatim with no model call; its chips use kind: "authored".
Send a public chat message
{
"message": "What can this workspace answer?",
"stream": false,
"conversationId": "optional-conversation-id",
"bootstrapGreetingId": "optional-bootstrap-greeting-id"
}The public chat request also supports startConversation: true for bootstrap greeting requests, userExpectedLocale, and inputMetadata.
pageContext.browserTimeZone carries the visitor’s IANA time zone, so “today” and “tomorrow” in a question mean the visitor’s own day. The website embed and the hosted chat page send the browser’s zone with every message. A message without one, or with a zone the server does not know, reads today in the workspace time zone (see Settings); a value longer than 64 characters is dropped rather than failing the message.
Bootstrap greetings are not saved as conversations by themselves. A startConversation: true response can omit conversationId and include bootstrapGreetingId. If the visitor replies, send that bootstrapGreetingId with the first normal message and no conversationId; Radioso will save the displayed greeting before the user’s message. Use the conversationId returned by that first normal message for history and follow-up continuity.
Sending a newer message while a turn is running
If a newer message arrives for the same conversation before the current assistant
reply starts, Radioso cancels the current reply and answers from the latest
history. A non-streaming superseded request returns HTTP 409 with error code
chat_turn_superseded.
For a streaming request, the superseded stream ends with this terminal event:
event: cancelled
data: {"conversationId":"...","reason":"superseded","stage":"rendering"}Treat cancelled as terminal and clear the pending reply state. It contains no
assistant copy. Before an answer or cancellation, the stream can emit status
with stage interpreting, searching, or composing. Status is transient UI
state and does not prevent cancellation. If the first assistant chunk has already streamed, the first turn
finishes and the newer message runs next.
Answer chunks are incremental delivery. Guarded or durability-sensitive replies may be validated or committed before their first chunk, then replayed in bounded chunks. Clients should not assume every chunk is a live provider token.
If the assistant cannot produce a public response body, the route returns a
JSON error instead of an empty 204 response:
{
"error": {
"code": "service_unavailable",
"message": "Public chat response is unavailable.",
"details": {
"code": "public_chat_empty_response"
}
}
}Chat suggestions
A chat response can include suggestions. Each suggestion has text, a
provider-defined kind string, and an optional action. id appears only on
the authored exact-content chips described in Bootstrap greeting
response; the follow-up and action chips below
carry no id.
Two kinds of chips can appear together:
- Follow-up question chips have no
action(oraction.kindis"ask_followup"). They are pure text prompts: rendersuggestion.textand, when the visitor selects one, send it as a new user message withinputMetadata.method = "suggestion_click"andsuggestionSourceMessageId = <assistant message id>. - Action chips carry
action.kind === "start_intent"with anintent.skillName(and an optionalintent.intentName). They route to a specific assistant skill instead of asking a follow-up question. When the visitor activates one, sendinputMetadata.method = "intent_click"together with theintentobject verbatim, plussuggestionSourceMessageId. Sendingsuggestion_clickfor an action chip will work as a plain text turn but will bypass the structured skill handoff the chip was meant to trigger.
At most one action chip is returned per assistant turn. Action chips are in addition to the up-to-two follow-up question chips.
Example response fragment:
{
"suggestions": [
{
"text": "Contact us",
"kind": "contact_human",
"action": {
"kind": "start_intent",
"intent": {
"skillName": "human_contact.request",
"intentName": "no_context_refusal"
}
}
},
{ "text": "What's covered here?", "kind": "deeper" }
]
}The set of action kind values is open: new modules can register additional
providers, so clients should treat kind as an opaque string and decide
behavior from action rather than from kind.
History in the anonymous flow
Use GET /api/v1/public/chat/{token} to list conversations for the current anonymous session.
Use GET /api/v1/public/chat/{token}/history/{conversationId} to fetch one conversation from that same anonymous session.
Use GET /api/v1/public/chat/{token}/tail/{conversationId} to read messages created after a cursor. A conversation detail response includes tailCursor; send that cursor to receive only later visitor, AI, or human-agent messages. If cursor is omitted, tail returns the newest bounded page and a cursor for the newest returned message. Public tail responses do not include operator ownership metadata.
Use GET /api/v1/public/chat/{token}/events/{conversationId} to open a server-sent event stream for the same anonymous session. The stream emits ready after subscription and message.created when a new message is available. Browser clients that store the public session in local storage should consume this stream with fetch, not native EventSource, so they can send x-radioso-public-session.
Endpoint reference
Session and message payloads are typed in full in the API Reference.
POST /api/v1/public/chat/{token}/sessions
GET /api/v1/public/chat/{token}
POST /api/v1/public/chat/{token}
GET /api/v1/public/chat/{token}/history/{conversationId}
GET /api/v1/public/chat/{token}/tail/{conversationId}
GET /api/v1/public/chat/{token}/events/{conversationId}
GET /api/v1/public/chat/{token}/embed-config
GET /api/v1/public/chat/{token}/assistant-logo
POST /api/v1/public/chat/{token}/voice/sessions
POST /api/v1/public/chat/{token}/voice/sessions/{sessionKey}/vendor-conversationPOST /api/v1/public/chat/{token}/voice/sessions starts a voice call for a website embed public session: it opens a session on the agent’s voice runtime with the workspace’s vendor key and returns the vendor’s signed URL and a session key for the page to hand the vendor’s client. A browser on another origin calls it on the Radioso app host as POST /api/public/chat/{token}/voice/sessions, which answers the CORS preflight. Starts have their own small budgets, and every failure to start reads as 503 voice_unavailable. The website widget calls it for you when a visitor presses Talk; embed-config tells the widget whether to show that button with voice: { "available": true }, which is true only when the operator turned on calls for the website (surfaceSettings.websiteEmbed.voiceCallsOffered, false by default) and the agent’s voice is ready: the voice channel on, a voice runtime bound, and the workspace holding that vendor’s key. The session start itself needs only the voice to be ready, so a page of your own can start calls with the switch off. The answer carries recordingNotice when the agent has a recording notice for the session’s language; show it to the caller as the call starts. On an agent connected to an ElevenLabs agent Radioso maintains, it also carries firstMessage, what the call says first: the recording notice, then the agent’s greeting in the session’s language, as plain speech. Pass it to the ElevenLabs client as overrides.agent.firstMessage, and pass no override when it is absent, because an ElevenLabs agent that doesn’t allow the override closes the session. On such an agent the answer also carries vendorLanguage, the session’s language as ElevenLabs names it (it for it-CH, pt-br for pt-BR), when the call can start in that language and it differs from the ElevenLabs agent’s own; pass it as overrides.agent.language, and the same way, none when it is absent. The call then starts in the caller’s language: the greeting, the voice, and the language its speech recognition listens for.
Once the vendor’s client has opened the session, POST /api/v1/public/chat/{token}/voice/sessions/{sessionKey}/vendor-conversation with { "vendorConversationId": "conv_…" } tells Radioso which vendor conversation the call is (on ElevenLabs, what the client’s getId() returns), so the Inbox can play its recording. Radioso checks with the vendor, using the workspace’s key, that the conversation was held on the vendor agent the session was started on. It takes the id once, from the same public session the session was started for, within an hour of the start, and answers 204. A second report, another public session’s, a key Radioso never minted, or a conversation on another vendor agent is 404. 503 voice_unavailable means the id could not be checked now, which includes a vendor that does not show the conversation yet right after the call connects; send it again, as the widget does when the call ends. The widget sends it for you, and a page of your own calls it on the app host as /api/public/chat/{token}/voice/sessions/{sessionKey}/vendor-conversation.
Common failure modes
403on a website embed session usually means the origin is not admitted by the website embed origin setting.404usually means the public token is unknown or not active.409withchat_turn_supersededmeans a newer message replaced this turn before its reply started.503on a public chat message means Radioso accepted the public session but could not produce a response body.429on public chat means the optional server-side public chat rate limit has been reached.- If history appears empty, the browser or cookie jar is probably not preserving the HttpOnly anonymous session cookie.