# Widget preflight

`GET https://stylor.ai/api/v1/widget/preflight`

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

Tells an embed whether the widget can be shown for the organization
behind the public key, and hands back everything it needs to do so: the
resolved theme colours and an analytics session token. The Stylor embed
script calls this once per page load; call it yourself if you host your
own launcher.

## Readiness

`ready` is `true` when the organization has at least one active
dataset. When you pass `datasetIds`, readiness is judged on those
datasets instead: `ready` is `true` if at least one of them belongs to
your organization and is not archived, so a scope made only of draft
datasets is ready. This is the same rule search applies to an explicit
`datasetIds`. Otherwise `ready` is `false` with a `reason`, the HTTP
status is still `200`, and no session is issued — there is nothing to
measure if the widget is not shown. The theme is returned in both cases.

## Theme

The flat `theme` object holds the colours and launcher label the widget
should render with. It is resolved in this order: the preset named by
`themeId` if it belongs to your organization; otherwise your most
recently modified preset; otherwise Stylor's defaults. An unknown
`themeId` is not an error — it silently falls through to the next rule.
Any colour a preset leaves unset falls back to the default for that key.

## Analytics session

`session.token` is the credential for the events endpoint. Send it as
the `X-Stylor-Session` header on every `POST /v1/events`. It is bound to
your organization and, when the request carried an `Origin` header, to
that storefront origin. It expires after `expiresIn` seconds (two
hours); calling preflight again issues a fresh one.

Pass the previous `session.sessionId` back as `sid` on the next page
load so a shopper browsing several pages stays one session. A `sid`
that is not a UUID is ignored and a new id is minted. `session` is
`null` when the server cannot sign tokens; the widget still loads, it
simply sends no analytics.

## CORS

The route answers `OPTIONS` preflight with `204` and allows any
origin, so it can be called directly from a storefront page.

## Example request

```bash
curl -X GET "https://stylor.ai/api/v1/widget/preflight" \
  -H "Authorization: Bearer $STYLOR_API_KEY"
```

## Query parameters

- `themeId` (string): Id of a widget theme preset belonging to your organization. Falls back to the most recently modified preset, then to defaults, when absent or unknown. [example `"7e6d5c4b-3a2f-4e1d-9c8b-7a6f5e4d3c2b"`]
- `datasetIds` (string): Comma-separated dataset ids the widget is scoped to. When present, readiness counts these datasets (draft included, archived excluded) instead of the organization's active datasets. At most 50 ids are read. [example `"ds_1a2b3c,ds_4d5e6f"`]
- `sid` (string (uuid)): The `session.sessionId` from an earlier preflight in the same browser tab, so navigation within the store stays one visit. Must be a UUID; anything else is ignored and a new session id is minted. [example `"4d2a9c1e-7b3f-4e8a-9c6d-1f2e3a4b5c6d"`]

## Headers

- `Origin` (string): Set automatically by browsers. When present it is recorded in the session token, and later event batches from a different origin are refused. [example `"https://shop.example.com"`]

## Responses

### 200

Readiness, theme and (when ready) the analytics session.

Content type `application/json`:

- `ready` (boolean, required): `true` when the organization has at least one active dataset and the widget may be shown. [example `true`]
- `reason` (string): Only present when `ready` is `false`. Currently always `No active datasets found for this organization.` [example `"No active datasets found for this organization."`]
- `theme` (WidgetTheme, required): Flat set of colours (CSS hex) and the launcher label. Every key is always present; unset preset values fall back to the defaults shown.
  - `buttonBg` (string, required): Launcher button background. [default `"#2D2926"`]
  - `buttonHover` (string, required): Launcher button background on hover. [default `"#231F1C"`]
  - `buttonText` (string, required): Launcher button text colour. [default `"#FFFFFF"`]
  - `buttonLabel` (string, required): Launcher button label. [default `"Talk to stylist"`]
  - `typingDot` (string, required): Colour of the typing indicator dots. [default `"#2D2926"`]
  - `accent` (string, required): Primary accent inside the panel. [default `"#2D2926"`]
  - `accentHover` (string, required): Accent on hover. [default `"#4B4641"`]
  - `textMuted` (string, required): Secondary text colour. [default `"#434656"`]
  - `textPrimary` (string, required): Primary text colour. [default `"#1B1C1A"`]
  - `bgPage` (string, required): Panel page background. [default `"#FFFFFF"`]
  - `bgSurface` (string, required): Card and surface background. [default `"#FBF9F5"`]
  - `border` (string, required): Border colour. [default `"#C4C5D9"`]
  - `inputBg` (string, required): Message input background. [default `"#F5F3EF"`]
  - `footerBg` (string, required): Panel footer background. [default `"#FFFFFF"`]
- `session` (WidgetSession | null, nullable): Present only when `ready` is `true`. `null` when the server cannot issue tokens; the widget should then send no analytics.
  - Option 1: WidgetSession
    - `token` (string, required): Signed session token. Send as `X-Stylor-Session` on `POST /v1/events`. [example `"eyJ2IjoxLCJzaWQiOiI0ZDJhOWMxZS03YjNmLTRlOGEtOWM2ZC0xZjJlM2E0YjVjNmQiLCJvcmciOiIw..."`]
    - `sessionId` (string (uuid), required): The session id, lowercase. Equal to `sid` when a valid one was passed, otherwise newly minted. Pass it back as `sid` on subsequent page loads. [example `"4d2a9c1e-7b3f-4e8a-9c6d-1f2e3a4b5c6d"`]
    - `expiresIn` (integer, required): Seconds until the token expires. Always `7200`. [example `7200`]
  - Option 2: null

Example:

```json
{
  "ready": true,
  "theme": {
    "buttonBg": "#2D2926",
    "buttonHover": "#231F1C",
    "buttonText": "#FFFFFF",
    "buttonLabel": "Talk to stylist",
    "typingDot": "#2D2926",
    "accent": "#2D2926",
    "accentHover": "#4B4641",
    "textMuted": "#434656",
    "textPrimary": "#1B1C1A",
    "bgPage": "#FFFFFF",
    "bgSurface": "#FBF9F5",
    "border": "#C4C5D9",
    "inputBg": "#F5F3EF",
    "footerBg": "#FFFFFF"
  },
  "session": {
    "token": "eyJ2IjoxLCJzaWQiOiI0ZDJhOWMxZS03YjNmLTRlOGEtOWM2ZC0xZjJlM2E0YjVjNmQiLCJvcmciOiIw...",
    "sessionId": "4d2a9c1e-7b3f-4e8a-9c6d-1f2e3a4b5c6d",
    "expiresIn": 7200
  }
}
```

## Errors

Every error is a JSON object with an `error` string. Match on the status code; the message is written for people.

This endpoint has no errors of its own.

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