Widget & SDK
On the visitor side, integration happens in two steps: create a visitor session (which returns a short-lived signed token), then open a WebSocket authenticated by that token. The SDKs do this sequence for you.
The session lifecycle
Section titled “The session lifecycle”- The widget calls
GET /create/visitor-session(orGET /fetch/widget-configfirst, to render the launcher without yet creating a session). - The relay returns a visitor token (JWT, ~1 h) bound to
(tenant_id, widget_public_key, origin), plus the widget’s public config. - The widget opens
WSS /ws/widget?token=<jwt>and receives awelcomeframe. - On expiry, the widget calls
POST /refresh/visitor-token(with the old token, even expired, asBearer) to resume the same conversation — the Redis session lives much longer (~30 days) than the JWT.
Create a visitor session
Section titled “Create a visitor session”POST /create/visitor-session is public but CORS-gated: the relay
validates origin against the tenant’s allowed_origins (otherwise 403).
curl $BASE_URL/api/v1/create/visitor-session \ -X POST \ -H "Content-Type: application/json" \ -d '{ "tenant_id": "019e4ae7-1a2b-7c3d-8e4f-5a6b7c8d9e0f", "widget_public_key": "pk_...", "origin": "https://app.acme.com", "locale": "en" }'{ "status_code": 201, "data": { "session_id": "vs_...", "visitor_token": "<jwt>", "expires_at": "2026-06-21T11:00:00Z", "widget_config": { "tenant_name": "Demo Boutique", "is_online": true, "...": "..." } }, "message": "Visitor session created."}Body fields: tenant_id, widget_public_key, origin (required),
locale (default fr), metadata (free-form context), routing_key
(destination queue, e.g. a store id) and mode (bot by default, or
human to go straight to the operator queue).
The /ws/widget WebSocket
Section titled “The /ws/widget WebSocket”The visitor WebSocket is not a regular HTTP endpoint: the token is passed
as a query parameter (?token=<jwt>), because a browser cannot set a
custom header on the handshake of a WebSocket upgrade.
WSS $BASE_URL/api/v1/ws/widget?token=<visitor_jwt>Application-level close codes to handle on the client:
4401— invalid visitor token (re-create/refresh a session).4404— session expired server-side (re-create a session).
Frames are JSON discriminated by a type field. A few examples from the
protocol:
| Direction | type | Role |
|---|---|---|
| visitor → server | msg | Send a message (client_msg_id + content) |
| visitor → server | typing | Typing indicator (state: start|stop) |
| visitor → server | ping | Heartbeat (the server replies pong) |
| server → visitor | welcome | First frame after connection (session + config) |
| server → visitor | ack | Acknowledgement of a message (echoes client_msg_id) |
| server → visitor | msg | Message from the bot, an agent or the system |
| server → visitor | typing | Typing from the bot or the agent |
You normally don’t have to speak this protocol by hand: the SDKs wrap it.
The web SDKs
Section titled “The web SDKs”An Angular library with standalone components and a signal-based service, SSR-compatible. Targets Angular 17+.
npm install @bloonio/chat-angular @bloonio/chat-client-coreImport the stylesheet once in styles.css:
@import "@bloonio/chat-angular/styles.css";Declare the provider:
import { provideBloonioChat } from '@bloonio/chat-angular';
export const appConfig = { providers: [ provideBloonioChat({ tenantId: '019e4ae7-1a2b-7c3d-8e4f-5a6b7c8d9e0f', publicKey: 'pk_...', baseUrl: '$BASE_URL', locale: 'en', // Optional — link anonymous sessions to your users. // Your backend computes HMAC-SHA256(tenant_secret, user_id). // visitorTokenProvider: () => ... // returns "userId:signature" }), ],};Three rendering modes, all sharing the same BloonioChatService:
| Mode | Component | When |
|---|---|---|
| Floating | <bloonio-chat-launcher /> | Corner bubble that opens on click. |
| Docked panel | <bloonio-chat-panel mode="docked" /> | Permanent chat in a dedicated column. |
| Chromeless thread | <bloonio-chat-thread /> | Just the messages + the input, to embed. |
Programmatic access via the service:
import { inject } from '@angular/core';import { BloonioChatService } from '@bloonio/chat-angular';
const chat = inject(BloonioChatService);chat.unreadCount; // Signal<number>chat.botTyping; // Signal<boolean>chat.openPanel();chat.sendMessage('Where is my order?');The bootstrap (fetchWidgetConfig) is deferred via afterNextRender, so
server rendering makes no network call; the components connect on
hydration.
A framework-agnostic TypeScript core, zero runtime dependencies — it is the transport layer that the other SDKs wrap. Use it directly for a custom widget or an uncovered framework.
npm install @bloonio/chat-client-coreimport { ChatClient, WSClient } from '@bloonio/chat-client-core';
const client = new ChatClient({ baseUrl: '$BASE_URL' });
// 1. Bootstrap (config only, no session)const cfg = await client.fetchWidgetConfig({ tenantId: '019e4ae7-1a2b-7c3d-8e4f-5a6b7c8d9e0f', widgetPublicKey: 'pk_...', origin: window.location.origin,});
// 2. Create a visitor session (signed JWT)const sess = await client.createVisitorSession({ tenantId: cfg.tenant_id, widgetPublicKey: 'pk_...', origin: window.location.origin,});
// 3. Open the WebSocketconst ws = new WSClient( { baseUrl: '$BASE_URL', visitorToken: sess.visitor_token }, { onWelcome: (f) => console.log('welcome', f), onMessage: (f) => console.log('msg', f), onTyping: (f) => console.log('typing', f), onError: (f) => console.error('proto error', f), },);ws.connect();
// 4. Send a messagews.send({ type: 'msg', client_msg_id: 'c1', content: 'Where is my order?' });WSClient handles ping/pong and reconnection with exponential back-off
(0.5 s → 30 s). ChatClient wraps /fetch/widget-config,
/create/visitor-session and /identify/visitor.
Identify a visitor
Section titled “Identify a visitor”To link an anonymous session to one of your logged-in users, call
POST /identify/visitor with the visitor token as Bearer, and a
user_signature computed server-side:
hex(hmac_sha256(tenant_secret, user_id)). The server signature prevents a
malicious script from forging identities.
Next steps
Section titled “Next steps”- The exact REST surface. See the API reference.
- Receive events on your backend. See Webhooks.
- Wire up human operators. See Operators.