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.

  1. Create a conversation — POST /sdk/v1/conversations
  2. Get a connection token — POST /sdk/v1/auth/connect-token
  3. Connect — Open a WebSocket to wss://realtime.freeday.ai/connection/websocket with the token. See the Centrifugo client SDK docs.
  4. Get a subscription token — POST /sdk/v1/auth/subscribe-token
  5. Subscribe — Subscribe to the channel from step 4 with a getToken callback.

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.

  1. Send the first call, usually Create a conversation, without X-Session-Id.
  2. Read the X-Session-Id header from the response and store it, for example in local storage.
  3. Send the stored ID as X-Session-Id on 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:

  1. Delete the stored session ID and the current conversation ID.
  2. Create a conversation without X-Session-Id, and store the new session ID from the response.
  3. 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:

  1. Get an access token with the client-credentials grant and scope=conversations:create realtime:access.
  2. Create a conversation: POST /realtime/v1/conversations with Authorization: Bearer <token> and the same body as POST /sdk/v1/conversations.
  3. Get a connection token: POST /realtime/v1/auth/connect-token with the bearer token.
  4. Connect to wss://realtime.freeday.ai/connection/websocket with the connection token.
  5. Get a subscription token: POST /realtime/v1/auth/subscribe-token with the bearer token and the conversation_id.
  6. 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 company
  • conversationId — 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.