# Send a message to the agent

`POST https://stylor.ai/api/v2/agent/chat`

- Authentication: public API key, sent as `Authorization: Bearer sgpt-pk-…`
- Rate-limit resource: `api.v2.agent.chat`
- Demo key: accepted
- Streaming: Server-Sent Events by default; send `"stream": false` for one JSON response
- Web page: https://stylor.ai/guides/rest/v2/agent-chat

Runs 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](https://stylor.ai/guides/rest/v2/agent-streaming.md) 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](https://stylor.ai/guides/rest/v2/introduction.md).

## 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.

## Example request

```bash
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"
  }
}'
```

## Request body

Content type `application/json`, required.

- `messages` (array<UserMessage | AssistantMessage | FunctionCall | FunctionCallResult>, required): The 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`. [min items 1]
- `datasetIds` (array<string>): 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. [default `[]`]
- `cart` (CartSnapshot | null): 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](https://stylor.ai/guides/rest/v2/cart.md). [default `null`]
  - Option 1: CartSnapshot
    - `itemCount` (integer, required): Total units across all lines. [example `2`]
    - `total` (number, required): Cart total in major currency units. [example `168`]
    - `currency` (string, required): ISO 4217 currency code for `total` and each line's `price`. [example `"USD"`]
    - `items` (array<CartLine>, required): The cart lines. May be empty.
      - `key` (string, required): Your store's stable identifier for this line. The agent hands it back on `quantity`, `swap` and `remove` cart actions as `lineKey`, so it must be whatever your storefront needs to address the line. [example `"43210987654321:8f2c1a9e"`]
      - `title` (string, required): The product name as the shopper sees it. [example `"Coastal Linen Shirt"`]
      - `variant` (string, nullable): The variant or size label, or `null` for single-variant products. [example `"M / Navy"`]
      - `quantity` (integer, required): How many of this line are in the cart. [example `1`]
      - `price` (number, required): The line total in major currency units (for example `89.00`, not cents). [example `89`]
      - `sku` (string, nullable): The SKU of the variant in the cart. This is how the agent links a cart line back to your catalogue when changing its size — omit it and size changes fall back to an exact product-name match. [example `"CLS-NVY-M"`]
      - `image` (string, nullable): An absolute image URL for the line, shown on cart receipts. Never sent to the model. [example `"https://cdn.example.com/products/coastal-linen-shirt-navy.jpg"`]
  - Option 2: null
- `thinking` (string | boolean | 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. [default `null`; example `"low"`]
  - Option 1: string
  - Option 2: boolean
  - Option 3: null
- `page` (object, nullable): 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`. [default `null`]
  - `product` (object)
    - `title` (string)
    - `vendor` (string)
    - `type` (string): Product type
    - `url` (string)
    - `currency` (string) [example `"USD"`]
    - `price` (integer): In the currency's minor unit (cents)
    - `priceMin` (integer)
    - `priceMax` (integer)
    - `compareAtPrice` (integer, nullable)
    - `available` (boolean)
    - `description` (string): HTML or plain text. Converted to markdown and cut to 1
    - `options` (array<object>)
      - `name` (string)
      - `values` (array<string>)
    - `variants` (array<object>): At most 60 are read.
      - `id` (integer | string)
      - `title` (string)
      - `sku` (string, nullable)
      - `available` (boolean)
    - `selectedVariantId` (string | integer, nullable): The variant the shopper has selected, if any.
- `stream` (boolean): 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. [default `true`]
- `sessionId` (string, nullable): 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. [default `null`; example `"3b0d4a1e-6f52-4b8a-9d3c-7e2f1a0b9c8d"`]
- `session` (Session | null): Visit metadata for analytics. Ignored unless it is useful to you. [default `null`]
  - Option 1: Session
    - `surface` (string): Where the shopper is talking to the agent. Defaults to `widget` when omitted. [example `"widget"`]
    - `agentVersion` (string): Which agent version your client is built against. Use `v2`. [example `"v2"`]
    - `storageMode` (string): How your client persists the visit id — for example `memory`, `session` or `local`. Defaults to `memory`. [example `"local"`]
    - `tz` (string, nullable): The shopper's IANA time zone. [example `"America/Toronto"`]
    - `locale` (string, nullable): The shopper's BCP 47 locale. [example `"en-CA"`]
    - `device` (string, nullable): A coarse device class, typically `mobile` or `desktop`. [example `"desktop"`]
  - Option 2: null
- `store` (object | 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. [default `null`]
  - Option 1: object
    - `origin` (string, nullable) [example `"https://kitandace.com"`]
    - `shop` (string, nullable) [example `"kit-and-ace.myshopify.com"`]
  - Option 2: null
- `country` (string, nullable): 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. [default `null`; example `"US"`]

### Examples

#### First message

Start a conversation and stream the reply.

```json
{
  "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"
  }
}
```

#### Follow-up

Send the previous history plus the new message.

```json
{
  "messages": [
    {
      "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_3fX9qL2m",
      "name": "get_articles",
      "arguments": "{\"gender\":\"mens\"}",
      "status": "completed"
    },
    {
      "type": "function_call_result",
      "callId": "call_3fX9qL2m",
      "name": "get_articles",
      "status": "completed",
      "output": {
        "type": "text",
        "text": "{\"slots\":[{\"slot\":\"top\",\"count\":142,\"price\":\"$29-$189\",\"families\":[\"shirt (96, $49-$189)\",\"t shirt (46, $29-$65)\"]},{\"slot\":\"bottom\",\"count\":88,\"price\":\"$79-$240\",\"families\":[\"pant (80, $79-$240)\",\"short (8, $59-$95)\"]}],\"totalItems\":230,\"note\":\"Each family is \\\"name (count, price range)\\\". Filter lookup_products by these family names exactly as written, and by slot for a whole category.\"}"
      }
    },
    {
      "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\",\"Cheaper options\"]}",
      "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\\\", showing name, image and price. Do NOT list or name them back.\"}"
      }
    },
    {
      "type": "message",
      "role": "assistant",
      "status": "completed",
      "content": [
        {
          "type": "output_text",
          "text": "All three are linen, so they will breathe on a warm afternoon."
        }
      ]
    },
    {
      "role": "user",
      "content": "Show me trousers to match"
    }
  ]
}
```

#### With a photo

Attach an image to the message.

```json
{
  "messages": [
    {
      "role": "user",
      "content": [
        {
          "type": "input_text",
          "text": "What would go with this jacket?"
        },
        {
          "type": "input_image",
          "image": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ..."
        }
      ]
    }
  ]
}
```

#### JSON response

Get one JSON body instead of a stream.

```json
{
  "messages": [
    {
      "role": "user",
      "content": "Do you carry white trousers?"
    }
  ],
  "stream": false
}
```

#### With a cart

Send a cart snapshot to enable the cart tools.

```json
{
  "messages": [
    {
      "role": "user",
      "content": "Add the navy linen shirt in a medium."
    }
  ],
  "sessionId": "3b0d4a1e-6f52-4b8a-9d3c-7e2f1a0b9c8d",
  "cart": {
    "itemCount": 1,
    "total": 79,
    "currency": "USD",
    "items": [
      {
        "key": "43210987654322:1b7e33c0",
        "title": "Everyday Chino",
        "variant": "32 / Stone",
        "quantity": 1,
        "price": 79,
        "sku": "EDC-STN-32",
        "image": "https://cdn.example.com/products/everyday-chino-stone.jpg"
      }
    ]
  }
}
```

## Responses

### 200

With `stream` omitted or `true`: a Server-Sent Events stream. With `stream: false`: one JSON body describing the whole turn.

Headers:

- `X-RateLimit-Resource`: The 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-Limit`: Requests allowed in the current billing period, or `unlimited`.
- `X-RateLimit-Quota-Remaining`: Requests left in the current billing period, or `unlimited`.
- `X-RateLimit-Quota-Reset`: Unix epoch seconds at which the billing period resets.
- `X-RateLimit-RPM-Limit`: Requests allowed per minute for this resource.
- `X-RateLimit-RPM-Remaining`: Requests left in the current one-minute window.
- `X-RateLimit-RPM-Reset`: Unix epoch seconds at which the one-minute window resets.

Content type `text/event-stream`:

```text
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."}
```

Content type `application/json`:

- `output` (string, required): The assistant's final text. May be empty when a row or a card was the whole answer. [example `"All three are linen, so they will breathe on a warm afternoon."`]
- `history` (array<UserMessage | AssistantMessage | FunctionCall | FunctionCallResult>, required): Replay-ready conversation. Send it back as `messages` next turn.
- `searches` (array<SearchResults>, required): Every product row the agent displayed, in order.
  - `label` (string, required): Heading for the row. [example `"Wedding shirts"`]
  - `kind` (string, required): `outfit` — worn together; `list` — alternatives to choose between. [one of `"outfit"`, `"list"`; example `"list"`]
  - `query` (string, required): Repeats `label`. [example `"Wedding shirts"`]
  - `count` (integer, required): Number of entries in `results`. [example `3`]
  - `results` (array<Product>, required)
    - `itemId` (string, required): The item's full id. This is the value to send as `itemIds` when rendering a look. Inside the agent's own tool calls (visible on `tool_called` / `tool_output` events and in `history`) items are referenced by the first 13 characters of this id. [example `"8f2c1a9e-4b7d-4c1e-9a3f-2d6b7e5c1a90"`]
    - `organizationId` (string) [example `"2d6b7e5c-1a90-4c1e-9a3f-8f2c1a9e4b7d"`]
    - `datasetId` (string, required) [example `"9c21f0aa-3e4b-4d2a-b6d1-0f7e8a9b1c2d"`]
    - `status` (string, required): Always `active` on agent payloads — the agent only shows active items. [example `"active"`]
    - `version` (number): Catalogue schema version of the record. [example `1`]
    - `metadata` (ProductMetadata, required): Attributes Stylor's product labeller derived from the product at ingestion. Values are lowercase. `slot`, `family`, `features`, `color_family`, `pattern`, `material` and `fit` come from one taxonomy shared by every catalogue (see the v1 `DatasetItemMetadata` for the full lists).
      - `description` (string): The text the product is embedded as for search, built from its labelled attributes. Not prose; older products hold a one-sentence summary. [example `"mens navy relaxed shirt linen collared short sleeve"`]
      - `gender` (string): Department the item is filed under, typically `mens`, `womens` or `unisex`. [example `"mens"`]
      - `age_group` (string, nullable): Who it is sized for, `adult`, `kids` or `baby`, separate from gender. `null` on products processed before September 2026. [example `"adult"`]
      - `color_family` (string): The item's dominant colour family. [example `"navy"`]
      - `color_rgb` (array<integer>): The product's own colour as `[r, g, b]`, measured from its photos with the background removed.
      - `pattern` (string) [example `"solid"`]
      - `material` (array<string>)
      - `fit` (string, nullable): How a garment is cut relative to the body. `null` for anything without a fit (footwear, bags, jewellery). [example `"relaxed"`]
      - `slot` (string): Where the item is worn, or what kind of thing it is, for example `top`, `bottom`, `footwear`, `full body`. [example `"top"`]
      - `family` (string): What kind of thing it is within its slot, for example `shirt`, `pant`, `dress`, `boot`; `other` when the taxonomy has no name for it. The agent filters by it. [example `"shirt"`]
      - `features` (array<object>): How the item varies, one `{ axis, value, confidence }` answer per attribute that applies to its family.
        - `axis` (string) [example `"neckline"`]
        - `value` (string) [example `"collared"`]
        - `confidence` (number, nullable) [example `0.95`]
      - `featureKeys` (array<string>): `features` flattened to sorted `axis:value` strings.
    - `product` (ProductCore, required): The product's own details as ingested from your store.
      - `name` (string) [example `"Coastal Linen Shirt"`]
      - `url` (string (uri)): The product page on your store. The agent sends it on `add` and `swap` cart actions. [example `"https://shop.example.com/products/coastal-linen-shirt"`]
      - `sku` (string): The product-level SKU. [example `"CLS-NVY"`]
      - `price` (number): Product price in major currency units, in the market named by `market`: the shopper's when the request sent `country` and the store sells there, otherwise the store's default market (see `marketExact`). [example `89`]
      - `currency` (string): ISO 4217 code of `price`. [example `"USD"`]
      - `market` (string, nullable): Handle of the store price list the prices, sale prices and stock are in, such as `us`, `ca` or `international-xof` (a Shopify market, split by currency when one market charges several). `null` for catalogues with no markets (uploads, a store's public product feed). [example `"us"`]
      - `marketExact` (boolean): `true` when the prices are the shopper's own market's. `false` when the store's default market's prices are standing in, with the reason in `marketFallback`; label such prices rather than present them as what the shopper will pay. [example `true`]
      - `marketFallback` (string): Present only when `marketExact` is `false`. `no-country`: the request sent no usable `country`. `not-served`: the store does not sell into that country. `unread`: the shopper's market could not be read. `no-row`: the product has no prices stored for that market yet. `no-markets`: the catalogue has no markets. `stale`: the stored market prices were built against a different default market than the product is in. [one of `"no-country"`, `"not-served"`, `"unread"`, `"no-row"`, `"no-markets"`, `"stale"`; example `"no-country"`]
      - `soldHere` (boolean): Present, as `false`, only when the product is not sold in the shopper's market. Searches leave such products out and the agent does not put them on screen or in a cart, so this is rare. [example `false`]
      - `available` (boolean): `true` when the shopper can buy it: sold in their market, with at least one variant in stock there. The Stylor agent never recommends a product for which this is `false`. [example `true`]
      - `onSale` (boolean): `true` when a variant the shopper can buy (in stock) has a `compareAtPrice` above its `price` in their market. A reduced price on a sold-out variant does not count. [example `true`]
      - `discount` (integer): The largest percent off among in-stock variants, rounded; `0` when `onSale` is `false`. [example `20`]
      - `referencePrice` (number): The product's price in the store's default market, whichever market it is shown in. [example `119`]
      - `referenceCurrency` (string): The currency of `referencePrice`. [example `"CAD"`]
      - `description` (string): Product description, often HTML, as ingested. [example `"<p>Breezy linen with a camp collar.</p>"`]
      - `careInstructions` (string) [example `"Machine wash cold."`]
      - `variants` (array<ProductVariant>): Purchasable variants. Empty when the store exposes none.
        - `variantId` (string): The storefront's own variant id, when the source reported one. Empty for catalogues read from a store's public product feed. [example `"gid://shopify/ProductVariant/41611609636909"`]
        - `options` (array<object>): Each option axis separately. Axis names are merchant-authored and their casing is not dependable — match case-insensitively.
          - `name` (string) [example `"Color"`]
          - `value` (string) [example `"Navy"`]
        - `compareAtPrice` (number, nullable): The variant's list price when it is reduced, otherwise `null`. In the shopper's market, like `price`. [example `129`]
        - `label` (string): The variant label exactly as your store lists it. The agent uses these verbatim when it asks the shopper to pick one. [example `"M / Navy"`]
        - `sku` (string): The variant SKU. Sent on `add` and `swap` cart actions so your storefront can add the right variant. [example `"CLS-NVY-M"`]
        - `price` (number): Variant price in major currency units, in the shopper's market (see `ProductCore.market`). [example `89`]
        - `referencePrice` (number): The variant's price in the store's default market, in the product's `referenceCurrency`. Present when the variant was re-priced for the shopper's market. [example `119`]
        - `inStock` (boolean): Whether the variant can be bought right now in the shopper's market. The agent refuses to add out-of-stock variants. [example `true`]
        - `quantity` (integer, nullable): Units in stock, or `null` when the source did not report a count. `null` and `0` are different answers — a public product feed never reports a count. [example `4`]
      - `assets` (object)
        - `images` (array<ProductImage>)
          - `originalImageUrl` (string (uri)): The image URL as it was ingested from your store. [example `"https://cdn.example.com/products/coastal-linen-shirt-navy.jpg"`]
          - `type` (string): What the photo shows, for example `flat front`, `model full body` or `detail` (see the v1 `DatasetItemImage` for the full list). Older items carry an earlier vocabulary; empty when unknown. [example `"model full body"`]
          - `uploaded` (object): Stylor's stored copy of the image.
            - `id` (string): Stored image id. [example `"img_7c1d2e"`]
            - `key` (string): Storage key. **This is not a URL.** Turn it into one with the v1 image endpoint: `https://stylor.ai/api/v1/image/<key>`, optionally followed by transforms such as `/-/resize/600x/-/format/jpeg`. [example `"org_5f3a/ds_9c21/8f2c1a9e-4b7d/0.jpg"`]
        - `generated` (object)
          - `thumbnail` (object)
            - `url` (string, nullable) [example `null`]
          - `segmented` (array<object>)
      - `skus` (array<string>): Every SKU on the item — the product's own and each variant's. Derived from `sku` and `sizes[].sku` on every write; present on agent payloads today but not part of the v1 REST item shape, so prefer the fields it is derived from.
      - `setItems` (array<string>)
      - `recommendedItems` (array<string>)
      - `model` (object): The model wearing the product photo, when known.
        - `height` (string) [example `"6'1\""`]
        - `size` (string) [example `"M"`]
    - `origin` (ProductOrigin): Where the product came from.
      - `siteUrl` (string) [example `"https://shop.example.com"`]
      - `brandId` (string) [example `""`]
      - `brand` (string) [example `"Coastal Co."`]
      - `country` (string) [example `"CA"`]
    - `createdAt` (string (date-time)) [example `"2026-05-02T14:11:09.000Z"`]
    - `updatedAt` (string (date-time)) [example `"2026-08-30T08:47:51.000Z"`]
- `looks` (array<Look>, required): Every look the agent composed. Render each with the looks endpoint.
  - `label` (string, required): Name of the look; pass it to the looks endpoint. [example `"Garden wedding"`]
  - `items` (array<Product>, required): The garments worn together, as full `Product` records. Pass their `itemId`s to the looks endpoint. [min items 2; max items 6]
    - `itemId` (string, required): The item's full id. This is the value to send as `itemIds` when rendering a look. Inside the agent's own tool calls (visible on `tool_called` / `tool_output` events and in `history`) items are referenced by the first 13 characters of this id. [example `"8f2c1a9e-4b7d-4c1e-9a3f-2d6b7e5c1a90"`]
    - `organizationId` (string) [example `"2d6b7e5c-1a90-4c1e-9a3f-8f2c1a9e4b7d"`]
    - `datasetId` (string, required) [example `"9c21f0aa-3e4b-4d2a-b6d1-0f7e8a9b1c2d"`]
    - `status` (string, required): Always `active` on agent payloads — the agent only shows active items. [example `"active"`]
    - `version` (number): Catalogue schema version of the record. [example `1`]
    - `metadata` (ProductMetadata, required): Attributes Stylor's product labeller derived from the product at ingestion. Values are lowercase. `slot`, `family`, `features`, `color_family`, `pattern`, `material` and `fit` come from one taxonomy shared by every catalogue (see the v1 `DatasetItemMetadata` for the full lists).
      - `description` (string): The text the product is embedded as for search, built from its labelled attributes. Not prose; older products hold a one-sentence summary. [example `"mens navy relaxed shirt linen collared short sleeve"`]
      - `gender` (string): Department the item is filed under, typically `mens`, `womens` or `unisex`. [example `"mens"`]
      - `age_group` (string, nullable): Who it is sized for, `adult`, `kids` or `baby`, separate from gender. `null` on products processed before September 2026. [example `"adult"`]
      - `color_family` (string): The item's dominant colour family. [example `"navy"`]
      - `color_rgb` (array<integer>): The product's own colour as `[r, g, b]`, measured from its photos with the background removed.
      - `pattern` (string) [example `"solid"`]
      - `material` (array<string>)
      - `fit` (string, nullable): How a garment is cut relative to the body. `null` for anything without a fit (footwear, bags, jewellery). [example `"relaxed"`]
      - `slot` (string): Where the item is worn, or what kind of thing it is, for example `top`, `bottom`, `footwear`, `full body`. [example `"top"`]
      - `family` (string): What kind of thing it is within its slot, for example `shirt`, `pant`, `dress`, `boot`; `other` when the taxonomy has no name for it. The agent filters by it. [example `"shirt"`]
      - `features` (array<object>): How the item varies, one `{ axis, value, confidence }` answer per attribute that applies to its family.
        - `axis` (string) [example `"neckline"`]
        - `value` (string) [example `"collared"`]
        - `confidence` (number, nullable) [example `0.95`]
      - `featureKeys` (array<string>): `features` flattened to sorted `axis:value` strings.
    - `product` (ProductCore, required): The product's own details as ingested from your store.
      - `name` (string) [example `"Coastal Linen Shirt"`]
      - `url` (string (uri)): The product page on your store. The agent sends it on `add` and `swap` cart actions. [example `"https://shop.example.com/products/coastal-linen-shirt"`]
      - `sku` (string): The product-level SKU. [example `"CLS-NVY"`]
      - `price` (number): Product price in major currency units, in the market named by `market`: the shopper's when the request sent `country` and the store sells there, otherwise the store's default market (see `marketExact`). [example `89`]
      - `currency` (string): ISO 4217 code of `price`. [example `"USD"`]
      - `market` (string, nullable): Handle of the store price list the prices, sale prices and stock are in, such as `us`, `ca` or `international-xof` (a Shopify market, split by currency when one market charges several). `null` for catalogues with no markets (uploads, a store's public product feed). [example `"us"`]
      - `marketExact` (boolean): `true` when the prices are the shopper's own market's. `false` when the store's default market's prices are standing in, with the reason in `marketFallback`; label such prices rather than present them as what the shopper will pay. [example `true`]
      - `marketFallback` (string): Present only when `marketExact` is `false`. `no-country`: the request sent no usable `country`. `not-served`: the store does not sell into that country. `unread`: the shopper's market could not be read. `no-row`: the product has no prices stored for that market yet. `no-markets`: the catalogue has no markets. `stale`: the stored market prices were built against a different default market than the product is in. [one of `"no-country"`, `"not-served"`, `"unread"`, `"no-row"`, `"no-markets"`, `"stale"`; example `"no-country"`]
      - `soldHere` (boolean): Present, as `false`, only when the product is not sold in the shopper's market. Searches leave such products out and the agent does not put them on screen or in a cart, so this is rare. [example `false`]
      - `available` (boolean): `true` when the shopper can buy it: sold in their market, with at least one variant in stock there. The Stylor agent never recommends a product for which this is `false`. [example `true`]
      - `onSale` (boolean): `true` when a variant the shopper can buy (in stock) has a `compareAtPrice` above its `price` in their market. A reduced price on a sold-out variant does not count. [example `true`]
      - `discount` (integer): The largest percent off among in-stock variants, rounded; `0` when `onSale` is `false`. [example `20`]
      - `referencePrice` (number): The product's price in the store's default market, whichever market it is shown in. [example `119`]
      - `referenceCurrency` (string): The currency of `referencePrice`. [example `"CAD"`]
      - `description` (string): Product description, often HTML, as ingested. [example `"<p>Breezy linen with a camp collar.</p>"`]
      - `careInstructions` (string) [example `"Machine wash cold."`]
      - `variants` (array<ProductVariant>): Purchasable variants. Empty when the store exposes none.
        - `variantId` (string): The storefront's own variant id, when the source reported one. Empty for catalogues read from a store's public product feed. [example `"gid://shopify/ProductVariant/41611609636909"`]
        - `options` (array<object>): Each option axis separately. Axis names are merchant-authored and their casing is not dependable — match case-insensitively.
          - `name` (string) [example `"Color"`]
          - `value` (string) [example `"Navy"`]
        - `compareAtPrice` (number, nullable): The variant's list price when it is reduced, otherwise `null`. In the shopper's market, like `price`. [example `129`]
        - `label` (string): The variant label exactly as your store lists it. The agent uses these verbatim when it asks the shopper to pick one. [example `"M / Navy"`]
        - `sku` (string): The variant SKU. Sent on `add` and `swap` cart actions so your storefront can add the right variant. [example `"CLS-NVY-M"`]
        - `price` (number): Variant price in major currency units, in the shopper's market (see `ProductCore.market`). [example `89`]
        - `referencePrice` (number): The variant's price in the store's default market, in the product's `referenceCurrency`. Present when the variant was re-priced for the shopper's market. [example `119`]
        - `inStock` (boolean): Whether the variant can be bought right now in the shopper's market. The agent refuses to add out-of-stock variants. [example `true`]
        - `quantity` (integer, nullable): Units in stock, or `null` when the source did not report a count. `null` and `0` are different answers — a public product feed never reports a count. [example `4`]
      - `assets` (object)
        - `images` (array<ProductImage>)
          - `originalImageUrl` (string (uri)): The image URL as it was ingested from your store. [example `"https://cdn.example.com/products/coastal-linen-shirt-navy.jpg"`]
          - `type` (string): What the photo shows, for example `flat front`, `model full body` or `detail` (see the v1 `DatasetItemImage` for the full list). Older items carry an earlier vocabulary; empty when unknown. [example `"model full body"`]
          - `uploaded` (object): Stylor's stored copy of the image.
            - `id` (string): Stored image id. [example `"img_7c1d2e"`]
            - `key` (string): Storage key. **This is not a URL.** Turn it into one with the v1 image endpoint: `https://stylor.ai/api/v1/image/<key>`, optionally followed by transforms such as `/-/resize/600x/-/format/jpeg`. [example `"org_5f3a/ds_9c21/8f2c1a9e-4b7d/0.jpg"`]
        - `generated` (object)
          - `thumbnail` (object)
            - `url` (string, nullable) [example `null`]
          - `segmented` (array<object>)
      - `skus` (array<string>): Every SKU on the item — the product's own and each variant's. Derived from `sku` and `sizes[].sku` on every write; present on agent payloads today but not part of the v1 REST item shape, so prefer the fields it is derived from.
      - `setItems` (array<string>)
      - `recommendedItems` (array<string>)
      - `model` (object): The model wearing the product photo, when known.
        - `height` (string) [example `"6'1\""`]
        - `size` (string) [example `"M"`]
    - `origin` (ProductOrigin): Where the product came from.
      - `siteUrl` (string) [example `"https://shop.example.com"`]
      - `brandId` (string) [example `""`]
      - `brand` (string) [example `"Coastal Co."`]
      - `country` (string) [example `"CA"`]
    - `createdAt` (string (date-time)) [example `"2026-05-02T14:11:09.000Z"`]
    - `updatedAt` (string (date-time)) [example `"2026-08-30T08:47:51.000Z"`]
  - `customInstructions` (string, nullable, required): 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. [example `"Shirt untucked with the top button open; outdoor daylight setting."`]
- `questions` (array<QuestionCard>, required): Question cards the agent asked (at most one per turn in practice).
  - `questionId` (string (uuid), required) [example `"5f0e8c3a-2b1d-4e7f-9a6c-3d2e1f0a9b8c"`]
  - `question` (string, required) [example `"Who am I styling for?"`]
  - `options` (array<QuestionOption>, required) [min items 2; max items 10]
    - `label` (string, required): What to show the shopper. Send this back as the next user message when picked. [example `"Menswear"`]
    - `value` (string, required): A machine-readable value for your own use. [example `"mens"`]
  - `allowFreeText` (boolean, required): Whether the shopper may type their own answer instead of picking an option. [example `false`]
  - `steps` (array<object>): 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>`. [min items 2; max items 6]
    - `topic` (string, nullable, required): A short name for what is being asked, used to label the answer. [example `"Shirt size"`]
    - `question` (string, required) [example `"Which size in the Coastal Linen Shirt?"`]
    - `options` (array<QuestionOption>, required) [min items 2; max items 10]
      - `label` (string, required): What to show the shopper. Send this back as the next user message when picked. [example `"Menswear"`]
      - `value` (string, required): A machine-readable value for your own use. [example `"mens"`]
    - `allowFreeText` (boolean, required) [example `false`]
- `suggestions` (array<string>, required): Follow-up chips from `suggest_replies`.

Example:

```json
{
  "output": "We stock white trousers in womenswear only — in menswear the closest is stone or cream.",
  "history": [
    {
      "role": "user",
      "content": "Do you carry white trousers?"
    },
    {
      "type": "message",
      "role": "assistant",
      "status": "completed",
      "content": [
        {
          "type": "output_text",
          "text": "Checking what we carry."
        }
      ]
    },
    {
      "type": "function_call",
      "callId": "call_1aB2cD3e",
      "name": "get_colors",
      "arguments": "{\"families\":[\"pant\"],\"gender\":null,\"age_group\":null}",
      "status": "completed"
    },
    {
      "type": "function_call_result",
      "callId": "call_1aB2cD3e",
      "name": "get_colors",
      "status": "completed",
      "output": {
        "type": "text",
        "text": "{\"scope\":\"all departments combined\",\"colors\":{\"pant\":[\"stone (14)\",\"navy (12)\",\"white (9)\",\"cream (6)\"]},\"note\":\"This list is COMPLETE for the whole catalogue.\"}"
      }
    },
    {
      "type": "message",
      "role": "assistant",
      "status": "completed",
      "content": [
        {
          "type": "output_text",
          "text": "We stock white trousers in womenswear only — in menswear the closest is stone or cream."
        }
      ]
    }
  ],
  "searches": [],
  "looks": [],
  "questions": [],
  "suggestions": []
}
```

## Stream events

Each frame is `event: <name>`, a newline, `data: <json>`, and a blank line. Events are listed in the order they normally arrive.

### `thinking`

The 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.

```json
{
  "status": "started"
}
```

### `thinking_delta`

A 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.

```json
{
  "delta": "They want a dinner look; I'll check dresses and heels first."
}
```

### `thinking_break`

The reasoning summary began a new paragraph. The payload is empty.
Only on a turn that reasons.

```json
{}
```

### `text_delta`

A 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.

```json
{
  "delta": "Let me pull together a few options."
}
```

### `tool_called`

The 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.

```json
{
  "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_results`

A 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.

```json
{
  "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"
              }
            }
          ]
        }
      }
    }
  ]
}
```

### `suggestions`

Two 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.

```json
{
  "suggestions": [
    "Show me trousers to match",
    "Something more formal",
    "Cheaper options"
  ]
}
```

### `question`

A 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.

```json
{
  "questionId": "5f0e8c3a-2b1d-4e7f-9a6c-3d2e1f0a9b8c",
  "question": "Who am I styling for?",
  "options": [
    {
      "label": "Menswear",
      "value": "mens"
    },
    {
      "label": "Womenswear",
      "value": "womens"
    }
  ],
  "allowFreeText": false
}
```

### `looks`

One 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' `itemId`s, the `label` and `customInstructions`, and show
each image as it returns. The items may include pieces that were
never shown as cards.

```json
{
  "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_action`

The 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](https://stylor.ai/guides/rest/v2/cart.md).

```json
{
  "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_output`

A 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.

```json
{
  "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."
  }
}
```

### `done`

The 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.

```json
{
  "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."
        }
      ]
    }
  ]
}
```

### `error`

The 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.

```json
{
  "error": "Agent run failed."
}
```

## Errors

Every error is a JSON object with an `error` string. Match on the status code; the message is written for people.

| Status | Message | When |
| --- | --- | --- |
| 400 | Request body must be valid JSON. | The body could not be parsed as JSON. |
| 400 | Request body must be a JSON object. | The body is empty, or is valid JSON that is not an object (for example an array). |
| 422 | messages must be an array. | `messages` is missing or not a JSON array. Returned as JSON even when streaming was requested. |
| 422 | messages cannot be empty. | `messages` is an empty array. Returned as JSON even when streaming was requested. |

### Shared authentication, quota and rate-limit errors

| Status | Message | When |
| --- | --- | --- |
| 401 | API key missing. | The `Authorization` header is absent, is not `Bearer <key>`, or the key does not have three dash-separated segments. |
| 401 | Authentication failed: public key is invalid or not recognized. | A public-key endpoint was called with a key that does not exist. |
| 401 | Authentication failed: private key provided instead of a public key. | A public-key endpoint was called with an `sgpt-sk-…` key. |
| 401 | Authentication failed: token is not recognized. | A private-key endpoint was called with a key whose lookup segment does not exist. |
| 401 | Authentication failed: private key is invalid. | A private-key endpoint was called with a key whose secret does not match. |
| 401 | Authentication failed: public key provided instead of a private key. | A private-key endpoint was called with an `sgpt-pk-…` key. |
| 401 | Authentication failed: bearer token format is invalid. | A private key was sent without its `::` lookup segment. |
| 401 | Authentication failed: API key could not be read. Regenerate the key. | The key's embedded organization data could not be decrypted or parsed. |
| 403 | Demo mode is not available for private-key routes. Demos are only supported with public API keys. | The demo key was sent to a private-key endpoint. |
| 403 | Demo mode is not supported on this API route. Check the documentation for available demo endpoints. | The demo key was sent to a public-key endpoint that opts out of demo mode. |
| 403 | No active subscription for this organization. | The organization that owns the key has no active subscription. |
| 403 | No quota found for organization "<organizationId>". | The organization has no quota record for the current billing period. |
| 403 | No entitlement for "<resource>". | The plan's quota template has no entry for this endpoint. |
| 403 | Access denied to "<resource>". | The plan's quota template turns this endpoint off. |
| 429 | Monthly quota exceeded for "<resource>" (<quota>/<quota> used). | The billing-period quota for this endpoint is exhausted. `Retry-After` gives the seconds until the period resets. |
| 429 | RPM limit exceeded for "<resource>" (<used>/<limit> used). | More than the allowed requests per minute were sent. `Retry-After` is 60. |
| 500 | Internal server error. | An unexpected failure on the server. The message is always this string; details are logged, never returned. Quota consumed by the request is refunded, as it is for every 5xx response. |
