# Introduction

The Stylor Agent API is a stylist that plans its own work. You send the conversation so far, and an agent decides what the reply needs: checking what the store stocks, searching your catalogue, choosing which products to put on screen, composing outfits into rendered looks, asking the shopper a multiple-choice question, offering follow-up chips and, when you allow it, reading and changing the shopper's cart.

Every one of those steps runs on Stylor's servers. Each result goes back to the model before it writes, so the answer is based on what your store actually stocks, not on guesses.

## Base URL and authentication

All requests go to:

```text
https://stylor.ai/api
```

Every Agent API endpoint takes an organization public API key as a Bearer token. Public keys are safe to use in browser and mobile code. Private keys are rejected with `401`.

```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":"Do you carry linen shirts?"}],"stream":false}'
```

## Endpoints

| Method | Path | What it does | Metered as |
| --- | --- | --- | --- |
| `POST` | `/v2/agent/chat` | Runs one agent turn. Streams Server-Sent Events by default. | `api.v2.agent.chat` |
| `POST` | `/v2/agent/looks` | Renders one outfit as an image. Call once per look the agent announces. | `api.v2.agent.looks` |
| `POST` | `/v2/agent/match` | Experimental. Shop the look: finds the store's products in a photo of someone dressed, with alternates for each. About a second. | `api.v2.agent.match` |

The Agent API has its own quota resources, separate from the v1 Chat and Outfit endpoints, and every plan includes them. Each request uses one unit, however many tools the agent calls during the turn. Every response, streamed ones included, carries the `X-RateLimit-*` headers.

## The request lifecycle

One call to `/v2/agent/chat` goes through these stages:

1. Checks. The API key, subscription, quota and requests-per-minute limit are checked, then the body is validated. A failure here returns an ordinary JSON error with the matching HTTP status, and no stream is opened.
2. Stream opens. The response starts with status `200` and `Content-Type: text/event-stream`.
3. The agent loop. The model writes text and calls tools. Each tool's result goes back to the model, which then writes more text or calls more tools. A turn is capped at ten model calls.
4. Events. Text arrives as `text_delta`. Tool activity arrives as `tool_called` and `tool_output`. Anything meant for the screen arrives as its own event: `search_results`, `looks`, `question`, `suggestions` and `cart_action`.
5. Finish. The stream ends with `done`, which carries the final text and a replay-ready `history`. If the run fails partway, it ends with `error` instead.

Nothing is stored on the server between requests. The `history` you get back is the whole conversation state, and you send it back on the next turn.

## Agent API (v2) versus Chat API (v1)

| | v1 Chat | v2 Agent |
| --- | --- | --- |
| Streaming | Opt-in | On by default; pass `stream: false` for JSON |
| Product search | The model emits search descriptors and your client runs the searches | The agent runs searches itself, sees the results and picks what to show |
| What you receive | Search slots to execute | Finished product rows with full product records |
| Conversation state | Stored on the server under a chat id | Kept by you as the `history` array; there is no chat id |
| Outfit images | Composed from the stored chat | The agent names the exact items in each look; you render each look with `/v2/agent/looks` |
| Questions and chips | Not structured | `question` cards and `suggestions` chips as events |
| Cart | Not supported | Cart tools when you send a cart snapshot |

Because the agent sees its own search results, it can retry a search that came back empty, show the best four of twelve items, and tell the shopper honestly when the store does not carry something.

## A minimal end-to-end flow

