Docs menu

Generations

Turn one screenshot into one store-ready image - create, wait or poll, idempotent retries, list, cancel, delete and the device catalogue.

View as Markdown

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.

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

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

curl

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}'

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

curl

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"

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

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.

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.

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.

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.

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.