# StoreShot API reference

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

- Contract version: 1.18.0 (changelog: https://storeshot.art/docs/changelog.md)
- Base URL: `https://api.storeshot.art/v1` (Production)
- OpenAPI 3.1 document: https://api.storeshot.art/v1/openapi.json

## Credentials

| Credential | Header | Description |
|---|---|---|
| API key | `Authorization: Bearer ss_live_… / ss_test_…` | Storeshot API key. Create one with POST /api-keys; the secret is shown once. |
| Access token | `Authorization: Bearer JWT` | Draggable Auth access token (console and apps). Holds every scope. |

## Resources

- [System](https://storeshot.art/docs/api/system.md): Service status. (2 operations)
- [Catalog](https://storeshot.art/docs/api/catalog.md): Static reference data. (3 operations)
- [Account](https://storeshot.art/docs/api/account.md): The authenticated caller. (1 operation)
- [API keys](https://storeshot.art/docs/api/api-keys.md): Create, list and revoke API keys. (5 operations)
- [Usage](https://storeshot.art/docs/api/usage.md): Credit balance and generation usage. (2 operations)
- [Uploads](https://storeshot.art/docs/api/uploads.md): Hand over images once; reference them in generations. (6 operations)
- [Generations](https://storeshot.art/docs/api/generations.md): Turn a screenshot into a store-ready marketing image. (5 operations)
- [Screenshot sets](https://storeshot.art/docs/api/screenshot-sets.md): A whole store listing (every screen on every device) in one call. (7 operations)
- [Projects](https://storeshot.art/docs/api/projects.md): Group generations and screenshot sets per app. (7 operations)
- [Styles](https://storeshot.art/docs/api/styles.md): Brand kits: save a palette, logo, background and caption tone once and reuse them with `styleId`. (5 operations)
- [Palettes](https://storeshot.art/docs/api/palettes.md): Free brand palettes from your images. (1 operation)
- [Webhooks](https://storeshot.art/docs/api/webhooks.md): Signed HTTPS callbacks when generations and sets finish. (8 operations, 5 webhook events)
- [Activity](https://storeshot.art/docs/api/activity.md): Who changed what in the account. (1 operation)
- [Billing](https://storeshot.art/docs/api/billing.md): Buy credit packs (hosted checkout). (10 operations)
- [Schemas](https://storeshot.art/docs/api/schemas.md): every object of the contract.

The Storeshot public API. Generate App Store and Google Play marketing screenshots.

## Conventions
- JSON fields are camelCase; enum values are lower snake_case; error codes are UPPER_SNAKE.
- Collections are returned as `{ "data": [...] }`.
- Every response carries an `X-Request-Id` header. Send your own (1–128 chars of `A-Z a-z 0-9 _ - =`) to correlate logs.
- Every error uses the `ErrorEnvelope`: `{ "error": { "code", "message", "requestId", "docUrl", "details?" } }`.

Error code reference: `/v1/docs/errors`.

## For AI agents
Start with the agent quickstart (https://storeshot.art/docs/agents-quickstart.md) or https://storeshot.art/llms.txt (every docs page is also Markdown; all of it in one file: https://storeshot.art/llms-full.txt). The usual task, a store listing from raw app screenshots:
1. Read the key from the `STORESHOT_API_KEY` environment variable. If it is missing, stop and ask your human to create a test key (`ss_test_…`, free) in the developer console (https://console.storeshot.art/) and export it. Never print, log or commit a key, and never ask for one in chat.
2. `GET /me` checks the key; `credential.environment` is `test` (free sample images) or `live` (real images, credits).
3. `POST /uploads` once per raw screenshot (PNG, JPEG or WebP).
4. `POST /screenshot-sets?dryRun=true` quotes the set for free. With a live key, tell your human the `credits` before creating it.
5. `POST /screenshot-sets` with an `Idempotency-Key` (a retry never charges twice), then poll `GET /screenshot-sets/{id}` every 3–5 seconds until `status` is no longer `queued` or `running`.
6. `GET /screenshot-sets/{id}/archive?layout=fastlane` and unzip it in the project root (the folders fastlane `deliver` and `supply` read).

**Costs.** A live key spends 1 credit per generated image (a set: screens × devices, +1 for a feature graphic), charged at create and refunded automatically for images that fail or are canceled before they start. Test keys never spend credits. Every other operation is free; each operation ends with its **Credits.** line.

**Asynchronous work.** Creates answer `202` with a `queued` resource: poll it, send `Prefer: wait=60` on generations, or register a webhook. **Errors.** Retry `429` after `Retry-After` and `5xx` with the same `Idempotency-Key`; on `402 INSUFFICIENT_CREDITS` give your human `details.topUpUrl`; quote `X-Request-Id` when asking for help.

## Authentication
Send `Authorization: Bearer <credential>`, where the credential is an API key (`ss_live_…` or `ss_test_…`) or a Draggable Auth access token. Never put credentials in the URL. Test keys (`ss_test_…`) never incur charges. Each operation lists the scope an API key needs; access tokens have every scope. No key yet? An agent can get one with the approval of its human: `POST /auth/device`, then poll `POST /auth/device/token` (RFC 8628 device authorization; it may be disabled on a server).

## Client identification
Optionally send `X-Storeshot-Client: <surface>/<name>`, where surface is `api`, `web`, `mcp` or `cli` and name is 1–100 characters of `A-Z a-z 0-9 . _ @ + -` (for example `cli/storeshot-cli@0.3.1`). It labels your usage (`GET /usage?groupBy=source`); it is not authentication. Without it, usage is labelled `api` and your `User-Agent` is recorded.

## Rate limits
Authenticated requests are counted per minute in two buckets: per API key (`key`, 60 requests) and per account (`user`, 120 requests, shared by all API keys and access tokens of the account; access tokens use only this bucket). Test keys count like live keys. A request is counted only if every bucket allows it; a refused request counts nowhere. The numbers here are the current defaults and may differ per plan: `RateLimit-Policy` is authoritative.

Every authenticated response carries `RateLimit-Policy` (each policy as `"name";q=<requests>;w=<seconds>`) and `RateLimit` (the most restrictive policy as `"name";r=<remaining>;t=<seconds until reset>`), following draft-ietf-httpapi-ratelimit-headers. Over the limit you get `429 RATE_LIMITED` with `Retry-After` and `details.scope`.

Billed generations are also capped per account: 3 in progress at once (`429 CONCURRENCY_LIMITED`, retry after 10 s) and 100 per UTC day (`429 RATE_LIMITED`, `details.policy` `daily_generations`, retry after the next UTC midnight). `/health`, `/status`, `/devices`, this document and the docs are never rate limited. Browsers can read these headers and `X-Request-Id` (CORS).

## Uploads and retention
Hand an image over once and reference its `id` in generations. Three ways: `POST /uploads` with `multipart/form-data` (one part `file`); `POST /uploads/presign`, then `PUT` the file to the returned URL from your server and call `POST /uploads/{id}/complete`; or `POST /uploads` with `{ "url": "https://…" }` to have the server fetch it. Each way ends in the same `ready` upload with the same metadata.

Accepted: PNG, JPEG and WebP, detected from the bytes (never from the file name or declared type), at most 20 MB and 50 megapixels (16384 pixels per edge). HEIC/HEIF is not supported. An account can hold 500 uploads or 2 GB (pending and ready uploads count; deleting frees space). Test keys may upload too (same limits).

URL imports fetch `https://` URLs on port 443 only, and only from public addresses (private, loopback, link-local and other special-purpose addresses are refused, also after a redirect), with at most 3 redirects and 15 seconds in total. Only the origin and path of the URL are stored.

**Retention:** uploads, with their files, are deleted 30 days after creation (pending or failed uploads after 24 hours); you can delete one earlier with `DELETE /uploads/{id}`. Generated results are kept until you delete them (results of test keys: 30 days).

## Generations
`POST /generations` turns one screenshot into one marketing image of exactly the size a store requires (a `GET /devices` id, or its `width`×`height`). It returns at once with `202` and a generation in `queued`; a worker moves it to `running`, then to `succeeded` (with `result.image.url`), `failed` (with `error.code`) or `canceled`. Retries after a temporary provider error happen inside `running`.

**Waiting.** Poll `GET /generations/{id}` (every 2–5 seconds is plenty), or send `Prefer: wait=<seconds>` (or `?wait=<seconds>`, at most 60) on `POST /generations` or `GET /generations/{id}` to hold the response until the generation is final: `200` when it is, `202` when the time ran out, with `Preference-Applied: wait=<seconds>` when the wait was honoured. A busy server may answer `202` at once without `Preference-Applied`; keep polling then.

**Idempotency.** Send `Idempotency-Key: <1–255 printable ASCII characters>` with every `POST /generations`. Repeating a request with the same key and body returns the original generation (with `Idempotent-Replayed: true`) and never creates or charges a second one; the same key with a different body, or from a credential of the other environment, is `409 IDEMPOTENCY_CONFLICT`. Keys are kept at least as long as the generation exists. A refused request (4xx) stores nothing, so you can retry it with the same key.

**Credits.** A generation with a live key costs 1 credit, charged when it is created (`creditsCharged`). If it fails, or you cancel it before it starts (`POST /generations/{id}/cancel`), the credit is refunded automatically (`creditsRefunded`). Without enough credits you get `402 INSUFFICIENT_CREDITS` with `details.topUpUrl`; nothing is created. Live generations also count towards the concurrency and daily caps (see Rate limits).

**Test keys.** With an `ss_test_…` key everything works the same way (validation, uploads of the test environment, queue, statuses, idempotency, result URLs and delete), but no credits are used and no image model runs: the result is a sample image of exactly the requested size, and a missing caption becomes `Sample caption (test mode)`. Test generations do not count towards the generation caps.

**Results and retention.** Result URLs are signed and expire after 15 minutes (`result.urlsExpireAt`); `GET` the generation again for fresh ones. Results are kept until you `DELETE /generations/{id}` (results of test keys are deleted 30 days after they finish); a deleted generation keeps its record with `deletedAt` set. Input uploads expire after 30 days, so a generation queued from an expired upload fails with `UPLOAD_NOT_READY` and is refunded.

Samples in curl, Node.js and Python: [Quickstart](https://storeshot.art/docs/quickstart/).

## Screenshot sets
`POST /screenshot-sets` turns 1–10 screenshots into a whole store listing in one call: every screen on every device of one or both stores, plus an optional Google Play feature graphic (`featureGraphic: true`), with one caption per screen and one palette and logo for all. Each image is a normal generation (listed by `GET /generations?screenshotSetId=<id>`), so statuses, refunds and test keys work exactly as above.

**Devices.** Without `devices`, a store gets the sizes its required screenshot groups ask for: the App Store `app_store_1290x2796` (6.9") and `app_store_1179x2556` (6.3"), Google Play `google_play_1080x1920`. Pass `devices` to choose fewer (or others, such as iPad or tablet sizes): only sizes that meet the store rules (`GET /stores/{store}/requirements`) are accepted, one per device class. A set has at most 50 images.

**Credits.** With a live key every image costs 1 credit, so a set costs `screens × devices (+1 for the feature graphic)`. Ask first with `?dryRun=true`: the quote (`images`, `credits`, `availableCredits`, `sufficient`) validates the request exactly like a create but creates and charges nothing. The create charges all images at once or none (`402 INSUFFICIENT_CREDITS` with `details.requiredCredits`); the `202` response shows `creditsCharged`. A failed or canceled image is refunded; `POST /screenshot-sets/{id}/retry` creates new images for the failed ones only (1 credit each, again all or none). Repeating a retry never charges twice. A set counts as one generation towards the concurrency cap and all its images towards the daily cap.

**Status and download.** `GET /screenshot-sets/{id}` reports `queued`, `running`, `succeeded`, `partially_failed`, `failed` or `canceled`, with `counts` and every image. `GET /screenshot-sets/{id}/archive` streams a ZIP of the succeeded images plus `manifest.json` (`complete: false` lists what is missing): `<store>/<device>/<NN>.png`, or with `?layout=fastlane` the folders fastlane `deliver` (`fastlane/screenshots/en-US/`) and `supply` (`fastlane/metadata/android/en-US/images/`) read. `supply` replaces the images already in your Play listing.

Samples in curl, Node.js and Python: [Quickstart](https://storeshot.art/docs/quickstart/).

## Webhooks
Instead of polling, register an `https://` endpoint with `POST /webhooks` (scope `webhooks:write`, not granted by default) and receive a signed `POST` when a generation finishes (`generation.succeeded`, `generation.failed` incl. canceled), a screenshot set finishes (`screenshot_set.completed`) or the live balance falls below `creditsLowThreshold` (`credits.low`). A webhook belongs to one environment: test-key generations and sets reach test webhooks only. `POST /webhooks/{id}/test` sends a `webhook.test` event; `GET /webhooks/{id}/deliveries` shows every attempt. The event bodies are documented under `webhooks` in this document.

**Delivery.** At least once, unordered, from `StoreshotWebhooks/1.0`. Any 2xx within 10 seconds acknowledges; redirects are not followed. Failures are retried after about 1 min, 5 min, 30 min, 1 h, 3 h, 6 h and 12 h (8 attempts, ~23 h), then the delivery is `exhausted`. Destinations must stay public: every attempt resolves the host again and refuses private addresses. Deduplicate on `Storeshot-Event-Id`. Generation payloads carry no download URLs: `GET /generations/{id}` for fresh ones.

**Signatures.** `Storeshot-Signature: t=<unix seconds>,v1=<hex>` where `v1` = HMAC-SHA256(secret, `<t>.<raw body>`). Compute it over the exact bytes received, compare in constant time, accept any `v1` (two during a secret rotation grace) and reject a `t` more than 5 minutes away. The secret (`whsec_…`) is shown only by create and `POST /webhooks/{id}/rotate-secret`. Verification code for Node and Python: `docs/api/webhooks.md`.

**Limits.** 10 webhooks per environment, 10 test events per webhook per hour. URLs are shown masked (`https://<host>/…<last 4 path characters>`); delivery logs are kept 30 days.

## Billing
Buy one-time credit packs: `GET /billing/packs` lists them (empty while web payments are switched off), `POST /billing/checkout` with a `pack` returns a hosted checkout `url` for the buyer, and `GET /billing/checkouts/{id}` reports `open`, `processing`, `paid`, `partially_refunded`, `refunded`, `expired` or `failed`. Purchased credits are added once the payment succeeds and never expire; the payment provider is the merchant of record and applies taxes at checkout. A refund takes back the refunded share of the credits as far as they are unused. Scopes `billing:read` and `billing:write` are not granted by default; checkouts need a live credential. At most 10 checkouts per account per hour.

## Helpers
Free building blocks for agents (no credits; counted by the request rate limits only). `POST /palettes` extracts a brand palette from an upload or an `https://` URL with the same local algorithm as the Storeshot app (`extract`), or the dominant colour of up to 10 images (`dominant`); no AI model is involved and URL inputs are never stored. `GET /stores` and `GET /stores/{store}/requirements` describe what the App Store and Google Play need (formats, counts, sizes or dimension rules, required classes, the feature graphic), with the official pages they come from and which `GET /devices` sizes meet them.
