# A feed for the shopper

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

- 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-feed

A page of your catalogue chosen for the shopper. Each of their styles
gets a share of the page by how strong it is, near-duplicates are
dropped, and one place in ten goes to something new, never near
something they passed on. Only products in stock are chosen, and
never products that are not worn (slot `non fashion`: homeware, toys,
gift cards). Send `ageGroup: adult` on an adult shopper's feed when
your catalogue also sells children's clothes.

With no token, or a profile that has learnt nothing yet, the feed is a
spread across the catalogue to react to (`reason` says so). When your
organization has shopper profiles off, the token is not read and the
feed is always that spread.

Items are compact cards. Open one with
[Retrieve products](https://stylor.ai/guides/rest/v1/retrieve-products.md) for its
variants and photos.

## Example request

```bash
curl -X POST "https://stylor.ai/api/v1/shoppers/me/feed" \
  -H "Authorization: Bearer $STYLOR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "slot": "top",
  "gender": "womens",
  "ageGroup": "adult",
  "exclude": [
    "701e5cee-41da-4e91-a688-69e8c8dd5fe3",
    "a342cbe3-965b-4055-8ac3-20f63103cade"
  ],
  "limit": 24,
  "country": "US"
}'
```

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

- `slot` (string): Only this garment slot, as products carry it in `metadata.slot` (`top`, `bottom`, `one-piece`, `outerwear`, `shoes`, …).
- `gender` (string): Only this department, plus unisex products. [one of `"womens"`, `"mens"`]
- `ageGroup` (string): Only this age group, as products carry it in `metadata.age_group`. `adult` keeps products with no age group (labelled before age groups existed), as search does. [one of `"adult"`, `"kids"`, `"baby"`]
- `occasion` (string): Leans the feed toward styles they reacted to for this occasion (the `occasion` sent with signals).
- `exclude` (array<string>): itemIds not to show again (already rated or seen). Only the first 1000 are read. [max items 1000]
- `limit` (integer): Products to return. Clamped to 1–60. [default `24`; minimum 1; maximum 60]
- `country` (string): The shopper's two-letter country. Prices are in their market, and products their market does not sell are left out.

### Examples

#### Tops

A page of womenswear tops, skipping two already seen.

```json
{
  "slot": "top",
  "gender": "womens",
  "ageGroup": "adult",
  "exclude": [
    "701e5cee-41da-4e91-a688-69e8c8dd5fe3",
    "a342cbe3-965b-4055-8ac3-20f63103cade"
  ],
  "limit": 24,
  "country": "US"
}
```

## Responses

### 200

The feed, in order.

Content type `application/json`:

- `items` (array<ShopperProductCard>, required)
  - `itemId` (string): The product. Open it with Retrieve products.
  - `name` (string, nullable): The product's name.
  - `url` (string, nullable): The product's page on its store.
  - `price` (number, nullable): Price in `currency`, in the shopper's market when `country` was sent.
  - `currency` (string, nullable): ISO currency code.
  - `compareAtPrice` (number, nullable): The price before a sale, when on sale.
  - `image` (string, nullable): A 600 px wide WebP of the product's best photo.
  - `slot` (string, nullable): The garment slot.
  - `colour` (string, nullable): Its colour family.
  - `score` (number): How well it fits their taste, or how alike it is for similar.
  - `reason` (string, nullable): Why it is here, in a short sentence the shopper can read.
  - `style` (string, nullable): The id of the style it was chosen for, from `profile.styles`. Null for something new.

Example:

```json
{
  "items": [
    {
      "itemId": "35a725c8-23fb-4009-a52a-2a778d2e504a",
      "name": "SLIM JEAN DENIM",
      "url": "https://ca.stussy.com/products/slim-jean-denim",
      "price": 195,
      "currency": "CAD",
      "compareAtPrice": null,
      "image": "https://stylor.ai/api/v1/image/v1/datasets/images/598fb45a.jpeg?resize=600x&format=webp",
      "slot": "bottom",
      "colour": "blue",
      "score": 0.41,
      "reason": "Fits your dark, slim, jeans.",
      "style": "3f9a1c2e"
    }
  ]
}
```

## 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. Leave it out for an unpersonalised feed. |
| 422 | ageGroup must be one of adult, kids, baby. | `ageGroup` was sent (non-null) but is another value. |

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