Live Chat API (Public)

These endpoints power the visitor-facing side of the SimplerDevelopment live chat widget. Use them to start a conversation session, send messages as a visitor, and subscribe to a real-time stream of agent replies. All three endpoints are public — no platform API key is required — but message and stream calls require the short-lived ephemeralToken you receive when starting a conversation.

Base URL: https://<your-tenant-domain>/api/public/chat

Authentication: These are public endpoints. See authentication.md for platform-level auth. The visitor-scoped ephemeralToken (returned by POST /start) is the only credential used here — it is scoped to a single conversation and cannot be used to access any other tenant's data.


Endpoints#

POST/api/public/chat/start#

Start (or resume) a visitor conversation for a given chat widget.

The call is idempotent on (widgetId, visitorId): if the visitor already has an open or assigned conversation on this widget, the existing conversation is returned and contact details are patched if they were previously missing.

  • Auth: Public — no API key needed.

Request body

FieldTypeRequiredDescription
widgetIdnumber | stringYesID of the chat widget, as configured in the portal.
visitorIdstringYesStable, client-generated visitor identifier (max 64 chars). You are responsible for generating and persisting this (e.g. localStorage UUID).
namestringNoVisitor's display name (max 255 chars). Stored on first supply; ignored if the conversation already has a name.
emailstringNoVisitor's email address (max 255 chars). Stored on first supply; ignored if the conversation already has an email.
{
  "widgetId": 42,
  "visitorId": "anon-a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "name": "Jane Smith",
  "email": "jane@example.com"
}

Response

{
  "success": true,
  "data": {
    "conversationId": 1017,
    "widgetId": 42,
    "ephemeralToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…",
    "greetingMessage": "Hi there! How can we help you today?",
    "primaryColor": "#6366f1",
    "position": "bottom-right",
    "awayMessage": "We're away right now — leave a message and we'll be in touch soon."
  }
}

Store conversationId and ephemeralToken — both are required for all subsequent calls.

Errors

StatusMessageCause
400Invalid JSON bodyRequest body could not be parsed as JSON.
400widgetId is requiredwidgetId is missing, non-numeric, or <= 0.
400visitorId is requiredvisitorId is missing, empty, or longer than 64 chars.
404Widget not availableNo widget found with that ID, or the widget is disabled.

curl example

curl -X POST https://<your-tenant-domain>/api/public/chat/start \
  -H "Content-Type: application/json" \
  -d '{
    "widgetId": 42,
    "visitorId": "anon-a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "name": "Jane Smith",
    "email": "jane@example.com"
  }'

POST/api/public/chat/messages#

Send a message from the visitor into the conversation.

  • Auth: Ephemeral token — pass the ephemeralToken from POST /start in the request body.

Request body

FieldTypeRequiredDescription
conversationIdnumberYesID returned by POST /start.
ephemeralTokenstringYesToken returned by POST /start. Must match the conversationId — mismatches are rejected.
bodystringYesMessage text. Max 4,000 characters.
{
  "conversationId": 1017,
  "ephemeralToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…",
  "body": "Hi, I have a question about my recent invoice."
}

Response

Returns the inserted message record.

{
  "success": true,
  "data": {
    "id": 8821,
    "conversationId": 1017,
    "clientId": 7,
    "authorKind": "visitor",
    "authorName": "Jane Smith",
    "body": "Hi, I have a question about my recent invoice.",
    "occurredAt": "2026-06-04T12:34:56.000Z"
  }
}

Errors

StatusMessageCause
400Invalid JSON bodyRequest body could not be parsed as JSON.
400Message body is requiredbody is missing or blank after trimming.
401Invalid tokenephemeralToken is missing, expired, or malformed.
401Token / conversation mismatchThe token was valid but was issued for a different conversation.
404Conversation not foundNo conversation exists for the verified conversationId.
409Conversation is closedThe conversation has been closed by an agent; no more messages can be sent.
413Message too longbody exceeds 4,000 characters.
429Too many messages, slow downVisitor-level rate limit exceeded. Respect the Retry-After response header (seconds) before retrying.

curl example

curl -X POST https://<your-tenant-domain>/api/public/chat/messages \
  -H "Content-Type: application/json" \
  -d '{
    "conversationId": 1017,
    "ephemeralToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…",
    "body": "Hi, I have a question about my recent invoice."
  }'

GET/api/public/chat/stream#

Open a Server-Sent Events (SSE) stream to receive real-time messages from the agent.

Connect once after calling POST /start and keep the connection open. The server pushes each event as the agent replies. Most browsers handle reconnection automatically via the EventSource API; if your client disconnects, simply re-open the stream.

Node runtime only. This endpoint runs on Node (not Edge) because the underlying Postgres LISTEN/NOTIFY subscription requires a persistent socket.

  • Auth: Ephemeral token — pass as the token query parameter.

Query parameters

NameTypeRequiredDescription
conversationIdnumberYesThe conversation to subscribe to.
tokenstringYesThe ephemeralToken from POST /start. Must match conversationId.

Response headers

Content-Type: text/event-stream
Cache-Control: no-cache, no-transform
Connection: keep-alive
X-Accel-Buffering: no

SSE event format

Each event follows the standard SSE wire format:

event: <kind>
data: <JSON payload>

hello event — sent immediately on connection to confirm the stream is live.

event: hello
data: {"conversationId":1017}

message event — sent each time the agent (or anyone else) posts to the conversation. The top-level kind field drives the SSE event: line; message fields are nested under data.

event: message
data: {
  "kind": "message",
  "eventId": "lp3k2abc-xy4z9w",
  "occurredAt": "2026-06-04T12:35:10.000Z",
  "data": {
    "id": 8834,
    "conversationId": 1017,
    "authorKind": "agent",
    "authorName": "Support Team",
    "body": "Happy to help — can you share your order number?",
    "occurredAt": "2026-06-04T12:35:10.000Z"
  }
}

Heartbeat — a comment line (: ping) is sent every 25 seconds to prevent proxies and load balancers from dropping idle connections. The EventSource API silently ignores comment lines.

: ping

Errors

StatusBodyCause
401Unauthorizedtoken is missing, invalid, or does not match conversationId.
404Not foundNo conversation found for the verified conversationId.

Browser EventSource example

const conversationId = 1017;
const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…";
const url = `https://<your-tenant-domain>/api/public/chat/stream` +
  `?conversationId=${conversationId}&token=${encodeURIComponent(token)}`;

const es = new EventSource(url);

es.addEventListener("hello", (e) => {
  console.log("Stream connected:", JSON.parse(e.data));
});

es.addEventListener("message", (e) => {
  const msg = JSON.parse(e.data);
  // msg.kind === "message"; message fields are under msg.data
  console.log(`[${msg.data.authorKind}] ${msg.data.body}`);
});

es.onerror = () => {
  // EventSource reconnects automatically on transient failures.
};

curl example

curl -N "https://<your-tenant-domain>/api/public/chat/stream?conversationId=1017&token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…"