Skip to content

Public chat API reference

9 min read

API version 1.0

Build your own chat client on a public agent. Routes, headers, server-sent events, error codes and limits of the public chat API, version 1.


The public chat API lets your application talk to a conversation agent without the platform user interface. You create a chat session, send messages and receive the reply as a stream. The Embed guide covers the no-code widget. This page covers the HTTP API behind it.

This API is a versioned contract. Read the versioning and deprecation policy and follow the changelog.

Before you start

  • You need an embed token. A workspace member with access to the agent finds it on the Embed tab of the agent editor, inside the embed snippet (data-token) and in the Public page link (embedToken).
  • Embedding must be turned on for the agent (Enable embed on the same tab). The API answers 403 while it is off.
  • The embed token identifies the agent. It is not a secret. Restrict Allowed origins when browsers call the API from your pages.
  • No API key is needed. Server-to-server calls work with the embed token alone.

Base URL

Base URL of this platform: https://connect-1077003934033.europe-west9.run.app/api

Every path on this page is relative to this base URL. The examples call it $API_BASE.

All routes of version 1 start with /public/v1/agents/{embedToken}.

Version

This page documents version 1.0. The major version is in the path (v1). Minor versions add fields or routes and never change existing ones. The changelog lists every version. Your client must ignore unknown fields and unknown event types. This keeps it compatible with future minor versions.

Authentication

Embed token

The embed token is the {embedToken} segment of every path. The API rejects an unknown token with 401 and a disabled one with 403.

Session token

A session is one conversation. Create a session returns a sessionToken. Send it in the X-Session-Token header on every session route. The token is returned once. The server stores only a hash of it. Store it on your side.

Session tokens do not expire. The workspace retention rule can empty the conversation content later (see Limits and lifetimes). When a session route answers 401, discard the stored session and create a new one.

Conventions

  • Responses wrap their content in { "data": ... }.
  • Request bodies wrap their content in { "payload": ... }. Send Content-Type: application/json.
  • Times are numbers: milliseconds since 1970-01-01 UTC.
  • Identifiers and tokens are UUID strings. Treat them as opaque.
  • A field with no value is null. An optional field can be absent.
  • Ignore fields and event types you do not know.

Routes

MethodPathPurpose
GET/public/v1/agents/{embedToken}/configRead the agent’s public configuration
POST/public/v1/agents/{embedToken}/sessionsCreate a session
GET/public/v1/agents/{embedToken}/sessions/{sessionId}Read a session and its messages
GET/public/v1/agents/{embedToken}/sessions/{sessionId}/mcp-app-htmlRead the HTML of MCP App cards
GET/public/v1/agents/{embedToken}/sessions/{sessionId}/messages/streamSend a message and stream the reply

Get the agent configuration

GET /public/v1/agents/{embedToken}/config

Returns the branding of the agent. Use it to render your own header.

curl "$API_BASE/public/v1/agents/$EMBED_TOKEN/config"
{
  "data": {
    "agentName": "Helpful Assistant",
    "title": "Help Center",
    "logoUrl": "https://example.com/logo.png",
    "primaryColor": "#2563eb",
    "bannerText": "Test version, for the pilot team only"
  }
}
FieldTypeDescription
agentNamestringName of the agent
titlestring or nullWidget title. Fall back to agentName when null
logoUrlstring or nullLogo image URL
primaryColorstring or nullBrand color, hex
bannerTextstring or nullNotice to pin above the conversation

Status: 200. Errors: 401, 403.

Create a session

POST /public/v1/agents/{embedToken}/sessions

Creates a conversation and returns its token. Call it once per visitor, then store sessionId and sessionToken.

curl -X POST "$API_BASE/public/v1/agents/$EMBED_TOKEN/sessions" \
  -H "Content-Type: application/json" \
  -d '{ "payload": { "externalVisitorId": "visitor-42" } }'
Request fieldTypeDescription
payload.externalVisitorIdstring, optionalYour own stable identifier for the visitor. Tools receive it. Do not put personal data in it
{
  "data": {
    "sessionId": "8d1f5c2e-3b7a-4e9c-9f0d-2a6b7c8d9e10",
    "sessionToken": "0f9e8d7c-6b5a-4433-2211-00ffeeddccbb"
  }
}

Status: 201. Errors: 401, 403.

When the agent has a greeting message, the session starts with that message as its first assistant message. Read it with Get a session.

Get a session

GET /public/v1/agents/{embedToken}/sessions/{sessionId}
X-Session-Token: {sessionToken}

Returns the session and all its messages, oldest first.

