Skip to content

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

  1. Enable public chat or website embed in general settings.
  2. Create a public chat session from the launch token.
  3. Send the returned publicSessionToken as x-radioso-public-session on chat and history requests.
  4. Store the returned resumeToken if the visitor should resume the same history after the public session expires.
  5. 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:

text
POST /api/v1/public/chat/{token}/sessions

For a public link:

json
{
  "channel": "anonymous_link"
}

For an Enterprise website embed, call the route from an origin admitted by the website embed origin setting:

json
{
  "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:

json
{
  "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.

json
{
  "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

json
{
  "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:

text
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:

json
{
  "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 (or action.kind is "ask_followup"). They are pure text prompts: render suggestion.text and, when the visitor selects one, send it as a new user message with inputMetadata.method = "suggestion_click" and suggestionSourceMessageId = <assistant message id>.
  • Action chips carry action.kind === "start_intent" with an intent.skillName (and an optional intent.intentName). They route to a specific assistant skill instead of asking a follow-up question. When the visitor activates one, send inputMetadata.method = "intent_click" together with the intent object verbatim, plus suggestionSourceMessageId. Sending suggestion_click for 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:

json
{
  "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.

text
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-conversation

POST /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

  • 403 on a website embed session usually means the origin is not admitted by the website embed origin setting.
  • 404 usually means the public token is unknown or not active.
  • 409 with chat_turn_superseded means a newer message replaced this turn before its reply started.
  • 503 on a public chat message means Radioso accepted the public session but could not produce a response body.
  • 429 on 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.