# Screenshot sets API reference

> HTML version: https://storeshot.art/docs/api/screenshot-sets/

A whole store listing (every screen on every device) in one call. Contract 1.18.0; base URL `https://api.storeshot.art/v1`. Schemas: https://storeshot.art/docs/api/schemas.md

## GET /screenshot-sets

**List screenshot sets** (operation `listScreenshotSets`). Auth: API key with scope `generations:read` or `generations:write`, or access token.

Requires `generations:read` or `generations:write`. Screenshot sets of the account that are not deleted (every API key and environment), newest first.

**Credits.** Free: no credits.

**Parameters:**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `limit` | query | `integer` | no | 1–100, default 20. Default: 20 |
| `cursor` | query | `string` | no | `nextCursor` of the previous page. |
| `metadata` | query | `string` | no | `<key>:<value>`: only sets whose metadata has exactly this value. |
| `environment` | query | `ApiKeyEnvironment` | no |  |
| `status` | query | `ScreenshotSetStatus` | no |  |
| `projectId` | query | `string` | no | Since 1.15.0: only sets of this project, or `none` for sets in no project. |

**Responses:**

| Status | Description | Schema | Headers |
|---|---|---|---|
| `200` | A page of screenshot sets. | `ScreenshotSetList` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `400` | `INVALID_INPUT`. A credential sent in the query string is refused before authentication, without RateLimit headers. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `401` | `UNAUTHORIZED`: missing, malformed, unknown, revoked or expired credential. | `ErrorEnvelope` | `X-Request-Id` |
| `403` | `FORBIDDEN`: see `details.reason`. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `429` | `RATE_LIMITED`: the request rate of the API key (`details.scope` `key`) or of the account (`user`) is exhausted for this window, or too many failed authentication attempts came from this IP (no `details.scope` and no RateLimit headers). Retry after `Retry-After` seconds. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy`, `Retry-After` |
| `default` | Error (see ErrorEnvelope and /v1/docs/errors). | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |

Example 200 response:

```json
{
  "data": [
    {
      "id": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d",
      "status": "queued",
      "environment": "live",
      "app": {
        "name": "string",
        "description": "string"
      },
      "orientation": "portrait",
      "palette": [],
      "paletteSource": "given",
      "logo": {
        "uploadId": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d"
      },
      "metadata": {},
      "screens": [
        {}
      ],
      "stores": [
        {}
      ],
      "images": 1,
      "counts": {
        "queued": 0,
        "running": 0,
        "succeeded": 0,
        "failed": 0,
        "canceled": 0
      },
      "creditsCharged": 0,
      "creditsRefunded": 0,
      "children": [
        {}
      ],
      "archiveUrl": "/v1/screenshot-sets/5c1e…/archive",
      "createdAt": "2026-10-21T12:00:00.000Z",
      "updatedAt": "2026-10-21T12:00:00.000Z",
      "finishedAt": "2026-10-21T12:00:00.000Z",
      "deletedAt": "2026-10-21T12:00:00.000Z",
      "preview": {
        "generationId": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
        "store": "app_store",
        "device": "app_store_1290x2796",
        "thumbnail": {
          "url": "https://storage.example.com/platform/users/5b0c7d2e-1f3a-4c5b-8d6e-7f8091a2b3c4/generations/job-9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d-a1.png.thumb.webp?X-Amz-Expires=900&X-Amz-Signature=def456",
          "contentType": "image/webp"
        },
        "urlsExpireAt": "2026-10-24T12:15:00.000Z"
      }
    }
  ],
  "nextCursor": "string"
}
```

## POST /screenshot-sets

**Create a screenshot set** (operation `createScreenshotSet`). Auth: API key with scope `generations:write`, or access token.

Requires `generations:write`. Turns 1–10 screenshots into a full store listing: every screen on every requested (or default) device of one or both stores, plus an optional Google Play feature graphic, with one caption per screen and one palette and logo for all. Each image is one generation (`GET /generations?screenshotSetId=`) and costs 1 credit with a live key: the whole set is created and charged at once, or nothing is (`402` with `details.requiredCredits`). Failed images are refunded; retry them with `POST /screenshot-sets/{id}/retry`. Test keys are free. `?dryRun=true` validates the same way and answers `200` with a quote (images and credits) without creating or charging anything. Send an `Idempotency-Key` so a retried request never creates (or charges) twice.

**When to use.** For a store listing: every screen on every device of one or both stores in one call. Quote it first with `?dryRun=true`, then create it with an `Idempotency-Key` and poll `GET /screenshot-sets/{id}`.

**Credits.** 1 credit per image with a live key (screens × devices, +1 for a feature graphic), all charged at create or none (`402`); `?dryRun=true` quotes for free. Failed or canceled images are refunded; free with a test key (`ss_test_…`).

**Parameters:**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `dryRun` | query | `string` | no | `true`: validate and quote only (`200 ScreenshotSetQuote`); nothing is created or charged. One of: "true", "false" |
| `idempotency-key` | header | `string` | no | 1–255 printable ASCII characters. Retrying with the same key and body returns the original generation (`Idempotent-Replayed: true`) instead of creating and charging another. Keys are kept as long as the generation exists. |

**Request body** (`application/json`, required, schema `CreateScreenshotSetRequest`):

| Field | Type | Required | Description |
|---|---|---|---|
| `app` | `object` | yes |  |
| `app.name` | `string` | yes |  |
| `app.description` | `string` | no | Default: "" |
| `screens` | `array of ScreenshotSetScreen` | yes | 1–10 screenshots, in listing order. |
| `stores` | `array of ScreenshotSetTarget` | yes |  |
| `orientation` | `Orientation` | no | Of the screenshots (the feature graphic is always landscape). Default: "portrait" |
| `palette` | `array of PaletteColor` | no | 1–8 colours. Absent: extracted once from screen 1 and used for every image. |
| `logo` | `SetUploadRef` | no | Your app icon or logo, used on every image. |
| `metadata` | `GenerationMetadata` | no |  |
| `projectId` | `string` | no | Since 1.15.0: put the set (and so every image of it, retries included) in this project of yours. Format: uuid |
| `styleId` | `string` | no | Since 1.18.0: a style of yours (`GET /styles`). It fills `palette` and `logo` when you leave them out and adds its background mood, description and caption tone to every image. It is copied into the set and its retries: editing or deleting the style later changes nothing here. With `?dryRun=true` the style is checked; styles are free, so the quote is the same. Format: uuid |

Example request body:

```json
{
  "app": {
    "name": "Trailmate",
    "description": "Offline hiking maps"
  },
  "screens": [
    {
      "input": {
        "uploadId": "6f1c1f0e-8a5e-4b8e-9a51-0f0e1d2c3b4a"
      },
      "caption": "Find every trail"
    },
    {
      "input": {
        "uploadId": "7a2d3c4b-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
      },
      "caption": "Works offline"
    }
  ],
  "stores": [
    {
      "store": "app_store"
    },
    {
      "store": "google_play",
      "devices": [
        "google_play_1080x1920",
        "google_play_1600x2560"
      ],
      "featureGraphic": true
    }
  ],
  "orientation": "portrait",
  "palette": [
    {
      "hex": "#0F766E",
      "name": "Teal"
    }
  ],
  "metadata": {
    "release": "2.4"
  }
}
```

**Responses:**

| Status | Description | Schema | Headers |
|---|---|---|---|
| `200` | `?dryRun=true`: the quote (nothing created or charged). Or a replay of a finished set. | application/json | `X-Request-Id`, `RateLimit`, `RateLimit-Policy`, `Location`, `Idempotent-Replayed` |
| `202` | Accepted: every image is queued. | `ScreenshotSet` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy`, `Location`, `Idempotent-Replayed` |
| `400` | `INVALID_INPUT`. A credential sent in the query string is refused before authentication, without RateLimit headers. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `401` | `UNAUTHORIZED`: missing, malformed, unknown, revoked or expired credential. | `ErrorEnvelope` | `X-Request-Id` |
| `402` | `INSUFFICIENT_CREDITS`: nothing was created or charged. See `details.availableCredits` and `details.topUpUrl`. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `403` | `FORBIDDEN`: see `details.reason`. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `409` | `IDEMPOTENCY_CONFLICT` (the `Idempotency-Key` was used for a different request; `details.screenshotSetId`) or `UPLOAD_NOT_READY` (an input upload is not usable; `details.reason`, `details.path`). | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `413` | `PAYLOAD_TOO_LARGE`. | `ErrorEnvelope` | `X-Request-Id` |
| `422` | `NOT_A_SCREENSHOT`: an input is not screenshot-sized (`details.path`). | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `429` | `RATE_LIMITED`: the request rate of the API key (`details.scope` `key`) or of the account (`user`) is exhausted for this window, or too many failed authentication attempts came from this IP (no `details.scope` and no RateLimit headers). Retry after `Retry-After` seconds. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy`, `Retry-After` |
| `503` | `SERVICE_UNAVAILABLE`: uploads are busy. Retry after `Retry-After` seconds. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy`, `Retry-After` |
| `default` | Error (see ErrorEnvelope and /v1/docs/errors). | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |

