# Cart integration

The agent can read the shopper's cart and change it: add an item in a specific size, change a quantity, switch a line to a different size, or remove a line. It never writes to your store itself. The shopper's cart session belongs to your storefront, so the agent tells your client what to change, and your client makes the change and shows the result.

## How it works

1. Before each chat request, your client reads the current cart from the storefront.
2. It sends that snapshot as `cart` in the request body.
3. Receiving a snapshot adds the cart tools to the agent for that turn.
4. When the agent reads or changes the cart, the stream sends a `cart_action` event.
5. For each change, your client updates the storefront cart and shows whether it worked.
6. Optionally, your client reports the outcome using the `intent` token from the event.
7. On the next turn, your client sends a fresh snapshot that reflects the changes.

## The cart snapshot

```json
{
  "itemCount": 2,
  "total": 168,
  "currency": "USD",
  "items": [
    {
      "key": "43210987654321:8f2c1a9e",
      "title": "Coastal Linen Shirt",
      "variant": "M / Navy",
      "quantity": 1,
      "price": 89,
      "sku": "CLS-NVY-M",
      "image": "https://cdn.example.com/products/coastal-linen-shirt-navy.jpg"
    },
    {
      "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"
    }
  ]
}
```

| Field | Type | Description |
| --- | --- | --- |
| `itemCount` | integer | Total units across all lines. |
| `total` | number | Cart total in major currency units (`89.00`, not cents). |
| `currency` | string | ISO 4217 code for `total` and each line's `price`. |
| `items` | array | The cart lines. Can be empty. |
| `items[].key` | string | Your storefront's stable id for the line. The agent sends it back as `lineKey`, so use whatever your cart API needs to find the line. |
| `items[].title` | string | The product name as the shopper sees it. |
| `items[].variant` | string or null | The variant label, or `null` for single-variant products. |
| `items[].quantity` | integer | Units of this line. |
| `items[].price` | number | The line total in major currency units. |
| `items[].sku` | string or null | The SKU of the variant in the cart. Changing a line's size needs this to find the product in your catalogue. Without it, a size change can only match on the exact product name. |
| `items[].image` | string or null | An absolute image URL, used for receipts. It is never sent to the model. |

The server does not validate the snapshot. Send the shape shown above: if a field is missing or wrong, the cart tools report errors to the agent instead of doing anything useful.

## Enabling the cart tools

Add `cart` to the chat request:

```bash
curl https://stylor.ai/api/v2/agent/chat \
  -H "Authorization: Bearer $STYLOR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "messages": [{ "role": "user", "content": "Add the navy linen shirt in a medium." }],
    "sessionId": "3b0d4a1e-6f52-4b8a-9d3c-7e2f1a0b9c8d",
    "cart": { "itemCount": 0, "total": 0, "currency": "USD", "items": [] }
  }'
```

- Sending a snapshot, even an empty one, enables `get_cart`, `add_to_cart`, `set_cart_quantity`, `change_cart_size` and `remove_from_cart` for that turn.
- Omit `cart` (or send `null`) on storefronts where your client cannot change the cart. The agent then has no cart tools at all, so it never offers something it cannot do.
- Send a fresh snapshot with every request. The agent answers "what's in my cart?" from the snapshot, and uses the line `key` values in it to target changes.
- Keep streaming on. With `stream: false`, cart actions are not returned, so the agent would ask for changes your client never receives.
- Send `sessionId`, the `session.sessionId` from the widget preflight. Without it, `add` and `swap` actions carry `intent: null` and their outcomes cannot be recorded.

The agent changes the cart only when the shopper clearly asks it to. It asks which size before adding anything that comes in more than one.

## Cart events

Every cart tool call that has something to show sends one `cart_action` event. The `action` field tells you which kind:

| `action` | Sent by | Meaning | Your client must |
| --- | --- | --- | --- |
| `read` | `get_cart` | The agent read the snapshot | Show it as a receipt. Nothing to change. |
| `add` | `add_to_cart` | Add a variant | Add `quantity` of `sku` from the product at `url`. |
| `quantity` | `set_cart_quantity` | Set a line's quantity | Set line `lineKey` to `quantity` (`0` removes it). |
| `swap` | `change_cart_size` | Change a line's size | Add `sku` at the line's `quantity`, then remove line `lineKey`. |
| `remove` | `remove_from_cart` | Remove a line | Remove line `lineKey`. |

By the time an event is sent, the server has already checked everything it can. For `add` and `swap` that means the item exists in your catalogue, the size is one of its variants, and the variant is in stock. For line actions it means `lineKey` is in your snapshot. Requests that fail those checks never produce an event: the agent is told why, and usually tells the shopper or asks a follow-up question. No event is sent either when the change would do nothing, such as setting a quantity to its current value or switching to the size the line already has.

The agent is told not to claim a change worked. Your interface is where the shopper sees the confirmation.

### read

