Realtime WebSocket messaging
Real-time messaging API for the Freeday webchat, powered by Centrifugo. The typed message payloads are in the Message schemas reference; this page covers how the protocol works.
Quick Start
| Service | URL |
|---|---|
| REST API | https://api.freeday.ai |
| WebSocket | wss://realtime.freeday.ai |
All REST calls require the X-Company header (from Settings → Company in the Freeday platform). After the first call, they also require the X-Session-Id header. See Sessions.
Pass X-Channel-Id as well to keep each channel's conversation history separate: when it is set, listing conversations and reading their messages is limited to the digital employees routed from that channel, so a conversation started in one widget or app does not surface in another.
- Create a conversation —
POST /sdk/v1/conversations - Get a connection token —
POST /sdk/v1/auth/connect-token - Connect — Open a WebSocket to
wss://realtime.freeday.ai/connection/websocketwith the token. See the Centrifugo client SDK docs. - Get a subscription token —
POST /sdk/v1/auth/subscribe-token - Subscribe — Subscribe to the
channelfrom step 4 with agetTokencallback.
Example (centrifuge-js):
const sub = client.newSubscription(channel, {
getToken: async () => {
const res = await fetch("https://api.freeday.ai/sdk/v1/auth/subscribe-token", {
method: "POST",
headers: { "Content-Type": "application/json", "X-Session-Id": sessionId, "X-Company": companyId },
body: JSON.stringify({ conversation_id: conversationId }),
});
return (await res.json()).subscription_token;
},
});
sub.on("publication", (ctx) => handleEnvelope(ctx.data));
sub.subscribe();
Sessions
The server issues the session ID. Do not generate one yourself.
- Send the first call, usually Create a conversation, without
X-Session-Id. - Read the
X-Session-Idheader from the response and store it, for example in local storage. - Send the stored ID as
X-Session-Idon every later call. It links the visitor to their conversations.
A session expires after 30 days without activity. A call with an expired or unknown session ID gets 403 with this body:
{ "type": "client_error", "errors": [{ "code": "session_invalid", "detail": "Invalid session", "attr": null }] }
When you get session_invalid, start over with a new session:
- Delete the stored session ID and the current conversation ID.
- Create a conversation without
X-Session-Id, and store the new session ID from the response. - Get a new connection token and subscription token, and subscribe to the new conversation.
The new session cannot see the conversations of the expired one. Requests for an old conversation, such as a subscription token, are refused.
Server-to-server (API client)
A server can talk to a digital employee without an end-user session, with an API client. The API client owns the conversation, so use the same client for every step:
- Get an access token with the client-credentials grant and
scope=conversations:create realtime:access. - Create a conversation:
POST /realtime/v1/conversationswithAuthorization: Bearer <token>and the same body asPOST /sdk/v1/conversations. - Get a connection token:
POST /realtime/v1/auth/connect-tokenwith the bearer token. - Connect to
wss://realtime.freeday.ai/connection/websocketwith the connection token. - Get a subscription token:
POST /realtime/v1/auth/subscribe-tokenwith the bearer token and theconversation_id. - Subscribe, then send and receive messages as described below.
These calls need no X-Session-Id or X-Company header: the access token carries the company.
Authentication
JWT tokens are used for both connection and channel subscription authentication:
- Connection token — obtained via
POST /sdk/v1/auth/connect-token(15 min expiry). See the REST API reference. - Subscription token — obtained via
POST /sdk/v1/auth/subscribe-token(24 hour expiry). See the REST API reference.
Conversation channel
Channel address: messages:companies.{companyId}.conversations.{conversationId}
companyId— UUID of the companyconversationId— UUID of the conversation
Clients subscribe to receive messages from the digital employee, agents, and system, and publish to send user messages.
History: 100 messages are retained for 1 hour. See the Centrifugo history documentation for retrieval details.
Message flow
Sending: Publish a SendMessage via subscription.publish().
Receiving: The server streams MessageEnvelope objects. Each envelope carries a sequence number — discard out-of-order envelopes, and ignore envelopes with an unrecognized proto version. A message streams in via replace → … → complete.
Streaming example:
{ "operation": "replace", "sequence": 1, "message_snapshot": { "status": "partial", "blocks": [{ "id": "b1", "type": "typing", "typing": true }] } }
{ "operation": "replace", "sequence": 2, "message_snapshot": { "status": "partial", "blocks": [{ "id": "b2", "type": "markdown", "content": "Hello! I can" }] } }
{ "operation": "complete", "sequence": 3, "message_snapshot": { "status": "complete", "blocks": [{ "id": "b2", "type": "markdown", "content": "Hello! I can help you with that." }] } }
Message schemas
The full typed payloads (MessageEnvelope, Message, SendMessage, block types, …) are in the Message schemas reference nested under this section. The machine-readable AsyncAPI 3.1 spec is available in the repo at docs/async-api/webchat-sdk-async.yaml.