# Shopper profiles

A shopper profile is what one shopper likes, what they told you, the sizes they wear and their measurements. It is their whole "algorithm": feeds, rankings and sizes read it, and so does the Stylor chat agent. The shopper endpoints let your own app or site build on it with your public key, the same way the Stylor widget does.

There is one engine for everyone. An anonymous shopper and one who saved their profile to an email differ only in how long the profile lasts and where it can be used.

## Two credentials

Every shopper call carries two things, with different jobs.

| | Your public key | The shopper's token |
| --- | --- | --- |
| Header | `Authorization: Bearer sgpt-pk-…` | `X-Shopper-Token: <token>` |
| Says | Which organization is calling | Which shopper this is |
| Decides | Rate limits, quota, and the catalogue feeds, similar and rank choose from | Whose profile is read and changed |

Both are safe on a device. The token opens one profile and nothing else.

Shopper calls are metered as `api.v1.shoppers`. Until your plan carries that resource they draw on the chat's allowance, `api.v2.agent.chat`; the `X-RateLimit-Resource` header on each response says which was charged. If your organization turned shopper profiles off in its agent settings, calls that need a profile fail with `403`, and feed, similar and rank run unpersonalised.

## Getting a token

### Anonymous

Call `POST /v1/shoppers/auth/start` with no token. It makes an anonymous profile and returns `{ token, profile }`. Keep the token on the device and send it on every shopper call from then on.

```bash
curl -X POST https://stylor.ai/api/v1/shoppers/auth/start \
  -H "Authorization: Bearer $STYLOR_PUBLIC_KEY"
```

You often don't need to call it. Recording a signal, remembering something, sizing with `save: true` or asking for a sign-in code all make a profile when no token is sent, and return its `token` in the same response. Until you hold a token, send those calls one at a time: three likes sent at once by a new shopper make three profiles.

An anonymous profile belongs to your organization. Its token opens it only with your organization's key, and it lasts 180 days from its last use.

### Signed in with an email

To keep a profile for good, and to open it on another device or at another store that runs Stylor, the shopper confirms their email with a six-digit code.

1. `POST /v1/shoppers/auth/code` with `{ "email": "maya@example.com" }` and the token you hold. The code is emailed in your store's name and lasts 10 minutes.
2. `POST /v1/shoppers/auth/verify` with `{ "email", "code" }` and the same token.
3. Keep the `token` from the verify response in place of the old one. It is the account's token and works with any organization's key.

If the email already has a profile, what was learnt on this device moves into it and `merged` is `true`. On a new device, start with no token: `auth/code` makes a profile for the code, and `auth/verify` swaps it for the saved one.

### When a token stops working

A `401` with `That shopper token is not valid here. Start a profile or sign in again.` means the token has expired, its profile was deleted, or it is another organization's anonymous profile. Drop it, and either carry on without one or start again. Tokens last 400 days and are renewed whenever a response returns one.

## Signals

`POST /v1/shoppers/me/signals` records what the shopper does: likes, dislikes, clicks, cart adds, purchases, whole looks they loved or passed on, and taste they put in words (`said`). Each is learnt in the same call, so the next feed or ranking already knows.

```json
{
  "signals": [
    { "kind": "like", "itemId": "701e5cee-41da-4e91-a688-69e8c8dd5fe3" },
    { "kind": "look_dislike", "itemIds": ["6a6dca06-…", "35a725c8-…"] },
    { "kind": "said", "text": "minimal earthy workwear", "occasion": "work" }
  ]
}
```

A purchase counts most and a click least; a dislike pushes down only what is close to it. Reactions fade with a 60-day half-life, so taste can move. To take a reaction back (an accidental like, or a heart tapped again), send `{ "kind": "undo", "undoes": "like", "itemId": "…" }`: the latest such reaction is deleted and the taste relearnt without it.

Taste is kept as several styles per garment slot, not one average, so a shopper who likes black streetwear and neutral office wear gets both. `GET /v1/shoppers/me` shows them in words.

## Feed, similar and rank

- `POST /v1/shoppers/me/feed` returns a page of your catalogue for the shopper. Each of their styles gets a share of the page by its strength, near-duplicates are dropped, and one place in ten goes to something new so the profile keeps learning. A new shopper gets a spread to react to. Filter by `slot`, `gender` and `ageGroup`; products that are not worn (homeware, toys) are never included.
- `POST /v1/shoppers/me/similar` returns products like one they picked, nudged by their taste.
- `POST /v1/shoppers/me/rank` puts any list of your products (a category page, your own search results) in the shopper's order.