```json
{
  "action": "read",
  "itemCount": 2,
  "total": 168,
  "currency": "USD",
  "items": [
    { "name": "Coastal Linen Shirt", "size": "M / Navy", "quantity": 1, "price": 89, "image": "https://cdn.example.com/products/coastal-linen-shirt-navy.jpg" },
    { "name": "Everyday Chino", "size": "32 / Stone", "quantity": 1, "price": 79, "image": "https://cdn.example.com/products/everyday-chino-stone.jpg" }
  ]
}
```

This is your own snapshot with the fields renamed for display (`title` becomes `name`, `variant` becomes `size`). It is already complete, so show it as a finished receipt.

### add

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

Find the product on your storefront using `url`, then add `quantity` (between 1 and 10) of the variant with `sku`. `price` is the variant price, falling back to the product price. `image` is a storage key, not a URL: build the URL as `https://stylor.ai/api/v1/image/<key>`. `url` can be an empty string when the catalogue has no URL for the item.

### quantity

```json
{
  "action": "quantity",
  "lineKey": "43210987654322:1b7e33c0",
  "name": "Everyday Chino",
  "size": "32 / Stone",
  "from": 1,
  "quantity": 2,
  "price": null,
  "image": "https://cdn.example.com/products/everyday-chino-stone.jpg"
}
```

Set the line to `quantity` (between 0 and 50); `from` is the quantity before the change. `price` is always `null`, because the line total changes with the quantity.

### swap

```json
{
  "action": "swap",
  "lineKey": "43210987654321:8f2c1a9e",
  "name": "Coastal Linen Shirt",
  "fromSize": "M / Navy",
  "size": "L / Navy",
  "sku": "CLS-NVY-L",
  "url": "https://shop.example.com/products/coastal-linen-shirt",
  "quantity": 1,
  "image": "org_5f3a/ds_9c21/8f2c1a9e-4b7d/0.jpg",
  "intent": "eyJ2IjoxLCJqdGkiOiI0YzJhIn0.Zt7mK1pQ..."
}
```

Add `quantity` of the new `sku` first. Remove line `lineKey` only after the add succeeds. This way a failed add leaves the shopper with the item they already had, and the agent has already told them this is how size changes work. `image` is the product's storage key, or the line's own image URL when the product has no stored image.

### remove

```json
{
  "action": "remove",
  "lineKey": "43210987654322:1b7e33c0",
  "name": "Everyday Chino",
  "size": "32 / Stone",
  "quantity": 1,
  "price": 79,
  "image": "https://cdn.example.com/products/everyday-chino-stone.jpg"
}
```

Remove the line. `price` is the line total being removed.

## Handling cart events in the client

Show the receipt as soon as the event arrives. Do the change in the background, and mark the receipt as done or failed when the storefront responds. When several changes arrive in one turn, apply them in the order they arrived.

```javascript
async function handleCartAction(action) {
  const receipt = showReceipt(action, action.action === 'read' ? 'done' : 'pending');
  if (action.action === 'read') return;

  try {
    switch (action.action) {
      case 'add':
        await storefront.addToCart({ url: action.url, sku: action.sku, quantity: action.quantity });
        break;
      case 'quantity':
        await storefront.setQuantity({ lineKey: action.lineKey, quantity: action.quantity });
        break;
      case 'swap':
        await storefront.addToCart({ url: action.url, sku: action.sku, quantity: action.quantity });
        await storefront.removeLine({ lineKey: action.lineKey });
        break;
      case 'remove':
        await storefront.removeLine({ lineKey: action.lineKey });
        break;
    }
    receipt.markDone();
    await reportOutcome(action, null);
  } catch (err) {
    receipt.markFailed(err.message);
    await reportOutcome(action, err.message);
  }
}
```

## Reporting outcomes with intent tokens

`add` and `swap` events include `intent`, a signed, single-use token covering what the server verified: the item, the size and the price. Report whether the change succeeded by sending it to the v1 Track events endpoint, under the event name that matches the action:

| `action` | Event name |
| --- | --- |
| `add` | `cart.item_added` |
| `swap` | `cart.size_changed` |

The product, size and price are read from the token. Your client only reports the outcome:

```bash
curl https://stylor.ai/api/v1/events \
  -H "X-Stylor-Session: $STYLOR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "events": [{
      "eventId": "2e3f4a5b-6c7d-4e8f-8a9b-0c1d2e3f4a5b",
      "name": "cart.item_added",
      "occurredAt": "2026-09-12T14:05:02.118Z",
      "seq": 9,
      "intent": "eyJ2IjoxLCJqdGkiOiI3ZjFlIn0.q8vT2rN9...",
      "data": { "outcome": "ok", "error": null }
    }]
  }'
```

- The events endpoint is authenticated with the session token from the widget preflight, not with your API key.
- A token is tied to the `sessionId` you sent with the chat request, is valid for five minutes, and can be used only once.
- When `intent` is `null` (no `sessionId` was sent), skip the report.
- `quantity`, `remove` and `read` actions have no intent and nothing to report.

See the v1 Track events reference for the full event schema and rejection reasons.

## After the turn

The snapshot you sent is from before the agent's changes. Read the cart again after your client has applied them, and send the new snapshot with the shopper's next message. The agent then works from the real cart, including any change that failed.
