Generations
Turn one screenshot into one store-ready image - create, wait or poll, idempotent retries, list, cancel, delete and the device catalogue.
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.