# Generations

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

A generation turns one uploaded screenshot into one marketing image of exactly the size a store
requires. Creating one needs `generations:write`; reading needs `generations:read` or
`generations:write`. To make every image of a store listing at once, use a
[screenshot set](/docs/screenshot-sets/).

```text
POST ──▶ queued ──worker──▶ running ──▶ succeeded   (result.image.url)
            │                  └──────▶ failed      (error.code; a live credit is refunded)
            └──cancel──▶ canceled                    (a live credit is refunded)
```

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

## Create and wait

Send one input (`uploadId`, or `url` to import an image in the same call), the `store`, and either
a `device` id from [`GET /devices`](#devices) or a `size` of that store. `app.name` is required;
`caption`, `palette`, `logo` and `metadata` are optional (without a caption or palette StoreShot
derives them from the screenshot).

**Brand kits.** Send `styleId` to apply a saved brand kit: it fills the palette and logo you
leave out and adds its background mood and caption tone. Fields you send win. See
[Brand kits](/docs/styles/).

`Prefer: wait=60` holds the answer until the generation is final, for at most 60 seconds: `200`
with the finished generation, or `202` if it is still running (keep polling then).

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

BODY='{"inputs":[{"uploadId":"'"$UPLOAD"'"}],"store":"app_store","device":"app_store_1290x2796",
  "app":{"name":"Trailmate","description":"Offline hiking maps"},
  "caption":"Find every trail, even offline",
  "palette":[{"hex":"#0F766E","name":"Teal"}],
  "metadata":{"campaign":"spring","locale":"en-US"}}'

GENERATION=$(curl -sS https://api.storeshot.art/v1/generations \
  -H "Authorization: Bearer $STORESHOT_API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: home-en-spring' -H 'Prefer: wait=60' \
  -d "$BODY")
GENERATION_ID=$(echo "$GENERATION" | jq -r .id)
echo "$GENERATION" | jq '{id, status, creditsCharged, image: .result.image}'
```

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

const api = 'https://api.storeshot.art/v1';
const headers = { Authorization: `Bearer ${process.env.STORESHOT_API_KEY}` };

const form = new FormData();
form.append('file', new Blob([await readFile('home.png')]), 'home.png');
const upload = await (await fetch(`${api}/uploads`, { method: 'POST', headers, body: form })).json();

const created = await fetch(`${api}/generations`, {
  method: 'POST',
  headers: {
    ...headers,
    'Content-Type': 'application/json',
    'Idempotency-Key': 'home-en-spring',
    Prefer: 'wait=60',
  },
  body: JSON.stringify({
    inputs: [{ uploadId: upload.id }],
    store: 'app_store',
    device: 'app_store_1290x2796',
    app: { name: 'Trailmate', description: 'Offline hiking maps' },
    caption: 'Find every trail, even offline',
    palette: [{ hex: '#0F766E', name: 'Teal' }],
    metadata: { campaign: 'spring', locale: 'en-US' },
  }),
});
let generation = await created.json();
console.log(created.status, generation.status, generation.result?.image.url);
```

```python
import os, requests

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

with open("home.png", "rb") as f:
    upload = requests.post(f"{api}/uploads", headers=headers, files={"file": f}).json()

created = requests.post(
    f"{api}/generations",
    headers={**headers, "Idempotency-Key": "home-en-spring", "Prefer": "wait=60"},
    json={
        "inputs": [{"uploadId": upload["id"]}],
        "store": "app_store",
        "device": "app_store_1290x2796",
        "app": {"name": "Trailmate", "description": "Offline hiking maps"},
        "caption": "Find every trail, even offline",
        "palette": [{"hex": "#0F766E", "name": "Teal"}],
        "metadata": {"campaign": "spring", "locale": "en-US"},
    },
)
generation = created.json()
print(created.status_code, generation["status"], (generation["result"] or {}).get("image"))
```

A `succeeded` generation has `result.image` (`url`, `contentType`, `width`, `height`), a
best-effort `result.thumbnail` and `result.urlsExpireAt`: the URLs are signed for 15 minutes, so
download the image right away or `GET` the generation again for fresh URLs. A `failed` one has
`error.code` (`GENERATION_FAILED`, `GENERATION_TIMEOUT`, `NOT_A_SCREENSHOT`, `UPLOAD_NOT_READY`
or `INTERNAL_ERROR`) and, with a live key, `creditsRefunded: 1`.

## Poll

Without `Prefer: wait` (or when it answered `202`), poll `GET /generations/{id}` every 2–5 seconds
until the status is `succeeded`, `failed` or `canceled`. `?wait=<seconds>` works on the `GET` too.

<!-- tabs -->
```sh
while :; do
  STATUS=$(curl -sS "https://api.storeshot.art/v1/generations/$GENERATION_ID" \
    -H "Authorization: Bearer $STORESHOT_API_KEY" | jq -r .status)
  case "$STATUS" in queued|running) sleep 3 ;; *) break ;; esac
done
echo "$STATUS"
```

```js
for (;;) {
  generation = await (await fetch(`${api}/generations/${generation.id}`, { headers })).json();
  if (generation.status !== 'queued' && generation.status !== 'running') break;
  await new Promise((resolve) => setTimeout(resolve, 3000));
}
console.log(generation.status, generation.result?.image.url ?? generation.error);
```

```python
import time

while True:
    generation = requests.get(f"{api}/generations/{generation['id']}", headers=headers).json()
    if generation["status"] not in ("queued", "running"):
        break
    time.sleep(3)
print(generation["status"], (generation["result"] or {}).get("image") or generation["error"])
```

## Idempotency

Send an `Idempotency-Key` with every create. Repeating the same request with the same key returns
the original generation with `Idempotent-Replayed: true`: nothing is created or charged again, even
when the first request is still in flight. The same key with a different body (or from a key of the
other environment) is `409 IDEMPOTENCY_CONFLICT` with `details.generationId`. A refused request
(any `4xx`) stores nothing, so you can retry it with the same key, for example after topping up
credits.

```sh
curl -sS -o /dev/null -D - https://api.storeshot.art/v1/generations \
  -H "Authorization: Bearer $STORESHOT_API_KEY" \
  -H 'Content-Type: application/json' -H 'Idempotency-Key: home-en-spring' \
  -d "$BODY" | grep -i '^idempotent-replayed'
```

## List

`GET /generations` lists the account's generations, newest first, 20 per page (`limit` up to
100), without deleted ones. Filters: `status`, `environment` (`live` or `test`), `metadata`
(`key:value`), `screenshotSetId`, `source` and `projectId`. Pass `nextCursor` back as `cursor`
with the same filters for the next page.

