# Shop the look

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

- Authentication: public API key, sent as `Authorization: Bearer sgpt-pk-…`
- Rate-limit resource: `api.v2.agent.match`
- Demo key: accepted
- Web page: https://stylor.ai/guides/rest/v2/match-outfit

Finds the products in a photo of someone dressed. The shop-the-look
models read the photo once, find every item the person wears
(garments, shoes, bags, jewellery, eyewear, hats), and match each one
against your store's products. For every item you get the best
product and the other high-scoring ones, best first, and whether the
top one is the exact product (`match: exact`) or the closest your
store sells (`similar`).

> **Experimental.** Shop the look is new. Its request and response
> fields may change, and its matches are still improving.

A run takes about a second. No language model is involved: the
models are trained for this and run on our servers.

## The photo

Send it inline as a `data:` URI (PNG, JPEG or WebP, at most 8,000,000
characters), or as `imageUrl`, a public http(s) URL we fetch (a domain
name, not an IP address; redirects are not followed). Photos of a
person wearing the outfit work best; about 1024px on the long side is
plenty.

## Which store

One store is searched. With `itemId`, the photo is that product's own
(a product page's model shot): its store is searched, the product is
read with its title, and it is never answered for itself — nor are
its other colourways, or the other pieces of a set it belongs to. This
is how the complete-the-look strip reads a product page. Without
`itemId`, `datasetIds` (default: every active dataset) is searched,
narrowed to one storefront by `store`.

A store's products are searchable once they are indexed, which
happens after each sync; `indexed: false` means none is yet.

## Prices

Send `country` and every card is priced in that shopper's market. A
product the market does not sell gives way to the next alternate that
it does.

## Example request

```bash
curl -X POST "https://stylor.ai/api/v2/agent/match" \
  -H "Authorization: Bearer $STYLOR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "image": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ...",
  "store": "shop.example.com",
  "country": "US"
}'
```

## Request body

Content type `application/json`, required.

- `image` (string): The photo as a `data:` URI — `data:image/png;base64,…`, `data:image/jpeg;base64,…` or `data:image/webp;base64,…`. Longer than 8,000,000 characters is refused with `413`. [max length 8000000; example `"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ..."`]
- `imageUrl` (string (uri)): Or a public http(s) URL of the photo, fetched by us: a domain name, never an IP address or an internal host, with no redirects, at most 12 MB. Used when `image` is absent. [example `"https://cdn.shop.example.com/files/moto-jacket-look.jpg"`]
- `itemId` (string, nullable): The product the photo belongs to, when it is a product's own photo. Its store is searched, it is read with its title, and it, its colourways and its set's pieces are never offered. A `404` when it is not a product in the resolved datasets. [default `null`; example `"8f2c1a9e-4b7d-4c1e-9a3f-2d6b7e5c1a90"`]
- `datasetIds` (array<string>): Search these datasets. Omit or send `[]` to use every active dataset in the organization. Ignored for scope when `itemId` is sent (the product's store is searched), but still checked. [default `[]`]
- `store` (string, nullable): A storefront URL or domain, narrowing the datasets to that store's. A `404` when no dataset in scope is that store. [default `null`; example `"shop.example.com"`]
- `country` (string, nullable): The shopper's country, ISO 3166-1 alpha-2 in any case (`US`, `ca`); on a Shopify storefront, `Shopify.country`. Cards are priced in that shopper's market, and a product the market does not sell is replaced by its next alternate that it does. Omitted, or for a country the store does not sell into, cards carry the store's default market's prices, flagged `marketExact: false`. [default `null`; example `"US"`]
- `alternates` (integer): How many products to return per item besides the top one. [default `4`; minimum 0; maximum 4; example `2`]

### Examples

#### A photo

Match a shopper's photo against one storefront.

```json
{
  "image": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ...",
  "store": "shop.example.com",
  "country": "US"
}
```

#### A product's own photo

Everything else the model on a product page wears, by URL.

```json
{
  "imageUrl": "https://cdn.shop.example.com/files/moto-jacket-look.jpg",
  "itemId": "8f2c1a9e-4b7d-4c1e-9a3f-2d6b7e5c1a90",
  "alternates": 2
}
```

## Responses

### 200

The items found, each with its products.

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 `application/json`:

- `garments` (array<MatchGarment>, required): The items found, in the order the model is most sure of them. Empty when nothing the store sells is in the photo.
  - `slot` (string, required): What it is, in a shopper's word — the top product's family (`jacket`, `sneaker`, `tote bag`), or the family read from the photo when the product has none. [example `"jacket"`]
  - `family` (string, required): What the model read in the photo, as `slot/family` in the product taxonomy. [example `"outerwear/jacket"`]
  - `colour` (string, nullable, required): The colour the model read, one of the taxonomy's colour families. [example `"black"`]
  - `box` (array<number>, required): Where the item is in the photo, `[x0, y0, x1, y1]` as fractions of its width and height. [min items 4; max items 4]
  - `match` (string, required): `exact` — the model is sure `item` is the product in the photo (on stores it never trained on, 98.6% of its exact calls are right). `similar` — `item` is the closest the store sells: a look-alike, or the exact product when the model is not sure enough to say so. [one of `"exact"`, `"similar"`; example `"exact"`]
  - `item` (MatchCard, required): A catalogue item as the match endpoint presents it — enough to draw a card and link out, without the full `Product` record.
    - `id` (string, required): A 13-character short reference to the item. [example `"8f2c1a9e-4b7d"`]
    - `itemId` (string, required): The full item id, as on a `Product`. Use this for anything else, including the looks endpoint. [example `"8f2c1a9e-4b7d-4c1e-9a3f-2d6b7e5c1a90"`]
    - `name` (string, required): Product name; `Unnamed product` when the catalogue has none. [example `"Moto Leather Jacket"`]
    - `brand` (string, nullable, required) [example `"Acme"`]
    - `price` (number, nullable, required): The first variant's price, in the shopper's market when `country` was sent. [example `340`]
    - `currency` (string, required): Currency of `price`; `USD` when the catalogue has none. [example `"USD"`]
    - `wasPrice` (number): The price before reduction, for the price shown (the first variant's `compareAtPrice`). Present only when it is above `price`. [example `425`]
    - `onSale` (boolean): Present, as `true`, only when a size the shopper can buy (in stock) is reduced in their market. [example `true`]
    - `discount` (integer): The largest percent off among in-stock sizes, rounded. Present only with `onSale`. [example `20`]
    - `market` (string): Handle of the store price list the prices are in, such as `us` or `international-xof`. Absent for catalogues with no markets. [example `"us"`]
    - `marketExact` (boolean): Present, as `false`, only when the prices are not the shopper's own market's and the store's default prices stand in (no `country` sent, a country the store does not sell into, a catalogue with no markets, or no price stored for that market yet). Absent when they are the shopper's. [example `false`]
    - `url` (string (uri), nullable, required): The product page. [example `"https://shop.example.com/products/moto-leather-jacket"`]
    - `image` (string (uri), nullable, required): The item's best photo for a card (a flat or product shot where one exists), already resolved to a 600px JPEG URL ready for an `<img src>`. `null` when the item has no image. [example `"https://stylor.ai/api/v1/image/org_5f3a/ds_9c21/8f2c1a9e-4b7d/0.jpg/-/resize/600x/-/format/jpeg"`]
    - `slot` (string, nullable, required): The item's taxonomy slot (`metadata.slot`), for example `outerwear`, `top`, `footwear`. [example `"outerwear"`]
    - `family` (string, nullable, required): The item's taxonomy family within its slot (`metadata.family`), for example `jacket`, `dress`, `sneaker`. [example `"jacket"`]
    - `colour` (string, nullable, required) [example `"black"`]
  - `alternates` (array<MatchCard>, required): The next highest-scoring products, best first, one per product (colourways of one product are not repeated). As many as `alternates` asked for, or fewer. [max items 4]
    - `id` (string, required): A 13-character short reference to the item. [example `"8f2c1a9e-4b7d"`]
    - `itemId` (string, required): The full item id, as on a `Product`. Use this for anything else, including the looks endpoint. [example `"8f2c1a9e-4b7d-4c1e-9a3f-2d6b7e5c1a90"`]
    - `name` (string, required): Product name; `Unnamed product` when the catalogue has none. [example `"Moto Leather Jacket"`]
    - `brand` (string, nullable, required) [example `"Acme"`]
    - `price` (number, nullable, required): The first variant's price, in the shopper's market when `country` was sent. [example `340`]
    - `currency` (string, required): Currency of `price`; `USD` when the catalogue has none. [example `"USD"`]
    - `wasPrice` (number): The price before reduction, for the price shown (the first variant's `compareAtPrice`). Present only when it is above `price`. [example `425`]
    - `onSale` (boolean): Present, as `true`, only when a size the shopper can buy (in stock) is reduced in their market. [example `true`]
    - `discount` (integer): The largest percent off among in-stock sizes, rounded. Present only with `onSale`. [example `20`]
    - `market` (string): Handle of the store price list the prices are in, such as `us` or `international-xof`. Absent for catalogues with no markets. [example `"us"`]
    - `marketExact` (boolean): Present, as `false`, only when the prices are not the shopper's own market's and the store's default prices stand in (no `country` sent, a country the store does not sell into, a catalogue with no markets, or no price stored for that market yet). Absent when they are the shopper's. [example `false`]
    - `url` (string (uri), nullable, required): The product page. [example `"https://shop.example.com/products/moto-leather-jacket"`]
    - `image` (string (uri), nullable, required): The item's best photo for a card (a flat or product shot where one exists), already resolved to a 600px JPEG URL ready for an `<img src>`. `null` when the item has no image. [example `"https://stylor.ai/api/v1/image/org_5f3a/ds_9c21/8f2c1a9e-4b7d/0.jpg/-/resize/600x/-/format/jpeg"`]
    - `slot` (string, nullable, required): The item's taxonomy slot (`metadata.slot`), for example `outerwear`, `top`, `footwear`. [example `"outerwear"`]
    - `family` (string, nullable, required): The item's taxonomy family within its slot (`metadata.family`), for example `jacket`, `dress`, `sneaker`. [example `"jacket"`]
    - `colour` (string, nullable, required) [example `"black"`]
- `indexed` (boolean, required): `false` when none of the store's products is indexed yet (a store connected minutes ago); try again after its first sync has been processed. [example `true`]

Example:

```json
{
  "indexed": true,
  "garments": [
    {
      "slot": "jacket",
      "family": "outerwear/jacket",
      "colour": "black",
      "box": [
        0.18,
        0.21,
        0.83,
        0.58
      ],
      "match": "exact",
      "item": {
        "id": "8f2c1a9e-4b7d",
        "itemId": "8f2c1a9e-4b7d-4c1e-9a3f-2d6b7e5c1a90",
        "name": "Moto Leather Jacket",
        "brand": "Acme",
        "price": 340,
        "currency": "USD",
        "url": "https://shop.example.com/products/moto-leather-jacket",
        "image": "https://stylor.ai/api/v1/image/org_5f3a/ds_9c21/8f2c1a9e-4b7d/0.jpg/-/resize/600x/-/format/jpeg",
        "slot": "outerwear",
        "family": "jacket",
        "colour": "black"
      },
      "alternates": [
        {
          "id": "1b7e33c0-9a2d",
          "itemId": "1b7e33c0-9a2d-4f8e-b5c6-7d8e9f0a1b2c",
          "name": "Biker Jacket",
          "brand": "Acme",
          "price": 290,
          "currency": "USD",
          "url": "https://shop.example.com/products/biker-jacket",
          "image": "https://stylor.ai/api/v1/image/org_5f3a/ds_9c21/1b7e33c0-9a2d/0.jpg/-/resize/600x/-/format/jpeg",
          "slot": "outerwear",
          "family": "jacket",
          "colour": "black"
        }
      ]
    }
  ]
}
```

## 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). |
| 404 | No active datasets found for this organization. Activate one via the dashboard (Dataset → Settings) or through the REST API before proceeding. | `datasetIds` was omitted and the organization has datasets, but none is active. |
| 404 | All requested datasets are archived. Reactivate at least one via the dashboard or REST API. | Every dataset in `datasetIds` is archived. |
| 404 | No datasets found for this organization. Create one via the dashboard or REST API, or pass datasetIds to use draft/archived datasets before proceeding. | The organization has no datasets at all. |
| 404 | itemId is not a product in these datasets. | `itemId` is not a product of this organization in the resolved datasets. |
| 404 | store matches none of these datasets. | `store` is not the storefront (`storeDomain`) of any dataset in scope. |
| 413 | image is too large. | `image` is longer than 8,000,000 characters, or the photo at `imageUrl` is over 12 MB. |
| 422 | Send the photo as image (a data URL) or imageUrl. | Neither `image` nor `imageUrl` was sent. |
| 422 | image must be a base64 data URL (png, jpeg or webp). | `image` is not a string starting with `data:image/png;base64,`, `data:image/jpeg;base64,` (or `jpg`) or `data:image/webp;base64,`. |
| 422 | imageUrl: <reason> | `imageUrl` is not an http(s) URL on a public domain name (an IP address, `localhost` or an internal name is refused). |
| 422 | imageUrl could not be fetched. | The photo at `imageUrl` could not be fetched (a network error, a timeout, a redirect, or an error status, which is quoted), or is not an image. |
| 422 | alternates must be an integer from 0 to 4. | `alternates` is present and not an integer from 0 to 4. |
| 422 | datasetIds must be an array of strings. | `datasetIds` is present and is not an array, or contains a value that is not a string. |
| 422 | Invalid datasetIds for this organization: <ids> | One or more of `datasetIds` does not belong to the organization. The offending ids are listed, comma-separated. |

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