# Keep notes, sizes, measurements

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

- 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/remember-for-shopper

Keeps what the shopper told you for next time: notes about them, the
sizes they wear at other brands, and their body measurements. The chat
agent and the size endpoint read all three.

A note already kept (the same words) is not added twice. A size
replaces the one kept for the same brand, slot and kind. A measurement
replaces the one kept, and the measurements not sent are kept. A
profile keeps the latest 30 notes and 40 sizes.

Measurements are checked before anything is kept: one outside the
range a person can have (a waist of 32 sent as `cm` was meant as
inches) fails the whole call with `422`, and nothing is written.
They are kept in centimetres whatever unit you send.

Without a token, a profile is made and its `token` is returned.

## Example request

```bash
curl -X POST "https://stylor.ai/api/v1/shoppers/me/remember" \
  -H "Authorization: Bearer $STYLOR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "notes": [
    {
      "text": "Starting a new office job in March",
      "kind": "occasion"
    }
  ],
  "sizes": [
    {
      "brand": "Levi'\''s",
      "kind": "jeans",
      "slot": "bottom",
      "label": "30",
      "fit": "right"
    },
    {
      "brand": "Uniqlo",
      "kind": "tops",
      "slot": "top",
      "label": "M",
      "fit": "small"
    }
  ]
}'
```

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

- `notes` (array<object>): Things that hold beyond one visit. Each is trimmed to 280 characters.
  - `text` (string, required): The note, in a sentence. [max length 280]
  - `kind` (string): What the note is about. Anything else is kept as `other`. [default `"other"`; one of `"about"`, `"likes"`, `"avoids"`, `"occasion"`, `"budget"`, `"other"`]
