# Generations API reference

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

Turn a screenshot into a store-ready marketing image. Contract 1.18.0; base URL `https://api.storeshot.art/v1`. Schemas: https://storeshot.art/docs/api/schemas.md

## GET /generations

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

Requires `generations:read` or `generations:write`. Generations 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. |
| `status` | query | `GenerationStatus` | no |  |
| `metadata` | query | `string` | no | `<key>:<value>`: only generations whose metadata has exactly this value. |
| `screenshotSetId` | query | `string` | no | Only the images of this screenshot set (every attempt). Format: uuid |
| `environment` | query | `ApiKeyEnvironment` | no |  |
| `source` | query | `GenerationSource` | no |  |
| `projectId` | query | `string` | no | Since 1.15.0: only generations of this project (its own images and the images of its screenshot sets), or `none` for generations in no project. |

**Responses:**

| Status | Description | Schema | Headers |
|---|---|---|---|
| `200` | A page of generations. | `GenerationList` | `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": "0b8e7a52-41c6-4f38-9d0e-7f6a1c2b3d4e",
      "status": "succeeded",
      "environment": "live",
      "inputs": [
        {
          "uploadId": "6f1c1f0e-8a5e-4b8e-9a51-0f0e1d2c3b4a",
          "sourceUrl": null
        }
      ],
      "store": "app_store",
      "device": "app_store_1290x2796",
      "size": {
        "width": 1290,
        "height": 2796
      },
      "orientation": "portrait",
      "app": {
        "name": "Trailmate",
        "description": "Offline hiking maps"
      },
      "caption": "Find every trail, even offline",
      "palette": [
        {
          "hex": "#0F766E",
          "name": "Teal"
        }
      ],
      "logo": null,
      "metadata": {
        "campaign": "spring",
        "locale": "en-US"
      },
      "creditsCharged": 1,
      "creditsRefunded": 0,
      "result": {
        "image": {
          "url": "https://storage.example.com/platform/users/5b0c7d2e-1f3a-4c5b-8d6e-7f8091a2b3c4/generations/job-9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d-a1.png?X-Amz-Expires=900&X-Amz-Signature=abc123",
          "contentType": "image/png",
          "width": 1290,
          "height": 2796
        },
        "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-22T10:15:00.000Z",
        "caption": "Find every trail, even offline",
        "palette": [
          {
            "hex": "#0F766E",
            "name": "Teal"
          }
        ]
      },
      "error": null,
      "createdAt": "2026-10-22T10:00:00.000Z",
      "startedAt": "2026-10-22T10:00:01.000Z",
      "finishedAt": "2026-10-22T10:00:24.000Z",
      "deletedAt": null,
      "source": "cli",
      "clientName": "cli/storeshot@1.2.0",
      "requestId": "3f1c2b4a-5d6e-4f70-8a9b-0c1d2e3f4a5b",
      "project": {
        "id": "2a4b6c8d-0e1f-4a2b-8c3d-4e5f6a7b8c9d",
        "name": "Trailmate"
      }
    }
  ],
  "nextCursor": "string"
}
```

## POST /generations

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

Requires `generations:write`. Turns one screenshot into one store-ready marketing image of exactly the requested size. Costs 1 credit with a live key, charged now and refunded if the generation fails or is canceled before it starts; test keys (`ss_test_…`) are free and return a sample image. Answers `202` with the queued generation (poll `GET /generations/{id}`), or `200` when it is already final (after `Prefer: wait`, or a replay of a finished one). Send an `Idempotency-Key` so a retried request never creates (or charges) twice.

**When to use.** For one image (one screenshot, one device). For a whole listing (several screens, devices or both stores) use `POST /screenshot-sets` instead.

**Credits.** 1 credit with a live key, charged at create and refunded automatically if the generation fails or is canceled before it starts; free with a test key (`ss_test_…`). Without enough credits: `402 INSUFFICIENT_CREDITS`, nothing created.

