Complete the look

post/v2/agent/looks/complete

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.

Public keyapi.v2.agent.looks.completeNo demo

Request body

application/jsonrequired
itemIdstringnullable

Our full item id, when the page renders it. Wins over the other identifiers.

e.g. "8f2c1a9e-4b7d-4c1e-9a3f-2d6b7e5c1a90"
skustringnullable

The SKU of the variant on display.

e.g. "CLS-NVY-M"
namestringnullable

The product title.

e.g. "Coastal Linen Shirt"
urlstringnullable

The product page URL or path; its handle tells apart products that share a name.

e.g. "https://shop.example.com/products/coastal-linen-shirt"
imagestringnullable

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.

maxLength: 2048e.g. "https://shop.example.com/cdn/shop/files/coastal-linen-shirt-model.jpg"
datasetIdsarray<string>

Datasets to look in. Omit or send [] for every active dataset.

default: []
matchStoreboolean

Narrow the scope to the storefront named by the request's Origin header. Only an explicit false turns it off.

default: true
countrystringnullable

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: nulle.g. "US"
objectnullable

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
currencystring

The currency the page shows, Shopify.currency.active.

e.g. "EUR"
ratenumber

The storefront's conversion rate, Shopify.currency.rate. Above 0 and at most 100000.

e.g. 0.92
shownPricenumbernullable

The price the page shows for this product, in major units, from which the storefront's rounding is learned.

e.g. 82
languagestringnullable

The page's language tag (Shopify.locale). With it, a ready answer carries the strip's default words in that language as strings.

default: nulle.g. "fr"

Response

200The look, or why there is none yet.

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

statestringrequired

ready: garments holds the look. pending: not known yet; ask again a few seconds later. none: nothing to show.

One ofreadypendingnone
e.g. "ready"
array<object>required

What the model wears with the product, one entry per garment; empty unless state is ready.

slotstringrequired

What it is, in a shopper's word (trousers, sneaker).

e.g. "trousers"
MatchCardrequired

A catalogue item as the match endpoint presents it — enough to draw a card and link out, without the full Product record.

array<MatchCard>required

The next closest products, best first, without any the shopper cannot buy.

photostringnullablerequired

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.

e.g. "coastal-linen-shirt-model"
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.

headingstring
e.g. "Complete the look"
subtitlestring
e.g. "The rest of what the model is wearing"
singlestring
e.g. "Wear it with"
morestring
e.g. "Show {count} more"

Errors

24

Every error is JSON with a single error string unless noted. Match on the status code; the message is for humans.

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.

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"
}'
Response · 200
{
  "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"
  }
}