Assistant
Prefix: /v1/assistant on the Public API.
The assistant is a streaming chat. Clients must render blocks from the stream (and from saved messages). Do not invent prices, stock, or product names.
Auth
| Client | How identity is sent |
|---|---|
| Signed-in customer | Authorization: Bearer <accessToken> |
| Guest | Only if GET /v1/init → store.assistant.allowGuests is true |
Guests need a stable 32-character hex key: generate once, persist, and send X-Assistant-Guest: <32-hex> on every assistant call. After OTP login, guest threads merge onto the customer; you can drop the header.
If Settings → Assistant has enabled: false, all assistant routes fail. If guests are disabled, unsigned callers get AUTHENTICATION_REQUIRED.
Rate limits: list conversations 60/min, send message 20/min.
Endpoints
| Method | Path | Auth | Response | Purpose |
|---|---|---|---|---|
GET | /v1/assistant/conversations | Customer or guest | JSON envelope | Paginated conversation list |
GET | /v1/assistant/conversations/:conversationId | Owner | JSON envelope | Conversation + messages with hydrated blocks |
POST | /v1/assistant/messages | Customer or guest | SSE (text/event-stream) | Send a turn |
POST | /v1/assistant/actions/:actionId/confirm | Owner | JSON envelope | Apply a proposed cart / delivery mutation |
POST | /v1/assistant/conversations/:conversationId/handoff | Owner | JSON envelope | Create a support ticket from the thread |
POST | /v1/assistant/messages/:messageId/feedback | Owner | JSON envelope | { "feedback": "up" | "down" | null } |
Conversation status: active | handed_off | closed. A thread closes when it hits the message/token cap; the next send starts a new conversation.
Send a message (SSE)
POST /v1/assistant/messages
Content-Type: application/json
Accept: text/event-stream
Authorization: Bearer <accessToken>
X-Assistant-Guest: <32-hex> # guests on mobile
{
"conversationId": "665f0c0c0c0c0c0c0c0c0c0f",
"message": "Add two bottles of milk"
}| Body field | Required | Notes |
|---|---|---|
message | yes | 1–2000 characters |
conversationId | no | Omit on the first message; the stream returns a new id |
This endpoint is not the JSON envelope. Each SSE frame looks like:
event: <name>
data: { ...json... }Stream events (in order)
event | data | Client should |
|---|---|---|
message_start | { "conversationId": "<id>" } | Persist the conversation id |
user_message | { "conversationId", "message" } | Show the user turn (message has _id, role: "user", content, blocks, feedback) |
text_delta | { "delta": "..." } | Append to the live assistant text |
tool_start | { "name", "callId" } | Optional: show a “looking up…” state |
tool_end | { "name", "callId", "ok" } | Clear the tool spinner |
block | { "block": { "kind": "...", ... } } | Append a UI card (see kinds below) |
error | { "code", "message" } | Show the error; code is a statusMessage or HTTP-like code |
message_end | { "conversationId", "message" } | Replace the live turn with the full assistant message (hydrated blocks) |
Typical sequence:
message_startuser_message- zero or more
text_delta/tool_start/tool_end/block message_endorerror
Keep the connection open until message_end or a terminal error. Then close.
Message object
Returned on GET .../conversations/:id, on user_message, and on message_end:
| Field | Meaning |
|---|---|
_id | Message id (use for feedback) |
conversationId | Parent thread |
role | user | assistant | system |
content | Plain text of the turn |
blocks | Ordered UI cards (see kind) |
feedback | up | down | null |
createdAt, updatedAt | ISO timestamps |
Block kind values
Every block is an object with a kind string. Switch on kind and render the matching fields. Ignore unknown kinds so new cards do not crash old apps.
kind | Fields | Render |
|---|---|---|
text | text | Markdown/plain assistant prose |
products | products[] | Product cards (localized name, prices in fils, images) |
product_detail | product | Single product card |
cart_action | actionId, items[], status, optional estimatedTotal | Proposed cart change — Confirm bound to actionId while status is pending |
cart_summary | cart | Current cart (same shape as GET /v1/cart) |
order | order | Order summary (_id, orderNumber, status, total, itemCount, thumbnails) |
order_status | order | Same summary, shown as a status update |
offers | offers[], optional couponCode | Promotion cards |
recipe | recipe, servings, ingredientCount | Recipe card |
faq | items[] | FAQ Q&A from store content |
categories | categories[] | Aisle cards |
brands | brands[] | Brand cards |
delivery_slots | days[] | Slot picker (Kuwait YYYY-MM-DD) |
delivery_info | optional areaName, zoneName, fee, etaMinutes | Current delivery context (fee in fils) |
locations | items[] of { label, address?, phone?, lat, lng } | Map / branch pins |
handoff | ticketId, ticketNumber | Link to the created support ticket |
actions | suggestions[] of { label, prompt } | Chip buttons: sending prompt as the next user message |
error | code, optional message | Inline error card |
cart_action status
status | Meaning |
|---|---|
pending | Show Confirm. Cart is unchanged until confirm succeeds |
confirmed | Already applied |
cancelled | Abandoned |
expired | Confirm window passed |
items[] on cart_action: { productId, variantId, quantity, product? }.
Do not call cart mutation endpoints yourself for assistant proposals. Always POST /v1/assistant/actions/:actionId/confirm.
Confirm a proposed action
POST /v1/assistant/actions/{actionId}/confirm
Authorization: Bearer <accessToken>results:
| Field | Meaning |
|---|---|
message | Localized acknowledgement |
blocks | Updated cards (typically cart_summary and a cart_action with status: "confirmed") |
Until this call succeeds, the cart is unchanged. Confirm is owner-only (same customer or guest key).
Pending action types the model may propose (payload is server-side; the client only confirms):
add_to_cart, update_cart_item, remove_cart_item, apply_coupon, apply_loyalty, add_recipe_to_cart, reorder, select_delivery_area, select_address, clear_cart, clear_coupon, clear_loyalty, set_express.
Handoff to support
POST /v1/assistant/conversations/{conversationId}/handoff
Content-Type: application/json
{ "subject": "Missing item", "category": "order", "subcategory": "missing_items" }All body fields are optional. results includes ticketId, ticketNumber, and message. Conversation status becomes handed_off. A handoff block may also appear in the thread.
Feedback
POST /v1/assistant/messages/{messageId}/feedback
Content-Type: application/json
{ "feedback": "up" }feedback is up, down, or null (clear).
Client checklist
- Call
GET /v1/initand hide chat ifstore.assistant.enabledis false. - Parse SSE by
eventname; JSON-parse eachdataline. - Switch UI cards on
block.kind. - For
kind: "cart_action"withstatus: "pending", confirm via the actions endpoint — never mutate/v1/cartfor that proposal. - After
message_end, prefer the hydratedmessage.blocksover the live deltas if they disagree. - Do not invent catalog data; only render what the API sent.