Feed and similar answer with compact cards: name, price, one image and why it was chosen. To open a product page, read the whole product with [`POST /v1/products`](https://stylor.ai/guides/rest/v1/retrieve-products.md), which returns every variant with its stock, every photo and the store it is from, priced in the shopper's market. All three work without a token, unpersonalised.

## Remembering and forgetting

`POST /v1/shoppers/me/remember` keeps what the shopper tells you:

- `notes`: things that hold beyond one visit ("starting a new office job in March"), each with a `kind`.
- `sizes`: sizes they wear, by brand (`{ "brand": "Levi's", "kind": "jeans", "label": "30", "fit": "right" }`). A size replaces the one kept for the same brand and kind.
- `body`: their measurements, `bust`, `waist`, `hips`, `inseam` and `height`, with a `unit` of `cm` or `in`, and how they like things to fit (`fitted`, `regular` or `relaxed`). Kept in centimetres. A measurement sent replaces the kept one; the rest stay.

Measurements are checked before anything is kept. One outside the range a person can have fails the call with `422` and nothing is written: a waist of `32` sent as `cm` was meant as inches, and kept as given it would size someone else.

`POST /v1/shoppers/me/forget` takes something back: a note or a style by its `id`, every size kept for a brand (`sizeBrand`), or all their measurements (`{ "body": true }`). `DELETE /v1/shoppers/me` deletes the profile and everything learnt from it.

Give shoppers a screen that shows their profile (`GET /v1/shoppers/me`) with a way to forget each part. What was read from their reactions is labelled as such in the response, never as something they said.

## Sizing

`POST /v1/shoppers/me/size` gives the size to buy in up to 24 products. It is the sizing the Stylor chat agent uses, so an app and the chat give the same answer.

It reads the sizes the shopper wears at other brands against those brands' published charts (about 90 brands) for an estimate of their measurements, takes their own measurements over that, and lays the result on each product's own store's size guide.

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

```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
    }
  ]
}
```

- **What is read.** The sizes and measurements in the request, laid over the ones the profile keeps, the request winning. The token is optional: without one, only the request is read. Send `save: true` to keep what you sent on the profile.
- **How sure.** `certain` is said plainly. `hedged` is a best guess for the one reason in `basis` (between sizes, sizes that disagree, a single weak source, no size guide to check against). Between two sizes, `options` gives both, each with what it gives.
- **Stock.** `inStock` says whether the recommended size is in stock now, in the shopper's market when you send `country`.
- **Nothing to go on.** With no sizes or measurements sent or kept, each `size` is `null` and the response's `needs` says what to ask for: `["sizes", "measurements"]`, or only `measurements` when the product's store does not size from other brands.

Each product is sized under its store's own rules: brands the store left out are not read, and a size the shopper wears at the store itself is their size there.

### Asking for sizes: the sizes card

When `needs` asks for sizes, show the same "Which brands do you wear?" card the Stylor chat shows. `POST /v1/shoppers/me/size/card` returns it as data, built by the same code as the chat's:

```json
{ "department": "mens", "garments": ["tshirt", "jeans"], "country": "US" }
```

The answer's `card` holds the rows to ask (`garments`), every brand with its size chips per department and garment (`brands[].sizes["mens:jeans"]`), the bottom sizes that usually go with each top size (`brands[].likely`), the fit choices (`fits`: tight, right, loose), and the sizes the profile already keeps (`saved`) when you send the token. Every field is optional: with no `garments` it asks a t-shirt and jeans, and with no `department` it holds both and you ask womenswear or menswear first. A brand with `logo: true` has a public logo at `/brand-logos/<slug>.png` on the API's host; show logos only while the card's `logos` is `true`.

The card is your organization's, not one store's, so it is built under the default rules: logos on and no brand left out. A store's own settings still apply when its products are sized.

Save what the shopper picks with `POST /v1/shoppers/me/remember`, one row per brand and garment, as the card has them:

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

Remember keeps `tight` and `loose` as the profile's `small` and `large`, and reads the `kind` from `garment` when you leave it out. Then size the products again with no `sizes` in the request: the profile's are read.

## Good practices

- Keep the token in the device's secure storage, and replace it whenever a response returns a new one.
- Record reactions as they happen rather than in a batch at the end of a session.
- Ask for an email only when the shopper wants to keep their profile; everything works anonymously.
- When the shopper asks to delete their profile, call `DELETE /v1/shoppers/me` and drop the token.