Example 200 response:

```json
{
  "dryRun": true,
  "environment": "live",
  "images": 1,
  "credits": 0,
  "availableCredits": 0,
  "sufficient": true,
  "screens": 1,
  "breakdown": [
    {
      "store": "app_store",
      "device": "app_store_1290x2796",
      "size": {
        "width": 1,
        "height": 1
      },
      "orientation": "portrait",
      "images": 1,
      "defaulted": true
    }
  ]
}
```

Example 202 response:

```json
{
  "id": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d",
  "status": "queued",
  "environment": "live",
  "app": {
    "name": "string",
    "description": "string"
  },
  "orientation": "portrait",
  "palette": [
    {
      "hex": "#0F766E",
      "name": "Teal"
    }
  ],
  "paletteSource": "given",
  "logo": {
    "uploadId": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d"
  },
  "metadata": {},
  "screens": [
    {
      "position": 1,
      "input": {
        "uploadId": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d"
      },
      "caption": "string"
    }
  ],
  "stores": [
    {
      "store": "app_store",
      "devices": [
        "string"
      ],
      "featureGraphic": {
        "screen": 1,
        "caption": "string"
      }
    }
  ],
  "images": 1,
  "counts": {
    "queued": 0,
    "running": 0,
    "succeeded": 0,
    "failed": 0,
    "canceled": 0
  },
  "creditsCharged": 0,
  "creditsRefunded": 0,
  "children": [
    {
      "position": 1,
      "store": "app_store",
      "device": "string",
      "size": {
        "width": 1,
        "height": 1
      },
      "featureGraphic": true,
      "generationId": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d",
      "attempt": 1,
      "status": "queued",
      "error": {
        "code": "GENERATION_FAILED",
        "message": "string"
      }
    }
  ],
  "archiveUrl": "/v1/screenshot-sets/5c1e…/archive",
  "createdAt": "2026-10-21T12:00:00.000Z",
  "updatedAt": "2026-10-21T12:00:00.000Z",
  "finishedAt": "2026-10-21T12:00:00.000Z",
  "deletedAt": "2026-10-21T12:00:00.000Z",
  "preview": {
    "generationId": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
    "store": "app_store",
    "device": "app_store_1290x2796",
    "thumbnail": {
      "url": "https://storage.example.com/platform/users/5b0c7d2e-1f3a-4c5b-8d6e-7f8091a2b3c4/generations/job-9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d-a1.png.thumb.webp?X-Amz-Expires=900&X-Amz-Signature=def456",
      "contentType": "image/webp"
    },
    "urlsExpireAt": "2026-10-24T12:15:00.000Z"
  },
  "source": "mobile",
  "project": {
    "id": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d",
    "name": "Trailmate"
  },
  "style": {
    "id": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d",
    "name": "Trailmate brand",
    "applied": []
  }
}
```

