# Create a widget theme

`POST https://stylor.ai/api/v1/widget/presets`

- Authentication: private API key, sent as `Authorization: Bearer sgpt-sk-…::…`
- Rate-limit resource: `api.v1.widget.presets.create`
- Demo key: not accepted
- Web page: https://stylor.ai/guides/rest/v1/create-widget-preset

Saves a new widget theme. Every colour, a name and a launcher label are
required; `roundness` defaults to 100.

## Example request

```bash
curl -X POST "https://stylor.ai/api/v1/widget/presets" \
  -H "Authorization: Bearer $STYLOR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Ranch",
  "buttonLabel": "Ask a stylist",
  "buttonColor": "#2D2926",
  "buttonHoverColor": "#000000",
  "buttonTextColor": "#FFFFFF",
  "typingDotColor": "#999999",
  "accentColor": "#B5651D",
  "accentHoverColor": "#8A4B12",
  "mutedTextColor": "#6B6B6B",
  "primaryTextColor": "#2D2926",
  "pageColor": "#FBF8F3",
  "surfaceColor": "#FFFFFF",
  "borderColor": "#E6E1DA",
  "inputColor": "#F3EFE9",
  "footerColor": "#FBF8F3",
  "roundness": 60
}'
```

## Request body

Content type `application/json`, required.

- `name` (string, required): Shown in the theme builder. At most 100 characters. [max length 100]
- `buttonLabel` (string, required): The launcher button's text. At most 40 characters. [max length 40]
- `buttonColor` (string, required): Launcher button background. A hex colour, like `#FFFFFF` or `#FFF`, with or without alpha.
- `buttonHoverColor` (string, required): Launcher button background on hover. A hex colour, like `#FFFFFF` or `#FFF`, with or without alpha.
- `buttonTextColor` (string, required): Launcher button text. A hex colour, like `#FFFFFF` or `#FFF`, with or without alpha.
- `typingDotColor` (string, required): Typing indicator dots. A hex colour, like `#FFFFFF` or `#FFF`, with or without alpha.
- `accentColor` (string, required): Selected options, links and emphasis. A hex colour, like `#FFFFFF` or `#FFF`, with or without alpha.
- `accentHoverColor` (string, required): Accent on hover. A hex colour, like `#FFFFFF` or `#FFF`, with or without alpha.
- `mutedTextColor` (string, required): Labels, placeholders and secondary text. A hex colour, like `#FFFFFF` or `#FFF`, with or without alpha.
- `primaryTextColor` (string, required): What the stylist says, product names, prices. A hex colour, like `#FFFFFF` or `#FFF`, with or without alpha.
- `pageColor` (string, required): The background the conversation sits on. A hex colour, like `#FFFFFF` or `#FFF`, with or without alpha.
- `surfaceColor` (string, required): Product cards, look panels and question cards. A hex colour, like `#FFFFFF` or `#FFF`, with or without alpha.
- `borderColor` (string, required): Card outlines and dividers. A hex colour, like `#FFFFFF` or `#FFF`, with or without alpha.
- `inputColor` (string, required): The shopper's own messages and the message box. A hex colour, like `#FFFFFF` or `#FFF`, with or without alpha.
- `footerColor` (string, required): The strip behind the message box. A hex colour, like `#FFFFFF` or `#FFF`, with or without alpha.
- `roundness` (integer): Corner roundness for the whole widget, 0 (square) to 100 (as shipped). [default `100`; minimum 0; maximum 100]

### Examples

#### Warm ranch theme

A full theme in warm neutrals with softer corners.

```json
{
  "name": "Ranch",
  "buttonLabel": "Ask a stylist",
  "buttonColor": "#2D2926",
  "buttonHoverColor": "#000000",
  "buttonTextColor": "#FFFFFF",
  "typingDotColor": "#999999",
  "accentColor": "#B5651D",
  "accentHoverColor": "#8A4B12",
  "mutedTextColor": "#6B6B6B",
  "primaryTextColor": "#2D2926",
  "pageColor": "#FBF8F3",
  "surfaceColor": "#FFFFFF",
  "borderColor": "#E6E1DA",
  "inputColor": "#F3EFE9",
  "footerColor": "#FBF8F3",
  "roundness": 60
}
```

## Responses

### 201

The saved theme.

Content type `application/json`:

- `preset` (WidgetPreset, required): A saved widget theme, as stored.
  - `id` (string (uuid), required): Pass it to the embed, or to preflight, as `themeId`.
  - `name` (string, required): Shown in the theme builder.
  - `options` (object, required): The colours, grouped as stored.
    - `button` (object): The launcher button.
      - `label` (string): The button's text.
      - `color` (string): Background.
      - `hoverColor` (string): Background on hover.
      - `textColor` (string): Text.
    - `widget` (object): The chat: `typingDotColor`, `accentColor`, `accentHoverColor`, `mutedTextColor`, `primaryTextColor`, `pageColor`, `surfaceColor`, `borderColor`, `inputColor`, `footerColor` (hex) and `roundness` (0 to 100).
  - `created` (string (date-time), required): When it was saved first.
  - `modified` (string (date-time), required): When it was last changed.

Example:

```json
{
  "preset": {
    "id": "7e6d5c4b-3a2f-4e1d-9c8b-7a6f5e4d3c2b",
    "name": "Ranch",
    "options": {
      "button": {
        "label": "Ask a stylist",
        "color": "#2D2926",
        "hoverColor": "#000000",
        "textColor": "#FFFFFF"
      },
      "widget": {
        "typingDotColor": "#999999",
        "accentColor": "#B5651D",
        "accentHoverColor": "#8A4B12",
        "mutedTextColor": "#6B6B6B",
        "primaryTextColor": "#2D2926",
        "pageColor": "#FBF8F3",
        "surfaceColor": "#FFFFFF",
        "borderColor": "#E6E1DA",
        "inputColor": "#F3EFE9",
        "footerColor": "#FBF8F3",
        "roundness": 60
      }
    },
    "created": "2026-10-04T18:20:03.001Z",
    "modified": "2026-10-04T18:20:03.001Z"
  }
}
```

## 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. | The body is not parseable JSON. |
| 400 | Request body must be a JSON object. | The body is empty, an array, or a JSON primitive. |
| 422 | Validation failed. | More than one field is missing or invalid. `details` names each one.Body: `{"error":"Validation failed.","details":[{"path":"pageColor","message":"pageColor is required."},{"path":"accentColor","message":"Accent colour must be a hex colour, like"}]}` |
| 422 | name is required. | `name` is missing and nothing else is wrong. Each field has its own message, the same as in `details`. |

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