- `sizes` (array<ShopperKeptSizeInput>): Sizes they wear, by brand. An entry without `brand` or `label` is skipped. Takes the [sizes card](https://stylor.ai/guides/rest/v1/shopper-sizes-card.md)'s rows as they are: `fit` as the card says it (`tight`, `right`, `loose`) or as the profile keeps it (`small`, `right`, `large`), and a `garment` for the `kind`.
  - `brand` (string, required): The brand or store, as the shopper said it ("Levi's", "Uniqlo"). [max length 60]
  - `kind` (string, nullable): What the size is for. Jeans and trousers share the bottom slot but are kept apart. Missing or unknown, it is read from `garment`. [one of `"tops"`, `"jeans"`, `"trousers"`, `"dresses"`, `"outerwear"`, `"suits"`, `null`]
  - `garment` (string, nullable): The card's row it was picked in. Gives the `kind` when none is sent (a t-shirt is `tops`, a suit jacket `suits`); not kept itself. [one of `"tshirt"`, `"shirt"`, `"top"`, `"jacket"`, `"suit"`, `"jeans"`, `"trousers"`, `"shorts"`, `"leggings"`, `"skirt"`, `"dress"`, `null`]
  - `slot` (string, nullable): The garment slot (`top`, `bottom`, `one-piece`, `outerwear`). Missing, it comes from the kind.
  - `label` (string, required): The size as the brand writes it ("M", "30", "32x32", "UK 10"). [max length 20]
  - `fit` (string): How that size fits them. `tight` is kept as `small`, `loose` as `large`. Anything else is kept as `right`. [default `"right"`; one of `"right"`, `"small"`, `"large"`, `"tight"`, `"loose"`]
- `body` (ShopperBodyInput): Body measurements to keep, in `unit`. Each must be a number in the range a person can have: bust 60–180, waist 45–170, hips 65–180, inseam 50–100, height 120–230 cm. Kept in centimetres.
  - `bust` (number): Bust or chest.
  - `waist` (number): Natural waist.
  - `hips` (number): Hips at the widest.
  - `inseam` (number): Inside leg.
  - `height` (number): Height.
  - `unit` (string): The unit of the measurements. Required with any measurement. [one of `"cm"`, `"in"`]
  - `fit` (string): How they like things to fit. [one of `"fitted"`, `"regular"`, `"relaxed"`]

### Examples

#### Sizes and a note

A size at two brands and a note about an occasion.

```json
{
  "notes": [
    {
      "text": "Starting a new office job in March",
      "kind": "occasion"
    }
  ],
  "sizes": [
    {
      "brand": "Levi's",
      "kind": "jeans",
      "slot": "bottom",
      "label": "30",
      "fit": "right"
    },
    {
      "brand": "Uniqlo",
      "kind": "tops",
      "slot": "top",
      "label": "M",
      "fit": "small"
    }
  ]
}
```

#### From the sizes card

The sizes card's rows as they are, one per brand and garment.

```json
{
  "sizes": [
    {
      "brand": "Uniqlo",
      "garment": "tshirt",
      "kind": "tops",
      "label": "M",
      "fit": "tight"
    },
    {
      "brand": "Levi's",
      "garment": "jeans",
      "kind": "jeans",
      "label": "32",
      "fit": "loose"
    }
  ]
}
```

#### Measurements

Body measurements in inches and a fit preference.

```json
{
  "body": {
    "waist": 30,
    "hips": 38.5,
    "inseam": 31,
    "height": 66,
    "unit": "in",
    "fit": "relaxed"
  }
}
```

## Responses

### 200

Kept. The profile as it now is.

Content type `application/json`:

- `profile` (ShopperProfile, required): A profile in words. Never the vectors behind it.
  - `saved` (boolean, required): `true` once the shopper confirmed an email: the profile is theirs, lasts until they delete it, and works at every store that runs Stylor.
  - `email` (string, nullable, required): The confirmed email. Null on an anonymous profile.
  - `styles` (array<object>, required): Styles learnt from their reactions, strongest first. At most four per slot.
    - `id` (string): Send as `styleId` to forget it.
    - `name` (string): The style in words, from the labels of what they liked ("black, relaxed, t shirt").
    - `slot` (string, nullable): The garment slot; null for taste they put in words.
    - `strength` (number): How much it weighs now. Reactions fade with a 60-day half-life.
  - `outfits` (array<object>, required): Kinds of whole look they liked, strongest first.
    - `id` (string): The outfit style's id.
    - `name` (string): Its traits in words ("all neutrals, separates").
    - `strength` (number): How much it weighs now.
  - `avoidStyles` (array<object>, required): Styles they passed on often enough to say.
    - `id` (string): The style's id.
    - `name` (string): The style in words.
    - `slot` (string, nullable): The garment slot.
  - `likes` (array<string>, required): Labels they clearly lean toward, read from their reactions, never said by them ("colour: black"). At most 8.
  - `avoids` (array<string>, required): Labels they clearly lean away from. At most 8.
  - `notes` (array<object>, required): What they told you, kept with remember.
    - `id` (string): Send as `noteId` to forget it.
    - `text` (string): The note.
    - `kind` (string): What it is about. [one of `"about"`, `"likes"`, `"avoids"`, `"occasion"`, `"budget"`, `"other"`]
  - `sizes` (array<ShopperKeptSize>, required): Sizes they wear, by brand or store.
    - `brand` (string, required): The brand or store, as the shopper said it ("Levi's", "Uniqlo"). [max length 60]
    - `kind` (string, nullable): What the size is for. Jeans and trousers share the bottom slot but are kept apart. [one of `"tops"`, `"jeans"`, `"trousers"`, `"dresses"`, `"outerwear"`, `"suits"`, `null`]
    - `slot` (string, nullable): The garment slot (`top`, `bottom`, `one-piece`, `outerwear`).
    - `label` (string, required): The size as the brand writes it ("M", "30", "32x32", "UK 10"). [max length 20]
    - `fit` (string): How that size fits them. `small` is tight, `large` is loose. [default `"right"`; one of `"right"`, `"small"`, `"large"`]
  - `body` (ShopperBody | null, required): Their body measurements in centimetres, and how they like things to fit. Null when none are kept.
    - Option 1: ShopperBody
      - `bust` (number): Bust or chest, cm.
      - `waist` (number): Waist, cm.
      - `hips` (number): Hips, cm.
      - `inseam` (number): Inside leg, cm.
      - `height` (number): Height, cm. Kept for you; no size chart we read sizes by height.
      - `fit` (string): How they like things to fit. Decides between two sizes. [one of `"fitted"`, `"regular"`, `"relaxed"`]
      - `unit` (string, required): Always `cm`. [one of `"cm"`]
    - Option 2: null
  - `signals` (integer, required): Reactions learnt from.
- `token` (string): Only when no token was sent and this call made the profile.

Example:

```json
{
  "profile": {
    "saved": false,
    "email": null,
    "styles": [],
    "outfits": [],
    "avoidStyles": [],
    "likes": [],
    "avoids": [],
    "notes": [],
    "sizes": [],
    "body": {
      "waist": 76.2,
      "hips": 97.8,
      "inseam": 78.7,
      "height": 167.6,
      "fit": "relaxed",
      "unit": "cm"
    },
    "signals": 0
  }
}
```

## 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. | A body was sent and could not be parsed as JSON. |
| 400 | Request body must be a JSON object. | A body was sent and 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. |
| 403 | Shopper profiles are switched off for this organization. | The organization turned shopper profiles off. |
| 404 | No such profile. | The profile was deleted during the call. |
| 422 | body must be an object. | `body` was sent but is not an object. |
| 422 | body must hold at least one measurement or a fit. | `body` is an empty object. |
| 422 | body.<key> is not a measurement we keep. Allowed: bust, waist, hips, inseam, height, unit, fit. | `body` holds a key that is not one of those. |
| 422 | body.unit must be "cm" or "in". | `body` holds a measurement and `unit` is missing or another value. |
| 422 | body.<key> must be a number. | A measurement is not a number (for example the string `"76"`). |
| 422 | body.<key> must be between <low>–<high> cm. | A measurement is outside what a person can have: bust 60–180, waist 45–170, hips 65–180, inseam 50–100 and height 120–230 cm. In inches when `unit` is `in`. |
| 422 | body.fit must be one of fitted, regular, relaxed. | `body.fit` is another value. |
| 422 | Validation failed. | `body` has more than one problem. `details` lists each, by path.Body: `{"error":"Validation failed.","details":[{"path":"body.unit","message":"body.unit must be \"cm\" or \"in\"."},{"path":"body.waist","message":"body.waist must be a number."}]}` |

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