## GET /screenshot-sets/{id}

**Get a screenshot set** (operation `getScreenshotSet`). Auth: API key with scope `generations:read` or `generations:write`, or access token.

Requires `generations:read` or `generations:write`. The status is computed from the current image of every slot. Includes deleted sets (`deletedAt` set). Poll every few seconds until the status is final (`succeeded`, `partially_failed`, `failed` or `canceled`).

**When to use.** To poll a set every 3–5 seconds until `status` is `succeeded`, `partially_failed`, `failed` or `canceled`.

**Credits.** Free: no credits.

**Parameters:**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | `string` | yes | Screenshot set id. An unknown or malformed id is 404. |

**Responses:**

| Status | Description | Schema | Headers |
|---|---|---|---|
| `200` | The screenshot set. | `ScreenshotSet` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `400` | `INVALID_INPUT`. A credential sent in the query string is refused before authentication, without RateLimit headers. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `401` | `UNAUTHORIZED`: missing, malformed, unknown, revoked or expired credential. | `ErrorEnvelope` | `X-Request-Id` |
| `403` | `FORBIDDEN`: see `details.reason`. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `404` | `NOT_FOUND`: no such resource for this caller. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `429` | `RATE_LIMITED`: the request rate of the API key (`details.scope` `key`) or of the account (`user`) is exhausted for this window, or too many failed authentication attempts came from this IP (no `details.scope` and no RateLimit headers). Retry after `Retry-After` seconds. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy`, `Retry-After` |
| `default` | Error (see ErrorEnvelope and /v1/docs/errors). | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |

Example 200 response:

```json
{
  "id": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d",
  "status": "queued",
  "environment": "live",
  "app": {
    "name": "string",
    "description": "string"
  },
  "orientation": "portrait",
  "palette": [
    {
      "hex": "#0F766E",
      "name": "Teal"
    }
  ],
  "paletteSource": "given",
  "logo": {
    "uploadId": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d"
  },
  "metadata": {},
  "screens": [
    {
      "position": 1,
      "input": {
        "uploadId": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d"
      },
      "caption": "string"
    }
  ],
  "stores": [
    {
      "store": "app_store",
      "devices": [
        "string"
      ],
      "featureGraphic": {
        "screen": 1,
        "caption": "string"
      }
    }
  ],
  "images": 1,
  "counts": {
    "queued": 0,
    "running": 0,
    "succeeded": 0,
    "failed": 0,
    "canceled": 0
  },
  "creditsCharged": 0,
  "creditsRefunded": 0,
  "children": [
    {
      "position": 1,
      "store": "app_store",
      "device": "string",
      "size": {
        "width": 1,
        "height": 1
      },
      "featureGraphic": true,
      "generationId": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d",
      "attempt": 1,
      "status": "queued",
      "error": {
        "code": "GENERATION_FAILED",
        "message": "string"
      }
    }
  ],
  "archiveUrl": "/v1/screenshot-sets/5c1e…/archive",
  "createdAt": "2026-10-21T12:00:00.000Z",
  "updatedAt": "2026-10-21T12:00:00.000Z",
  "finishedAt": "2026-10-21T12:00:00.000Z",
  "deletedAt": "2026-10-21T12:00:00.000Z",
  "preview": {
    "generationId": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
    "store": "app_store",
    "device": "app_store_1290x2796",
    "thumbnail": {
      "url": "https://storage.example.com/platform/users/5b0c7d2e-1f3a-4c5b-8d6e-7f8091a2b3c4/generations/job-9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d-a1.png.thumb.webp?X-Amz-Expires=900&X-Amz-Signature=def456",
      "contentType": "image/webp"
    },
    "urlsExpireAt": "2026-10-24T12:15:00.000Z"
  },
  "source": "mobile",
  "project": {
    "id": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d",
    "name": "Trailmate"
  },
  "style": {
    "id": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d",
    "name": "Trailmate brand",
    "applied": []
  }
}
```

## DELETE /screenshot-sets/{id}

**Delete a screenshot set** (operation `deleteScreenshotSet`). Auth: API key with scope `generations:write`, or access token.

Requires `generations:write`. Deletes the result files of every image (retries included) and marks the set deleted; it keeps its record with `deletedAt` set and no longer appears in lists. Idempotent. Not possible while images are queued or running (`409 SCREENSHOT_SET_STATE_CONFLICT`): cancel the set or wait.

**Credits.** Free: no credits and no refund (the images were made).

**Parameters:**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | `string` | yes | Screenshot set id. An unknown or malformed id is 404. |

**Responses:**

| Status | Description | Schema | Headers |
|---|---|---|---|
| `204` | Deleted. | — | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `400` | `INVALID_INPUT`. A credential sent in the query string is refused before authentication, without RateLimit headers. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `401` | `UNAUTHORIZED`: missing, malformed, unknown, revoked or expired credential. | `ErrorEnvelope` | `X-Request-Id` |
| `403` | `FORBIDDEN`: see `details.reason`. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `404` | `NOT_FOUND`: no such resource for this caller. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `409` | `SCREENSHOT_SET_STATE_CONFLICT`: not possible in the current status (`details.status`, `details.action`). | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `429` | `RATE_LIMITED`: the request rate of the API key (`details.scope` `key`) or of the account (`user`) is exhausted for this window, or too many failed authentication attempts came from this IP (no `details.scope` and no RateLimit headers). Retry after `Retry-After` seconds. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy`, `Retry-After` |
| `default` | Error (see ErrorEnvelope and /v1/docs/errors). | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |

