# Brand kits (styles)

> HTML version: https://storeshot.art/docs/styles/

A brand kit (a *style* in the API) saves the look of your store images once: colours, a logo, a
background and a caption tone. Send its `id` as `styleId` when you create a
[generation](/docs/generations/) or a [screenshot set](/docs/screenshot-sets/) instead of repeating
them. Brand kits are free. Creating, changing and deleting one needs `generations:write`; reading
needs `generations:read` or `generations:write`. Brand kits belong to the account: every key of
the account sees them, in either environment.

The Node.js and Python samples on this page continue from the first one (`api`, `headers`,
`logo`).

## What a brand kit holds

| Field | What it is |
|---|---|
| `name` | 1–100 characters, required. Not unique. |
| `palette` | Up to 8 colours (`{ "hex": "#0F766E", "name": "Teal" }`). |
| `logo` | A copy of one of your uploads (PNG, JPEG or WebP, at most 5 MB and 4096 × 4096 pixels, from either environment). Deleting the upload later keeps the logo. |
| `backgroundMood` | `minimal`, `light`, `dark`, `vibrant`, `gradient` or `textured`. |
| `backgroundPrompt` | What the background looks like, in at most 200 characters on one line (no line breaks or control characters). It is passed to the model as a quoted description of the look, never as instructions. |
| `captionTone` | The voice of the headline: `professional`, `friendly`, `playful`, `bold` or `premium`. A caption you send is kept word for word; the tone guides its typography and any wording the model writes. |
| `referenceGenerationId` | Optional: a generation to copy colours and choices from ([below](#copy-a-look-from-a-generation)). |

Every field except `name` is optional; a brand kit with only a name applies nothing.

## Create one

Upload the logo first, like any image ([Uploads](/docs/uploads/)). Here `home.png` stands in for
your logo file.

<!-- tabs -->
```sh
LOGO=$(curl -sS https://api.storeshot.art/v1/uploads \
  -H "Authorization: Bearer $STORESHOT_API_KEY" -F file=@home.png | jq -r .id)
echo "$LOGO"
```

```js
import { readFile } from 'node:fs/promises';

const api = 'https://api.storeshot.art/v1';
const headers = { Authorization: `Bearer ${process.env.STORESHOT_API_KEY}` };
const jsonHeaders = { ...headers, 'Content-Type': 'application/json' };

const uploadFile = async (file) => {
  const form = new FormData();
  form.append('file', new Blob([await readFile(file)]), file);
  return (await fetch(`${api}/uploads`, { method: 'POST', headers, body: form })).json();
};
const logo = await uploadFile('home.png');
console.log(logo.id, logo.status);
```

```python
import os, requests

api = "https://api.storeshot.art/v1"
headers = {"Authorization": f"Bearer {os.environ['STORESHOT_API_KEY']}"}

def upload_file(name):
    with open(name, "rb") as f:
        return requests.post(f"{api}/uploads", headers=headers, files={"file": f}).json()

logo = upload_file("home.png")
print(logo["id"], logo["status"])
```

Then create the brand kit with an `Idempotency-Key`. The answer is `201` with the brand kit and
its `id`; `logo` is now the brand kit's own copy (`url`, signed for 15 minutes, and `expiresAt`).

<!-- tabs -->
```sh
STYLE_BODY='{"name":"Trailmate brand",
  "palette":[{"hex":"#0F766E","name":"Teal"},{"hex":"#F8FAFC","name":"Off White"}],
  "logo":{"uploadId":"'"$LOGO"'"},
  "backgroundMood":"textured",
  "backgroundPrompt":"soft paper texture with mountain line art",
  "captionTone":"friendly"}'

STYLE=$(curl -sS https://api.storeshot.art/v1/styles \
  -H "Authorization: Bearer $STORESHOT_API_KEY" \
  -H 'Content-Type: application/json' -H 'Idempotency-Key: brand-kit-1' \
  -d "$STYLE_BODY" | jq -r .id)
echo "$STYLE"
```

```js
const created = await fetch(`${api}/styles`, {
  method: 'POST',
  headers: { ...jsonHeaders, 'Idempotency-Key': 'brand-kit-1' },
  body: JSON.stringify({
    name: 'Trailmate brand',
    palette: [
      { hex: '#0F766E', name: 'Teal' },
      { hex: '#F8FAFC', name: 'Off White' },
    ],
    logo: { uploadId: logo.id },
    backgroundMood: 'textured',
    backgroundPrompt: 'soft paper texture with mountain line art',
    captionTone: 'friendly',
  }),
});
const style = await created.json();
console.log(created.status, style.id, style.name);
```

```python
created = requests.post(
    f"{api}/styles",
    headers={**headers, "Idempotency-Key": "brand-kit-1"},
    json={
        "name": "Trailmate brand",
        "palette": [
            {"hex": "#0F766E", "name": "Teal"},
            {"hex": "#F8FAFC", "name": "Off White"},
        ],
        "logo": {"uploadId": logo["id"]},
        "backgroundMood": "textured",
        "backgroundPrompt": "soft paper texture with mountain line art",
        "captionTone": "friendly",
    },
)
style = created.json()
print(created.status_code, style["id"], style["name"])
```

Repeating the request with the same key and body creates nothing: it answers the brand kit as it
is now with `200` and `Idempotent-Replayed: true`. The same key with a different body is
`409 IDEMPOTENCY_CONFLICT` with `details.styleId`.

```sh
curl -sS -o /dev/null -D - https://api.storeshot.art/v1/styles \
  -H "Authorization: Bearer $STORESHOT_API_KEY" \
  -H 'Content-Type: application/json' -H 'Idempotency-Key: brand-kit-1' \
  -d "$STYLE_BODY" | grep -i '^idempotent-replayed'
```

`GET /styles` lists your brand kits, most recently updated first, 20 per page (`limit` up to 100;
pass `nextCursor` back as `cursor`). `GET /styles/{id}` reads one.

<!-- tabs -->
```sh
curl -sS 'https://api.storeshot.art/v1/styles?limit=10' \
  -H "Authorization: Bearer $STORESHOT_API_KEY" \
  | jq -r '.data[] | "\(.id) \(.name) \(.backgroundMood) \(.captionTone)"'
```

```js
const styles = await (await fetch(`${api}/styles?limit=10`, { headers })).json();
for (const s of styles.data) console.log(s.id, s.name, s.backgroundMood, s.captionTone);
```

```python
styles = requests.get(f"{api}/styles", params={"limit": 10}, headers=headers).json()
for s in styles["data"]:
    print(s["id"], s["name"], s["backgroundMood"], s["captionTone"])
```

## Use it

Send `styleId` when you create a generation. This one sends no palette and no logo, so both come
from the brand kit, together with its background mood, description and caption tone.

<!-- tabs -->
```sh
SCREEN=$(curl -sS https://api.storeshot.art/v1/uploads \
  -H "Authorization: Bearer $STORESHOT_API_KEY" -F file=@screen-1.png | jq -r .id)

GENERATION=$(curl -sS https://api.storeshot.art/v1/generations \
  -H "Authorization: Bearer $STORESHOT_API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: home-brand-kit' -H 'Prefer: wait=60' \
  -d '{"inputs":[{"uploadId":"'"$SCREEN"'"}],"store":"app_store","device":"app_store_1290x2796",
    "app":{"name":"Trailmate"},"caption":"Find every trail, even offline",
    "styleId":"'"$STYLE"'"}')
GENERATION_ID=$(echo "$GENERATION" | jq -r .id)
echo "$GENERATION" | jq '{id, status, style, palette}'
```

```js
const screen = await uploadFile('screen-1.png');
const response = await fetch(`${api}/generations`, {
  method: 'POST',
  headers: { ...jsonHeaders, 'Idempotency-Key': 'home-brand-kit', Prefer: 'wait=60' },
  body: JSON.stringify({
    inputs: [{ uploadId: screen.id }],
    store: 'app_store',
    device: 'app_store_1290x2796',
    app: { name: 'Trailmate' },
    caption: 'Find every trail, even offline',
    styleId: style.id,
  }),
});
const generation = await response.json();
console.log(response.status, generation.status, generation.style);
```

```python
screen = upload_file("screen-1.png")
response = requests.post(
    f"{api}/generations",
    headers={**headers, "Idempotency-Key": "home-brand-kit", "Prefer": "wait=60"},
    json={
        "inputs": [{"uploadId": screen["id"]}],
        "store": "app_store",
        "device": "app_store_1290x2796",
        "app": {"name": "Trailmate"},
        "caption": "Find every trail, even offline",
        "styleId": style["id"],
    },
)
generation = response.json()
print(response.status_code, generation["status"], generation.get("style"))
```

The generation's `style` names the brand kit it was made with and what it supplied:
`{ "id": "…", "name": "Trailmate brand", "applied": ["palette", "logo", "backgroundMood",
"backgroundPrompt", "captionTone"] }` (`null` when no brand kit was used). `palette` shows the
colours used; `logo` stays `null` when the logo came from the brand kit. With a test key the
result is a sample image as usual ([Test mode](/docs/test-mode/)); its colour differs from an
unstyled one because the sample is seeded from the prompt, which now carries the brand kit.

A screenshot set takes the same `styleId` and applies it to every image. It also works with
`?dryRun=true`: the brand kit is checked and nothing is copied. Brand kits are free, so the quote
is the same as without one.

<!-- tabs -->
```sh
curl -sS 'https://api.storeshot.art/v1/screenshot-sets?dryRun=true' \
  -H "Authorization: Bearer $STORESHOT_API_KEY" -H 'Content-Type: application/json' \
  -d '{"app":{"name":"Trailmate"},
    "screens":[{"input":{"uploadId":"'"$SCREEN"'"},"caption":"Find every trail"}],
    "stores":[{"store":"app_store","devices":["app_store_1290x2796"]}],
    "styleId":"'"$STYLE"'"}' | jq '{images, credits, sufficient}'
```

```js
const quote = await (
  await fetch(`${api}/screenshot-sets?dryRun=true`, {
    method: 'POST',
    headers: jsonHeaders,
    body: JSON.stringify({
      app: { name: 'Trailmate' },
      screens: [{ input: { uploadId: screen.id }, caption: 'Find every trail' }],
      stores: [{ store: 'app_store', devices: ['app_store_1290x2796'] }],
      styleId: style.id,
    }),
  })
).json();
console.log(quote.images, quote.credits, quote.sufficient);
```

```python
quote = requests.post(
    f"{api}/screenshot-sets",
    params={"dryRun": "true"},
    headers=headers,
    json={
        "app": {"name": "Trailmate"},
        "screens": [{"input": {"uploadId": screen["id"]}, "caption": "Find every trail"}],
        "stores": [{"store": "app_store", "devices": ["app_store_1290x2796"]}],
        "styleId": style["id"],
    },
).json()
print(quote["images"], quote["credits"], quote["sufficient"])
```

## What wins: your fields

The brand kit only fills what the request leaves out.

| Input | You send it | You leave it out, the brand kit has it | Neither |
|---|---|---|---|
| `palette` | yours | the brand kit's | extracted from the screenshot (a set: from screen 1) |
| `logo` | yours | the brand kit's | no logo |
| background mood, description, caption tone | (no request field) | the brand kit's | not used |
| `app`, `caption`, screens, devices, `metadata`, `projectId` | yours | (a brand kit holds none of these) | as without a brand kit |

`style.applied` lists exactly what came from the brand kit; fields you sent yourself are not
listed. A request that wants no logo or no brand kit colours does not send `styleId`.

## Change or delete it

`PATCH /styles/{id}` changes only the fields you send: `null` clears `logo`, `backgroundMood`,
`backgroundPrompt` or `captionTone`, and `palette: []` clears the colours. A new `logo` is copied
like on create. `DELETE /styles/{id}` answers `204`.

A generation or set keeps a copy of the brand kit as it was when it was created, including its own
copy of the logo. Changing or deleting the brand kit never changes images you already made, or
their retries; only new generations and sets use the new values. Once the brand kit is deleted,
their `style.id` is `null` and `style.name` and `style.applied` stay.

<!-- tabs -->
```sh
curl -sS -X PATCH "https://api.storeshot.art/v1/styles/$STYLE" \
  -H "Authorization: Bearer $STORESHOT_API_KEY" -H 'Content-Type: application/json' \
  -d '{"captionTone":"bold"}' | jq '{name, backgroundMood, captionTone, updatedAt}'
```

```js
const updated = await (
  await fetch(`${api}/styles/${style.id}`, {
    method: 'PATCH',
    headers: jsonHeaders,
    body: JSON.stringify({ captionTone: 'bold' }),
  })
).json();
console.log(updated.name, updated.captionTone);
```

```python
updated = requests.patch(
    f"{api}/styles/{style['id']}", headers=headers, json={"captionTone": "bold"}
).json()
print(updated["name"], updated["captionTone"])
```

<!-- tabs -->
```sh
curl -sS -X DELETE "https://api.storeshot.art/v1/styles/$STYLE" \
  -H "Authorization: Bearer $STORESHOT_API_KEY" -o /dev/null -w '%{http_code}\n'
curl -sS "https://api.storeshot.art/v1/generations/$GENERATION_ID" \
  -H "Authorization: Bearer $STORESHOT_API_KEY" | jq .style
```

```js
const deleted = await fetch(`${api}/styles/${style.id}`, { method: 'DELETE', headers });
console.log(deleted.status);
const after = await (await fetch(`${api}/generations/${generation.id}`, { headers })).json();
console.log(after.style);
```

```python
deleted = requests.delete(f"{api}/styles/{style['id']}", headers=headers)
print(deleted.status_code)
after = requests.get(f"{api}/generations/{generation['id']}", headers=headers).json()
print(after.get("style"))
```

## Copy a look from a generation

`referenceGenerationId` (on create or `PATCH`) names a succeeded generation of yours, from either
environment. Its colours, and its background and caption choices when it was made with a brand
kit, are copied once into the fields the same request leaves out. Its image and its logo are never
used. The id stays on the brand kit as `referenceGenerationId`, for reference only: later changes
to the generation change nothing in the brand kit. Sending an id again copies again;
`referenceGenerationId: null` forgets where the values came from and keeps them.

The generation above was made with the brand kit before the `PATCH`, so this copies its colours,
mood and description and the tone it used then (`friendly`), although the brand kit itself is
deleted by now:

```sh
curl -sS https://api.storeshot.art/v1/styles \
  -H "Authorization: Bearer $STORESHOT_API_KEY" \
  -H 'Content-Type: application/json' -H 'Idempotency-Key: brand-kit-from-home' \
  -d '{"name":"Trailmate from home","referenceGenerationId":"'"$GENERATION_ID"'"}' \
  | jq '{id, palette, backgroundMood, captionTone, referenceGenerationId}'
```

## Limits and errors

- At most 50 brand kits per account.
- A logo is at most 5 MB and 4096 × 4096 pixels (PNG, JPEG or WebP).
- `backgroundPrompt` is at most 200 characters on one line.

| Situation | Answer |
|---|---|
| `styleId` unknown or not yours (create or dry run) | `400 INVALID_INPUT`, `issues[0].path` `["styleId"]`, "No such style." |
| `GET`, `PATCH` or `DELETE` of an unknown, deleted or another account's brand kit | `404 NOT_FOUND` |
| 50 brand kits already | `403 FORBIDDEN`, `details.reason` `style_limit_reached`, `details.limit` `50` |
| Logo upload unknown, not ready, expired or another account's | `400 INVALID_INPUT`, `issues[0].path` `["logo","uploadId"]` |
| Logo over 5 MB or 4096 × 4096 pixels | `400 INVALID_INPUT`, `issues[0].path` `["logo","uploadId"]` |
| `referenceGenerationId` not a succeeded generation of yours | `400 INVALID_INPUT`, `issues[0].path` `["referenceGenerationId"]` |
| Same `Idempotency-Key`, different body | `409 IDEMPOTENCY_CONFLICT`, `details.styleId` |
| Set retry after its copy of the brand kit logo was deleted | `409 UPLOAD_NOT_READY`, `details.reason` `expired`, `details.path` `["styleId"]` |

Every code is described in [Errors](/docs/errors/).

## How long it is kept

- A brand kit and its copy of your logo are kept until you delete the brand kit. Deleting it
  deletes its logo copy at once.
- Generations and screenshot sets made with a brand kit keep their own copy of its logo. That copy
  is deleted 30 days after the last generation or screenshot set that used it was created; a set
  retry after that answers `409 UPLOAD_NOT_READY` (see the table above).
- Closing your account deletes your brand kits and those logo copies.

See also the [data retention policy](/data-retention/).
