# Projects API reference

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

Group generations and screenshot sets per app. Contract 1.18.0; base URL `https://api.storeshot.art/v1`. Schemas: https://storeshot.art/docs/api/schemas.md

## GET /projects

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

Requires `generations:read` or `generations:write`. Projects of the account (every API key and environment), most recently updated 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. |

**Responses:**

| Status | Description | Schema | Headers |
|---|---|---|---|
| `200` | A page of projects. | `ProjectList` | `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": "2a4b6c8d-0e1f-4a2b-8c3d-4e5f6a7b8c9d",
      "name": "Trailmate",
      "description": "Offline hiking maps",
      "store": "app_store",
      "palette": [
        {
          "hex": "#0F766E",
          "name": "Teal"
        }
      ],
      "logo": null,
      "counts": {
        "generations": 3,
        "screenshotSets": 1
      },
      "preview": null,
      "createdAt": "2026-10-29T10:00:00.000Z",
      "updatedAt": "2026-10-29T10:05:00.000Z"
    }
  ],
  "nextCursor": "string"
}
```

## POST /projects

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

Requires `generations:write`. A project groups the generations and screenshot sets of one app (name, store, palette, logo). Add items with `POST /projects/{id}/items` or `projectId` when creating them. At most 200 projects per account (`403 FORBIDDEN`, `details.reason` `project_limit_reached`).

**Credits.** Free: no credits.

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

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | `string` | yes |  |
| `description` | `string` | no |  |
| `store` | `StoreTarget` | no | Default `google_play`. |
| `palette` | `array of PaletteColor` | no |  |
| `logo` | `object` | no |  |
| `logo.uploadId` | `string` | yes | A `ready` upload of this account (PNG, JPEG or WebP; either environment). It is copied: deleting the upload later keeps the logo. Format: uuid |

Example request body:

```json
{
  "name": "Trailmate",
  "description": "Offline hiking maps",
  "store": "app_store",
  "palette": [
    {
      "hex": "#0F766E",
      "name": "Teal"
    }
  ]
}
```

**Responses:**

| Status | Description | Schema | Headers |
|---|---|---|---|
| `201` | Created. | `Project` | `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 201 response:

```json
{
  "id": "2a4b6c8d-0e1f-4a2b-8c3d-4e5f6a7b8c9d",
  "name": "Trailmate",
  "description": "Offline hiking maps",
  "store": "app_store",
  "palette": [
    {
      "hex": "#0F766E",
      "name": "Teal"
    }
  ],
  "logo": null,
  "counts": {
    "generations": 3,
    "screenshotSets": 1
  },
  "preview": null,
  "createdAt": "2026-10-29T10:00:00.000Z",
  "updatedAt": "2026-10-29T10:05:00.000Z"
}
```

## GET /projects/{id}

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

Requires `generations:read` or `generations:write`. List its items with `GET /generations?projectId=` and `GET /screenshot-sets?projectId=`.

**Credits.** Free: no credits.

**Parameters:**

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

**Responses:**

| Status | Description | Schema | Headers |
|---|---|---|---|
| `200` | The project. | `Project` | `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": "2a4b6c8d-0e1f-4a2b-8c3d-4e5f6a7b8c9d",
  "name": "Trailmate",
  "description": "Offline hiking maps",
  "store": "app_store",
  "palette": [
    {
      "hex": "#0F766E",
      "name": "Teal"
    }
  ],
  "logo": null,
  "counts": {
    "generations": 3,
    "screenshotSets": 1
  },
  "preview": null,
  "createdAt": "2026-10-29T10:00:00.000Z",
  "updatedAt": "2026-10-29T10:05:00.000Z"
}
```

## DELETE /projects/{id}

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

Requires `generations:write`. Its generations and screenshot sets are kept, in no project; its logo is deleted.

**Credits.** Free: no credits.

**Parameters:**

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | `string` | yes | Project 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` |
| `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` |

## PATCH /projects/{id}

**Update a project** (operation `updateProject`). Auth: API key with scope `generations:write`, or access token.

Requires `generations:write`. Changes only the fields sent: `palette: []` clears the palette, `logo: null` removes the logo.

**Credits.** Free: no credits.

**Parameters:**

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

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

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | `string` | no |  |
| `description` | `string` | no |  |
| `store` | `StoreTarget` | no |  |
| `palette` | `array of PaletteColor` | no | Replaces the palette; `[]` clears it. |
| `logo` | `object`, nullable | no | A new logo, or `null` to remove it. |
| `logo.uploadId` | `string` | yes | A `ready` upload of this account (PNG, JPEG or WebP; either environment). It is copied: deleting the upload later keeps the logo. Format: uuid |

Example request body:

```json
{
  "name": "Trailmate 2",
  "logo": null
}
```

**Responses:**

| Status | Description | Schema | Headers |
|---|---|---|---|
| `200` | The updated project. | `Project` | `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": "2a4b6c8d-0e1f-4a2b-8c3d-4e5f6a7b8c9d",
  "name": "Trailmate",
  "description": "Offline hiking maps",
  "store": "app_store",
  "palette": [
    {
      "hex": "#0F766E",
      "name": "Teal"
    }
  ],
  "logo": null,
  "counts": {
    "generations": 3,
    "screenshotSets": 1
  },
  "preview": null,
  "createdAt": "2026-10-29T10:00:00.000Z",
  "updatedAt": "2026-10-29T10:05:00.000Z"
}
```

## POST /projects/{id}/items

**Add items to a project** (operation `addProjectItems`). Auth: API key with scope `generations:write`, or access token.

Requires `generations:write`. Puts standalone generations and whole screenshot sets in this project; an item in another project moves here (an item is in one project at most). Repeating it changes nothing. An image of a screenshot set is in the project of its set and cannot be added alone.

**Credits.** Free: no credits.

**Parameters:**

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

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

| Field | Type | Required | Description |
|---|---|---|---|
| `generationIds` | `array of string` | no | Ids of your standalone generations (an image of a set moves with its set). |
| `screenshotSetIds` | `array of string` | no | Ids of your screenshot sets. |

Example request body:

```json
{
  "generationIds": [
    "0b8e7a52-41c6-4f38-9d0e-7f6a1c2b3d4e"
  ]
}
```

**Responses:**

| Status | Description | Schema | Headers |
|---|---|---|---|
| `200` | The items are in the project. | `ProjectItemsAdded` | `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
{
  "projectId": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d",
  "added": 0
}
```

## POST /projects/{id}/items/remove

**Remove items from a project** (operation `removeProjectItems`). Auth: API key with scope `generations:write`, or access token.

Requires `generations:write`. Takes the items out of this project (they are kept, in no project). Items that are not in this project are left alone.

**Credits.** Free: no credits.

**Parameters:**

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

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

| Field | Type | Required | Description |
|---|---|---|---|
| `generationIds` | `array of string` | no | Ids of your standalone generations (an image of a set moves with its set). |
| `screenshotSetIds` | `array of string` | no | Ids of your screenshot sets. |

Example request body:

```json
{
  "generationIds": [
    "0b8e7a52-41c6-4f38-9d0e-7f6a1c2b3d4e"
  ]
}
```

**Responses:**

| Status | Description | Schema | Headers |
|---|---|---|---|
| `200` | The items left it. | `ProjectItemsRemoved` | `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
{
  "projectId": "7d0e1a52-3c4b-4f6e-9a8d-2b1c0f9e8a7d",
  "removed": 0
}
```