## POST /screenshot-sets/{id}/retry

**Retry failed images of a screenshot set** (operation `retryScreenshotSet`). Auth: API key with scope `generations:write`, or access token.

Requires `generations:write` and a credential of the set’s environment. Creates a new image (attempt + 1, at most 5 per slot) for every failed or canceled image, or for the given `generationIds`, at 1 credit each with a live key, all at once or none (`402`). Images that are superseded, still running or succeeded are skipped (`retry.skipped`); if nothing is left to retry the answer is `409 SCREENSHOT_SET_STATE_CONFLICT` and nothing is charged, so repeating a retry never charges twice.

**When to use.** After a set ends `partially_failed` or `failed`, to make new images for the failed slots only.

**Credits.** 1 credit per new image with a live key, all or none (`402`); nothing when there is nothing to retry (`409`); free with a test key (`ss_test_…`).

**Parameters:**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | `string` | yes | Screenshot set id. An unknown or malformed id is 404. |

**Request body** (`application/json`, optional, schema `RetryScreenshotSetRequest`):

| Field | Type | Required | Description |
|---|---|---|---|
| `generationIds` | `array of string` | no | Images of this set to retry. Absent: every failed or canceled image. |

Example request body:

```json
{
  "generationIds": [
    "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d"
  ]
}
```

