# Start a profile

`POST https://stylor.ai/api/v1/shoppers/auth/start`

- 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/start-shopper-profile

Makes an anonymous shopper profile and returns its token. Keep the
token on the device and send it as `X-Shopper-Token` on every other
shopper call.

An anonymous profile belongs to your organization: its token opens it
only with your organization's key. It lasts 180 days from its last use.
Confirming an email (`auth/code`, then `auth/verify`) saves it to that
email, after which it lasts until the shopper deletes it and works with
any organization's key.

You rarely need to call this first. Signals, remember, size with
`save: true` and `auth/code` make a profile themselves when no token is
sent, and return its `token` in the same response. Sent with a valid
token, this returns that profile and a renewed token instead of making
another.

Every shopper endpoint is metered as `api.v1.shoppers`. Until your plan
carries that resource, calls draw on the chat's allowance
(`api.v2.agent.chat`); the `X-RateLimit-Resource` header says which was
charged.

## Example request

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

## 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…"`]

## Responses

### 200

The profile and the token to keep.

Content type `application/json`:

- `token` (string, required): The token to keep and send as `X-Shopper-Token`. Renewed on every call that returns it; good for 400 days.
- `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.

Example:

```json
{
  "token": "eyJ2IjoxLCJ0eXAiOiJwcm9maWxlIiwicCI6IjRmM2Ei…",
  "profile": {
    "saved": false,
    "email": null,
    "styles": [],
    "outfits": [],
    "avoidStyles": [],
    "likes": [],
    "avoids": [],
    "notes": [],
    "sizes": [],
    "body": null,
    "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. The body is optional. |
| 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 is not a profile token, has expired, names a deleted profile, or is another organization's anonymous profile. Drop it and call again without one. |
| 403 | Shopper profiles are switched off for this organization. | The organization turned shopper profiles off in its agent settings. Nothing is made, read or kept. |

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