# Create outfit compositions

`POST https://stylor.ai/api/v1/outfit/compositions`

- Authentication: public API key, sent as `Authorization: Bearer sgpt-pk-…`
- Rate-limit resource: `api.v1.outfit.compositions`
- Demo key: accepted
- Streaming: Server-Sent Events when the body has `"stream": true`
- Deprecated: yes
- Web page: https://stylor.ai/guides/rest/v1/create-outfit-compositions

> **Retired.** This endpoint answers `410 Gone` for every API caller.
> It is documented for reference only; new integrations should render
> looks through the v2 agent API instead.

Takes the products the stylist recommended in a chat and turns them into
complete outfits: a language model picks coherent combinations from the
most recent recommendation, then an image model renders each combination
as a single composed look (optionally on the shopper's own photo).

## What it needs from the chat

The chat identified by `chatId` must belong to your organization and its
most recent assistant message must contain at least one search slot with
results — that is what "the most recent recommendation" means. Slots from
earlier turns are also passed to the model as context and may be drawn
on, but the latest turn is the primary source. Up to five products per
slot are considered.

## How many outfits you get

`count` asks for 1–10 outfits. It is then clamped to what the latest
recommendation can actually support: the product of
`min(results in slot, 3)` over the latest slots, capped at 10 and never
below 1. A single-slot recommendation with two results therefore yields
at most two outfits however large `count` was. Individual renders can
fail; those are dropped, so the response may contain fewer outfits than
the clamped count. The request only fails when *none* succeeds.

## Images

Each render is uploaded to Stylor's image storage and returned as an
`imageUrl`. When the upload fails the image is returned inline instead as
a compressed JPEG data URL (`imageSource: compressed`), and if even that
fails, as the original PNG data URL (`imageSource: original`). Renders
use the `9:16` aspect ratio by default; override it through
`preferences.aspectRatio`. At most three product photos (two when a user
photo is supplied) are handed to the image model; the remaining products
are described in text.

## JSON vs. streaming

By default the call blocks until every outfit has been selected and
rendered, then returns everything at once. Send `stream: true` to receive
a `text/event-stream` instead: each outfit is announced as soon as the
model has chosen it, its image follows as soon as it is rendered, and a
`done` event carries the summary. Validation and quota errors are still
returned as JSON with the usual status code before the stream opens;
failures after that arrive as `error` events on the stream with HTTP
status `200`.

## Persistence

Every successful outfit is stored with a `compositionId` (the `id` in
the response) under your organization and the chat. When `slotId` is
given, the ids are also written to that slot's `compositions` list in the
chat, so they can be retrieved with the conversation. A slot update that
fails does not fail the request.

## Example request

```bash
curl -X POST "https://stylor.ai/api/v1/outfit/compositions" \
  -H "Authorization: Bearer $STYLOR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "chatId": "8b4d3f6e-2c1a-4f0e-9d7b-5a6c1e2f3a4b",
  "count": 3
}'
```

## Request body

Content type `application/json`, required.

- `chatId` (string, required): Id of a chat owned by your organization whose latest assistant message contains search slots with results. [example `"8b4d3f6e-2c1a-4f0e-9d7b-5a6c1e2f3a4b"`]
- `count` (integer): Outfits to generate. Parsed as an integer (`"3"` and `3.7` both become `3`); values outside 1–10 are rejected. Then clamped to the number of distinct combinations the latest recommendation supports. [default `5`; minimum 1; maximum 10; example `3`]
- `userPhotoUrl` (string): Optional photo of the shopper to dress. A `data:image/…;base64,…` URL or a publicly fetchable `https` image URL. When present, the render composes the outfit on this person and only two product photos (preferring flat-lay shots) are passed to the image model. [example `"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD..."`]
- `preferences` (OutfitPreferences): Hints passed to the models. Stored alongside each composition. Unknown keys are kept but not read.
  - `gender` (string): Presented to the outfit-selection model as the shopper's gender. Free text; not validated. [example `"women"`]
  - `style` (string): Free-text style direction for the outfit-selection model (for example `relaxed weekend brunch`). [example `"relaxed weekend brunch"`]
  - `aspectRatio` (string): Aspect ratio of the rendered image, as accepted by the image model (for example `9:16`, `1:1`, `3:4`). [default `"9:16"`; example `"1:1"`]
- `slotId` (string): Optional id of a slot in the chat's latest assistant message. The generated composition ids are written to that slot's `compositions` list. [example `"3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9"`]
- `stream` (boolean): Send `true` to receive a `text/event-stream` instead of a single JSON body. [default `false`; example `false`]

### Examples

#### Three outfits

Compose outfits from the chat's latest recommendation.

```json
{
  "chatId": "8b4d3f6e-2c1a-4f0e-9d7b-5a6c1e2f3a4b",
  "count": 3
}
```

#### Streaming

Get each outfit as soon as it is rendered.

```json
{
  "chatId": "8b4d3f6e-2c1a-4f0e-9d7b-5a6c1e2f3a4b",
  "count": 5,
  "stream": true,
  "slotId": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9"
}
```

#### Shopper photo

Render onto the shopper's own photo in a square frame.

```json
{
  "chatId": "8b4d3f6e-2c1a-4f0e-9d7b-5a6c1e2f3a4b",
  "count": 2,
  "userPhotoUrl": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD...",
  "preferences": {
    "gender": "womens",
    "style": "relaxed weekend brunch",
    "aspectRatio": "1:1"
  }
}
```

## Responses

### 200

In JSON mode, every outfit that rendered successfully plus a summary. In streaming mode (`stream: true`) the body is a `text/event-stream`; see the stream events below.

Content type `application/json`:

- `visualizations` (array<OutfitVisualization>, required): One entry per outfit that rendered successfully.
  - `id` (string (uuid), required): Composition id. Also stored on the chat slot when `slotId` was supplied. [example `"a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"`]
  - `imageUrl` (string, required): Where to load the rendered image. An `https://stylor.ai/api/v1/image/…` URL when `imageSource` is `cdn`; otherwise a `data:image/…;base64,…` URL. [example `"https://stylor.ai/api/v1/image/images/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d.png"`]
  - `imageSource` (string, required): `cdn` — uploaded and served by URL; `compressed` — upload failed, inline JPEG (max 800px wide, quality 80); `original` — compression also failed, inline original PNG. [one of `"cdn"`, `"compressed"`, `"original"`; example `"cdn"`]
  - `imageData` (string): JSON mode only. The uncompressed render as a `data:image/…;base64,…` URL, always included regardless of `imageSource`. This makes JSON responses large; prefer `imageUrl` and consider streaming mode, which omits this field. [example `"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."`]
  - `reasoning` (string, required): Same text as `combination.reasoning`. [example `"The cream knit and straight-leg jeans share a relaxed silhouette."`]
  - `combination` (OutfitCombination, required)
    - `name` (string, required): Short style name for the outfit. [example `"Weekend Neutrals"`]
    - `description` (string, required): One or two sentences describing the look. [example `"A soft, tonal outfit for a relaxed brunch."`]
    - `reasoning` (string, required): Why these products work together. [example `"The cream knit and straight-leg jeans share a relaxed silhouette, and the white sneakers keep the whole look light."`]
    - `items` (array<OutfitCombinationItem>, required): The products in the outfit.
      - `slotId` (string, required): Id of the chat slot the product was taken from. [example `"3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9"`]
      - `slotType` (string, required): Slot category as labelled in the chat (for example `top`, `bottom`, `shoes`, `outerwear`). [example `"top"`]
      - `productId` (string, required): The dataset item's `itemId`. [example `"9f1c2d3e-4b5a-4c6d-8e7f-0a1b2c3d4e5f"`]
      - `confidence` (number, required): The model's 0–1 confidence that this product belongs in the outfit. [minimum 0; maximum 1; example `0.92`]
- `metadata` (OutfitCompositionsMetadata, required)
  - `totalCombinations` (integer, required): Number of outfits returned (renders that failed are not counted). [example `3`]
  - `generationTime` (integer, required): Wall-clock time for the whole request, in milliseconds. [example `21870`]
  - `usedUserPhoto` (boolean, required): Whether `userPhotoUrl` was supplied. [example `false`]
  - `cdnUploads` (integer, required): Outfits whose image was uploaded (`imageSource` of `cdn`). [example `3`]
  - `compressedImages` (integer, required): Outfits returned as inline compressed JPEG. [example `0`]
  - `originalImages` (integer, required): Outfits returned as the inline original image. [example `0`]

Example:

```json
{
  "visualizations": [
    {
      "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
      "imageUrl": "https://stylor.ai/api/v1/image/images/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d.png",
      "imageSource": "cdn",
      "imageData": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...",
      "reasoning": "The cream knit and straight-leg jeans share a relaxed silhouette, and the white sneakers keep the whole look light.",
      "combination": {
        "name": "Weekend Neutrals",
        "description": "A soft, tonal outfit for a relaxed brunch.",
        "reasoning": "The cream knit and straight-leg jeans share a relaxed silhouette, and the white sneakers keep the whole look light.",
        "items": [
          {
            "slotId": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9",
            "slotType": "top",
            "productId": "9f1c2d3e-4b5a-4c6d-8e7f-0a1b2c3d4e5f",
            "confidence": 0.92
          },
          {
            "slotId": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
            "slotType": "bottom",
            "productId": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
            "confidence": 0.88
          },
          {
            "slotId": "0e1f2a3b-4c5d-4e6f-8a7b-9c0d1e2f3a4b",
            "slotType": "shoes",
            "productId": "5e6f7a8b-9c0d-4e1f-8a2b-3c4d5e6f7a8b",
            "confidence": 0.81
          }
        ]
      }
    }
  ],
  "metadata": {
    "totalCombinations": 1,
    "generationTime": 18432,
    "usedUserPhoto": false,
    "cdnUploads": 1,
    "compressedImages": 0,
    "originalImages": 0
  }
}
```

Content type `text/event-stream`:

```text
event: preparing
data: {"count":3}

event: combination
data: {"id":"a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d","index":0,"combination":{"name":"Weekend Neutrals","description":"A soft, tonal outfit for a relaxed brunch.","reasoning":"The cream knit and straight-leg jeans share a relaxed silhouette.","items":[{"slotId":"3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9","slotType":"top","productId":"9f1c2d3e-4b5a-4c6d-8e7f-0a1b2c3d4e5f","confidence":0.92}]}}

event: visualization
data: {"id":"a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d","index":0,"imageUrl":"https://stylor.ai/api/v1/image/images/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d.png","imageSource":"cdn","reasoning":"The cream knit and straight-leg jeans share a relaxed silhouette.","combination":{"name":"Weekend Neutrals","description":"A soft, tonal outfit for a relaxed brunch.","reasoning":"The cream knit and straight-leg jeans share a relaxed silhouette.","items":[{"slotId":"3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9","slotType":"top","productId":"9f1c2d3e-4b5a-4c6d-8e7f-0a1b2c3d4e5f","confidence":0.92}]}}

event: visualization_error
data: {"id":"a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d","index":1,"error":"Failed to generate this visualization"}

event: done
data: {"metadata":{"totalCombinations":3,"generationTime":21870,"usedUserPhoto":false,"cdnUploads":3,"compressedImages":0,"originalImages":0}}

event: error
data: {"error":"No outfit items found in most recent recommendation"}
```

## Stream events

Each frame is `event: <name>`, a newline, `data: <json>`, and a blank line. Events are listed in the order they normally arrive.

### `preparing`

Sent immediately, before any lookup, so a client can lay out placeholder cards. `count` is the validated (not yet clamped) request count.

```json
{
  "count": 3
}
```

### `combination`

One outfit has been selected. `index` is its 0-based position and `id` the composition id; the matching `visualization` (or `visualization_error`) will carry the same `id` and `index`. Combinations arrive in order; images do not.

```json
{
  "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
  "index": 0,
  "combination": {
    "name": "Weekend Neutrals",
    "description": "A soft, tonal outfit for a relaxed brunch.",
    "reasoning": "The cream knit and straight-leg jeans share a relaxed silhouette.",
    "items": [
      {
        "slotId": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9",
        "slotType": "top",
        "productId": "9f1c2d3e-4b5a-4c6d-8e7f-0a1b2c3d4e5f",
        "confidence": 0.92
      }
    ]
  }
}
```

### `visualization`

The render for one combination is ready. Renders run concurrently, so these can arrive in any order and may interleave with later `combination` events. Unlike the JSON response, no inline `imageData` is included; `imageUrl` is a data URL when `imageSource` is not `cdn`.

```json
{
  "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
  "index": 0,
  "imageUrl": "https://stylor.ai/api/v1/image/images/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d.png",
  "imageSource": "cdn",
  "reasoning": "The cream knit and straight-leg jeans share a relaxed silhouette.",
  "combination": {
    "name": "Weekend Neutrals",
    "description": "A soft, tonal outfit for a relaxed brunch.",
    "reasoning": "The cream knit and straight-leg jeans share a relaxed silhouette.",
    "items": [
      {
        "slotId": "3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9",
        "slotType": "top",
        "productId": "9f1c2d3e-4b5a-4c6d-8e7f-0a1b2c3d4e5f",
        "confidence": 0.92
      }
    ]
  }
}
```

### `visualization_error`

The render for one combination failed. The outfit is dropped from the saved set; the stream continues.

```json
{
  "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
  "index": 1,
  "error": "Failed to generate this visualization"
}
```

### `done`

Every render has settled and the surviving outfits are saved. `metadata` matches the JSON response. The stream closes after this event.

```json
{
  "metadata": {
    "totalCombinations": 3,
    "generationTime": 21870,
    "usedUserPhoto": false,
    "cdnUploads": 3,
    "compressedImages": 0,
    "originalImages": 0
  }
}
```

### `error`

A fatal failure; the stream closes after this event and no `done`
follows. `error` is one of `Chat not found or access denied`,
`No outfit items found in most recent recommendation`,
`Failed to generate outfit combinations`,
`Failed to generate any visualizations`, or
`Something went wrong generating your outfit visualizations. Let's try that again.`
for any unexpected failure.

```json
{
  "error": "No outfit items found in most recent recommendation"
}
```

## 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 request body could not be parsed as JSON. Checked before authentication. |
| 400 | Request body must be a JSON object. | The request body is empty, or parsed to something other than a JSON object. Checked before authentication. |
| 404 | Chat not found or access denied | No chat with that `chatId` belongs to your organization. Streaming mode: sent as an `error` event. |
| 410 | This endpoint has been retired (v1/outfit/compositions). | Always, for API callers. This check runs before authentication and consumes no quota. |
| 422 | chatId is required | `chatId` is missing or not a string. In streaming mode this is returned as JSON before the stream opens. |
| 422 | count must be between 1 and 10 | `count` does not parse as an integer from 1 to 10. In streaming mode this is returned as JSON before the stream opens. |
| 422 | No outfit items found in most recent recommendation | The chat's latest assistant message has no search slot with results. Streaming mode: sent as an `error` event. |
| 502 | Failed to generate outfit combinations | The language model returned no usable combinations. Streaming mode: sent as an `error` event. |
| 502 | Failed to generate any visualizations | Every image render failed (for example no product had a usable photo). Streaming mode: sent as an `error` event. |

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