**Parameters:**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `wait` | query | `integer`, nullable | no | Seconds (0–60) to hold the response until the generation finishes. Overrides `Prefer: wait`. |
| `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. |
| `prefer` | header | `string` | no | `wait=<seconds>` (at most 60): hold the response until the generation finishes. |

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

| Field | Type | Required | Description |
|---|---|---|---|
| `inputs` | `array of GenerationInput` | yes | Exactly one screenshot in this version. |
| `store` | `StoreTarget` | yes |  |
| `device` | `string` | no | A `GET /devices` id of the same store. Exactly one of `device` and `size` is required. |
| `size` | `GenerationSize` | no |  |
| `orientation` | `Orientation` | no | Default: from `size` (wider than tall is landscape), else portrait (the feature graphic: landscape). Must agree with the size and the device. |
| `app` | `object` | yes |  |
| `app.name` | `string` | yes |  |
| `app.description` | `string` | no | Default: "" |
| `caption` | `string` | no | The marketing caption. Absent or blank: derived from the screenshot. |
| `palette` | `array of PaletteColor` | no | 1–8 colours. Absent: extracted from the screenshot. |
| `logo` | `GenerationInput` | no | Your app icon or logo (any shape; PNG, JPEG or WebP). |
| `metadata` | `GenerationMetadata` | no |  |
| `projectId` | `string` | no | Since 1.15.0: put the generation in this project of yours (`GET /projects`). The project does not change how it is generated. 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. It is copied into the generation: editing or deleting the style later changes nothing here. Format: uuid |

Example request body:

```json
{
  "inputs": [
    {
      "uploadId": "6f1c1f0e-8a5e-4b8e-9a51-0f0e1d2c3b4a"
    }
  ],
  "store": "app_store",
  "device": "app_store_1290x2796",
  "orientation": "portrait",
  "app": {
    "name": "Trailmate",
    "description": "Offline hiking maps"
  },
  "caption": "Find every trail, even offline",
  "palette": [
    {
      "hex": "#0F766E",
      "name": "Teal"
    }
  ],
  "metadata": {
    "campaign": "spring",
    "locale": "en-US"
  }
}
```

**Responses:**

| Status | Description | Schema | Headers |
|---|---|---|---|
| `200` | Final already (waited, or a replay of a finished generation). | `Generation` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy`, `Preference-Applied`, `Vary`, `Location`, `Idempotent-Replayed` |
| `202` | Accepted: queued or running. | `Generation` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy`, `Preference-Applied`, `Vary`, `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.generationId`) 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` (the JSON body is over 1 MB; refused before authentication, without RateLimit headers) or `IMAGE_TOO_LARGE` (a URL input is too large; see `details.reason`). | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `415` | `UNSUPPORTED_FORMAT`: not a PNG, JPEG or WebP image (see `details.reason`). | `ErrorEnvelope` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy` |
| `422` | `NOT_A_SCREENSHOT` (an input is not screenshot-sized; `details.path`) or `URL_FETCH_FAILED` (a URL input could not be fetched; 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` |
| `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
{
  "id": "0b8e7a52-41c6-4f38-9d0e-7f6a1c2b3d4e",
  "status": "succeeded",
  "environment": "live",
  "inputs": [
    {
      "uploadId": "6f1c1f0e-8a5e-4b8e-9a51-0f0e1d2c3b4a",
      "sourceUrl": null
    }
  ],
  "store": "app_store",
  "device": "app_store_1290x2796",
  "size": {
    "width": 1290,
    "height": 2796
  },
  "orientation": "portrait",
  "app": {
    "name": "Trailmate",
    "description": "Offline hiking maps"
  },
  "caption": "Find every trail, even offline",
  "palette": [
    {
      "hex": "#0F766E",
      "name": "Teal"
    }
  ],
  "logo": null,
  "metadata": {
    "campaign": "spring",
    "locale": "en-US"
  },
  "creditsCharged": 1,
  "creditsRefunded": 0,
  "result": {
    "image": {
      "url": "https://storage.example.com/platform/users/5b0c7d2e-1f3a-4c5b-8d6e-7f8091a2b3c4/generations/job-9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d-a1.png?X-Amz-Expires=900&X-Amz-Signature=abc123",
      "contentType": "image/png",
      "width": 1290,
      "height": 2796
    },
    "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-22T10:15:00.000Z",
    "caption": "Find every trail, even offline",
    "palette": [
      {
        "hex": "#0F766E",
        "name": "Teal"
      }
    ]
  },
  "error": null,
  "createdAt": "2026-10-22T10:00:00.000Z",
  "startedAt": "2026-10-22T10:00:01.000Z",
  "finishedAt": "2026-10-22T10:00:24.000Z",
  "deletedAt": null,
  "source": "cli",
  "clientName": "cli/storeshot@1.2.0",
  "requestId": "3f1c2b4a-5d6e-4f70-8a9b-0c1d2e3f4a5b",
  "project": {
    "id": "2a4b6c8d-0e1f-4a2b-8c3d-4e5f6a7b8c9d",
    "name": "Trailmate"
  }
}
```