**Responses:**

| Status | Description | Schema | Headers |
|---|---|---|---|
| `202` | Accepted: the new images are queued. | `RetriedScreenshotSet` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `400` | `INVALID_INPUT`. A credential sent in the query string is refused before authentication, without RateLimit headers. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `401` | `UNAUTHORIZED`: missing, malformed, unknown, revoked or expired credential. | `ErrorEnvelope` | `X-Request-Id` |
| `402` | `INSUFFICIENT_CREDITS`: nothing was created or charged. See `details.availableCredits` and `details.topUpUrl`. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `403` | `FORBIDDEN`: see `details.reason`. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `404` | `NOT_FOUND`: no such resource for this caller. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `409` | `SCREENSHOT_SET_STATE_CONFLICT` (nothing to retry; `details.status`) or `UPLOAD_NOT_READY` (an input upload expired or was deleted; `details.path`). | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `429` | `RATE_LIMITED`: the request rate of the API key (`details.scope` `key`) or of the account (`user`) is exhausted for this window, or too many failed authentication attempts came from this IP (no `details.scope` and no RateLimit headers). Retry after `Retry-After` seconds. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy`, `Retry-After` |
| `default` | Error (see ErrorEnvelope and /v1/docs/errors). | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |

Example 202 response:

```json
{
  "id": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d",
  "status": "queued",
  "environment": "live",
  "app": {
    "name": "string",
    "description": "string"
  },
  "orientation": "portrait",
  "palette": [
    {}
  ],
  "paletteSource": "given",
  "logo": {
    "uploadId": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d"
  },
  "metadata": {},
  "screens": [
    {
      "position": 1,
      "input": {},
      "caption": "string"
    }
  ],
  "stores": [
    {
      "devices": [],
      "featureGraphic": {}
    }
  ],
  "images": 1,
  "counts": {
    "queued": 0,
    "running": 0,
    "succeeded": 0,
    "failed": 0,
    "canceled": 0
  },
  "creditsCharged": 0,
  "creditsRefunded": 0,
  "children": [
    {
      "position": 1,
      "device": "string",
      "size": {},
      "featureGraphic": true,
      "generationId": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d",
      "attempt": 1,
      "error": {}
    }
  ],
  "archiveUrl": "/v1/screenshot-sets/5c1e…/archive",
  "createdAt": "2026-10-21T12:00:00.000Z",
  "updatedAt": "2026-10-21T12:00:00.000Z",
  "finishedAt": "2026-10-21T12:00:00.000Z",
  "deletedAt": "2026-10-21T12:00:00.000Z",
  "preview": {
    "generationId": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
    "store": "app_store",
    "device": "app_store_1290x2796",
    "thumbnail": {
      "url": "https://storage.example.com/platform/users/5b0c7d2e-1f3a-4c5b-8d6e-7f8091a2b3c4/generations/job-9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d-a1.png.thumb.webp?X-Amz-Expires=900&X-Amz-Signature=def456",
      "contentType": "image/webp"
    },
    "urlsExpireAt": "2026-10-24T12:15:00.000Z"
  },
  "source": "mobile",
  "project": {},
  "style": {},
  "retry": {
    "created": [
      {}
    ],
    "skipped": [
      {}
    ]
  }
}
```

## POST /screenshot-sets/{id}/cancel

**Cancel a screenshot set** (operation `cancelScreenshotSet`). Auth: API key with scope `generations:write`, or access token.

Requires `generations:write`. Cancels every image that has not started yet (each is refunded); running images finish normally. Canceling a canceled set returns it again; a set that already finished otherwise is `409 SCREENSHOT_SET_STATE_CONFLICT`.

**Credits.** Free; refunds every image that had not started.

**Parameters:**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | `string` | yes | Screenshot set id. An unknown or malformed id is 404. |

**Responses:**

| Status | Description | Schema | Headers |
|---|---|---|---|
| `200` | The set after canceling its queued images. | `ScreenshotSet` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `400` | `INVALID_INPUT`. A credential sent in the query string is refused before authentication, without RateLimit headers. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `401` | `UNAUTHORIZED`: missing, malformed, unknown, revoked or expired credential. | `ErrorEnvelope` | `X-Request-Id` |
| `403` | `FORBIDDEN`: see `details.reason`. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `404` | `NOT_FOUND`: no such resource for this caller. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `409` | `SCREENSHOT_SET_STATE_CONFLICT`: not possible in the current status (`details.status`, `details.action`). | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `429` | `RATE_LIMITED`: the request rate of the API key (`details.scope` `key`) or of the account (`user`) is exhausted for this window, or too many failed authentication attempts came from this IP (no `details.scope` and no RateLimit headers). Retry after `Retry-After` seconds. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy`, `Retry-After` |
| `default` | Error (see ErrorEnvelope and /v1/docs/errors). | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |

