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).
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /external/live-chat/config | publishable key | Runtime widget config — enabled state, greeting, theme. |
POST | /external/live-chat/conversations | publishable key | Start a conversation, or resume one by token. |
POST | /external/live-chat/conversations/:token/messages | conversation token | Visitor sends a message. |
GET | /external/live-chat/conversations/:token/messages | conversation token | History / catch-up poll. |
GET | /external/live-chat/bootstrap | broad workspace key | Server-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
- Publishable key (
data-publishable-keyon the widget, orpublishableKeyfrom the bootstrap) opens the door: it can fetch the widget config and start/resume a conversation. Nothing else. - 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
localStorageso 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.