The Conversational agent API (also referred to as CoCoAaS — Conversational Commerce as a Service) powers the chat experience that shoppers see embedded in a storefront. This page covers what you need to start a frontend integration: your identifiers, the two core endpoints, and how the event-based chat model works.
WarningThe current API is in Beta. The stable API is expected to differ slightly in paths, and have a token-based authentication mechanism. These changes will require small implementation changes when migrating.
For the full frontend integration guide, OpenAPI spec (request/response payloads, and status codes), and a ready-to-run reference implementation, see the conversational-agent-api GitHub repo:
This page is a quickstart, and the repo is the source of truth for implementation details.
Prerequisites
You'll get three values from Bloomreach during onboarding:
| Value | Description |
|---|---|
apiUrl | Your backend base URL. |
projectId | Your project identifier. |
personaId | The persona (assistant) identifier to chat with. |
The following two values are owned by your storefront:
| Value | Description |
|---|---|
currency | ISO 4217 currency code (USD, GBP, etc.) from your storefront config. Used to format prices and passed in FE.SET_CONTEXT. |
endCustomerId | A stable shopper identifier, typically from a cookie. Identifies the shopper across chats — it doesn't change when a new chat starts. |
Finally, your client generates and owns one more identifier:
| Value | Description |
|---|---|
chatId | Identifies a single conversation. Generate it with a UUID v7 library the first time the chat surface opens, persist it in localStorage, and rotate it on "New chat." One shopper can have many chats over time. |
import { v7 } from 'uuid'
const chatId =
localStorage.getItem('CHAT_ID') ??
(() => { const id = v7(); localStorage.setItem('CHAT_ID', id); return id })()Core endpoints
The whole integration is essentially two calls, plus optional conversation-starter endpoints:
| Method | Endpoint | Purpose |
|---|---|---|
GET | .../persona/{personaId}/general-settings | Branding and the full i18n translation dictionary. Call once on mount, cache for the session. |
POST | .../chat/{chatId}/send-event | The entire chat channel — one outbound event per request, a streamed array of inbound events in response. |
GET/POST | .../clarity-search/... | Optional conversation-starter questions for PDP, PLP, and search/autosuggest surfaces. See the repo's integration guide for details. |
Initialization flow
-
Fetch branding & translations. Call
general-settings(Get assistant settings) once on mount and cache the response inlocalStoragefor up to 6 hours, keyed byprojectId+personaId. Branding fields (assistantLogo,assistantName,agentPersonaName,description) can be null, so always provide neutral fallbacks. On a failed fetch, fall back to a stale cache entry rather than losing branding. -
Set the current context. Send an
FE.SET_CONTEXTevent viasend-eventso replies stay relevant to what the shopper is looking at — passcurrencyplus whatever the page knows (product IDs, cart contents, category, filters).sendEvent({ type: 'FE.SET_CONTEXT', currentItemIds: ['prod-123'], currentItemIdsType: 'product_id', currency: 'USD', }) -
Restore existing history (if reopening a chat). If the shopper is reopening a conversation with an existing
chatId, sendSYNC_EVENT_LOGto replay past events. Skip this for a freshly rotatedchatId— there's nothing to replay. De-duplicate replayed events by_id.if (!isBrandNewChat) { sendEvent({ type: 'SYNC_EVENT_LOG' }) }
From here, every interaction is an event. Each send-event request carries one outbound event, the current page url, and endCustomerId; the server streams back a growing JSON array of inbound events that you render into the conversation thread.
The event model
Events flow in one of three directions:
- Outbound: client → server (e.g.
ADD_MESSAGE.USER.TEXT,FE.SET_CONTEXT) - Inbound: server → client (e.g.
ADD_MESSAGE.ASSISTANT.TEXT,ADD_MESSAGE.ASSISTANT.CAROUSEL) - Both: replayed from history or echoed back by the server
The send-event response is a single JSON array written incrementally. Parse it as it streams rather than waiting for the connection to close:
const reader = response.body.getReader()
let buffer = '', lastIndex = -1, parsed = []
for (;;) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value)
try { parsed = JSON.parse(buffer) }
catch { try { parsed = JSON.parse(buffer + ']') } catch { continue } }
while (lastIndex + 1 < parsed.length) await onItem(parsed[++lastIndex])
}Only one send-event request should be in flight per chat at a time — disable the input and visible quick replies while streaming, and match the backend's 60-second connection hold with a client-side AbortController so a stalled stream can't lock the UI.
For the complete event catalog (basic messages, rich messages like carousels and quick replies, and advanced events like USER.DIRECT_CALL and USER.FEEDBACK), see the Frontend Integration Guide in the repo.

