Skip to content
Elara One docs

Web chat

Add a live chat widget to your storefront that talks to the store's inbox, for guests and signed-in shoppers.

Web chat lets shoppers message the store from any page. Their messages arrive in the store's inbox in the admin, beside SMS, and staff replies come back to the widget. Web chat is part of the Live chat module, which comes with a plan or as an add-on and is switched on in Settings → Modules.

Without the module, every chat route answers 404 MODULE_NOT_ENABLED. Use that to hide the widget.

How it fits together

RoutePurpose
GET /v1/chat/settingsThe widget's greeting, as staff set it.
POST /v1/chat/conversationsStart a conversation. A guest gets a chat token.
GET /v1/chat/conversationThe conversation and its messages, oldest first.
POST /v1/chat/messagesSend the shopper's message.

A guest is identified by X-Chat-Token, which works like the cart token: keep it in an httpOnly cookie on your domain and have your server send it. A signed-in shopper is identified by their bearer token instead, and needs no chat token.

The examples run on your server with both store headers, as the starter storefronts do. The chat routes also accept calls straight from the browser on one of the store's registered websites, with X-Tenant-Key only, as long as the store's Browser calls switch is on. Never send the server secret from a browser.

Show the greeting

curl https://api.elaraone.app/api/v1/chat/settings \
  -H "X-Tenant-Key: $ELARAONE_TENANT_KEY" \
  -H "X-Tenant-Secret: $ELARAONE_TENANT_SECRET"
Response
{ "data": { "greeting": "Hello. Ask us anything about Hearth & Oak and we will reply here." } }

Start a conversation

Create the conversation when the shopper sends their first message, not when the widget opens, so empty conversations do not fill the inbox.

curl -X POST https://api.elaraone.app/api/v1/chat/conversations \
  -H "X-Tenant-Key: $ELARAONE_TENANT_KEY" \
  -H "X-Tenant-Secret: $ELARAONE_TENANT_SECRET"

The answer is 201:

Response (guest)
{
  "data": {
    "chat_token": "01JB2X7K9Q4M6N8P0R2T4V6W8Y",
    "conversation": {
      "id": 77,
      "channel": "web",
      "status": "open",
      "last_message_at": "2026-10-24T09:41:00+00:00",
      "created_at": "2026-10-24T09:41:00+00:00"
    }
  }
}

For a signed-in shopper (bearer token sent), chat_token is null and the conversation belongs to their account straight away.

Signing in mid-chat. When a guest signs in, call POST /v1/chat/conversations again with both the bearer token and their X-Chat-Token. The guest conversation is moved to their account instead of a second one being opened. From then on, use the bearer token.

Send a message

curl -X POST https://api.elaraone.app/api/v1/chat/messages \
  -H "X-Tenant-Key: $ELARAONE_TENANT_KEY" \
  -H "X-Tenant-Secret: $ELARAONE_TENANT_SECRET" \
  -H "X-Chat-Token: $CHAT_TOKEN" \
  -H "Idempotency-Key: 7d1c2b3a-4e5f-4a6b-9c8d-0e1f2a3b4c5d" \
  -H "Content-Type: application/json" \
  -d '{ "body": "Does the oak desk lamp take an LED bulb?" }'

body is plain text, up to 4,096 characters. The Idempotency-Key means a retry after a lost response never sends the message twice. The answer is 201 with the message:

Response
{
  "data": {
    "id": 1203,
    "direction": "inbound",
    "body": "Does the oak desk lamp take an LED bulb?",
    "media": null,
    "delivered_at": null,
    "read_at": null,
    "created_at": "2026-10-24T09:41:05+00:00"
  }
}

Show the conversation

GET /v1/chat/conversation returns the messages oldest first, paginated (perPage default 24, maximum 100), with the conversation in meta.conversation. direction is inbound for the shopper's messages and outbound for the store's replies.

Response (trimmed)
{
  "data": [
    { "id": 1203, "direction": "inbound", "body": "Does the oak desk lamp take an LED bulb?", "created_at": "2026-10-24T09:41:05+00:00" },
    { "id": 1204, "direction": "outbound", "body": "It does: any E27 bulb up to 8 W.", "created_at": "2026-10-24T09:43:30+00:00" }
  ],
  "links": { "first": "…", "last": "…", "prev": null, "next": null },
  "meta": {
    "current_page": 1,
    "last_page": 1,
    "per_page": 24,
    "total": 2,
    "conversation": { "id": 77, "channel": "web", "status": "open", "last_message_at": "2026-10-24T09:43:30+00:00", "created_at": "2026-10-24T09:41:00+00:00" }
  }
}

Because the newest messages are on the last page, read meta.last_page and fetch that page to show the latest messages.

An unknown, expired or someone else's chat token gets 404 CONVERSATION_NOT_FOUND. Clear the cookie and start a new conversation when the shopper next writes.

Getting replies: polling

There is no push channel for web chat today. The widget fetches GET /v1/chat/conversation on a timer to see new replies. A sensible pattern:

  • poll every few seconds while the widget is open and the tab is visible;
  • slow down or stop when the tab is hidden or the widget is closed;
  • compare message ids to append only what is new.

The chat routes share a budget of 60 requests a minute per conversation (per chat token, signed-in shopper or IP), sends included. Polling every 5 seconds uses 12 of them. A 429 RATE_LIMITED comes with Retry-After.

In the store's inbox

Every conversation appears in the store's inbox, where staff read and reply, with templates if they like.

  • Signed-in shoppers: their messages are recorded on their CRM contact. A shopper the CRM has not met before becomes a contact at the Lead stage and opens a lead, which is routed like any other.
  • Guests: the conversation has no contact until the shopper signs in and the conversation is moved to their account.
  • Flows: each message can start flows with the Message received trigger (channel web), and the No reply trigger can follow up when a conversation goes quiet. See Flows.