{
  "data": {
    "id": "8d1f5c2e-3b7a-4e9c-9f0d-2a6b7c8d9e10",
    "agentId": "2c4e6a8b-0d1f-4a2b-8c3d-4e5f6a7b8c9d",
    "createdAt": 1788000000000,
    "messages": [
      {
        "id": "5a6b7c8d-9e0f-4a1b-8c2d-3e4f5a6b7c8d",
        "role": "user",
        "content": "What is your return policy?",
        "status": "completed",
        "createdAt": 1788000001000
      },
      {
        "id": "6b7c8d9e-0f1a-4b2c-9d3e-4f5a6b7c8d9e",
        "role": "assistant",
        "content": "You can return an item within 30 days.",
        "status": "completed",
        "createdAt": 1788000002000,
        "toolCalls": [
          {
            "id": "call-1",
            "name": "lookup_knowledge_base",
            "arguments": { "query": "return policy" }
          }
        ]
      }
    ]
  }
}
FieldTypeDescription
idstringSession identifier
agentIdstringAgent identifier
createdAtnumberCreation time
messages[].idstringMessage identifier
messages[].roleuser, assistant or toolAuthor of the message
messages[].contentstringText of the message
messages[].statusstreaming, completed, aborted or error, optionalState of an assistant reply
messages[].createdAtnumberCreation time
messages[].toolCallsarray, optionalTools the agent ran for this reply
toolCalls[].idstringCall identifier
toolCalls[].namestringTool name
toolCalls[].argumentsobjectArguments the agent sent
toolCalls[].resultany, optionalRaw tool result, present for MCP App cards
toolCalls[].mcpAppobject, optionalMCP App card pointer: mcpServerId, resourceUri

A message with status: "streaming" is still being written. Read the session again until the status settles. The reference widget reads it every 2 seconds. A reply that makes no progress for 5 minutes becomes aborted.

The mcpApp pointer never carries HTML here. Read it with the next route.

Status: 200. Errors: 401, 403.

Get MCP App card HTML

GET /public/v1/agents/{embedToken}/sessions/{sessionId}/mcp-app-html
X-Session-Token: {sessionToken}

Returns the current HTML of every MCP App card the session’s replies point at. The server contacts each MCP server, so call this route after you display the messages, and only when a message has toolCalls[].mcpApp.

{
  "data": [
    {
      "mcpServerId": "3d5f7a9b-1c2e-4f3a-9b4c-5d6e7f8a9b0c",
      "resourceUri": "ui://order-summary/card.html",
      "html": "<!doctype html>..."
    }
  ]
}

Match a card to its message with mcpServerId and resourceUri. The array is empty when no card exists.

Status: 200. Errors: 401, 403.

Send a message and stream the reply

GET /public/v1/agents/{embedToken}/sessions/{sessionId}/messages/stream?q={encodedPayload}
X-Session-Token: {sessionToken}
Accept: text/event-stream

Sends the visitor’s message and returns the reply as server-sent events. The payload travels URL-encoded in the q query parameter, so the route works with GET:

const q = encodeURIComponent(JSON.stringify({ payload: { content: "What is your return policy?" } }))
Payload fieldTypeDescription
payload.contentstring, requiredThe visitor’s message. Must not be empty
curl -N "$API_BASE/public/v1/agents/$EMBED_TOKEN/sessions/$SESSION_ID/messages/stream?q=%7B%22payload%22%3A%7B%22content%22%3A%22Hello%22%7D%7D" \
  -H "X-Session-Token: $SESSION_TOKEN" \
  -H "Accept: text/event-stream"

The browser EventSource API cannot send the X-Session-Token header. Use fetch and read the response body as a stream (see Quick start). POST on this path returns 404.

Status: 200 with Content-Type: text/event-stream. Errors before the stream starts: 401, 403. Errors on the payload arrive inside the stream (see below).

After the final event, read the session again to get the persisted reply and its toolCalls. The stream carries text only.

Server-sent events

Wire format

The response starts with one empty line. Then each event is two lines followed by an empty line:

id: 1
data: {"type":"start","messageId":"6b7c8d9e-0f1a-4b2c-9d3e-4f5a6b7c8d9e"}

id: 2
data: {"type":"chunk","messageId":"6b7c8d9e-0f1a-4b2c-9d3e-4f5a6b7c8d9e","content":"You can"}

id: 3
data: {"type":"chunk","messageId":"6b7c8d9e-0f1a-4b2c-9d3e-4f5a6b7c8d9e","content":" return an item within 30 days."}

id: 4
data: {"type":"end","messageId":"6b7c8d9e-0f1a-4b2c-9d3e-4f5a6b7c8d9e","fullContent":"You can return an item within 30 days."}
  • id counts from 1 for each response. It is not a resume token: the server ignores Last-Event-ID.
  • data is one JSON object. Its type field names the event.
  • Normal events have no event: line. Browsers deliver them as message events.
  • The server sends no heartbeat and no [DONE] marker. It closes the connection after the final event.

Event catalog

typeFieldsMeaning
startmessageIdThe reply starts. messageId is the identifier of the assistant message
chunkmessageId, contentA piece of text. Append it to the reply
notify_clienttoolNameThe agent ran a tool. Informational
endmessageId, fullContentThe reply is complete. fullContent is the whole text
errormessageId, errorThe reply failed. error is a message for logs