```sh
curl -sS 'https://api.storeshot.art/v1/generations?status=succeeded&metadata=campaign:spring&limit=10' \
  -H "Authorization: Bearer $STORESHOT_API_KEY" \
  | jq -r '.data[] | "\(.id) \(.status) \(.device)"'
```

## Cancel and delete

Only a `queued` generation can be canceled (`200`, credit refunded). Once a worker has started it,
cancel answers `409 GENERATION_STATE_CONFLICT` with `details.status`, and the generation finishes
normally. Test generations start almost at once, so this sample usually shows the `409`.

<!-- doctest: expect=409 -->
```sh
curl -sS -X POST "https://api.storeshot.art/v1/generations/$GENERATION_ID/cancel" \
  -H "Authorization: Bearer $STORESHOT_API_KEY" | jq '{status, error: .error.code}'
```

Only a finished generation can be deleted (`204`; while it is queued or running, `409`). Delete
removes the image and thumbnail at once; the generation stays readable by id with `deletedAt` set
and `result: null`, and leaves the lists. Live results are kept until you delete them; test results
are deleted 30 days after they finish.

```sh
curl -sS -X DELETE "https://api.storeshot.art/v1/generations/$GENERATION_ID" \
  -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 '{status, deletedAt, result}'
```

## Devices

`GET /devices` (no key needed) lists every size StoreShot produces, per store, with its label and
orientations. A `device` or `size` that is not in this catalogue is `400 INVALID_INPUT`. The Google
Play feature graphic is `google_play_1024x500` (landscape only). `google_play_1080x2400` is marked
`deprecated` (it breaks Google Play's 2:1 limit): use `google_play_1080x2160`.

```sh
curl -sS https://api.storeshot.art/v1/devices \
  | jq -r '.data[] | "\(.id)  \(.width)x\(.height)  \(.label)\(if .deprecated then "  (deprecated)" else "" end)"'
```

For each store's official rules (required sizes, counts, formats) see
`GET /stores/{store}/requirements` in the [catalog reference](/docs/api/catalog/).
