Brandfine Docs
REST API

Live Chat

Public chat endpoints for workspaces running the Brandfine Live Chat plugin — widget config, starting conversations with the publishable key, and sending/reading messages with a per-conversation token.

Five public endpoints under /external/live-chat/*. Unlike the other /external namespaces, Live Chat is authenticated with the scoped publishable key — a brandfine_pk_… credential that is safe to embed in public HTML because it can only start chat conversations for its workspace. The broad workspace API key is deliberately rejected on the browser-facing routes (and vice versa).

MethodPathAuthPurpose
GET/external/live-chat/configpublishable keyRuntime widget config — enabled state, greeting, theme.
POST/external/live-chat/conversationspublishable keyStart a conversation, or resume one by token.
POST/external/live-chat/conversations/:token/messagesconversation tokenVisitor sends a message.
GET/external/live-chat/conversations/:token/messagesconversation tokenHistory / catch-up poll.
GET/external/live-chat/bootstrapbroad workspace keyServer-side SDK bootstrap — returns the publishable key + config.

All endpoints inherit the permissive /external/* CORS. The first four are designed for browser calls — that is the entire point of the publishable key. The bootstrap endpoint is the one exception: it authenticates with the broad workspace key and must only ever be called server-side (your backend, RSC, or build step).

The two-step auth model

  1. Publishable key (data-publishable-key on the widget, or publishableKey from the bootstrap) opens the door: it can fetch the widget config and start/resume a conversation. Nothing else.
  2. Conversation token — returned once by the start endpoint — is the credential for everything after: sending messages and reading history for that one conversation. The widget persists it in localStorage so the visitor resumes the same thread across page loads.

A publishable key presented to any broad /external endpoint (posts, submissions, …) is rejected with 401, and the broad key is rejected on the publishable-only routes. Keys are minted per workspace when Live Chat is first activated; find yours in Plugins → Live Chat → Manage settings → Integrate.


GET /external/live-chat/config

The widget's first call on every page load. Resolved live — never baked into a build — so activating or deactivating the plugin in the CMS takes effect on the visitor's next page load with no redeploy of your site.

{
  "enabled": true,
  "greeting": "Hi! How can we help?",
  "offlineMessage": "We're away — leave a message.",
  "theme": { "--bf-chat-primary": "#0b1f2a" }
}

Returns { "enabled": false } when the plugin is deactivated, disabled, or the key is unknown — render nothing in that case.

POST /external/live-chat/conversations

Start or resume. Send the widget's stored token to resume; a token that no longer matches an open conversation in this workspace falls through to creating a fresh one.

// request
{
  "visitorSessionId": "9f2c…",        // widget-generated, localStorage
  "conversationToken": "8e5ff9…",     // optional — resume path
  "pageUrl": "https://example.com/pricing",
  "referrer": "https://google.com",
  "locale": "en-US"
}

// response
{
  "enabled": true,
  "conversationId": "cmrw…",
  "conversationToken": "8e5ff9…",
  "greeting": "Hi! How can we help?",
  "resumed": false
}

POST /external/live-chat/conversations/:token/messages

Visitor sends a message (201). Body is capped at 2000 chars; visitorName / visitorEmail are optional and only ever fill gaps — they never overwrite values the visitor gave earlier. Flood-guarded per conversation (409 beyond 20 messages/minute). 409 also when the conversation was closed by the team.

// request
{ "body": "Do you ship to Norway?", "visitorEmail": "sam@example.com" }

// response
{ "id": "cmrw…", "sender": "VISITOR", "body": "Do you ship to Norway?", "createdAt": "2026-07-22T21:26:40.997Z" }

GET /external/live-chat/conversations/:token/messages

History and catch-up poll. Pass ?after=<ISO timestamp> (your last-seen createdAt) to receive only newer messages. Fetching history marks the team's messages as read.

{
  "status": "OPEN",
  "messages": [
    { "id": "…", "sender": "VISITOR", "body": "Do you ship to Norway?", "createdAt": "…" },
    { "id": "…", "sender": "AGENT",   "body": "Yes — free over €100!",   "createdAt": "…" }
  ]
}

Render message bodies as text, never as HTML.

GET /external/live-chat/bootstrap

Server-side only — authenticates with the broad workspace key from your environment. Returns everything the @brandfine/client server half needs to render the widget, including the publishable key to bake into your HTML:

{
  "enabled": true,
  "publishableKey": "brandfine_pk_…",
  "greeting": "Hi! How can we help?",
  "offlineMessage": null,
  "theme": null,
  "scriptPath": "/widgets/live-chat.js"
}

Use it through bf.liveChat.getConfig() rather than calling it directly.

On this page