# Complete the look

`POST https://stylor.ai/api/v2/agent/looks/complete`

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

What else the model in a product's photo is wearing, from your store.
This is the endpoint behind the complete-the-look strip
(`complete-the-look.js`): called once per product page view, it
answers with the store's products the model wears with this one, each
with its alternates. The shop-the-look models read the product's own
model photos (a full-body photo first, then its others) with the
product as the anchor, so the product itself, its other colourways and
the other pieces of a set it belongs to are never answered.

It is the one Agent API endpoint meant to be called from a merchant's
own storefront domain, so it answers the browser's CORS preflight
(`OPTIONS`, `204`) and sends `Access-Control-Allow-Origin: *` on every
response, errors included.

## Naming the product

Name the product however the page can: `itemId` when the page renders
ours, otherwise the variant `sku`, the `name` and the page `url`, which
are resolved to one catalogue row between them. A product that cannot
be placed answers `none`. `image` is the photo the shopper is looking
at: when it is one of the product's own model photos (matched by file
name, never fetched), that photo's look is answered, and `photo` in
the response names it.

## Which store

One organization can run several storefronts under one public key.
The scope (`datasetIds`, default every active dataset) is narrowed to
the storefront the request's `Origin` header names, which the browser
sets and a page cannot change. `matchStore: false` turns that off.

## States

The answer is kept per product and photo for a week, so the first
visitor's request runs the models and later ones read it. `state` is
`ready` with `garments`, `none` when there is nothing to show (no
garment found or sold in the shopper's market, the product could not
be placed, or the organization has switched its widgets off), or
`pending` when a run took longer than 6 seconds (it carries on; ask
again a few seconds later) or the store's products are not indexed
yet. The field is `state`, not `status`.

## Prices

Send `country` (`Shopify.country`) and the cards are priced in the
shopper's market; a garment they cannot buy there (not sold, or sold
out in every size) is left out. For a market whose price list Stylor
does not hold, send `page` with the storefront's currency
(`Shopify.currency.active` and `.rate`) and the price the page shows
for this product: the cards are then converted as the storefront
converts them, and carry `converted: true`.

## Metering

Quota counts runs, not requests: a request that started no run (a
stored answer, a product that could not be placed, a store not yet
indexed, the widgets switched off) gives its unit of
`api.v2.agent.looks.complete` back. The requests-per-minute limit
counts every request. The demo key is not accepted.

## Example request

```bash
curl -X POST "https://stylor.ai/api/v2/agent/looks/complete" \
  -H "Authorization: Bearer $STYLOR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "sku": "CLS-NVY-M",
  "name": "Coastal Linen Shirt",
  "url": "https://shop.example.com/products/coastal-linen-shirt",
  "image": "https://shop.example.com/cdn/shop/files/coastal-linen-shirt-model.jpg",
  "country": "US",
  "language": "en"
}'
```

## Request body

Content type `application/json`, required.

- `itemId` (string, nullable): Our full item id, when the page renders it. Wins over the other identifiers. [example `"8f2c1a9e-4b7d-4c1e-9a3f-2d6b7e5c1a90"`]
- `sku` (string, nullable): The SKU of the variant on display. [example `"CLS-NVY-M"`]
- `name` (string, nullable): The product title. [example `"Coastal Linen Shirt"`]
- `url` (string, nullable): The product page URL or path; its handle tells apart products that share a name. [example `"https://shop.example.com/products/coastal-linen-shirt"`]
- `image` (string, nullable): The product photo the shopper is looking at, as the page has it. Used only when it is one of the product's own model photos, matched by file name and never fetched. [max length 2048; example `"https://shop.example.com/cdn/shop/files/coastal-linen-shirt-model.jpg"`]
- `datasetIds` (array<string>): Datasets to look in. Omit or send `[]` for every active dataset. [default `[]`]
- `matchStore` (boolean): Narrow the scope to the storefront named by the request's `Origin` header. Only an explicit `false` turns it off. [default `true`]
- `country` (string, nullable): The shopper's country, ISO 3166-1 alpha-2 in any case; on Shopify, `Shopify.country`. Cards are priced in their market; garments they cannot buy there are left out. A value that is not a two-letter code is treated as unknown. [default `null`; example `"US"`]
- `page` (object, nullable): What the storefront page shows about the shopper's currency, for a market Stylor holds no price list for. Anything malformed is ignored. [default `null`]
  - `currency` (string): The currency the page shows, `Shopify.currency.active`. [example `"EUR"`]
  - `rate` (number): The storefront's conversion rate, `Shopify.currency.rate`. Above 0 and at most 100000. [example `0.92`]
  - `shownPrice` (number, nullable): The price the page shows for this product, in major units, from which the storefront's rounding is learned. [example `82`]
- `language` (string, nullable): The page's language tag (`Shopify.locale`). With it, a `ready` answer carries the strip's default words in that language as `strings`. [default `null`; example `"fr"`]