Example 200 response:

```json
{
  "id": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d",
  "status": "queued",
  "environment": "live",
  "app": {
    "name": "string",
    "description": "string"
  },
  "orientation": "portrait",
  "palette": [
    {
      "hex": "#0F766E",
      "name": "Teal"
    }
  ],
  "paletteSource": "given",
  "logo": {
    "uploadId": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d"
  },
  "metadata": {},
  "screens": [
    {
      "position": 1,
      "input": {
        "uploadId": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d"
      },
      "caption": "string"
    }
  ],
  "stores": [
    {
      "store": "app_store",
      "devices": [
        "string"
      ],
      "featureGraphic": {
        "screen": 1,
        "caption": "string"
      }
    }
  ],
  "images": 1,
  "counts": {
    "queued": 0,
    "running": 0,
    "succeeded": 0,
    "failed": 0,
    "canceled": 0
  },
  "creditsCharged": 0,
  "creditsRefunded": 0,
  "children": [
    {
      "position": 1,
      "store": "app_store",
      "device": "string",
      "size": {
        "width": 1,
        "height": 1
      },
      "featureGraphic": true,
      "generationId": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d",
      "attempt": 1,
      "status": "queued",
      "error": {
        "code": "GENERATION_FAILED",
        "message": "string"
      }
    }
  ],
  "archiveUrl": "/v1/screenshot-sets/5c1e…/archive",
  "createdAt": "2026-10-21T12:00:00.000Z",
  "updatedAt": "2026-10-21T12:00:00.000Z",
  "finishedAt": "2026-10-21T12:00:00.000Z",
  "deletedAt": "2026-10-21T12:00:00.000Z",
  "preview": {
    "generationId": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
    "store": "app_store",
    "device": "app_store_1290x2796",
    "thumbnail": {
      "url": "https://storage.example.com/platform/users/5b0c7d2e-1f3a-4c5b-8d6e-7f8091a2b3c4/generations/job-9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d-a1.png.thumb.webp?X-Amz-Expires=900&X-Amz-Signature=def456",
      "contentType": "image/webp"
    },
    "urlsExpireAt": "2026-10-24T12:15:00.000Z"
  },
  "source": "mobile",
  "project": {
    "id": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d",
    "name": "Trailmate"
  },
  "style": {
    "id": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d",
    "name": "Trailmate brand",
    "applied": []
  }
}
```

