# Size products for the shopper

`POST https://stylor.ai/api/v1/shoppers/me/size`

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

The size to buy in each product, from the sizes the shopper wears at
other brands ("30 in Levi's jeans", "M at Uniqlo"), their body
measurements, or both. It is the same sizing the Stylor chat agent
uses, so an app and the chat give the same answer.

## How a size is worked out

1. Each size worn elsewhere is read against that brand's published
   size chart (Stylor holds about 90 brands') as a range of each body
   measurement: who that size is made for. Several sizes narrow it;
   sizes that disagree widen it and lower the confidence.
2. Body measurements, when sent or kept, replace what the sizes
   suggest for those measurements.
3. The result is laid on the product's own store's size guide, read
   from the store's pages, by what decides that garment (the chest for
   a top, the waist and hips for a bottom). A waist worn as a number
   sizes a pant sold by the waist directly. Between two sizes, the
   shopper's `fit` decides, then how the product runs and its cut.

A size the shopper wears at the product's own store is their size
there, with nothing to convert. A store with no size guide we can read
gets the label they wear, as a hedged guess.

## What is read

What you send is laid over what the profile keeps, yours winning: a
size for the same brand and kind replaces the kept one, and each
measurement sent replaces the kept one. Send no token to size from the
request alone. With `save: true` what you sent is kept on the profile
(as [remember](https://stylor.ai/guides/rest/v1/remember-for-shopper.md) would); without a
token a profile is made for it and its `token` returned.

Each product is sized under its store's own rules: a brand the store
chose to leave out is not read, and a store that turned sizes from
other brands off is sized from measurements only.

## Reading the answer

`confidence` is `certain` when the sizes agree or the measurements
decide it: say the size plainly. `hedged` names its one reason in
`basis` (between sizes, their sizes disagree, one weak source, no
chart): say it as a best guess. Between two sizes, `options` holds
both, each with what it gives. `inStock` is whether the recommended
size is in stock now.

When there is nothing to size from (no sizes or measurements sent or
kept), each result's `size` is `null` and `needs` says what to ask
for: `sizes` and `measurements`, or only `measurements` when the
product's store does not size from other brands.

## Example request

```bash
curl -X POST "https://stylor.ai/api/v1/shoppers/me/size" \
  -H "Authorization: Bearer $STYLOR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "itemIds": [
    "701e5cee-41da-4e91-a688-69e8c8dd5fe3",
    "a342cbe3-965b-4055-8ac3-20f63103cade",
    "35a725c8-23fb-4009-a52a-2a778d2e504a"
  ],
  "sizes": [
    {
      "brand": "Levi'\''s",
      "kind": "jeans",
      "label": "30"
    }
  ]
}'
```

## Headers

- `X-Shopper-Token` (string): The shopper's profile token, from `auth/start`, `auth/verify` or any call that made a profile. Optional here. [example `"eyJ2IjoxLCJ0eXAiOiJwcm9maWxlIiwicCI6IjRmM2Ei…"`]

## Request body

Content type `application/json`, required.

- `itemIds` (array<string>, required): The products to size, as full itemIds. Duplicates are sized once; ids not in your active catalogue are listed in `unknown`. [min items 1; max items 24]
- `sizes` (array<object>): Sizes they wear at other brands (or at the product's own store). Laid over the profile's, one per brand and kind. [max items 8]
  - `brand` (string, required): The brand as they said it. Spelling is forgiven ("levis", "Levi's 501", "h&m").
  - `kind` (string, nullable): What the size is for. Null or omitted when they did not say. [one of `"tops"`, `"jeans"`, `"trousers"`, `"dresses"`, `"outerwear"`, `"suits"`, `null`]
  - `label` (string, required): The size as they said it ("M", "30", "32x32", "US 6", "UK 10", "40R").
  - `fit` (string, nullable): How that size fits them. Null or omitted for "it fits". [one of `"tight"`, `"right"`, `"loose"`, `null`]
- `measurements` (object): Body measurements, laid over the profile's one by one. Same keys and ranges as remember's `body`, without `fit`.
  - `bust` (number): Bust or chest.
  - `waist` (number): Natural waist.
  - `hips` (number): Hips at the widest.
  - `inseam` (number): Inside leg.
  - `height` (number): Height. Kept with `save`; sizes nothing.
  - `unit` (string): The unit of the measurements. Required with any measurement. [one of `"cm"`, `"in"`]
- `fit` (string): How they like things to fit. Decides between two sizes. Else the profile's. [one of `"fitted"`, `"regular"`, `"relaxed"`]
- `department` (string): Whose brand charts to read the sizes on. Else each product's own department. [one of `"womens"`, `"mens"`]
- `country` (string): The shopper's two-letter country, so `inStock` is their market's stock.
- `save` (boolean): Keep the sizes, measurements and fit sent on the profile. Without a token, a profile is made and its `token` returned. [default `false`]

### Examples

#### From a brand size

The size in three pairs of jeans from a Levi's 30.

```json
{
  "itemIds": [
    "701e5cee-41da-4e91-a688-69e8c8dd5fe3",
    "a342cbe3-965b-4055-8ac3-20f63103cade",
    "35a725c8-23fb-4009-a52a-2a778d2e504a"
  ],
  "sizes": [
    {
      "brand": "Levi's",
      "kind": "jeans",
      "label": "30"
    }
  ]
}
```

#### From measurements

Measurements in inches and a fit, kept on the profile.

```json
{
  "itemIds": [
    "a342cbe3-965b-4055-8ac3-20f63103cade",
    "6a6dca06-2e8c-4cb8-b178-4c680c72c385"
  ],
  "measurements": {
    "waist": 32,
    "hips": 40,
    "inseam": 32,
    "height": 70,
    "unit": "in"
  },
  "fit": "relaxed",
  "save": true
}
```

#### From the profile

Sizes and measurements the profile already keeps.

```json
{
  "itemIds": [
    "35a725c8-23fb-4009-a52a-2a778d2e504a"
  ],
  "country": "CA"
}
```

## Responses

### 200

A size per product found, in the order asked.

Content type `application/json`:

- `results` (array<ShopperSizeResult>, required): One per product found, in the order asked.
  - `itemId` (string, required): The product.
  - `name` (string, nullable, required): The product's name.
  - `size` (string, nullable, required): The size to buy, as the product labels it. Null when it could not be sized.
  - `runnerUp` (string, nullable, required): The other size worth considering, on a hedged answer only, and only one in stock.
  - `options` (array<object>): Between two sizes, both, the recommended first, each with what it gives.
    - `size` (string): A size.
    - `gives` (string): What it gives ("for a relaxed fit").
  - `confidence` (string, nullable, required): `certain`: say it plainly. `hedged`: a best guess, for the reason in `basis`. [one of `"certain"`, `"hedged"`, `null`]
  - `basis` (string, nullable, required): Why it is this sure, in a few words (`agree`, `one source`, `measured`, `between sizes`, `one weak source`, `conflict`, `no store chart`, `their size with us`).
  - `reason` (string, nullable, required): One line of why, in the second person, in the store's voice ("Your 30 waist at Levi's: our 30 is your size.").
  - `inStock` (boolean, nullable, required): Whether the recommended size is in stock now. Null with no size.
  - `outside` (boolean): Present and `true` when their measurements are past the largest size this is made in.
  - `needs` (string): Present when the sizes given do not say what this garment's chart needs ("jeans or trousers").
- `unknown` (array<string>): Ids that are not products in your active catalogue. Present only when there are some.
- `unread` (array<object>): Sizes that could not be read (a brand we hold no chart for, a size not on its chart). Present only when there are some.
  - `said` (string): The size as read ("30 at Levi's").
  - `why` (string): Why it was not read.
- `needs` (array<string>): Present when there was nothing to size any product from. What to ask the shopper for. [one of `"sizes"`, `"measurements"`]
- `saved` (boolean): Present when `save` was sent. `true` when something was kept.
- `token` (string): Only when `save` made a profile. Keep it.

Example:

```json
{
  "results": [
    {
      "itemId": "701e5cee-41da-4e91-a688-69e8c8dd5fe3",
      "name": "Straight-Leg Jean — Washed Blue",
      "size": "30",
      "runnerUp": null,
      "confidence": "certain",
      "basis": "one source",
      "reason": "Your 30 waist at Levi's: our 30 is your size.",
      "inStock": true
    },
    {
      "itemId": "35a725c8-23fb-4009-a52a-2a778d2e504a",
      "name": "SLIM JEAN DENIM",
      "size": "30",
      "runnerUp": null,
      "confidence": "certain",
      "basis": "one source",
      "reason": "Your 30 waist at Levi's: our 30 is your size.",
      "inStock": false
    }
  ]
}
```

## 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. |
| 401 | That shopper token is not valid here. Start a profile or sign in again. | `X-Shopper-Token` was sent but cannot be opened with this key. Leave it out to size from the request alone. |
| 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 24 ids. | `itemIds` holds more than 24 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 | sizes must be an array. | `sizes` was sent (non-null) but is not an array. |
| 422 | sizes can hold at most 8 sizes. | `sizes` holds more than 8 entries. |
| 422 | sizes[<i>] must be an object. | An entry of `sizes` is not an object. |
| 422 | sizes[<i>].brand must be a brand name of 1–60 characters. | An entry's `brand` is missing, blank or too long. |
| 422 | sizes[<i>].label must be a size of 1–20 characters, as the brand writes it. | An entry's `label` is missing, blank or too long. |
| 422 | sizes[<i>].kind must be one of tops, jeans, trousers, dresses, outerwear, suits. | An entry's `kind` is another value. |
| 422 | sizes[<i>].fit must be tight, right or loose. | An entry's `fit` is another value. A profile's `small` and `large` are also read, as tight and loose. |
| 422 | measurements.<key> must be between <low>–<high> cm. | A measurement is outside what a person can have (in inches when `unit` is `in`). `measurements` is checked as remember's `body` is, with `measurements.` in each message, and takes no `fit`. |
| 422 | fit must be one of fitted, regular, relaxed. | `fit` is another value. |
| 422 | department must be womens or mens. | `department` is another value. |
| 422 | country must be a two-letter ISO 3166 country code, such as 'US'. | `country` was sent (non-null) but is not two letters. |
| 422 | save must be a boolean. | `save` was sent but is not `true` or `false`. |
| 422 | Validation failed. | More than one field is invalid. `details` lists each, by path; nothing is sized or kept.Body: `{"error":"Validation failed.","details":[{"path":"itemIds","message":"itemIds must be a non-empty array."},{"path":"sizes[0].kind","message":"sizes[0].kind must be one of tops, jeans, trousers, dresses, outerwear, suits."}]}` |

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