Example 202 response:

```json
{
  "id": "0b8e7a52-41c6-4f38-9d0e-7f6a1c2b3d4e",
  "status": "succeeded",
  "environment": "live",
  "inputs": [
    {
      "uploadId": "6f1c1f0e-8a5e-4b8e-9a51-0f0e1d2c3b4a",
      "sourceUrl": null
    }
  ],
  "store": "app_store",
  "device": "app_store_1290x2796",
  "size": {
    "width": 1290,
    "height": 2796
  },
  "orientation": "portrait",
  "app": {
    "name": "Trailmate",
    "description": "Offline hiking maps"
  },
  "caption": "Find every trail, even offline",
  "palette": [
    {
      "hex": "#0F766E",
      "name": "Teal"
    }
  ],
  "logo": null,
  "metadata": {
    "campaign": "spring",
    "locale": "en-US"
  },
  "creditsCharged": 1,
  "creditsRefunded": 0,
  "result": {
    "image": {
      "url": "https://storage.example.com/platform/users/5b0c7d2e-1f3a-4c5b-8d6e-7f8091a2b3c4/generations/job-9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d-a1.png?X-Amz-Expires=900&X-Amz-Signature=abc123",
      "contentType": "image/png",
      "width": 1290,
      "height": 2796
    },
    "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-22T10:15:00.000Z",
    "caption": "Find every trail, even offline",
    "palette": [
      {
        "hex": "#0F766E",
        "name": "Teal"
      }
    ]
  },
  "error": null,
  "createdAt": "2026-10-22T10:00:00.000Z",
  "startedAt": "2026-10-22T10:00:01.000Z",
  "finishedAt": "2026-10-22T10:00:24.000Z",
  "deletedAt": null,
  "source": "cli",
  "clientName": "cli/storeshot@1.2.0",
  "requestId": "3f1c2b4a-5d6e-4f70-8a9b-0c1d2e3f4a5b",
  "project": {
    "id": "2a4b6c8d-0e1f-4a2b-8c3d-4e5f6a7b8c9d",
    "name": "Trailmate"
  }
}
```

## GET /generations/{id}

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

Requires `generations:read` or `generations:write`. Includes deleted generations (`deletedAt` set, `result` null). Every call returns freshly signed result URLs (valid 15 minutes). `Prefer: wait=<seconds>` (or `?wait`) holds the response until it is final.

**When to use.** To poll a generation every 2–5 seconds (or once with `Prefer: wait=60`) until `status` is `succeeded`, `failed` or `canceled`, and for fresh result URLs.

**Credits.** Free: no credits.

