# Render a look

`POST https://stylor.ai/api/v2/agent/looks`

- Authentication: public API key, sent as `Authorization: Bearer sgpt-pk-…`
- Rate-limit resource: `api.v2.agent.looks`
- Demo key: accepted
- Web page: https://stylor.ai/guides/rest/v2/generate-look

Renders one outfit as a single styled image of a model wearing every
item, from the product photos alone. Call it once for each look
announced on a `looks` stream event (or in the non-streaming
response's `looks`), passing that look's item ids, label and
`customInstructions`. Fire the calls in parallel — each takes several
seconds and they are independent, so a three-look answer finishes about
as fast as a one-look answer.

The endpoint is not limited to agent output: any two to six active
items from your catalogue can be rendered together. `itemIds` must be
**full** item ids (the `itemId` on a `Product`), not the 13-character
references the agent uses in its tool calls.

Items are resolved within your organization's datasets only. Every id
must resolve to an active item in scope, or the request fails with
`404` naming the ids that did not — a look is never rendered with
pieces missing. When rendering a look from the agent, send the same
`datasetIds` you sent to the chat endpoint; the agent picked its items
from that scope, and the default (your active datasets) may not
contain them.

The image is generated at a **3:4** aspect ratio by default so nothing
is cropped when displayed in a portrait box; override it with
`preferences.aspectRatio`. It is returned inline as a `data:` URI —
there is no hosted URL, so store it yourself if you need to show it
again.

Each request consumes one unit of `api.v2.agent.looks`, a resource of
its own, separate from the v1 outfit endpoint. A `502` (or any other
5xx) is refunded.

## Example request

```bash
curl -X POST "https://stylor.ai/api/v2/agent/looks" \
  -H "Authorization: Bearer $STYLOR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "label": "Garden wedding",
  "itemIds": [
    "8f2c1a9e-4b7d-4c1e-9a3f-2d6b7e5c1a90",
    "1b7e33c0-9a2d-4f8e-b5c6-7d8e9f0a1b2c",
    "c4d5e6f7-0a1b-4c2d-8e9f-0a1b2c3d4e5f"
  ],
  "customInstructions": "Shirt untucked with the top button open; outdoor daylight setting.",
  "sessionId": "3b0d4a1e-6f52-4b8a-9d3c-7e2f1a0b9c8d",
  "session": {
    "surface": "widget",
    "agentVersion": "v2"
  }
}'
```

## Request body

Content type `application/json`, required.

- `itemIds` (array<string>, required): Full item ids of the garments worn together. Ids that do not resolve to an active item in scope are left out; at least two must resolve. [min items 2; max items 6]
- `label` (string): Name of the look, used in the render prompt. Defaults to `Look`. [default `"Look"`; example `"Garden wedding"`]
- `datasetIds` (array<string>): Datasets to resolve the items in. Omit or send `[]` for every active dataset. [default `[]`]
- `userPhotoUrl` (string): The shopper's own photo, to style the look onto them instead of a stock model. A base64 `data:` URL of a JPEG, PNG or WebP image, at most 3 MB decoded — never a web URL. Stylor uses it for this request only and stores neither the photo nor the render made from it, so keep both in the shopper's browser if you need them again. [example `"data:image/jpeg;base64,/9j/4AAQSkZJRg..."`]
- `preferences` (object): Render preferences. Only `aspectRatio` is read today; other keys are accepted and ignored.
  - `aspectRatio` (string): Output aspect ratio as `width:height`, for example `3:4`, `1:1`, `9:16`. Match it to the box you display the image in. [default `"3:4"`; example `"3:4"`]
- `customInstructions` (string, nullable): Free-text direction for anything the product photos cannot convey. Pass the value from the look unchanged; omit or `null` when there is none. [example `"Shirt untucked with the top button open; outdoor daylight setting."`]
- `sessionId` (string, nullable): Your identifier for the shopper's visit, for analytics only. [default `null`; example `"3b0d4a1e-6f52-4b8a-9d3c-7e2f1a0b9c8d"`]
- `session` (Session | null): Visit metadata for analytics. [default `null`]
  - Option 1: Session
    - `surface` (string): Where the shopper is talking to the agent. Defaults to `widget` when omitted. [example `"widget"`]
    - `agentVersion` (string): Which agent version your client is built against. Use `v2`. [example `"v2"`]
    - `storageMode` (string): How your client persists the visit id — for example `memory`, `session` or `local`. Defaults to `memory`. [example `"local"`]
    - `tz` (string, nullable): The shopper's IANA time zone. [example `"America/Toronto"`]
    - `locale` (string, nullable): The shopper's BCP 47 locale. [example `"en-CA"`]
    - `device` (string, nullable): A coarse device class, typically `mobile` or `desktop`. [example `"desktop"`]
  - Option 2: null

### Examples

#### From the agent

Render a look announced on a `looks` event.

```json
{
  "label": "Garden wedding",
  "itemIds": [
    "8f2c1a9e-4b7d-4c1e-9a3f-2d6b7e5c1a90",
    "1b7e33c0-9a2d-4f8e-b5c6-7d8e9f0a1b2c",
    "c4d5e6f7-0a1b-4c2d-8e9f-0a1b2c3d4e5f"
  ],
  "customInstructions": "Shirt untucked with the top button open; outdoor daylight setting.",
  "sessionId": "3b0d4a1e-6f52-4b8a-9d3c-7e2f1a0b9c8d",
  "session": {
    "surface": "widget",
    "agentVersion": "v2"
  }
}
```

#### Custom ratio

Limit to one dataset and render at 4:3.

```json
{
  "itemIds": [
    "8f2c1a9e-4b7d-4c1e-9a3f-2d6b7e5c1a90",
    "1b7e33c0-9a2d-4f8e-b5c6-7d8e9f0a1b2c"
  ],
  "datasetIds": [
    "9c21f0aa-3e4b-4d2a-b6d1-0f7e8a9b1c2d"
  ],
  "preferences": {
    "aspectRatio": "4:3"
  }
}
```

## Responses

### 200

The rendered look.

Headers:

- `X-RateLimit-Resource`: The quota resource this endpoint is metered against: `api.v2.agent.chat` for the chat endpoint, `api.v2.agent.looks` for the looks endpoint and `api.v2.agent.match` for the shop-the-look endpoint. These are separate from the v1 resources, so Agent API usage never draws down a v1 allowance.
- `X-RateLimit-Quota-Limit`: Requests allowed in the current billing period, or `unlimited`.
- `X-RateLimit-Quota-Remaining`: Requests left in the current billing period, or `unlimited`.
- `X-RateLimit-Quota-Reset`: Unix epoch seconds at which the billing period resets.
- `X-RateLimit-RPM-Limit`: Requests allowed per minute for this resource.
- `X-RateLimit-RPM-Remaining`: Requests left in the current one-minute window.
- `X-RateLimit-RPM-Reset`: Unix epoch seconds at which the one-minute window resets.

Content type `application/json`:

- `visualization` (Visualization, required)
  - `imageData` (string, required): The rendered image as a `data:<mime>;base64,…` URI, usable directly as an `<img src>`. Typically `image/png`. [example `"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAgAAAAKqCAYAAAB..."`]

Example:

```json
{
  "visualization": {
    "imageData": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAgAAAAKqCAYAAAB..."
  }
}
```

## 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 could not be parsed as JSON. |
| 400 | Request body must be a JSON object. | The body is empty, or is valid JSON that is not an object (for example an array). |
| 404 | No active datasets found for this organization. Activate one via the dashboard (Dataset → Settings) or through the REST API before proceeding. | `datasetIds` was omitted and the organization has datasets, but none is active. |
| 404 | All requested datasets are archived. Reactivate at least one via the dashboard or REST API. | Every dataset in `datasetIds` is archived. |
| 404 | No datasets found for this organization. Create one via the dashboard or REST API, or pass datasetIds to use draft/archived datasets before proceeding. | The organization has no datasets at all. |
| 404 | These items could not be found: <ids>. | One or more of `itemIds` did not match an active item in the resolved datasets. The unmatched ids are listed, comma-separated. Check that you sent full item ids from this organization's catalogue, and the same `datasetIds` the agent searched. |
| 422 | A look needs at least two itemIds. | `itemIds` is missing, not an array, or has fewer than two distinct entries. |
| 422 | A look can hold at most 6 items. | `itemIds` has more than six entries. |
| 422 | userPhotoUrl must be a base64 data: URL of a JPEG, PNG or WebP image. Shopper photos are never fetched from a URL or stored. | `userPhotoUrl` is a web URL, a storage key, or any other type of image. Send the photo's bytes inline. |
| 422 | userPhotoUrl can be at most 3 MB. | The photo in `userPhotoUrl` is larger than 3 MB once decoded. Downscale it in the browser first; around 1024px on the long edge is plenty. |
| 422 | Invalid datasetIds for this organization: <ids> | One or more of `datasetIds` does not belong to the organization. The offending ids are listed, comma-separated. |
| 502 | Could not generate this look. | The image model returned no image or failed, including when none of the product images could be fetched. Retrying usually succeeds. |

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