Docs menu

API reference

Every operation of the StoreShot API, generated from the OpenAPI 1.18.0 contract.

View as Markdown

Basics

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: Service status. (2 operations )
  • Catalog: Static reference data. (3 operations )
  • Account: The authenticated caller. (1 operation )
  • API keys: Create, list and revoke API keys. (5 operations )
  • Usage: Credit balance and generation usage. (2 operations )
  • Uploads: Hand over images once; reference them in generations. (6 operations )
  • Generations: Turn a screenshot into a store-ready marketing image. (5 operations )
  • Screenshot sets: A whole store listing (every screen on every device) in one call. (7 operations )
  • Projects: Group generations and screenshot sets per app. (7 operations )
  • Styles: Brand kits: save a palette, logo, background and caption tone once and reuse them with styleId. (5 operations )
  • Palettes: Free brand palettes from your images. (1 operation )
  • Webhooks: Signed HTTPS callbacks when generations and sets finish. (8 operations , 5 webhook events)
  • Activity: Who changed what in the account. (1 operation )
  • Billing: Buy credit packs (hosted checkout). (10 operations )
  • Schemas: 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.

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.

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.