**Parameters:**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | `string` | yes | Generation id. An unknown or malformed id is 404. |
| `wait` | query | `integer`, nullable | no | Seconds (0–60) to hold the response until the generation finishes. Overrides `Prefer: wait`. |
| `prefer` | header | `string` | no | `wait=<seconds>` (at most 60): hold the response until the generation finishes. |

**Responses:**

| Status | Description | Schema | Headers |
|---|---|---|---|
| `200` | Final: succeeded, failed or canceled. | `Generation` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy`, `Preference-Applied`, `Vary` |
| `202` | Not final yet: queued or running. | `Generation` | `X-Request-Id`, `RateLimit`, `RateLimit-Policy`, `Preference-Applied`, `Vary` |
| `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": "0b8e7a52-41c6-4f38-9d0e-7f6a1c2b3d4e",
  "status": "succeeded",
  "environment": "live",
  "inputs": [
    {
      "uploadId": "6f1c1f0e-8a5e-4b8e-9a51-0f0e1d2c3b4a",
      "sourceUrl": null
    }
  ],
  "store": "app_store",
  "device": "app_store_1290x2796",
  "size": {
    "width": 1290,
    "height": 2796
  },
  "orientation": "portrait",
  "app": {
    "name": "Trailmate",
    "description": "Offline hiking maps"
  },
  "caption": "Find every trail, even offline",
  "palette": [
    {
      "hex": "#0F766E",
      "name": "Teal"
    }
  ],
  "logo": null,
  "metadata": {
    "campaign": "spring",
    "locale": "en-US"
  },
  "creditsCharged": 1,
  "creditsRefunded": 0,
  "result": {
    "image": {
      "url": "https://storage.example.com/platform/users/5b0c7d2e-1f3a-4c5b-8d6e-7f8091a2b3c4/generations/job-9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d-a1.png?X-Amz-Expires=900&X-Amz-Signature=abc123",
      "contentType": "image/png",
      "width": 1290,
      "height": 2796
    },
    "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-22T10:15:00.000Z",
    "caption": "Find every trail, even offline",
    "palette": [
      {
        "hex": "#0F766E",
        "name": "Teal"
      }
    ]
  },
  "error": null,
  "createdAt": "2026-10-22T10:00:00.000Z",
  "startedAt": "2026-10-22T10:00:01.000Z",
  "finishedAt": "2026-10-22T10:00:24.000Z",
  "deletedAt": null,
  "source": "cli",
  "clientName": "cli/storeshot@1.2.0",
  "requestId": "3f1c2b4a-5d6e-4f70-8a9b-0c1d2e3f4a5b",
  "project": {
    "id": "2a4b6c8d-0e1f-4a2b-8c3d-4e5f6a7b8c9d",
    "name": "Trailmate"
  }
}
```

Example 202 response:

```json
{
  "id": "0b8e7a52-41c6-4f38-9d0e-7f6a1c2b3d4e",
  "status": "succeeded",
  "environment": "live",
  "inputs": [
    {
      "uploadId": "6f1c1f0e-8a5e-4b8e-9a51-0f0e1d2c3b4a",
      "sourceUrl": null
    }
  ],
  "store": "app_store",
  "device": "app_store_1290x2796",
  "size": {
    "width": 1290,
    "height": 2796
  },
  "orientation": "portrait",
  "app": {
    "name": "Trailmate",
    "description": "Offline hiking maps"
  },
  "caption": "Find every trail, even offline",
  "palette": [
    {
      "hex": "#0F766E",
      "name": "Teal"
    }
  ],
  "logo": null,
  "metadata": {
    "campaign": "spring",
    "locale": "en-US"
  },
  "creditsCharged": 1,
  "creditsRefunded": 0,
  "result": {
    "image": {
      "url": "https://storage.example.com/platform/users/5b0c7d2e-1f3a-4c5b-8d6e-7f8091a2b3c4/generations/job-9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d-a1.png?X-Amz-Expires=900&X-Amz-Signature=abc123",
      "contentType": "image/png",
      "width": 1290,
      "height": 2796
    },
    "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-22T10:15:00.000Z",
    "caption": "Find every trail, even offline",
    "palette": [
      {
        "hex": "#0F766E",
        "name": "Teal"
      }
    ]
  },
  "error": null,
  "createdAt": "2026-10-22T10:00:00.000Z",
  "startedAt": "2026-10-22T10:00:01.000Z",
  "finishedAt": "2026-10-22T10:00:24.000Z",
  "deletedAt": null,
  "source": "cli",
  "clientName": "cli/storeshot@1.2.0",
  "requestId": "3f1c2b4a-5d6e-4f70-8a9b-0c1d2e3f4a5b",
  "project": {
    "id": "2a4b6c8d-0e1f-4a2b-8c3d-4e5f6a7b8c9d",
    "name": "Trailmate"
  }
}
```

## DELETE /generations/{id}

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

Requires `generations:write`. Deletes the result files of a finished generation now; the generation keeps its metadata with `deletedAt` set and no longer appears in lists. Idempotent. A queued or running generation cannot be deleted (`409 GENERATION_STATE_CONFLICT`): cancel it or wait.

**Credits.** Free: no credits and no refund (the image was made).

**Parameters:**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | `string` | yes | Generation 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` | `GENERATION_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 /generations/{id}/cancel

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

Requires `generations:write`. Only a `queued` generation that has not started can be canceled; its credit is refunded. Canceling a canceled generation returns it again. Once it is running it cannot be canceled (`409 GENERATION_STATE_CONFLICT`).

**Credits.** Free; refunds the credit of a generation that had not started.

**Parameters:**

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

**Responses:**

| Status | Description | Schema | Headers |
|---|---|---|---|
| `200` | Canceled. | `Generation` | `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` | `GENERATION_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": "0b8e7a52-41c6-4f38-9d0e-7f6a1c2b3d4e",
  "status": "succeeded",
  "environment": "live",
  "inputs": [
    {
      "uploadId": "6f1c1f0e-8a5e-4b8e-9a51-0f0e1d2c3b4a",
      "sourceUrl": null
    }
  ],
  "store": "app_store",
  "device": "app_store_1290x2796",
  "size": {
    "width": 1290,
    "height": 2796
  },
  "orientation": "portrait",
  "app": {
    "name": "Trailmate",
    "description": "Offline hiking maps"
  },
  "caption": "Find every trail, even offline",
  "palette": [
    {
      "hex": "#0F766E",
      "name": "Teal"
    }
  ],
  "logo": null,
  "metadata": {
    "campaign": "spring",
    "locale": "en-US"
  },
  "creditsCharged": 1,
  "creditsRefunded": 0,
  "result": {
    "image": {
      "url": "https://storage.example.com/platform/users/5b0c7d2e-1f3a-4c5b-8d6e-7f8091a2b3c4/generations/job-9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d-a1.png?X-Amz-Expires=900&X-Amz-Signature=abc123",
      "contentType": "image/png",
      "width": 1290,
      "height": 2796
    },
    "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-22T10:15:00.000Z",
    "caption": "Find every trail, even offline",
    "palette": [
      {
        "hex": "#0F766E",
        "name": "Teal"
      }
    ]
  },
  "error": null,
  "createdAt": "2026-10-22T10:00:00.000Z",
  "startedAt": "2026-10-22T10:00:01.000Z",
  "finishedAt": "2026-10-22T10:00:24.000Z",
  "deletedAt": null,
  "source": "cli",
  "clientName": "cli/storeshot@1.2.0",
  "requestId": "3f1c2b4a-5d6e-4f70-8a9b-0c1d2e3f4a5b",
  "project": {
    "id": "2a4b6c8d-0e1f-4a2b-8c3d-4e5f6a7b8c9d",
    "name": "Trailmate"
  }
}
```