### Examples

#### A product page

Name the product by SKU, title and URL, as the strip does.

```json
{
  "sku": "CLS-NVY-M",
  "name": "Coastal Linen Shirt",
  "url": "https://shop.example.com/products/coastal-linen-shirt",
  "image": "https://shop.example.com/cdn/shop/files/coastal-linen-shirt-model.jpg",
  "country": "US",
  "language": "en"
}
```

#### By item id

Name the product by our item id, with no storefront narrowing.

```json
{
  "itemId": "8f2c1a9e-4b7d-4c1e-9a3f-2d6b7e5c1a90",
  "matchStore": false
}
```

## Responses

### 200

The look, or why there is none yet.

Headers:

- `X-RateLimit-Resource`: The quota resource this endpoint is metered against: `api.v2.agent.chat` for the chat endpoint, `api.v2.agent.looks.render` for the looks endpoint (`api.v2.agent.looks` on a plan not yet moved to it), `api.v2.agent.match` for shop the look, `api.v2.agent.looks.complete` for complete the look and `api.v2.agent.pose` for the pose 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`:

- `state` (string, required): `ready`: `garments` holds the look. `pending`: not known yet; ask again a few seconds later. `none`: nothing to show. [one of `"ready"`, `"pending"`, `"none"`; example `"ready"`]
- `garments` (array<object>, required): What the model wears with the product, one entry per garment; empty unless `state` is `ready`.
  - `slot` (string, required): What it is, in a shopper's word (`trousers`, `sneaker`). [example `"trousers"`]
  - `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): The item's colour family (`metadata.color_family`). [example `"black"`]
    - `converted` (boolean): Complete the look only. Present, as `true`, when the price was converted from the store's default currency the way the storefront converts it, for a market whose price list Stylor does not hold. [example `true`]
  - `alternates` (array<MatchCard>, required): The next closest products, best first, without any the shopper cannot buy.
    - `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): The item's colour family (`metadata.color_family`). [example `"black"`]
    - `converted` (boolean): Complete the look only. Present, as `true`, when the price was converted from the store's default currency the way the storefront converts it, for a market whose price list Stylor does not hold. [example `true`]
- `photo` (string, nullable, required): The model photo the answer is for, as a file-name key, or `null` for the product's default photo. Compare it with the photo on screen to tell its look from an unrelated image. [example `"coastal-linen-shirt-model"`]
- `strings` (object): Only on a `ready` answer when `language` was sent. The strip's default heading and lines in that language; `more` holds a `{count}` placeholder.
  - `heading` (string) [example `"Complete the look"`]
  - `subtitle` (string) [example `"The rest of what the model is wearing"`]
  - `single` (string) [example `"Wear it with"`]
  - `more` (string) [example `"Show {count} more"`]

Example:

```json
{
  "state": "ready",
  "photo": "coastal-linen-shirt-model",
  "garments": [
    {
      "slot": "trousers",
      "item": {
        "id": "1b7e33c0-9a2d",
        "itemId": "1b7e33c0-9a2d-4f8e-b5c6-7d8e9f0a1b2c",
        "name": "Everyday Chino",
        "brand": "Acme",
        "price": 79,
        "currency": "USD",
        "market": "us",
        "url": "https://shop.example.com/products/everyday-chino",
        "image": "https://stylor.ai/api/v1/image/org_5f3a/ds_9c21/1b7e33c0-9a2d/0.jpg/-/resize/600x/-/format/jpeg",
        "slot": "bottom",
        "family": "pant",
        "colour": "beige"
      },
      "alternates": []
    }
  ],
  "strings": {
    "heading": "Complete the look",
    "subtitle": "The rest of what the model is wearing, from our collection.",
    "single": "Wear it with",
    "more": "Show {count} more"
  }
}
```

## 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. |
| 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 | image must be a URL string of at most 2048 characters. | `image` is present and is not a string, or is longer than 2048 characters. |
| 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: this key's organization no longer exists. | The organization that owned the key has been deleted. |
| 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 | This API key cannot be used from <origin>. Add the website to the key's allowed websites in the Stylor dashboard. | The key is limited to its allowed websites and the request's `Origin` is not one of them. |
| 403 | This API key only accepts requests from its allowed websites. Allow apps and servers on the key, or call from one of its websites. | The key is limited to its allowed websites and the request has no `Origin` (an app or a server). |
| 403 | This API key is for Stylor's own use only. Use another key, or change its public key access in the Stylor dashboard. | The key is set to Stylor's own use and the request does not come from a Stylor site. |
| 403 | Demo mode is not supported on this API route. Check the documentation for available demo endpoints. | The demo key was sent to an 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 active quota for this organization's plan this billing period. | The organization has no quota record for the current billing period. A failure to read the quota is a `500`, never this. |
| 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. |
