Send a message to the agent
/v2/agent/chatRuns one agent turn over the conversation you send. The agent may read
what the store stocks, search the catalogue privately, put chosen
products on screen, compose an outfit into one or more looks, ask the
shopper a structured question, offer follow-up chips, and — when a
cart snapshot is present — read or change the cart. All of that
happens server-side inside a single request; tool results go back into
the model before it writes, capped at ten model calls per turn.
Streaming
The response streams by default. It is text/event-stream and each
thing the agent does arrives as its own event — see the
streaming guide and the event
catalogue below. Pass stream: false to receive one JSON body once the
turn has finished.
Conversation state
You own the conversation state. The turn ends with a replay-ready
history array (on the done event, or in the JSON body). Send it
back verbatim as messages on the next request, with the shopper's
new message appended, and the conversation continues. There is no
server-side chat id and nothing is stored between requests.
Dataset scope
datasetIds narrows the catalogue the agent works from; omit it to use
every active dataset. An invalid or archived dataset scope does not
fail the request — the agent's tools report the problem to the model
(you will see it on tool_output), and the model answers without
catalogue data.
Prices in the shopper's country
Send country (on a Shopify storefront, Shopify.country) and every
product in search_results, looks and the tool outputs, the prices
on cart actions, the sizes in stock and the sale information are the
shopper's market's. Without it the store's default market's prices
come back, flagged product.marketExact: false. An unrecognised
country is treated as absent, never as an error. See
Introduction.
Metering
One request consumes one unit of api.v2.agent.chat
regardless of how many tool calls the turn takes. The rate-limit
headers are set on the stream response as well as on JSON responses.
This resource is separate from v1 chat, so v2 usage never draws down
your POST /v1/chat allowance and vice versa.
Where errors arrive
Authentication, quota and validation failures
are decided before any stream exists, so they always come back as an
ordinary JSON error with the right HTTP status — check Content-Type
before you attach an SSE reader. Once the stream has started the
status is already 200; a failure mid-run is delivered as an error
event and the stream closes without a done event.
Request body
application/jsonrequiredThe whole conversation so far: the previous turn's history, if
any, followed by the shopper's new message. On the first turn this
is a single UserMessage.
Restrict the agent to these datasets. Omit or send [] to use every
active dataset in the organization, narrowed to one store when
store is sent. When present, these always win over store.
Must be an array when present.
An invalid scope does not fail the request; the agent's tools report
the problem to the model instead.
A snapshot of the shopper's cart. Sending one — even an empty cart
with items: [] — enables the cart tools for this turn; omitting it
or sending null leaves the agent with no cart capability at all.
The server does not validate the snapshot: any non-null value enables
the tools, and a malformed one makes the cart tools report errors to
the model (visible on tool_output) rather than failing the request.
See the cart guide.
Option 1: CartSnapshot
Total units across all lines.
Cart total in major currency units.
ISO 4217 currency code for total and each line's price.
The cart lines. May be empty.
Option 2: null
How much the model reasons before it acts, for this turn. Left out
(null, false or absent) it is low: the least that keeps a
turn from ending on a promise with nothing on screen. none is
the fast option — nothing then sits between the request and the
first word, about 700 ms sooner — at the cost of an occasional
dead turn. true also means low; a string picks a level. Accepted levels depend on the model and are checked before
the run: an unsupported one is a 422 naming the accepted ones.
With thinking on, the reasoning is summarised and streamed as
thinking, thinking_delta and thinking_break events so the wait
is visible; at the default none of those are sent.
Option 1: string
Option 2: boolean
Option 3: null
What the shopper is looking at. Send product when the chat is open
on a product page and the agent treats "this" and "it" as that
product, answering questions about its price, options and
availability without a lookup. It is context only: it enables no
tools. The server reads the fields below by name, clamps them, and
ignores everything else; a product without a title is ignored.
The Stylor widget fills this from the storefront's
/products/{handle}.js.
Omitted, true or any other truthy value streams Server-Sent Events.
false — or any falsy value such as null or 0 — returns one JSON
body when the turn is complete.
The analytics session this visit belongs to — the
session.sessionId returned by the v1 widget preflight — so events
the server records line up with events your storefront reports for
the same visit. Never used for authorization. It also binds cart
intent tokens: without it, add and swap cart actions carry
intent: null and their outcomes cannot be reported.
Visit metadata for analytics. Ignored unless it is useful to you.
Option 1: Session
Where the shopper is talking to the agent. Defaults to widget when omitted.
Which agent version your client is built against. Use v2.
How your client persists the visit id — for example memory, session or local. Defaults to memory.
The shopper's IANA time zone.
The shopper's BCP 47 locale.
A coarse device class, typically mobile or desktop.
Option 2: null
Which store the shopper is on, for an organization that runs
several stores under one key. origin is the storefront page's
origin (window.location.origin) and shop is Shopify's
permanent shop domain (Shopify.shop, e.g.
kit-and-ace.myshopify.com), which still identifies the store when
it serves from a different domain. When datasetIds is empty,
the agent searches only that store's active datasets and speaks as
that store. If neither matches a dataset, every active dataset is
used; when those belong to several stores, the agent says it
represents several stores, names the store each product comes
from, and points policy questions to that store's own site.
Ignored when datasetIds is sent. It can only narrow within your
own organization, never widen it. The Stylor chat embed sends it.
Option 1: object
Option 2: null
The shopper's country, ISO 3166-1 alpha-2 in any case (US, ca).
On a Shopify storefront, read it from Shopify.country. Every
product the agent finds, shows or puts in a look is priced in that
shopper's market: price, currency, each variant's price,
compareAtPrice and inStock, the sizes it offers, the prices on
cart actions, and onSale / discount. The lookup_products
tool's minPrice and maxPrice are in that currency, and its
onSale argument finds products reduced for this shopper.
Products their market does not sell are not shown. Omitted, or
for a country the store does not sell into, products carry the
store's default market's prices, flagged
product.marketExact: false. A value that is not a two-letter
code is treated as unknown, not refused.
Response
stream omitted or true: a Server-Sent Events stream. With
stream: false: one JSON body describing the whole turn.
Response headers
X-RateLimit-ResourceThe quota resource this endpoint is metered against: api.v2.agent.chat
for the chat endpoint, api.v2.agent.looks for the looks endpoint and
api.v2.agent.match for the shop-the-look endpoint. These are separate
from the v1 resources, so Agent API usage never draws down a v1
allowance.
X-RateLimit-Quota-LimitRequests allowed in the current billing period, or unlimited.
X-RateLimit-Quota-RemainingRequests left in the current billing period, or unlimited.
X-RateLimit-Quota-ResetUnix epoch seconds at which the billing period resets.
X-RateLimit-RPM-LimitRequests allowed per minute for this resource.
X-RateLimit-RPM-RemainingRequests left in the current one-minute window.
X-RateLimit-RPM-ResetUnix epoch seconds at which the one-minute window resets.
text/event-streamevent: <name>\ndata: <json>\n\n frames, one per event, in the
order listed under Stream events. The stream closes after
done (success) or error (failure). Cache-Control: no-cache
and Connection: keep-alive are set.
application/jsonThe assistant's final text. May be empty when a row or a card was the whole answer.
Replay-ready conversation. Send it back as messages next turn.
Every product row the agent displayed, in order.
Heading for the row.
outfit — worn together; list — alternatives to choose between.
outfitlistRepeats label.
Number of entries in results.
Every look the agent composed. Render each with the looks endpoint.
Name of the look; pass it to the looks endpoint.
The garments worn together, as full Product records. Pass their itemIds to the looks endpoint.
The agent's direction for the renderer — a garment the shopper owns, how pieces sit, the setting. Pass it through to the looks endpoint unchanged.
Question cards the agent asked (at most one per turn in practice).
Whether the shopper may type their own answer instead of picking an option.
Present only when the card asks more than one question. Every
question, in order, including the first — which question,
options and allowFreeText above repeat. Answer all of them and
reply once, a line per step: <topic>: <label>.
Follow-up chips from suggest_replies.
Stream events
13Each frame is event: <name> followed by data: <json> and a blank line. Events are listed in the order they normally arrive.
thinkingThe model started (status: started) or finished (status: done)
a stretch of reasoning. Sent only on a turn that reasons — every
turn by default, none of them when the request sets thinking to
none. Show a "thinking" state between the two — a summary may or
may not follow, so this pair is the reliable signal. The stretch
ends when text or a tool call begins.
{
"status": "started"
}thinking_deltaA chunk of the model's reasoning summary, in order, between a
thinking start and done. Written for a reader, not the raw chain
of thought, and not part of the reply. Only on a turn that reasons.
{
"delta": "They want a dinner look; I'll check dresses and heels first."
}thinking_breakThe reasoning summary began a new paragraph. The payload is empty. Only on a turn that reasons.
{}text_deltaA chunk of assistant text, in order. Append the chunks to render the
reply as it is written. Text can arrive in more than one run inside a
turn — typically a short opener before the tools run, and a closing
line after — so treat a tool_called event as the end of the current
paragraph.
{
"delta": "Let me pull together a few options."
}tool_calledThe model invoked a tool. arguments is the raw JSON string the
model produced (parse it if you need the values), or null. Useful
for showing progress — for instance "Searching…" while
lookup_products runs. Tool names: get_articles, get_colors,
get_details, lookup_products, find_product, show_products,
create_looks, ask_question, suggest_replies; list_store_pages and
read_store_page when the organization's catalogue is synced from a
Shopify store; and — only when a cart was sent —
get_cart, add_to_cart, set_cart_quantity, change_cart_size,
remove_from_cart. Anything a tool draws on screen
(search_results, looks, question, suggestions, cart_action)
is emitted between that tool's tool_called and tool_output.
When the model calls several tools in one step, every tool_called
for that step arrives first, then the tools' display events in the
order the tools produce them, then every tool_output — so match
display events by their type, not by position.
{
"name": "lookup_products",
"arguments": "{\"query\":\"linen wedding shirt\",\"slot\":null,\"family\":[\"shirt\"],\"age_group\":null,\"gender\":\"mens\",\"colors\":null,\"pattern\":null,\"fit\":null,\"materials\":[\"linen\"],\"features\":null,\"minPrice\":null,\"maxPrice\":300,\"onSale\":null,\"exclude_terms\":null,\"limit\":8}"
}search_resultsA row of products to display, from show_products. kind is
outfit when the items are meant to be worn together (at most one
per slot) or list when they are alternatives to choose between —
render them differently. query repeats label. results holds
full Product records in the order the agent chose, which is part of
the recommendation. The agent may emit several rows in one turn.
{
"label": "Wedding shirts",
"kind": "list",
"query": "Wedding shirts",
"count": 3,
"results": [
{
"itemId": "8f2c1a9e-4b7d-4c1e-9a3f-2d6b7e5c1a90",
"datasetId": "9c21f0aa-3e4b-4d2a-b6d1-0f7e8a9b1c2d",
"status": "active",
"metadata": {
"gender": "mens",
"color_family": "navy",
"slot": "top",
"family": "shirt"
},
"product": {
"name": "Coastal Linen Shirt",
"url": "https://shop.example.com/products/coastal-linen-shirt",
"price": 89,
"currency": "USD",
"variants": [
{
"variantId": "gid://shopify/ProductVariant/41611609636909",
"sku": "CLS-NVY-M",
"label": "M / Navy",
"options": [
{
"name": "Size",
"value": "M"
},
{
"name": "Color",
"value": "Navy"
}
],
"inStock": true,
"quantity": 4,
"price": 89,
"compareAtPrice": null
}
],
"assets": {
"images": [
{
"uploaded": {
"key": "org_5f3a/ds_9c21/8f2c1a9e-4b7d/0.jpg"
}
}
]
}
}
}
]
}suggestionsTwo to four follow-up chips, written in the shopper's voice. Emitted
by suggest_replies, and also alongside search_results and
looks when the agent attached chips to them. When tapped, send the
chip text as the next user message. A turn can emit this more than
once; keep the latest. Never shown together with a question card.
{
"suggestions": [
"Show me trousers to match",
"Something more formal",
"Cheaper options"
]
}questionA multiple-choice card from ask_question. The agent has stopped to
wait for an answer, so the turn ends shortly after this event. Render
the options; when the shopper picks one, send the option's label
(or their typed text if allowFreeText) as the next user message.
questionId is unique per card, for your own bookkeeping.
A card can carry several questions — typically a size for each
piece of an outfit being added to the cart. It then has a steps
array holding every question, the first included, and question,
options and allowFreeText repeat the first so a client that
ignores steps still shows something answerable. Collect an answer
to every step and send them as ONE user message, one line per step
in order: <topic>: <label>, e.g. Shirt size: M / White. Do not
send them as separate messages; the agent acts on the full set.
{
"questionId": "5f0e8c3a-2b1d-4e7f-9a6c-3d2e1f0a9b8c",
"question": "Who am I styling for?",
"options": [
{
"label": "Menswear",
"value": "mens"
},
{
"label": "Womenswear",
"value": "womens"
}
],
"allowFreeText": false
}looksOne or more complete outfits the agent wants rendered. Each look
carries its label, its full Product records in items (two to
six per look), and customInstructions — the agent's direction for
anything the product photos cannot show, or null. Nothing has been
rendered yet: call POST /v2/agent/looks once per look, passing the
items' itemIds, the label and customInstructions, and show
each image as it returns. The items may include pieces that were
never shown as cards.
{
"looks": [
{
"label": "Garden wedding",
"items": [
{
"itemId": "8f2c1a9e-4b7d-4c1e-9a3f-2d6b7e5c1a90",
"product": {
"name": "Coastal Linen Shirt",
"price": 89,
"currency": "USD"
}
},
{
"itemId": "1b7e33c0-9a2d-4f8e-b5c6-7d8e9f0a1b2c",
"product": {
"name": "Everyday Chino",
"price": 79,
"currency": "USD"
}
},
{
"itemId": "c4d5e6f7-0a1b-4c2d-8e9f-0a1b2c3d4e5f",
"product": {
"name": "Suede Loafer",
"price": 129,
"currency": "USD"
}
}
],
"customInstructions": "Shirt untucked with the top button open; outdoor daylight setting."
}
]
}cart_actionThe agent read or wants to change the shopper's cart. Only possible
when the request carried a cart snapshot. action is one of
read, add, quantity, swap or remove, and the rest of the
payload depends on it — see the CartAction schema. A read is
already satisfied (it was answered from your snapshot) and can be
shown as a receipt. Every other action is a request: the server
has verified the item, variant and stock, but your client must
perform the change on the storefront and show the outcome. add
and swap carry an intent token (or null when no sessionId
was sent) for reporting the result — see the
cart guide.
{
"action": "add",
"itemId": "8f2c1a9e-4b7d-4c1e-9a3f-2d6b7e5c1a90",
"name": "Coastal Linen Shirt",
"url": "https://shop.example.com/products/coastal-linen-shirt",
"sku": "CLS-NVY-M",
"size": "M / Navy",
"quantity": 1,
"price": 89,
"currency": "USD",
"image": "org_5f3a/ds_9c21/8f2c1a9e-4b7d/0.jpg",
"intent": "eyJ2IjoxLCJqdGkiOiI3ZjFlIn0.q8vT2rN9…"
}tool_outputA tool call finished. output is the value the model saw — usually an
object with a note written for the model, sometimes an error key
when the tool could not do its job (for example an invalid dataset
scope). Product searches return compact summaries here, never full
products; the full records travel on search_results. output is a
plain string rather than an object when the tool could not run at
all — for example when the model produced arguments that failed
validation, in which case it reads An error occurred while running
the tool. Please try again. Error: …. The agent sees that text and
usually retries; it does not end the stream.
{
"name": "lookup_products",
"output": {
"count": 8,
"items": [
{
"id": "8f2c1a9e-4b7d",
"description": "Coastal Linen Shirt — navy, linen, relaxed fit, $89"
}
],
"note": "The shopper saw NOTHING — this was for your eyes only."
}
}doneThe turn finished. output is the assistant's final text (may be an
empty string when a card or a row was the whole answer) and
history is the complete, replay-ready conversation — every user
message, assistant message, tool call and tool result. Store
`history` and send it back as `messages` on the next turn. The
stream closes after this event.
{
"output": "All three are linen, so they will breathe on a warm afternoon.",
"history": [
{
"role": "user",
"content": "I need something for a summer wedding, menswear, under $300."
},
{
"type": "message",
"role": "assistant",
"status": "completed",
"content": [
{
"type": "output_text",
"text": "Let me pull together a few options."
}
]
},
{
"type": "function_call",
"callId": "call_8kP2wN7r",
"name": "show_products",
"arguments": "{\"label\":\"Wedding shirts\",\"kind\":\"list\",\"itemIds\":[\"8f2c1a9e-4b7d\",\"1b7e33c0-9a2d\",\"c4d5e6f7-0a1b\"],\"suggestions\":[\"Show me trousers to match\",\"Something more formal\"]}",
"status": "completed"
},
{
"type": "function_call_result",
"callId": "call_8kP2wN7r",
"name": "show_products",
"status": "completed",
"output": {
"type": "text",
"text": "{\"shown\":3,\"note\":\"3 cards are now on the shopper's screen under \\\"Wedding shirts\\\".\"}"
}
},
{
"type": "message",
"role": "assistant",
"status": "completed",
"content": [
{
"type": "output_text",
"text": "All three are linen, so they will breathe on a warm afternoon."
}
]
}
]
}errorThe run failed after the stream had started — for example the model
exceeded the ten-call cap or the model provider returned an error.
The HTTP status is already 200; this event is the only signal. The
message is always Agent run failed.; the cause is logged on our
side. No done follows and no history is returned, so replay the
previous turn's history plus the shopper's message to retry. The
stream closes after this event.
{
"error": "Agent run failed."
}Errors
21Every error is JSON with a single error string unless noted. Match on the status code; the message is for humans.
Request body must be valid JSON.The body could not be parsed as JSON.
Request body must be a JSON object.The body is empty, or is valid JSON that is not an object (for example an array).
messages must be an array.messages is missing or not a JSON array. Returned as JSON even when streaming was requested.
messages cannot be empty.messages is an empty array. Returned as JSON even when streaming was requested.
curl -X POST "https://stylor.ai/api/v2/agent/chat" \
-H "Authorization: Bearer $STYLOR_API_KEY" \
-H "Content-Type: application/json" \
-N \
-d '{
"messages": [
{
"role": "user",
"content": "I need something for a summer wedding, menswear, under $300."
}
],
"datasetIds": [
"9c21f0aa-3e4b-4d2a-b6d1-0f7e8a9b1c2d"
],
"sessionId": "3b0d4a1e-6f52-4b8a-9d3c-7e2f1a0b9c8d",
"session": {
"surface": "widget",
"agentVersion": "v2",
"storageMode": "local",
"tz": "America/Toronto",
"locale": "en-CA",
"device": "desktop"
}
}'event: thinking
data: {"status":"started"}
event: thinking_delta
data: {"delta":"They want a dinner look; I'll check dresses and heels first."}
event: thinking_break
data: {}
event: text_delta
data: {"delta":"Let me pull together a few options."}
event: tool_called
data: {"name":"lookup_products","arguments":"{\"query\":\"linen wedding shirt\",\"slot\":null,\"family\":[\"shirt\"],\"age_group\":null,\"gender\":\"mens\",\"colors\":null,\"pattern\":null,\"fit\":null,\"materials\":[\"linen\"],\"features\":null,\"minPrice\":null,\"maxPrice\":300,\"onSale\":null,\"exclude_terms\":null,\"limit\":8}"}
event: search_results
data: {"label":"Wedding shirts","kind":"list","query":"Wedding shirts","count":3,"results":[{"itemId":"8f2c1a9e-4b7d-4c1e-9a3f-2d6b7e5c1a90","datasetId":"9c21f0aa-3e4b-4d2a-b6d1-0f7e8a9b1c2d","status":"active","metadata":{"gender":"mens","color_family":"navy","slot":"top","family":"shirt"},"product":{"name":"Coastal Linen Shirt","url":"https://shop.example.com/products/coastal-linen-shirt","price":89,"currency":"USD","variants":[{"variantId":"gid://shopify/ProductVariant/41611609636909","sku":"CLS-NVY-M","label":"M / Navy","options":[{"name":"Size","value":"M"},{"name":"Color","value":"Navy"}],"inStock":true,"quantity":4,"price":89,"compareAtPrice":null}],"assets":{"images":[{"uploaded":{"key":"org_5f3a/ds_9c21/8f2c1a9e-4b7d/0.jpg"}}]}}}]}
event: suggestions
data: {"suggestions":["Show me trousers to match","Something more formal","Cheaper options"]}
event: question
data: {"questionId":"5f0e8c3a-2b1d-4e7f-9a6c-3d2e1f0a9b8c","question":"Who am I styling for?","options":[{"label":"Menswear","value":"mens"},{"label":"Womenswear","value":"womens"}],"allowFreeText":false}
event: looks
data: {"looks":[{"label":"Garden wedding","items":[{"itemId":"8f2c1a9e-4b7d-4c1e-9a3f-2d6b7e5c1a90","product":{"name":"Coastal Linen Shirt","price":89,"currency":"USD"}},{"itemId":"1b7e33c0-9a2d-4f8e-b5c6-7d8e9f0a1b2c","product":{"name":"Everyday Chino","price":79,"currency":"USD"}},{"itemId":"c4d5e6f7-0a1b-4c2d-8e9f-0a1b2c3d4e5f","product":{"name":"Suede Loafer","price":129,"currency":"USD"}}],"customInstructions":"Shirt untucked with the top button open; outdoor daylight setting."}]}
event: cart_action
data: {"action":"add","itemId":"8f2c1a9e-4b7d-4c1e-9a3f-2d6b7e5c1a90","name":"Coastal Linen Shirt","url":"https://shop.example.com/products/coastal-linen-shirt","sku":"CLS-NVY-M","size":"M / Navy","quantity":1,"price":89,"currency":"USD","image":"org_5f3a/ds_9c21/8f2c1a9e-4b7d/0.jpg","intent":"eyJ2IjoxLCJqdGkiOiI3ZjFlIn0.q8vT2rN9…"}
event: tool_output
data: {"name":"lookup_products","output":{"count":8,"items":[{"id":"8f2c1a9e-4b7d","description":"Coastal Linen Shirt — navy, linen, relaxed fit, $89"}],"note":"The shopper saw NOTHING — this was for your eyes only."}}
event: done
data: {"output":"All three are linen, so they will breathe on a warm afternoon.","history":[{"role":"user","content":"I need something for a summer wedding, menswear, under $300."},{"type":"message","role":"assistant","status":"completed","content":[{"type":"output_text","text":"Let me pull together a few options."}]},{"type":"function_call","callId":"call_8kP2wN7r","name":"show_products","arguments":"{\"label\":\"Wedding shirts\",\"kind\":\"list\",\"itemIds\":[\"8f2c1a9e-4b7d\",\"1b7e33c0-9a2d\",\"c4d5e6f7-0a1b\"],\"suggestions\":[\"Show me trousers to match\",\"Something more formal\"]}","status":"completed"},{"type":"function_call_result","callId":"call_8kP2wN7r","name":"show_products","status":"completed","output":{"type":"text","text":"{\"shown\":3,\"note\":\"3 cards are now on the shopper's screen under \\\"Wedding shirts\\\".\"}"}},{"type":"message","role":"assistant","status":"completed","content":[{"type":"output_text","text":"All three are linen, so they will breathe on a warm afternoon."}]}]}
event: error
data: {"error":"Agent run failed."}