> For the complete documentation index, see [llms.txt](https://docs.firework.com/firework-for-developers/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.firework.com/firework-for-developers/api/images.md).

# Images

### 1. Overview

The Firework Image API manages a business-owned image library. It supports image ingestion, metadata and lifecycle management, folder trees, shoppable product listings, and downloadable CDN renditions.

**Base URL**: `https://api.firework.com`

> **Feature requirement:** Every endpoint in this document requires the business subscription's `images` feature. A request with valid authentication and scope returns `402 Payment Required` when the feature is unavailable.

Images can be created from exactly one source:

1. An `s3_key` produced by the image upload-signature flow
2. A public HTTP(S) `url` fetched by Firework
3. Inline `file_base64` bytes for images up to 5 MB decoded

Supported formats are JPEG (`jpg` / `jpeg`), PNG, and WebP. Original files must be between 1 KB and 50 MB, and neither dimension may exceed 16,384 pixels.

***

### 2. Authentication

All endpoints require an OAuth 2.0 bearer token.

| Scope          | Description                                                                                                                    |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `images:read`  | List/read images and folders, list images for a product, and obtain image download URLs                                        |
| `images:write` | Create/update/delete images and folders, manage lifecycle/product listings, and create upload signatures. Includes read access |

Use this header unless an endpoint says otherwise:

| Name            | Required | Description                           |
| --------------- | -------- | ------------------------------------- |
| `Authorization` | ✅        | `Bearer {ACCESS_TOKEN}`               |
| `Content-Type`  | ✅        | `application/json` for request bodies |

***

### 3. Endpoint Summary

| Endpoint                                    | Method | Scope          | Description                                   |
| ------------------------------------------- | ------ | -------------- | --------------------------------------------- |
| `/api/v1/images`                            | POST   | `images:write` | Create a library image                        |
| `/api/v1/images`                            | GET    | `images:read`  | List library images                           |
| `/api/v1/images/upload_signatures`          | POST   | `images:write` | Create a direct-to-S3 upload signature        |
| `/api/v1/images/{id}`                       | GET    | `images:read`  | Get an image                                  |
| `/api/v1/images/{id}`                       | PATCH  | `images:write` | Update image metadata and product listings    |
| `/api/v1/images/{id}`                       | DELETE | `images:write` | Soft-delete an image                          |
| `/api/v1/images/{id}/archive`               | POST   | `images:write` | Archive an image                              |
| `/api/v1/images/{id}/unarchive`             | POST   | `images:write` | Unarchive an image                            |
| `/api/v1/images/{id}/publish`               | POST   | `images:write` | Publish or schedule an image                  |
| `/api/v1/images/{id}/unpublish`             | POST   | `images:write` | Return an image to draft state                |
| `/api/v1/images/{id}/download`              | GET    | `images:read`  | Get an original or transformed download URL   |
| `/api/v1/images/{id}/products`              | POST   | `images:write` | Append product listings                       |
| `/api/v1/images/{id}/products/{product_id}` | PATCH  | `images:write` | Update one image-product listing              |
| `/api/v1/images/{id}/products/{product_id}` | DELETE | `images:write` | Remove one image-product listing              |
| `/api/v1/products/{product_id}/images`      | GET    | `images:read`  | List published images featuring a product     |
| `/api/v1/image_folders`                     | POST   | `images:write` | Create an image folder                        |
| `/api/v1/image_folders`                     | GET    | `images:read`  | List one level of the folder tree             |
| `/api/v1/image_folders/{id}`                | GET    | `images:read`  | Get an image folder                           |
| `/api/v1/image_folders/{id}`                | PATCH  | `images:write` | Rename or move an image folder                |
| `/api/v1/image_folders/{id}`                | DELETE | `images:write` | Cascade soft-delete a folder subtree          |
| `/api/v1/image_folders/{id}/archive`        | POST   | `images:write` | Archive a folder without changing its content |
| `/api/v1/image_folders/{id}/unarchive`      | POST   | `images:write` | Unarchive a folder                            |

***

### 4. Object Reference

#### 4.1. Image Object

| Field          | Type      | Nullable | Description                                                                          |
| -------------- | --------- | -------- | ------------------------------------------------------------------------------------ |
| `id`           | string    | ❌        | Encoded image ID                                                                     |
| `title`        | string    | ❌        | Image title, maximum 255 characters                                                  |
| `description`  | string    | ✅        | Description, maximum 5,000 characters                                                |
| `access`       | string    | ❌        | `public`, `private`, or `unlisted`                                                   |
| `badge`        | string    | ✅        | Optional badge; currently `ad`                                                       |
| `hashtags`     | string\[] | ❌        | Normalized lowercase hashtags, without automatic extraction from the title           |
| `locale`       | string    | ✅        | Normalized locale                                                                    |
| `source`       | string    | ❌        | `social_media`, `aigc`, or `api`                                                     |
| `folder_id`    | string    | ✅        | Encoded folder ID; `null` for an unfiled image                                       |
| `published_at` | string    | ✅        | ISO 8601 publish time; future values indicate a scheduled image                      |
| `is_published` | boolean   | ❌        | Evaluated at request time; true when `published_at` is present and not in the future |
| `archived_at`  | string    | ✅        | ISO 8601 archive time                                                                |
| `metadata`     | object    | ❌        | Metadata object; currently contains nullable `alt_text`                              |
| `url`          | string    | ✅        | Full-resolution CDN URL for the original file                                        |
| `format`       | string    | ❌        | `jpg`, `jpeg`, `png`, or `webp`                                                      |
| `width`        | integer   | ✅        | Original width in pixels                                                             |
| `height`       | integer   | ✅        | Original height in pixels                                                            |
| `file_size`    | integer   | ✅        | Original file size in bytes                                                          |
| `blur_hash`    | string    | ✅        | BlurHash placeholder when available                                                  |
| `products`     | object\[] | ❌        | Ordered image-product listings                                                       |
| `created_at`   | string    | ❌        | ISO 8601 creation time                                                               |
| `updated_at`   | string    | ❌        | ISO 8601 last-update time                                                            |

Example:

```json
{
  "id": "ImG7xQ",
  "title": "Summer outfit",
  "description": "Linen shirt and trousers",
  "access": "public",
  "badge": "ad",
  "hashtags": ["summer", "linen"],
  "locale": "en-US",
  "source": "api",
  "folder_id": "FoLd9K",
  "published_at": "2026-08-14T08:30:00.000000Z",
  "is_published": true,
  "archived_at": null,
  "metadata": { "alt_text": "Model wearing a beige linen outfit" },
  "url": "https://cdn.firework.com/medias/business/abc/images/original/summer-look.png",
  "format": "png",
  "width": 1600,
  "height": 1200,
  "file_size": 348201,
  "blur_hash": "L6PZfSi_.AyE_3t7t7R**0o#DgR4",
  "products": [
    {
      "product_id": "vWKDjg",
      "product_unit_id": null,
      "product_sort_position": 1,
      "image_sort_position": 2,
      "image_hidden": false,
      "product_hidden": false,
      "bounding_box": { "x": 0.1, "y": 0.15, "w": 0.35, "h": 0.7 }
    }
  ],
  "created_at": "2026-08-14T08:30:00.000000Z",
  "updated_at": "2026-08-14T08:30:00.000000Z"
}
```

#### 4.2. Image-Product Listing

| Field                   | Type    | Nullable | Description                                                                      |
| ----------------------- | ------- | -------- | -------------------------------------------------------------------------------- |
| `product_id`            | string  | ❌        | Encoded product ID                                                               |
| `product_unit_id`       | string  | ✅        | Encoded product-unit ID for a variant-specific listing                           |
| `product_sort_position` | integer | ❌        | Product order on the image; input array order starts at 1                        |
| `image_sort_position`   | integer | ✅        | Image order for reverse product lookup; `null` entries sort after ranked entries |
| `image_hidden`          | boolean | ❌        | Excludes this image from the product-to-images reverse lookup when true          |
| `product_hidden`        | boolean | ❌        | Marks the product as hidden on this image                                        |
| `bounding_box`          | object  | ✅        | Fractional product box with exactly `x`, `y`, `w`, and `h`                       |

Each bounding-box value is relative to the image dimensions. `x` and `y` must be at least 0, `w` and `h` must be greater than 0, and both `x + w` and `y + h` must be at most 1.

#### 4.3. Image Folder Object

| Field              | Type    | Nullable | Description                                                                         |
| ------------------ | ------- | -------- | ----------------------------------------------------------------------------------- |
| `id`               | string  | ❌        | Encoded folder ID                                                                   |
| `name`             | string  | ❌        | Folder name, maximum 255 characters                                                 |
| `parent_folder_id` | string  | ✅        | Encoded parent-folder ID; `null` at the root                                        |
| `archived_at`      | string  | ✅        | ISO 8601 archive time                                                               |
| `has_subfolders`   | boolean | ❌        | Whether the folder has a direct undeleted child; derived for list responses         |
| `has_images`       | boolean | ❌        | Whether the folder directly contains an undeleted image; derived for list responses |
| `created_at`       | string  | ❌        | ISO 8601 creation time                                                              |
| `updated_at`       | string  | ❌        | ISO 8601 last-update time                                                           |

For a folder fetched directly, `has_subfolders` and `has_images` are `false`; use the folder-list endpoint when those expansion hints are needed.

#### 4.4. Pagination Envelope

Image and folder collections use cursor pagination. `after` and `before` are mutually exclusive. Treat cursor values and `links.next` as opaque.

| Field                 | Type    | Nullable | Description                                 |
| --------------------- | ------- | -------- | ------------------------------------------- |
| `links.next`          | string  | ✅        | Relative URL for the next page              |
| `pagination.cursor`   | string  | ✅        | Opaque cursor for the next request          |
| `pagination.has_more` | boolean | ❌        | Whether another page is currently available |

The default `page_size` is 10 and the maximum is 100.

***

### 5. Create Image

Creates and synchronously ingests an image.

**Endpoint**: `POST /api/v1/images` **Scope**: `images:write`

#### 5.1. Request Body

Provide exactly one of `s3_key`, `url`, or `file_base64`.

| Field          | Type      | Required    | Default          | Description                                                                           |
| -------------- | --------- | ----------- | ---------------- | ------------------------------------------------------------------------------------- |
| `title`        | string    | ✅           | None             | Image title, maximum 255 characters                                                   |
| `s3_key`       | string    | Conditional | None             | Key returned by `POST /api/v1/images/upload_signatures`                               |
| `url`          | string    | Conditional | None             | Public HTTP(S) URL ending in a supported image extension                              |
| `file_base64`  | string    | Conditional | None             | Base64 bytes or an image data URI, maximum 5 MB decoded                               |
| `filename`     | string    | Conditional | None             | Required for base64 without a usable data-URI media type; extension determines format |
| `description`  | string    | ❌           | `null`           | Description, maximum 5,000 characters                                                 |
| `access`       | string    | ❌           | `public`         | `public`, `private`, or `unlisted`                                                    |
| `badge`        | string    | ❌           | `null`           | Currently `ad`                                                                        |
| `hashtags`     | string\[] | ❌           | `[]`             | Up to 150 normalized hashtags                                                         |
| `locale`       | string    | ❌           | Business default | Locale                                                                                |
| `source`       | string    | ❌           | `api`            | `social_media`, `aigc`, or `api`                                                      |
| `published_at` | string    | ❌           | Now              | ISO 8601 time up to 28 days ahead; explicit `null` creates a draft                    |
| `folder_id`    | string    | ❌           | `null`           | Encoded folder ID; omit or set `null` for unfiled                                     |
| `metadata`     | object    | ❌           | `{}`             | Supports `alt_text`                                                                   |
| `products`     | object\[] | ❌           | `[]`             | Ordered product listings, maximum 100                                                 |

Each `products` entry requires `product_id` and may include `product_unit_id`, `bounding_box`, `image_hidden`, and `product_hidden`. Products and units must belong to the image's business.

#### 5.2. Response

`201 Created` returns the full [Image Object](#41-image-object).

#### 5.3. Errors

| Status | Description                                                                                                    |
| ------ | -------------------------------------------------------------------------------------------------------------- |
| `400`  | Missing/multiple ingestion sources; malformed source or base64 input                                           |
| `401`  | Missing or invalid token                                                                                       |
| `402`  | The business does not have the `images` feature                                                                |
| `403`  | Missing scope, inaccessible business, or an `s3_key` owned by another business                                 |
| `404`  | Business, folder, or related resource not found                                                                |
| `422`  | Invalid metadata/listing, unsupported format, unreadable image, invalid size/dimensions, or invalid scheduling |

#### 5.4. Examples

Create from a URL:

```bash
curl -X POST "https://api.firework.com/api/v1/images" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Summer outfit",
    "url": "https://assets.example.com/summer-look.png",
    "metadata": {"alt_text": "Model wearing a beige linen outfit"},
    "products": [{
      "product_id": "vWKDjg",
      "bounding_box": {"x": 0.1, "y": 0.15, "w": 0.35, "h": 0.7}
    }]
  }'
```

Create an unpublished image from base64:

```bash
curl -X POST "https://api.firework.com/api/v1/images" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Draft swatch",
    "filename": "swatch.webp",
    "file_base64": "UklGRiIAAABXRUJQVlA4...",
    "published_at": null
  }'
```

***

### 6. List Images

Lists the business's images newest first.

**Endpoint**: `GET /api/v1/images` **Scope**: `images:read`

#### 6.1. Query Parameters

| Parameter     | Type    | Required | Default        | Description                                                                              |
| ------------- | ------- | -------- | -------------- | ---------------------------------------------------------------------------------------- |
| `business_id` | string  | ❌        | Token business | Encoded business ID; optional for app tokens                                             |
| `folder_id`   | string  | ❌        | None           | Encoded folder ID, or `unfiled` for images without a folder                              |
| `source`      | string  | ❌        | None           | `social_media`, `aigc`, or `api`                                                         |
| `access`      | string  | ❌        | None           | `public`, `private`, or `unlisted`                                                       |
| `badge`       | string  | ❌        | None           | Badge filter, such as `ad`                                                               |
| `hashtag`     | string  | ❌        | None           | Require this hashtag                                                                     |
| `archived`    | boolean | ❌        | `false`        | Omitted or `false` returns only non-archived images; `true` returns only archived images |
| `published`   | boolean | ❌        | None           | Filter by current published state                                                        |
| `after`       | string  | ❌        | None           | Opaque forward cursor                                                                    |
| `before`      | string  | ❌        | None           | Opaque backward cursor                                                                   |
| `page_size`   | integer | ❌        | 10             | Page size, maximum 100                                                                   |

#### 6.2. Response

`200 OK` returns `images`, `links`, and `pagination`.

```json
{
  "images": [{"id": "ImG7xQ", "title": "Summer outfit", "products": []}],
  "links": {"next": "/api/v1/images?page_size=10&after=Q3Vyc29yOjQy"},
  "pagination": {"cursor": "Q3Vyc29yOjQy", "has_more": true}
}
```

#### 6.3. Errors

`400` for invalid IDs/pagination, `401` for authentication, `402` for the feature gate, `403` for scope/business access, and `404` when the business is not found.

#### 6.4. Example

```bash
curl "https://api.firework.com/api/v1/images?folder_id=unfiled&published=true&page_size=25" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

***

### 7. Get, Update, and Delete an Image

#### 7.1. Get Image

**Endpoint**: `GET /api/v1/images/{id}` **Scope**: `images:read`

| Parameter | Location | Type   | Required | Description      |
| --------- | -------- | ------ | -------- | ---------------- |
| `id`      | Path     | string | ✅        | Encoded image ID |

`200 OK` returns the full Image object. Errors are `401`, `402`, `403`, and `404`.

```bash
curl "https://api.firework.com/api/v1/images/ImG7xQ" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

#### 7.2. Update Image

**Endpoint**: `PATCH /api/v1/images/{id}` **Scope**: `images:write`

The path parameter is the encoded image ID. The body accepts the editable fields from [Create Image](#51-request-body), excluding ingestion-only `s3_key`, `url`, `file_base64`, and `filename`. Only provided fields change.

Passing `products` replaces all current product listings in the supplied order; an empty array clears them. Omit `products` to preserve existing listings. Passing `published_at: null` makes the image a draft.

`200 OK` returns the updated Image object. Errors are `400`, `401`, `402`, `403`, `404`, and `422`.

```bash
curl -X PATCH "https://api.firework.com/api/v1/images/ImG7xQ" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title":"Updated summer outfit","folder_id":null,"products":[]}'
```

#### 7.3. Delete Image

**Endpoint**: `DELETE /api/v1/images/{id}` **Scope**: `images:write`

The path parameter is the encoded image ID. A successful request soft-deletes the image and returns `204 No Content`. Errors are `401`, `402`, `403`, and `404`.

```bash
curl -X DELETE "https://api.firework.com/api/v1/images/ImG7xQ" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

***

### 8. Direct-to-S3 Upload

Use this flow for images that should be uploaded by the client rather than fetched by Firework or embedded as base64.

#### 8.1. Create Upload Signature

**Endpoint**: `POST /api/v1/images/upload_signatures` **Scope**: `images:write`

| Field          | Type   | Required | Description                                       |
| -------------- | ------ | -------- | ------------------------------------------------- |
| `filename`     | string | ✅        | File name with a supported extension              |
| `content_type` | string | ✅        | `image/jpeg`, `image/png`, or `image/webp`        |
| `business_id`  | string | ❌        | Encoded business ID; defaults to the app business |

`201 Created` returns:

| Field        | Description                                          |
| ------------ | ---------------------------------------------------- |
| `key`        | S3 object key; reuse as `s3_key` when creating image |
| `post_url`   | S3 multipart form destination                        |
| `policy`     | Send as `Policy`                                     |
| `signature`  | Send as `X-Amz-Signature`                            |
| `date`       | Send as `X-Amz-Date`                                 |
| `credential` | Send as `X-Amz-Credential`                           |
| `algorithm`  | Send as `X-Amz-Algorithm`                            |
| `acl`        | Send as `acl`                                        |

Errors are `400`, `401`, `402`, `403`, and `404`.

```bash
curl -X POST "https://api.firework.com/api/v1/images/upload_signatures" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"filename":"summer-look.png","content_type":"image/png"}'
```

#### 8.2. Upload to S3 and Create the Image

POST the returned fields to `post_url` as `multipart/form-data`. Include a `Content-Type` field matching the requested content type and put the binary `file` part last.

```bash
curl -X POST "${POST_URL}" \
  -F "key=${KEY}" \
  -F "Policy=${POLICY}" \
  -F "X-Amz-Signature=${SIGNATURE}" \
  -F "X-Amz-Date=${DATE}" \
  -F "X-Amz-Credential=${CREDENTIAL}" \
  -F "X-Amz-Algorithm=${ALGORITHM}" \
  -F "acl=${ACL}" \
  -F "Content-Type=image/png" \
  -F "file=@summer-look.png"
```

Then call `POST /api/v1/images` with the returned key:

```json
{"title":"Summer outfit","s3_key":"medias/business/abc/images/original/123-summer-look.png"}
```

***

### 9. Archive and Publish Lifecycle

#### 9.1. Archive or Unarchive

**Endpoints**:

* `POST /api/v1/images/{id}/archive`
* `POST /api/v1/images/{id}/unarchive`

**Scope**: `images:write`

The path parameter is the encoded image ID. Both endpoints take no body and return `200 OK` with the full Image object. Archive populates `archived_at`; unarchive clears it. Errors are `401`, `402`, `403`, and `404`.

```bash
curl -X POST "https://api.firework.com/api/v1/images/ImG7xQ/archive" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

#### 9.2. Publish or Schedule

**Endpoint**: `POST /api/v1/images/{id}/publish` **Scope**: `images:write`

Omit the body to publish immediately. To schedule, send a future ISO 8601 `published_at` no more than 28 days ahead. Past times and times beyond the scheduling window return `422`.

```bash
curl -X POST "https://api.firework.com/api/v1/images/ImG7xQ/publish" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"published_at":"2026-08-20T12:00:00Z"}'
```

`200 OK` returns the full Image object. Errors are `400`, `401`, `402`, `403`, `404`, and `422`.

#### 9.3. Unpublish

**Endpoint**: `POST /api/v1/images/{id}/unpublish` **Scope**: `images:write`

Takes no body, clears `published_at`, and returns `200 OK` with the full Image object. Errors are `401`, `402`, `403`, and `404`.

```bash
curl -X POST "https://api.firework.com/api/v1/images/ImG7xQ/unpublish" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

***

### 10. Get Image Download URL

Returns the original CDN URL or an on-demand resized/converted rendition.

**Endpoint**: `GET /api/v1/images/{id}/download` **Scope**: `images:read`

#### 10.1. Parameters

| Parameter       | Location | Type    | Required | Description                                          |
| --------------- | -------- | ------- | -------- | ---------------------------------------------------- |
| `id`            | Path     | string  | ✅        | Encoded image ID                                     |
| `max_dimension` | Query    | integer | ❌        | Fit longest side to 16–16,384 pixels; downscale only |
| `format`        | Query    | string  | ❌        | Convert to `jpg`, `jpeg`, `png`, or `webp`           |

The two transformation parameters compose. CDN renditions are generated on demand, so the first access may require a browser-like client.

#### 10.2. Response

| Field          | Type    | Nullable | Description                                                    |
| -------------- | ------- | -------- | -------------------------------------------------------------- |
| `download_url` | string  | ✅        | Original or transformed CDN URL                                |
| `format`       | string  | ✅        | Format at `download_url`                                       |
| `width`        | integer | ✅        | Width at `download_url`                                        |
| `height`       | integer | ✅        | Height at `download_url`                                       |
| `file_size`    | integer | ✅        | Original byte size; `null` for resized or converted renditions |

```json
{
  "download_url": "https://cdn.firework.com/medias/business/abc/images/640_480/summer-look.webp",
  "format": "webp",
  "width": 640,
  "height": 480,
  "file_size": null
}
```

Errors are `401`, `402`, `403`, `404`, and `422`.

```bash
curl "https://api.firework.com/api/v1/images/ImG7xQ/download?max_dimension=640&format=webp" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

***

### 11. Manage Image-Product Listings

#### 11.1. Append Products

**Endpoint**: `POST /api/v1/images/{id}/products` **Scope**: `images:write`

| Field      | Type      | Required | Description                                       |
| ---------- | --------- | -------- | ------------------------------------------------- |
| `products` | object\[] | ✅        | One or more listing inputs in the order to append |

Existing positions are preserved, and new positions follow them. The combined total cannot exceed 100. A `(product_id, product_unit_id)` pair cannot be duplicated.

`200 OK` returns `{ "products": [...] }` containing the image's complete ordered listing set. Errors are `400`, `401`, `402`, `403`, `404`, and `422`.

```bash
curl -X POST "https://api.firework.com/api/v1/images/ImG7xQ/products" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"products":[{"product_id":"vWKDjg","product_unit_id":"nP3qRv"}]}'
```

#### 11.2. Update One Listing

**Endpoint**: `PATCH /api/v1/images/{id}/products/{product_id}` **Scope**: `images:write`

| Parameter         | Location | Type   | Required | Description                                                   |
| ----------------- | -------- | ------ | -------- | ------------------------------------------------------------- |
| `id`              | Path     | string | ✅        | Encoded image ID                                              |
| `product_id`      | Path     | string | ✅        | Encoded product ID                                            |
| `product_unit_id` | Query    | string | ❌        | Encoded unit ID required to select a variant-specific listing |

The JSON body may contain `image_hidden`, `product_hidden`, `image_sort_position`, or `bounding_box`. Set `image_sort_position` or `bounding_box` to `null` to clear it.

`200 OK` returns the updated listing. Errors are `400`, `401`, `402`, `403`, `404`, and `422`.

```bash
curl -X PATCH "https://api.firework.com/api/v1/images/ImG7xQ/products/vWKDjg?product_unit_id=nP3qRv" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"image_sort_position":1,"image_hidden":false}'
```

#### 11.3. Remove One Listing

**Endpoint**: `DELETE /api/v1/images/{id}/products/{product_id}` **Scope**: `images:write`

Use the same path/query identifiers as Update One Listing. A successful request returns `204 No Content`; remaining positions are not renumbered. Errors are `401`, `402`, `403`, and `404`.

```bash
curl -X DELETE "https://api.firework.com/api/v1/images/ImG7xQ/products/vWKDjg?product_unit_id=nP3qRv" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

#### 11.4. List Images for a Product

**Endpoint**: `GET /api/v1/products/{product_id}/images` **Scope**: `images:read`

| Parameter             | Location | Type    | Required | Default | Description                         |
| --------------------- | -------- | ------- | -------- | ------- | ----------------------------------- |
| `product_id`          | Path     | string  | ✅        | None    | Encoded product ID                  |
| `include_unpublished` | Query    | boolean | ❌        | `false` | Include drafts and scheduled images |
| `after`               | Query    | string  | ❌        | None    | Opaque forward cursor               |
| `before`              | Query    | string  | ❌        | None    | Opaque backward cursor              |
| `page_size`           | Query    | integer | ❌        | 10      | Page size, maximum 100              |

Results are ordered by `image_sort_position`, with unranked listings last. Deleted and archived images and listings with `image_hidden: true` are always excluded. By default, unpublished and scheduled images are excluded.

`200 OK` uses the standard image collection envelope. Errors are `400`, `401`, `402`, `403`, and `404`.

```bash
curl "https://api.firework.com/api/v1/products/vWKDjg/images?page_size=10" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

***

### 12. Image Folders

Folders form a business-scoped tree with a maximum depth of 10. Names must be unique among siblings.

#### 12.1. Create Folder

**Endpoint**: `POST /api/v1/image_folders` **Scope**: `images:write`

| Field              | Type   | Required | Description                                   |
| ------------------ | ------ | -------- | --------------------------------------------- |
| `name`             | string | ✅        | Folder name, maximum 255 characters           |
| `parent_folder_id` | string | ❌        | Encoded parent ID; omit for a root folder     |
| `business_id`      | string | ❌        | Encoded business ID; defaults to app business |

`201 Created` returns the Folder object. Errors are `400`, `401`, `402`, `403`, `404`, and `422`.

```bash
curl -X POST "https://api.firework.com/api/v1/image_folders" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"Campaigns","parent_folder_id":"FoLd9K"}'
```

#### 12.2. List a Folder Level

**Endpoint**: `GET /api/v1/image_folders` **Scope**: `images:read`

| Parameter          | Type    | Required | Default        | Description                                         |
| ------------------ | ------- | -------- | -------------- | --------------------------------------------------- |
| `business_id`      | string  | ❌        | Token business | Encoded business ID                                 |
| `parent_folder_id` | string  | ❌        | None           | Parent whose direct children to list; omit for root |
| `include_archived` | boolean | ❌        | `false`        | Include archived folders                            |
| `after`            | string  | ❌        | None           | Opaque forward cursor                               |
| `before`           | string  | ❌        | None           | Opaque backward cursor                              |
| `page_size`        | integer | ❌        | 10             | Page size, maximum 100                              |

`200 OK` returns `image_folders`, `links`, and `pagination`. Errors are `400`, `401`, `402`, `403`, and `404`.

```bash
curl "https://api.firework.com/api/v1/image_folders?parent_folder_id=FoLd9K&page_size=25" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

#### 12.3. Get Folder

**Endpoint**: `GET /api/v1/image_folders/{id}` **Scope**: `images:read`

The path parameter is the encoded folder ID. `200 OK` returns the Folder object. Errors are `401`, `402`, `403`, and `404`.

```bash
curl "https://api.firework.com/api/v1/image_folders/FoLd9K" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

#### 12.4. Update Folder

**Endpoint**: `PATCH /api/v1/image_folders/{id}` **Scope**: `images:write`

| Field              | Type   | Required | Description                                     |
| ------------------ | ------ | -------- | ----------------------------------------------- |
| `name`             | string | ❌        | New folder name                                 |
| `parent_folder_id` | string | ❌        | New parent; `null` moves the folder to the root |

Only supplied fields change. Cycles, sibling-name conflicts, foreign-business parents, and moves that exceed the depth limit return `422`. `200 OK` returns the updated Folder object. Other errors are `400`, `401`, `402`, `403`, and `404`.

```bash
curl -X PATCH "https://api.firework.com/api/v1/image_folders/FoLd9K" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"Seasonal campaigns","parent_folder_id":null}'
```

#### 12.5. Delete Folder Subtree

**Endpoint**: `DELETE /api/v1/image_folders/{id}` **Scope**: `images:write`

This is a cascade soft delete: the folder, all nested folders, and all images in the subtree are deleted in one transaction.

Unlike other delete endpoints, success returns `200 OK` with counts:

```json
{"deleted_folders":2,"deleted_images":7}
```

Errors are `401`, `402`, `403`, and `404`.

```bash
curl -X DELETE "https://api.firework.com/api/v1/image_folders/FoLd9K" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

#### 12.6. Archive or Unarchive Folder

**Endpoints**:

* `POST /api/v1/image_folders/{id}/archive`
* `POST /api/v1/image_folders/{id}/unarchive`

**Scope**: `images:write`

Both endpoints take no body and return `200 OK` with the Folder object. Folder archiving is cosmetic: it does not archive, unpublish, move, or delete images or subfolders. Errors are `401`, `402`, `403`, and `404`.

```bash
curl -X POST "https://api.firework.com/api/v1/image_folders/FoLd9K/archive" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

***

### 13. Error Format

Errors use the shared public API JSON shape:

```json
{"error":"Error Message"}
```

Validation errors may include a structured `errors` object when produced by a resource changeset.
