Skip to content

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.

  1. The widget calls GET /create/visitor-session (or GET /fetch/widget-config first, to render the launcher without yet creating a session).
  2. The relay returns a visitor token (JWT, ~1 h) bound to (tenant_id, widget_public_key, origin), plus the widget’s public config.
  3. The widget opens WSS /ws/widget?token=<jwt> and receives a welcome frame.
  4. On expiry, the widget calls POST /refresh/visitor-token (with the old token, even expired, as Bearer) to resume the same conversation — the Redis session lives much longer (~30 days) than the JWT.

POST /create/visitor-session is public but CORS-gated: the relay validates origin against the tenant’s allowed_origins (otherwise 403).

Fenêtre de terminal
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 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:

DirectiontypeRole
visitor → servermsgSend a message (client_msg_id + content)
visitor → servertypingTyping indicator (state: start|stop)
visitor → serverpingHeartbeat (the server replies pong)
server → visitorwelcomeFirst frame after connection (session + config)
server → visitorackAcknowledgement of a message (echoes client_msg_id)
server → visitormsgMessage from the bot, an agent or the system
server → visitortypingTyping from the bot or the agent

You normally don’t have to speak this protocol by hand: the SDKs wrap it.

An Angular library with standalone components and a signal-based service, SSR-compatible. Targets Angular 17+.

Fenêtre de terminal
npm install @bloonio/chat-angular @bloonio/chat-client-core

Import the stylesheet once in styles.css:

@import "@bloonio/chat-angular/styles.css";

Declare the provider:

app.config.ts
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:

ModeComponentWhen
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.

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.