The steps below go from a first message to rendered looks. Examples are in JavaScript. The [streaming guide](https://stylor.ai/guides/rest/v2/agent-streaming.md) has a fuller reader and a Python version.

### 1. Send a message

```javascript
const API = 'https://stylor.ai/api';
const KEY = process.env.STYLOR_API_KEY;

let history = [];

async function sendMessage(text) {
  const messages = [...history, { role: 'user', content: text }];

  const res = await fetch(`${API}/v2/agent/chat`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ messages }),
  });

  // Errors before the stream opens come back as JSON.
  const type = res.headers.get('content-type') || '';
  if (!res.ok || !type.includes('text/event-stream')) {
    const body = await res.json().catch(() => ({}));
    throw new Error(body.error || `HTTP ${res.status}`);
  }

  await readEvents(res, handleEvent);
}
```

### 2. Consume the stream

Each frame is `event: <name>`, a newline, `data: <json>`, and a blank line.

```javascript
async function readEvents(res, onEvent) {
  const reader = res.body.getReader();
  const decoder = new TextDecoder();
  let buffer = '';

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });

    const frames = buffer.split('\n\n');
    buffer = frames.pop();

    for (const frame of frames) {
      let name = '';
      let data = '';
      for (const line of frame.split('\n')) {
        if (line.startsWith('event: ')) name = line.slice(7);
        else if (line.startsWith('data: ')) data += line.slice(6);
      }
      if (name && data) onEvent(name, JSON.parse(data));
    }
  }
}
```

### 3. Render products and looks

```javascript
function handleEvent(name, data) {
  switch (name) {
    case 'text_delta':
      appendAssistantText(data.delta);
      break;

    case 'search_results':
      // data.kind is "outfit" (worn together) or "list" (alternatives).
      renderProductRow(data.label, data.kind, data.results.map(toCard));
      break;

    case 'looks':
      for (const look of data.looks) renderLook(look);
      break;

    case 'question':
      renderQuestionCard(data); // send the chosen option's label as the next message
      break;

    case 'suggestions':
      renderChips(data.suggestions); // keep only the latest set
      break;

    case 'done':
      history = data.history; // keep it for the next turn
      break;

    case 'error':
      showError(data.error);
      break;
  }
}

function toCard(product) {
  const key = product.product.assets?.images?.[0]?.uploaded?.key;
  return {
    id: product.itemId,
    name: product.product.name,
    price: product.product.price,
    currency: product.product.currency,
    url: product.product.url,
    image: key
      ? `${API}/v1/image/${key}`
      : product.product.assets?.images?.[0]?.originalImageUrl,
  };
}
```

The product records on `search_results` and `looks` are complete. Draw cards straight away; you don't need another lookup.

### 4. Call the looks endpoint once per announced look

A `looks` event lists outfits but contains no images. Request each image separately. Send all the requests at once so the images appear as they finish.

```javascript
function renderLook(look) {
  const slot = showLookPlaceholder(look.label, look.items.map(toCard));

  fetch(`${API}/v2/agent/looks`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      label: look.label,
      itemIds: look.items.map((item) => item.itemId),
      customInstructions: look.customInstructions,
    }),
  })
    .then((res) => res.json().then((body) => ({ res, body })))
    .then(({ res, body }) => {
      if (!res.ok) throw new Error(body.error);
      slot.setImage(body.visualization.imageData); // a data: URI
    })
    .catch(() => slot.showFailed());
}
```

Images are generated at a 3:4 aspect ratio unless you pass `preferences.aspectRatio`. Rendering takes several seconds. The image comes back inline as a `data:` URI and is not hosted anywhere, so store it yourself if you need to show it again later.

### 5. Continue the conversation

On the next turn, send the stored `history` followed by the shopper's new message. The `sendMessage` function above already does this.

```javascript
await sendMessage('I need something for a summer wedding, menswear.');
await sendMessage('Show me trousers to match'); // history from turn one is included
```

## Prices in the shopper's country

A Shopify store can sell in several countries at different prices. Send the shopper's country as `country` (on a storefront, `Shopify.country`) and products come back at their prices, with their sales and their stock. See [Prices in the shopper's country](https://stylor.ai/guides/rest/v2/markets.md).

## Where to go next

- [Streaming the agent](https://stylor.ai/guides/rest/v2/agent-streaming.md): every event with its payload, ordering, history handling, errors and retries.
- [Cart integration](https://stylor.ai/guides/rest/v2/cart.md): letting the agent read and change the shopper's cart.
- [Shop the look](https://stylor.ai/guides/rest/v2/outfit-match.md): the experimental endpoint that finds the store's products in a photo of someone dressed.
- [Prices in the shopper's country](https://stylor.ai/guides/rest/v2/markets.md): sending `country`, what comes back, sales, and what happens when a shopper's prices are not available.