Order and end of stream

  1. One start event.
  2. Any number of chunk and notify_client events, in any order.
  3. Exactly one end or error event.

Then the server closes the connection.

Stream-level errors

When the request cannot start a reply, the HTTP status is still 200 and the stream carries one error event, then the connection closes. messageId is empty because no reply was started:

id: 1
data: {"type":"error","messageId":"","error":"User content must not be empty"}
errorCause
Invalid query formatq is not valid JSON
User content must not be emptypayload.content is missing or blank
Agent not foundThe agent was deleted
Internal server errorServer fault. Retry later

A failed generation sends one error event with the messageId of the reply, then closes. Every failure takes this form. The stream never carries a named event: error frame.

Reconnection

There is no resume. When the connection drops, read the session: the server keeps writing the reply and settles its status on its own. Poll until the last assistant message is no longer streaming.

Errors

Error responses are JSON:

{ "statusCode": 401, "message": "Invalid embed token", "error": "Unauthorized" }
StatusmessageCauseWhat to do
401Invalid embed tokenUnknown embed tokenCheck the token on the Embed tab
403Embed access is disabled for this agentEnable embed is offTurn embedding on
403Origin not allowedThe browser Origin is not in Allowed originsAdd your origin, or clear the list
401Missing session tokenNo X-Session-Token headerSend the header
401Invalid session tokenToken and session do not matchCreate a new session
401Session does not belong to this agentSession created with another embed tokenCreate a new session
404Cannot GET /public/...Unknown path or methodCheck the route table
400(parser message)Malformed JSON bodyFix the body. This response has no error field
413request entity too largeBody over 500 kBShorten the body. This response has no error field
500Internal server errorServer faultRetry later

The stream route reports payload errors inside the stream, see Stream-level errors.

CORS

Browsers on any origin can call these routes:

  • The response Access-Control-Allow-Origin echoes the request Origin.
  • The preflight allows the headers you request, including X-Session-Token and Content-Type.
  • No cookies and no credentials are used.

When Allowed origins is set on the Embed tab, the API compares the browser Origin header to that list and answers 403 Origin not allowed on a mismatch. Server-to-server calls send no Origin header and are not filtered by the list.

Limits and lifetimes

  • JSON request bodies are limited to 500 kB (413 above).
  • The stream request travels in the URL. Keep the encoded q parameter under 8 kB.
  • One reply per stream request. Wait for end before you send the next message.
  • No rate limit is enforced today. Use the API fairly. A limit would be announced on the changelog as a minor change, with 429 responses.
  • Session content follows the workspace retention rule (30 days after creation by default). After that, the messages have empty content and no toolCalls. The session identifier and token still work. Create a new session for a new conversation.

Quick start

A minimal JavaScript client. It creates or restores a session, streams a reply, and reads the session again after the final event.

const API_BASE = "https://<your-api-host>"
const EMBED_TOKEN = "<embedToken>"

async function api(path, init = {}) {
  const response = await fetch(`${API_BASE}${path}`, init)
  if (!response.ok) throw new Error(`HTTP ${response.status}`)
  return response
}

// 1. Create or restore a session
let session = JSON.parse(localStorage.getItem("chat-session") ?? "null")
if (!session) {
  const response = await api(`/public/v1/agents/${EMBED_TOKEN}/sessions`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ payload: {} }),
  })
  session = (await response.json()).data
  localStorage.setItem("chat-session", JSON.stringify(session))
}

const sessionPath = `/public/v1/agents/${EMBED_TOKEN}/sessions/${session.sessionId}`
const sessionHeaders = { "X-Session-Token": session.sessionToken }

// 2. Send a message and stream the reply
async function send(content, onChunk) {
  const q = encodeURIComponent(JSON.stringify({ payload: { content } }))
  const response = await api(`${sessionPath}/messages/stream?q=${q}`, {
    headers: { ...sessionHeaders, Accept: "text/event-stream" },
  })
  const reader = response.body.getReader()
  const decoder = new TextDecoder()
  let buffer = ""
  while (true) {
    const { done, value } = await reader.read()
    if (done) break
    buffer += decoder.decode(value, { stream: true })
    const blocks = buffer.split("\n\n")
    buffer = blocks.pop() ?? ""
    for (const block of blocks) {
      const lines = block.split("\n")
      const data = lines.find((line) => line.startsWith("data: "))?.slice(6)
      if (!data) continue
      const event = JSON.parse(data)
      if (event.type === "chunk") onChunk(event.content)
      if (event.type === "error") throw new Error(event.error)
    }
  }
}

// 3. Read the persisted conversation
async function readSession() {
  const response = await api(sessionPath, { headers: sessionHeaders })
  return (await response.json()).data
}

await send("Hello!", (text) => process.stdout.write(text))
console.log(await readSession())

Handle 401 on a session route by clearing the stored session and creating a new one.

Last updated: September 10, 2026

Was this article helpful?