Skip to content

Reference

Widget API

The browser-facing API the widget uses. Most shops never call it directly. Use it to build a fully custom chat interface.

Browser only

Every endpoint takes your public site_key in the query string and only answers browsers on your allowed domains (CORS is decided per shop). Chat endpoints also need a session token obtained with a proof-of-work challenge. Server-side calls are rejected by design (see the security model).

Get the widget configuration

GET/v1/widget/config?site_key=pk_live_…

Returns the shop’s display info and its published widget configuration (with plan rules applied). Cacheable for 60 s; supports ETag / If-None-Match.

200 OK (abridged)json
{
  "shop": { "name": "Lumière Beauty", "defaultLocale": "en", "enabledLocales": ["en", "ro"], "currency": "EUR", "identifyEnabled": false },
  "config": { "theme": { "primaryColor": "#9d3b5a", "mode": "light", "…": "…" }, "launcher": { "…": "…" }, "behavior": { "…": "…" } },
  "active": true
}

Create a session (proof-of-work)

GET/v1/widget/challenge?site_key=pk_live_…
200 OKjson
{ "challenge": "kq3…", "salt": "9f2c…e1", "difficulty": 17, "expiresAt": "2026-10-01T12:02:00.000Z" }

Find a decimal nonce such that sha256(salt + ":" + nonce) starts with difficulty zero bits. Challenges are single-use and expire after 2 minutes. Then exchange it for a session:

POST/v1/widget/session?site_key=pk_live_…
FieldTypeDescription
visitor_anon_idrequiredstringRandom id you generate and keep per browser ([A-Za-z0-9_-]{8,64}). Create and store it only once the shopper uses the chat, as the AskMerra widget does (see the cookie policy).
challengerequiredstringThe challenge id.
noncerequiredstringYour solution.
200 OKjson
{ "token": "v1.eyJzIjoi….Qm9…", "expiresAt": "2026-10-01T12:30:00.000Z" }

The token is valid for 30 minutes for this shop, this visitor id and this website only. On 401 session_expired, get a new one and retry.

Solving the challenge (simple version)js
async function solve(salt, difficulty) {
  const enc = new TextEncoder();
  for (let nonce = 0; ; nonce++) {
    const hash = new Uint8Array(await crypto.subtle.digest('SHA-256', enc.encode(salt + ':' + nonce)));
    let bits = 0;
    for (const byte of hash) {
      if (byte === 0) { bits += 8; continue; }
      bits += Math.clz32(byte) - 24;
      break;
    }
    if (bits >= difficulty) return String(nonce);
  }
}

The official widget uses a synchronous SHA-256 in time slices (≈0.2 s at difficulty 17 on a laptop) so the page stays responsive.

Send a message (streaming)

POST/v1/chat/messages?site_key=pk_live_…
FieldTypeDescription
session_tokenrequiredstringFrom /widget/session.
visitor_anon_idrequiredstringSame id the session was created for.
messagerequiredstring ≤1000The visitor’s message.
conversation_iduuidContinue a conversation (from a previous meta event). Omit to start a new one.
localestringInterface language; the reply follows the language of the message.
page_urlstringCurrent page URL.
page_context.product_idstringexternal_id or SKU of the product on the current page.

The response is a text/event-stream:

EventData
meta{ conversationId, userMessageId, locale }, always first
delta{ text }. Append to the answer (Markdown-lite: bold, italic, lists, links)
products{ products: [{ externalId, sku, name, brand, url, imageUrl, price, salePrice, currency, inStock }] }
escalation{ reason }. Offer the contact form
unavailable{ reason }, one of paused | no_plan | quota | hard_cap | no_credits | billing | busy. Show a polite message + contact form
done{ messageId, route, locale }. Answer complete; messageId is used for feedback
error{ code, message }
Never render the text as HTML. Treat it as Markdown-lite and only allow http(s), mailto and tel links.

Conversation history

GET/v1/chat/conversations/:id?site_key=…&visitor_anon_id=…&session_token=…

Messages of a conversation from the last 24 hours, for restoring the chat after a page load. Returns 404 for older or foreign conversations.

Feedback

POST/v1/chat/feedback?site_key=pk_live_…
FieldTypeDescription
session_token / visitor_anon_idrequiredstringAs above.
message_idrequireduuidFrom the done event.
valuerequired1 | -1Thumbs up / down. A thumbs down removes the answer from the cache.

Contact form

POST/v1/chat/escalate?site_key=pk_live_…
FieldTypeDescription
conversation_idrequireduuidThe current conversation.
emailrequiredemailVisitor’s email, used as reply-to.
messagerequiredstring ≤2000Visitor’s message to the team.
namestringVisitor’s name.

Analytics events

POST/v1/widget/events?site_key=pk_live_…

Batches of up to 20 events (widget_open, product_click, add_to_cart_click, suggestion_click), each with an optional conversation_id and a small payload. Send the JSON body as text/plain so it works with navigator.sendBeacon on page unload. Responds 204.

Purchases (sales tracking)

POST/v1/widget/orders?site_key=pk_live_…

Links a purchase to the visitor’s chat. Body: session_token, visitor_anon_id and order with id (unique per order), value, currency (ISO 4217), items (up to 200 of { id, name?, price?, quantity }) and source (datalayer or api). Sent as text/plain JSON. Responds { "recorded": true } when the visitor chatted in the 7 days before; otherwise nothing is stored and it responds { "recorded": false }. The same order id is recorded once. The widget calls this only with the shopper’s analytics consent. 20 orders per visitor per hour.

Identify a visitor

POST/v1/widget/identify?site_key=pk_live_…

Attaches email and/or name to the visitor. Only allowed when visitor identification is enabled in the shop settings (otherwise 403 identify_disabled).