## GET /screenshot-sets/{id}/archive

**Download a screenshot set as a ZIP** (operation `getScreenshotSetArchive`). Auth: API key with scope `generations:read` or `generations:write`, or access token.

Requires `generations:read` or `generations:write`. Streams a ZIP of every succeeded image (the current attempt of each slot) and a `manifest.json` listing the files and the missing images. `layout=store` (default): `<store>/<device>/<NN>.<ext>`. `layout=fastlane`: `fastlane/screenshots/en-US/<NN>_<device>.png` for `deliver` and `fastlane/metadata/android/en-US/images/{phoneScreenshots,sevenInchScreenshots,tenInchScreenshots}/<NN>.png` + `featureGraphic.jpg` for `supply` (note: `supply` replaces the images already in your listing). `409 SCREENSHOT_SET_STATE_CONFLICT` when no image has succeeded yet; `503` with `Retry-After` when the server is busy building other archives.

**When to use.** Once the set is final, to save every image: `?layout=fastlane` for the fastlane folders, or the default `<store>/<device>/<NN>.png` layout.

**Credits.** Free: no credits.

**Parameters:**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | `string` | yes | Screenshot set id. An unknown or malformed id is 404. |
| `layout` | query | `string` | no | `store`: `<store>/<device>/<NN>.<ext>`. `fastlane`: `fastlane/screenshots/en-US/` (deliver) and `fastlane/metadata/android/en-US/images/` (supply). One of: "store", "fastlane" Default: "store" |

**Responses:**

| Status | Description | Schema | Headers |
|---|---|---|---|
| `200` | The ZIP (streamed). | application/zip | `X-Request-Id`, `RateLimit`, `RateLimit-Policy`, `Content-Disposition` |
| `400` | `INVALID_INPUT`. A credential sent in the query string is refused before authentication, without RateLimit headers. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `401` | `UNAUTHORIZED`: missing, malformed, unknown, revoked or expired credential. | `ErrorEnvelope` | `X-Request-Id` |
| `403` | `FORBIDDEN`: see `details.reason`. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `404` | `NOT_FOUND`: no such resource for this caller. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `409` | `SCREENSHOT_SET_STATE_CONFLICT`: not possible in the current status (`details.status`, `details.action`). | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `429` | `RATE_LIMITED`: the request rate of the API key (`details.scope` `key`) or of the account (`user`) is exhausted for this window, or too many failed authentication attempts came from this IP (no `details.scope` and no RateLimit headers). Retry after `Retry-After` seconds. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy`, `Retry-After` |
| `503` | `SERVICE_UNAVAILABLE`: too many archives are being built. Retry after `Retry-After` seconds. | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy`, `Retry-After` |
| `default` | Error (see ErrorEnvelope and /v1/docs/errors). | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
