# Retrieve products

`POST https://stylor.ai/api/v1/products`

- Authentication: public API key, sent as `Authorization: Bearer sgpt-pk-…`
- Rate-limit resource: `api.v1.search.query`
- Demo key: accepted
- Web page: https://stylor.ai/guides/rest/v1/retrieve-products

Returns whole products by their itemIds, in the same shape search
returns them: every variant with its price and stock, every photo, the
store it is from. Use it to open a product page for an item you got as
a compact card from feed, similar or rank, or a list of ids you saved.

Only products in your organization's **active** datasets are read. An
id that is not one of them (archived, deleted, mistyped, another
organization's) is left out of `products` without an error, so check
the length when you need every one. Products come back in the order
you asked for them, each once.

## Prices in the shopper's market

Send the shopper's `country` and each product is priced as search
prices it: `price`, `currency` and each variant's `price`,
`compareAtPrice` and `inStock` are their market's. Without it, or for
a country the store does not sell into, the store's default market is
used and `product.marketExact` is `false`. Unlike search, a product
their market does not sell is kept, marked `product.soldHere: false`,
so a saved product can still be shown as unavailable rather than
disappear.

Product reads draw on search's allowance, `api.v1.search.query`.

## Example request

```bash
curl -X POST "https://stylor.ai/api/v1/products" \
  -H "Authorization: Bearer $STYLOR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "itemIds": [
    "35a725c8-23fb-4009-a52a-2a778d2e504a",
    "a342cbe3-965b-4055-8ac3-20f63103cade"
  ],
  "country": "CA"
}'
```

## Request body

Content type `application/json`, required.

- `itemIds` (array<string>, required): Full itemIds. Duplicates are returned once. [min items 1; max items 50]
- `country` (string): The shopper's two-letter ISO 3166 country, such as `US`. Case does not matter.

### Examples

#### Open two products

Two products from a feed, priced for a shopper in Canada.

```json
{
  "itemIds": [
    "35a725c8-23fb-4009-a52a-2a778d2e504a",
    "a342cbe3-965b-4055-8ac3-20f63103cade"
  ],
  "country": "CA"
}
```

## Responses

### 200

The products found, in the order asked.

Content type `application/json`:

- `products` (array<DatasetItem>, required): One per id found. Ids not found are left out.
  - `itemId` (string (uuid), required): Unique item id. It is the same id the queue entry was given at upload time. [example `"8b0a1b62-7c2a-4b7f-9e2c-0f3a9b1f0c11"`]
  - `datasetId` (string (uuid), required): The dataset the item belongs to. [example `"c9b3c6c0-9b0f-4f42-bf3f-2b7a6b1b9d31"`]
  - `organizationId` (string (uuid), required): The organization that owns the dataset. [example `"0a2c1f4e-5b6d-7e8f-9012-3456789abcde"`]
  - `status` (string, required): Only `active` items are listed and searchable. [one of `"active"`, `"archived"`; example `"active"`]
  - `version` (number, required): Ingest pipeline version the item was processed with. [example `1`]
  - `metadata` (DatasetItemMetadata, required): What Stylor's product labeller read from the item's photos and listing when it was processed. Fields with an enum are closed lists; the taxonomy (`slot`, `family`, `features`) is shared by every catalogue. Items processed before a field existed may lack it.
    - `description` (string): The text the item is embedded as for search: its product name and product type, then its labelled attributes, most distinguishing first, with nothing said twice. Not prose. `searchTextVersion` says which recipe built it; items processed before September 2026 hold a one-sentence summary instead. [default `""`; max length 400; example `"lucile mini dress dresses womens black slim satin mini length cowl neck sleeveless zip closure"`]
    - `gender` (string, required): Target gender. If the dataset is configured with a target gender (`mens` or `womens`), every item takes that value. [one of `"mens"`, `"womens"`, `"unisex"`; example `"womens"`]
    - `age_group` (string, nullable): Who the item is sized for, separate from gender (a girls' dress is `womens` and `kids`). `null` on items processed before September 2026. [one of `"adult"`, `"kids"`, `"baby"`, `null`; example `"adult"`]
    - `color_rgb` (array<integer>, required): The item's own colour as `[r, g, b]`, each 0–255, measured from its photos with the background removed. [min items 3; max items 3]
    - `color_family` (string, required): Colour bucket of the colourway this listing sells. [one of `"black"`, `"charcoal"`, `"grey"`, `"silver"`, `"white"`, `"ivory"`, `"cream"`, `"beige"`, `"tan"`, `"camel"`, `"brown"`, `"chocolate"`, `"khaki"`, `"olive"`, `"green"`, `"mint"`, `"teal"`, `"turquoise"`, `"blue"`, `"navy"`, `"purple"`, `"lavender"`, `"pink"`, `"rose"`, `"coral"`, `"peach"`, `"red"`, `"burgundy"`, `"rust"`, `"orange"`, `"mustard"`, `"yellow"`, `"gold"`, `"multicolour"`, `"other"`; example `"black"`]
    - `colors` (array<string>): Every colour the item is sold in, read from its colour options (lower-case, as the store names them); `[color_family]` when it has none.
    - `pattern` (string, required): Pattern printed or woven into the fabric. [one of `"solid"`, `"striped"`, `"checked"`, `"plaid"`, `"gingham"`, `"floral"`, `"ditsy floral"`, `"animal print"`, `"geometric"`, `"abstract"`, `"polka dot"`, `"paisley"`, `"camouflage"`, `"chevron"`, `"herringbone"`, `"tie dye"`, `"colour block"`, `"graphic"`, `"other"`; example `"solid"`]
    - `material` (array<string>): Fibres the listing states, main fabric first. Empty when the listing does not say. [one of `"cotton"`, `"linen"`, `"hemp"`, `"wool"`, `"cashmere"`, `"alpaca"`, `"mohair"`, `"silk"`, `"down"`, `"leather"`, `"suede"`, `"faux leather"`, `"shearling"`, `"polyester"`, `"nylon"`, `"elastane"`, `"acrylic"`, `"viscose"`, `"modal"`, `"lyocell"`, `"acetate"`, `"metal"`, `"wood"`, `"glass"`, `"ceramic"`, `"rubber"`, `"straw"`, `"paper"`, `"other"`]
    - `fit` (string, nullable): How a garment is cut relative to the body. `null` for anything without a fit (footwear, bags, jewellery). [one of `"skinny"`, `"slim"`, `"regular"`, `"relaxed"`, `"oversized"`, `"other"`, `null`; example `"slim"`]
    - `slot` (string, required): Where the item is worn, or what kind of thing it is. The primary search filter. [one of `"accessory"`, `"bag"`, `"jewellery"`, `"headwear"`, `"eyewear"`, `"hosiery"`, `"bottom"`, `"footwear"`, `"outerwear"`, `"full body"`, `"sleepwear"`, `"swimwear"`, `"non fashion"`, `"top"`, `"underwear"`; example `"full body"`]
    - `family` (string): What kind of thing it is within its slot, for example `dress`, `boot`, `t shirt`; `other` when the taxonomy has no name for it. [example `"dress"`]
    - `features` (array<object>): How the item varies, one answer per attribute that applies to its family (a dress has a neckline and a length; a scarf has neither).
      - `axis` (string) [example `"neckline"`]
      - `value` (string) [example `"cowl"`]
      - `confidence` (number, nullable): The labeller's probability for this answer, 0–1. [example `0.86`]
    - `featureKeys` (array<string>): `features` flattened to sorted `axis:value` strings, for filtering.
    - `taxonomyVersion` (integer, nullable): The taxonomy version the item was labelled under. [example `4`]
    - `searchTextVersion` (integer, nullable): Which recipe built `description`. `2` leads with the product name and type (since September 2026); `null` on items embedded before versions were recorded. An item is re-embedded under the current recipe when it is next reprocessed. [example `2`]
  - `product` (DatasetItemProduct, required): Merchant-facing product data. Everything here comes from your upload or your dataset configuration, not from AI.
    - `sku` (string, required): The product-level SKU you supplied. Unique within a dataset. [example `"A1A2P-BB2J"`]
    - `url` (string (uri), required): Product page URL, built as `<dataset store URL>/products/<handle>`. [example `"https://shop.example.com/products/classic-crew-tee"`]
    - `name` (string, required): Product title. [example `"Classic Crew Tee"`]
    - `description` (string): Product description as supplied; may contain HTML. [default `""`; example `"<p>A heavyweight organic cotton tee with a relaxed fit.</p>"`]
    - `careInstructions` (string): Care instructions. Not populated by the ingest queue. [default `""`; example `""`]
    - `specs` (object): What `description` states, read by rules when the product is processed: measurements in centimetres with the size they were taken on, fibre composition, fabric weight, the model, how the size runs, care, stretch, lining and where it was made. A fact the description does not state is absent; the whole object is absent on products processed before it existed.
      - `version` (integer): Version of the rules that read it. [example `1`]
      - `measurements` (array<object>)
        - `name` (string): inseam, outseam, rise, backRise, legOpening, thigh, knee, length, chest, waist, hip, shoulder, sleeve, sweep, width, height, depth, diameter or strapDrop. [example `"inseam"`]
        - `cm` (number) [example `76`]
        - `cmMax` (number): Top of a stated range.
        - `size` (string, nullable): The size it was measured on [example `"M"`]
        - `flat` (boolean): Measured laid flat.
        - `circumference` (boolean): Stated as a circumference.
      - `materials` (array<object>)
        - `name` (string) [example `"cotton"`]
        - `pct` (number, nullable) [example `98`]
        - `part` (string) [one of `"main"`, `"lining"`, `"trim"`, `"fill"`, `"total"`; example `"main"`]
      - `weight` (object)
        - `gsm` (number, nullable) [example `500`]
        - `class` (string, nullable) [one of `"light"`, `"mid"`, `"heavy"`; example `"heavy"`]
      - `models` (array<object>)
        - `heightCm` (number, nullable) [example `185`]
        - `size` (string, nullable) [example `"M"`]
        - `measurements` (array<object>)
          - `name` (string) [one of `"chest"`, `"waist"`, `"hip"`]
          - `cm` (number)
      - `fit` (object)
        - `runs` (integer, nullable): -1 runs small, 0 true to size, 1 runs large. [one of `-1`, `0`, `1`; example `-1`]
        - `betweenSizes` (string, nullable) [one of `"up"`, `"down"`]
        - `notes` (array<string>)
      - `care` (object)
        - `wash` (string, nullable) [one of `"machine"`, `"hand"`, `"dry clean"`, `"spot clean"`; example `"machine"`]
        - `tumbleDry` (boolean, nullable) [example `false`]
      - `stretch` (string) [one of `"none"`, `"low"`, `"medium"`, `"high"`; example `"low"`]
      - `lining` (string) [one of `"full"`, `"partial"`, `"none"`; example `"full"`]
      - `origin` (string): Country it was made in. [example `"Japan"`]
    - `price` (number, required): Product price, in the market named by `market`: as stored, the store's default market; on a search result, the shopper's (see `referencePrice`). `0` when the upload had no price. [example `49`]
    - `currency` (string, required): ISO 4217 code of `price`. As stored, the currency the source reports (a Shopify Storefront API source reports its own), falling back to the dataset's configured currency, then `CAD`. On a search result, the shopper's market's currency. [example `"USD"`]
    - `market` (string, nullable): Handle of the store price list the prices, sale prices and stock are in, such as `us`, `ca` or `international-xof` (a Shopify market, split by currency when one market charges several). Stored on every item: the store's default market, or `null` for sources with no markets (uploads, a store's public product feed). On a search result, the market the product was priced for. [example `"us"`]
    - `marketExact` (boolean): Response only, on search results. `true` when the prices are the shopper's own market's. `false` when the store's default market's prices are standing in, with the reason in `marketFallback`. [example `true`]
    - `marketFallback` (string): Response only, present when `marketExact` is `false`. Why the shopper's own market could not be answered. `no-country`: the request sent no `country`. `not-served`: the store does not sell into that country. `unread`: the shopper's market could not be read. `no-row`: the product has no prices stored for that market yet (synced before the market existed). `no-markets`: the source has no markets. `stale`: the stored market prices were built against a different default market than the product is in. [one of `"no-country"`, `"not-served"`, `"unread"`, `"no-row"`, `"no-markets"`, `"stale"`; example `"no-country"`]
    - `available` (boolean): Response only, on search results. `true` when the shopper can buy it: sold in their market, with at least one variant in stock there. The Stylor agent never recommends a product for which this is `false`. [example `true`]
    - `onSale` (boolean): Response only, on search results. `true` when a variant the shopper can buy (in stock) has a `compareAtPrice` above its `price` in the shopper's market. A reduced price on a sold-out variant does not count. [example `true`]
    - `discount` (integer): Response only, on search results. The largest percent off among in-stock variants, rounded; `0` when `onSale` is `false`. [example `20`]
    - `referencePrice` (number): Response only, on search results. The stored price in the store's default market, before re-pricing for the shopper. [example `49`]
    - `referenceCurrency` (string): Response only, on search results. The currency of `referencePrice`. [example `"USD"`]
    - `model` (DatasetItemModelInfo): Details of the model shown in the product photography, when known. Empty strings when not provided.
      - `height` (string): Model height, free text. [default `""`; example `""`]
      - `size` (string): Size worn by the model. [default `""`; example `""`]
    - `setItems` (array<string>): SKUs of products sold together with this one. Not populated by the ingest queue.
    - `recommendedItems` (array<string>): SKUs of merchant-recommended companions. Not populated by the ingest queue.
    - `assets` (DatasetItemAssets): Images attached to the item.
      - `images` (array<DatasetItemImage>): Stored product images, in the order they were supplied. At most 10 images are kept per item; any beyond the tenth are dropped at ingest. [max items 10]
        - `originalImageUrl` (string (uri), required): The `src` you supplied at upload time. [example `"https://cdn.shopify.com/s/files/1/0001/products/tee-front.jpg"`]
        - `type` (string): What the photo shows: one of `flat front`, `flat back`, `mannequin`, `underside`, `detail`, `model full body`, `model cropped`, `hand`, `group`, `scene`, `not product`. Items processed before September 2026 carry an older vocabulary (`packshot`, `model_front`, `lifestyle`, …). [default `""`; example `"model full body"`]
        - `uploaded` (StoredImage, required): A copy of an image that Stylor stores itself. Only the object `key` is returned; turn it into a URL by prefixing the image delivery endpoint: `https://stylor.ai/api/v1/image/{key}`. Append transform segments such as `/-/resize/600x/-/format/webp` to that URL to get a resized or re-encoded variant.
          - `id` (string (uuid), required): Identifier of the stored copy. It is the UUID portion of `key`. [example `"3f9c2a1e-8d4b-4c6f-9a2e-1b7d5e0c4f88"`]
          - `key` (string, required): Object key to pass to `GET /v1/image/{key}`. Catalogue images use the form `images/<uuid>.jpeg`. [example `"images/3f9c2a1e-8d4b-4c6f-9a2e-1b7d5e0c4f88.jpeg"`]
      - `generated` (DatasetItemGeneratedAssets): Assets Stylor generates for an item. Generative assets are currently disabled platform-wide, so new items carry a `null` thumbnail URL and an empty `segmented` list.
        - `thumbnail` (object): An AI-generated product thumbnail.
          - `url` (string, nullable): Absolute URL of the thumbnail on the image delivery endpoint, or `null` when none was generated. [example `null`]
        - `segmented` (array<DatasetItemSegmentedImage>): Background-removed cut-outs.
          - `sourceImageId` (string, required): The `uploaded.id` of the product image the cut-out was made from. [example `"3f9c2a1e-8d4b-4c6f-9a2e-1b7d5e0c4f88"`]
          - `uploaded` (StoredImage, required): A copy of an image that Stylor stores itself. Only the object `key` is returned; turn it into a URL by prefixing the image delivery endpoint: `https://stylor.ai/api/v1/image/{key}`. Append transform segments such as `/-/resize/600x/-/format/webp` to that URL to get a resized or re-encoded variant.
            - `id` (string (uuid), required): Identifier of the stored copy. It is the UUID portion of `key`. [example `"3f9c2a1e-8d4b-4c6f-9a2e-1b7d5e0c4f88"`]
            - `key` (string, required): Object key to pass to `GET /v1/image/{key}`. Catalogue images use the form `images/<uuid>.jpeg`. [example `"images/3f9c2a1e-8d4b-4c6f-9a2e-1b7d5e0c4f88.jpeg"`]
    - `variants` (array<DatasetItemVariant>): Purchasable variants. Renamed from `sizes`, which is what the field had always held: a variant is the product of every option axis, so these rows are `S / Cream` and `Regular / XXS-UK6` — size crossed with colour, or with fit — not sizes.
      - `variantId` (string): The storefront's own variant id, when the source reported one. Empty for catalogues read from a store's public product feed, which does not return it. [example `"gid://shopify/ProductVariant/41611609636909"`]
      - `sku` (string, required): The variant SKU, exactly as supplied. [example `"A1A2P-BB2J-M"`]
      - `label` (string, required): The variant's full name, for example `Medium` or `M / Black`. [example `"M / Black"`]
      - `options` (array<object>): Each option axis separately, which `label` cannot reliably be parsed back into. Axis names are merchant-authored and their casing is not dependable — one store writes `Size` and `Color`, another `size` and `fit`. Match case-insensitively.
        - `name` (string) [example `"Color"`]
        - `value` (string) [example `"Black"`]
      - `inStock` (boolean, required): The variant's availability at the last sync, in the market the product is priced in: the store's default market as stored, the shopper's own on a search result. [example `true`]
      - `quantity` (integer, nullable): Units in stock, or `null` when the source did not report a count. `null` and `0` are different answers. A public product feed never reports a count; a Storefront API source reports one only when its token carries the inventory scope. [example `4`]
      - `price` (number, required): Variant price in the product's `currency`. On a search result, the shopper's market's price (see `DatasetItemProduct.market`). [example `49`]
      - `compareAtPrice` (number, nullable): The variant's list price when it is reduced, otherwise `null`. In the same market as `price`. [example `79`]
      - `referencePrice` (number, nullable): Response only, on search results. The variant's stored price in the store's default market (in the product's `referenceCurrency`), present when the variant was re-priced for the shopper's market. Never stored. [example `49`]
      - `imageRef` (string, nullable): This variant's own photo, when the source distinguishes one from the product's.
  - `origin` (DatasetItemOrigin, required): Where the item came from. Copied from the dataset's store configuration at processing time.
    - `siteUrl` (string): Store URL from the dataset configuration. [default `""`; example `"https://shop.example.com"`]
    - `brandId` (string): Store identifier from the dataset configuration. [default `""`; example `"shop-example"`]
    - `brand` (string): Store or brand name. [default `""`; example `"Example Apparel"`]
    - `country` (string): Store country from the dataset configuration. [default `""`; example `"CA"`]
  - `createdAt` (string (date-time), required): When the processed item was first written. [example `"2025-03-04T18:22:41.913Z"`]
  - `updatedAt` (string (date-time), required): When the item was last rewritten by a sync or reprocess. [example `"2025-03-04T18:22:41.913Z"`]

Example:

```json
{
  "products": [
    {
      "itemId": "35a725c8-23fb-4009-a52a-2a778d2e504a",
      "organizationId": "0b7c3d9e-1f2a-4b3c-9d8e-7f6a5b4c3d2e",
      "datasetId": "c1a2b3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
      "status": "active",
      "version": 4,
      "product": {
        "name": "SLIM JEAN DENIM",
        "url": "https://ca.stussy.com/products/slim-jean-denim",
        "price": 195,
        "currency": "CAD",
        "market": "ca",
        "marketExact": true,
        "variants": [
          {
            "label": "Dark Wash Indigo / 30",
            "options": [
              {
                "name": "Color",
                "value": "Dark Wash Indigo"
              },
              {
                "name": "Size",
                "value": "30"
              }
            ],
            "inStock": false,
            "price": 195
          },
          {
            "label": "Dark Wash Indigo / 34",
            "options": [
              {
                "name": "Color",
                "value": "Dark Wash Indigo"
              },
              {
                "name": "Size",
                "value": "34"
              }
            ],
            "inStock": true,
            "price": 195
          }
        ],
        "assets": {
          "images": [
            {
              "originalImageUrl": "https://ca.stussy.com/cdn/shop/files/slim-jean-1.jpg",
              "uploaded": {
                "key": "v1/datasets/images/598fb45a-b960-4224-a83c-e9663116830d.jpeg",
                "url": "https://stylor.ai/api/v1/image/v1/datasets/images/598fb45a-b960-4224-a83c-e9663116830d.jpeg"
              }
            }
          ]
        }
      },
      "metadata": {
        "gender": "mens",
        "age_group": "adult",
        "slot": "bottom",
        "family": "jeans",
        "color_family": "blue",
        "fit": "slim"
      },
      "origin": {
        "siteUrl": "https://ca.stussy.com",
        "brandId": "ca-stussy-com",
        "brand": "Stussy"
      }
    }
  ]
}
```

## 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 request body could not be parsed as JSON. |
| 400 | Request body must be a JSON object. | The request body is empty, or parsed to something other than a JSON object. |
| 404 | No active datasets found for this organization. Activate one via the dashboard (Dataset → Settings) or through the REST API before proceeding. | None of your datasets is active. |
| 422 | itemIds must be a non-empty array. | `itemIds` is missing, not an array, or empty. |
| 422 | itemIds can hold at most 50 ids. | `itemIds` holds more than 50 ids. |
| 422 | Each itemId must be a non-empty string. | An entry of `itemIds` is not a string, is blank, or is longer than 100 characters. |
| 422 | country must be a two-letter ISO 3166 country code, such as 'US'. | `country` was sent (non-null) but is not a string of exactly two letters. |

### 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 | This key does not have the "<scope>" scope. | A private-key endpoint was called with a key whose scopes do not include the one the endpoint needs. Nothing is counted against your quota. See Authentication. |
| 403 | This key's scopes do not cover this endpoint. | A key with scopes called a private endpoint that no scope grants yet. Give the key every scope to reach it. |
| 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. |
