# Assets

> The library — everything your services have produced and you have uploaded.

The library — everything your services have produced and you have uploaded. Reads and uploads on `assets`; renaming, moving, deleting and folders on `assets:write`.

When and why to call these: the [assets guide](https://docs.lumavox.ai/library/assets.md).

## List assets

`GET /v1/assets`

Everything your organisation's services have produced, newest first unless `sort` and `order` say otherwise. Paginated by cursor: pass the `next_cursor` from one response as `cursor` on the next, with the same `sort`, and stop when it comes back null.

```bash title="curl"
curl "https://api.lumavox.ai/v1/assets" \
  -H "Authorization: Bearer $API_KEY"
```

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `limit` | integer |  | How many to answer, 1 to 200. 1–200. Default `50`. |
| `cursor` | string |  | The `next_cursor` of the page before. At least 1 characters. |
| `kind` | `"input"`, `"output"` |  |  |
| `media_type` | `"audio"`, `"image"`, `"video"`, `"data"` |  |  |
| `capability` | `"tts"`, `"image"`, `"video"`, `"stt"`, `"skybox"` |  | Which capability produced it |
| `request_id` | string |  | `req_…` — one call's output |
| `search` | string |  | Match anywhere in the name, whatever the case At most 120 characters. |
| `sort` | `"created_at"`, `"name"`, `"size_bytes"`, `"duration_seconds"` |  | Default `"created_at"`. |
| `order` | `"asc"`, `"desc"` |  | Default `"desc"`. |

**Responses**

| Status | Description |
|---|---|
| `200` | `application/json` — One page of the library, newest first |
| `400` | The request could not be read: malformed JSON |
| `401` | Missing or invalid credentials |
| `403` | The key is valid but its scopes do not cover this capability — a key limited to `tts` gets this from `/v1/images` rather than a 401 |
| `422` | The request failed validation |
| `429` | Rate limit exceeded |

**`200` response body**

| Field | Type | Description |
|---|---|---|
| `data` | array of AssetSummary |  |
| `data[].id` | string | `ast_…` |
| `data[].name` | string |  |
| `data[].kind` | `"input"`, `"output"` | `input` if you uploaded it, `output` if a service made it |
| `data[].media_type` | `"audio"`, `"image"`, `"video"`, `"data"` | What to do with the bytes — coarser than `content_type`, and what the library filters on. `data` is a lip-sync control track |
| `data[].content_type` | string | The MIME type |
| `data[].size_bytes` | integer | Size of the stored object |
| `data[].duration_seconds` | number or null | How long the audio or video runs; null for a still |
| `data[].width` | integer or null |  |
| `data[].height` | integer or null |  |
| `data[].peaks` | array of integer or null | Waveform magnitudes, 0-100, for a mirrored strip. Empty means silent or unreadable; null means never computed. |
| `data[].folder_id` | string or null | `fld_…`, or null at the library root |
| `data[].capability` | `"tts"`, `"image"`, `"video"`, `"stt"`, `"skybox"` or null | Which capability produced it. Null for an upload |
| `data[].origin` | `"api"`, `"console"`, `"end_user"`, `"platform"` or null | Which door it came through. Null for older rows |
| `data[].thumbnail_url` | string or null | A short-lived link to a 512px square preview, for visual media only. Null for audio, for anything not yet converted, and for anything stored before previews existed. |
| `data[].status` | `"pending"`, `"ready"`, `"failed"` | `pending` while an upload converts, then `ready` or `failed`. Everything a service produces is `ready` immediately. |
| `data[].error` | string or null | Why an upload could not be converted. Null otherwise. |
| `data[].created_at` | string (date-time) |  |
| `next_cursor` | string or null | Pass as `cursor` for the next page; `null` when this is the last. |

## Ask where to upload a file

`POST /v1/assets/upload-url`

Returns a short-lived URL to `PUT` the file to, with the `Content-Type` you name, and the `asset_id` to confirm it with afterwards. **The bytes never pass through this API.**

Nothing is created by this call. Confirm with `POST /v1/assets/uploaded` once the `PUT` succeeds.

An image may weigh up to the platform's `limits.uploads.image_bytes`, a recording its `limits.uploads.audio_bytes` (`GET /v1/platform/limits`); the confirm refuses a larger file.

```bash title="curl"
curl -X POST "https://api.lumavox.ai/v1/assets/upload-url" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"content_type":"image/png"}'
```

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `content_type` | string | yes | What you are about to upload. Signed into the URL, so the upload must send exactly this `Content-Type`, and checked against the stored object when you confirm. |

**Responses**

| Status | Description |
|---|---|
| `200` | `application/json` — Where to `PUT` the bytes, and until when |
| `400` | The request could not be read: malformed JSON |
| `401` | Missing or invalid credentials |
| `402` | Out of credits, or the organisation is suspended — nothing was charged |
| `403` | The key is valid but its scopes do not cover this capability — a key limited to `tts` gets this from `/v1/images` rather than a 401 |
| `409` | The library holds the platform's `limits.org.asset_uploads` uploaded files |
| `422` | The request failed validation |
| `429` | Rate limit exceeded |
| `503` | Object storage is unavailable |

**`200` response body**

| Field | Type | Description |
|---|---|---|
| `asset_id` | string | Pass this back to confirm the upload. |
| `url` | string | PUT the file here, unmodified, with the `Content-Type` you asked for and no authentication headers — the URL carries its own. It expires; ask for another if it does. |
| `expires_at` | string (date-time) | When `url` stops accepting the upload. |

## Confirm an uploaded file

`POST /v1/assets/uploaded`

Call this once the `PUT` from `upload-url` has succeeded. The stored object's size and type are checked, the asset row is created and the conversion is queued. The asset comes back `pending`; poll `GET /v1/assets/{id}` until it is `ready`, or `failed` with the reason in `error`.

```bash title="curl"
curl -X POST "https://api.lumavox.ai/v1/assets/uploaded" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"asset_id":"ast_a1B2c3D4e5F6g7H8","content_type":"image/png"}'
```

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `asset_id` | string | yes | From the ticket. |
| `content_type` | string | yes | What was uploaded. |
| `filename` | string |  | The file's own name; the asset is called this, less its extension, when `name` is not given. |
| `name` | string |  | What to call the asset. |
| `folder_id` | string or null |  | `fld_…` — the folder to file it in; omit it, or `null`, for the root. A folder that is not yours is a 404. |

**Responses**

| Status | Description |
|---|---|
| `202` | `application/json` — Accepted. The asset is `pending` until it converts |
| `400` | The request could not be read: malformed JSON |
| `401` | Missing or invalid credentials |
| `402` | Out of credits, or the organisation is suspended — nothing was charged |
| `403` | The key is valid but its scopes do not cover this capability — a key limited to `tts` gets this from `/v1/images` rather than a 401 |
| `404` | No such asset or folder |
| `409` | The upload has not arrived — the `PUT` has not landed — or it was confirmed already |
| `422` | The request failed validation |
| `429` | Rate limit exceeded |
| `503` | Object storage is unavailable |

**`202` response body**

| Field | Type | Description |
|---|---|---|
| `id` | string | `ast_…` |
| `name` | string |  |
| `kind` | `"input"`, `"output"` | `input` if you uploaded it, `output` if a service made it |
| `media_type` | `"audio"`, `"image"`, `"video"`, `"data"` | What to do with the bytes — coarser than `content_type`, and what the library filters on. `data` is a lip-sync control track |
| `content_type` | string | The MIME type |
| `size_bytes` | integer | Size of the stored object |
| `duration_seconds` | number or null | How long the audio or video runs; null for a still |
| `width` | integer or null |  |
| `height` | integer or null |  |
| `peaks` | array of integer or null | Waveform magnitudes, 0-100, for a mirrored strip. Empty means silent or unreadable; null means never computed. |
| `folder_id` | string or null | `fld_…`, or null at the library root |
| `capability` | `"tts"`, `"image"`, `"video"`, `"stt"`, `"skybox"` or null | Which capability produced it. Null for an upload |
| `origin` | `"api"`, `"console"`, `"end_user"`, `"platform"` or null | Which door it came through. Null for older rows |
| `thumbnail_url` | string or null | A short-lived link to a 512px square preview, for visual media only. Null for audio, for anything not yet converted, and for anything stored before previews existed. |
| `status` | `"pending"`, `"ready"`, `"failed"` | `pending` while an upload converts, then `ready` or `failed`. Everything a service produces is `ready` immediately. |
| `error` | string or null | Why an upload could not be converted. Null otherwise. |
| `created_at` | string (date-time) |  |
| `request_id` | string or null | `req_…` — the call that produced this |
| `end_user_id` | string or null |  |
| `meta` | map of any or null |  |
| `url` | string or null | Time-limited link to the bytes. Null when object storage is unavailable. |
| `url_expires_at` | string (date-time) or null | When `url`, the variants' links and `preview_url` stop working; null when there is no `url`. Fetch the asset again for fresh ones. |
| `preview_url` | string or null | A displayable copy, when `url` is not displayable. Set for skyboxes, whose `url` is a Radiance `.hdr`; null otherwise. |
| `variants` | array of VariantInfo | Smaller copies of the same image, largest first. Empty for audio and for anything that has none. |
| `variants[].name` | string | `50`, `25` or `12.5` — the percentage of the original |
| `variants[].width` | integer |  |
| `variants[].height` | integer |  |
| `variants[].size_bytes` | integer |  |
| `variants[].url` | string or null | Time-limited link to this copy, good until the asset's `url_expires_at`. |

## Delete several assets

`POST /v1/assets/delete`

Needs `assets:write`. Soft-deletes a selection and reports how many rows it changed; ids that are not yours, or already deleted, are skipped.

```bash title="curl"
curl -X POST "https://api.lumavox.ai/v1/assets/delete" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"asset_ids":[]}'
```

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `asset_ids` | array of string | yes | 1–500 items. |

**Responses**

| Status | Description |
|---|---|
| `200` | `application/json` — How many assets were removed |
| `400` | The request could not be read: malformed JSON |
| `401` | Missing or invalid credentials |
| `403` | The key is valid but its scopes do not cover this capability — a key limited to `tts` gets this from `/v1/images` rather than a 401 |
| `422` | The request failed validation |
| `429` | Rate limit exceeded |

**`200` response body**

| Field | Type | Description |
|---|---|---|
| `affected` | integer |  |

## Move several assets

`POST /v1/assets/move`

Needs `assets:write`. Into one folder, or to the root with `folder_id: null`; ids that are not yours are skipped.

```bash title="curl"
curl -X POST "https://api.lumavox.ai/v1/assets/move" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"asset_ids":[],"folder_id":"..."}'
```

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `asset_ids` | array of string | yes | 1–500 items. |
| `folder_id` | string or null | yes | `fld_…`, or null for the root |

**Responses**

| Status | Description |
|---|---|
| `200` | `application/json` — How many assets moved |
| `400` | The request could not be read: malformed JSON |
| `401` | Missing or invalid credentials |
| `403` | The key is valid but its scopes do not cover this capability — a key limited to `tts` gets this from `/v1/images` rather than a 401 |
| `404` | No such asset or folder |
| `422` | The request failed validation |
| `429` | Rate limit exceeded |

**`200` response body**

| Field | Type | Description |
|---|---|---|
| `affected` | integer |  |

## Get one asset

`GET /v1/assets/{asset_id}`

Includes a time-limited `url` for the bytes.

```bash title="curl"
curl "https://api.lumavox.ai/v1/assets/$ASSET_ID" \
  -H "Authorization: Bearer $API_KEY"
```

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `asset_id` | string | yes |  |

**Responses**

| Status | Description |
|---|---|
| `200` | `application/json` — The asset, with signed links to its content and variants |
| `400` | The request could not be read: malformed JSON |
| `401` | Missing or invalid credentials |
| `403` | The key is valid but its scopes do not cover this capability — a key limited to `tts` gets this from `/v1/images` rather than a 401 |
| `404` | No such asset or folder |
| `422` | The request failed validation |
| `429` | Rate limit exceeded |

**`200` response body**

| Field | Type | Description |
|---|---|---|
| `id` | string | `ast_…` |
| `name` | string |  |
| `kind` | `"input"`, `"output"` | `input` if you uploaded it, `output` if a service made it |
| `media_type` | `"audio"`, `"image"`, `"video"`, `"data"` | What to do with the bytes — coarser than `content_type`, and what the library filters on. `data` is a lip-sync control track |
| `content_type` | string | The MIME type |
| `size_bytes` | integer | Size of the stored object |
| `duration_seconds` | number or null | How long the audio or video runs; null for a still |
| `width` | integer or null |  |
| `height` | integer or null |  |
| `peaks` | array of integer or null | Waveform magnitudes, 0-100, for a mirrored strip. Empty means silent or unreadable; null means never computed. |
| `folder_id` | string or null | `fld_…`, or null at the library root |
| `capability` | `"tts"`, `"image"`, `"video"`, `"stt"`, `"skybox"` or null | Which capability produced it. Null for an upload |
| `origin` | `"api"`, `"console"`, `"end_user"`, `"platform"` or null | Which door it came through. Null for older rows |
| `thumbnail_url` | string or null | A short-lived link to a 512px square preview, for visual media only. Null for audio, for anything not yet converted, and for anything stored before previews existed. |
| `status` | `"pending"`, `"ready"`, `"failed"` | `pending` while an upload converts, then `ready` or `failed`. Everything a service produces is `ready` immediately. |
| `error` | string or null | Why an upload could not be converted. Null otherwise. |
| `created_at` | string (date-time) |  |
| `request_id` | string or null | `req_…` — the call that produced this |
| `end_user_id` | string or null |  |
| `meta` | map of any or null |  |
| `url` | string or null | Time-limited link to the bytes. Null when object storage is unavailable. |
| `url_expires_at` | string (date-time) or null | When `url`, the variants' links and `preview_url` stop working; null when there is no `url`. Fetch the asset again for fresh ones. |
| `preview_url` | string or null | A displayable copy, when `url` is not displayable. Set for skyboxes, whose `url` is a Radiance `.hdr`; null otherwise. |
| `variants` | array of VariantInfo | Smaller copies of the same image, largest first. Empty for audio and for anything that has none. |
| `variants[].name` | string | `50`, `25` or `12.5` — the percentage of the original |
| `variants[].width` | integer |  |
| `variants[].height` | integer |  |
| `variants[].size_bytes` | integer |  |
| `variants[].url` | string or null | Time-limited link to this copy, good until the asset's `url_expires_at`. |

## Rename or move an asset

`PATCH /v1/assets/{asset_id}`

Needs `assets:write`. `folder_id` is tri-state: omitted leaves it alone, `null` moves the asset to the root, and an id moves it there.

```bash title="curl"
curl -X PATCH "https://api.lumavox.ai/v1/assets/$ASSET_ID" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
```

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `asset_id` | string | yes |  |

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string |  | 1–200 characters. |
| `folder_id` | string or null |  | `fld_…`, or null for the root; omit it to leave it |

**Responses**

| Status | Description |
|---|---|
| `200` | `application/json` — The asset as it now stands |
| `400` | The request could not be read: malformed JSON |
| `401` | Missing or invalid credentials |
| `403` | The key is valid but its scopes do not cover this capability — a key limited to `tts` gets this from `/v1/images` rather than a 401 |
| `404` | No such asset or folder |
| `422` | The request failed validation |
| `429` | Rate limit exceeded |

**`200` response body**

| Field | Type | Description |
|---|---|---|
| `id` | string | `ast_…` |
| `name` | string |  |
| `kind` | `"input"`, `"output"` | `input` if you uploaded it, `output` if a service made it |
| `media_type` | `"audio"`, `"image"`, `"video"`, `"data"` | What to do with the bytes — coarser than `content_type`, and what the library filters on. `data` is a lip-sync control track |
| `content_type` | string | The MIME type |
| `size_bytes` | integer | Size of the stored object |
| `duration_seconds` | number or null | How long the audio or video runs; null for a still |
| `width` | integer or null |  |
| `height` | integer or null |  |
| `peaks` | array of integer or null | Waveform magnitudes, 0-100, for a mirrored strip. Empty means silent or unreadable; null means never computed. |
| `folder_id` | string or null | `fld_…`, or null at the library root |
| `capability` | `"tts"`, `"image"`, `"video"`, `"stt"`, `"skybox"` or null | Which capability produced it. Null for an upload |
| `origin` | `"api"`, `"console"`, `"end_user"`, `"platform"` or null | Which door it came through. Null for older rows |
| `thumbnail_url` | string or null | A short-lived link to a 512px square preview, for visual media only. Null for audio, for anything not yet converted, and for anything stored before previews existed. |
| `status` | `"pending"`, `"ready"`, `"failed"` | `pending` while an upload converts, then `ready` or `failed`. Everything a service produces is `ready` immediately. |
| `error` | string or null | Why an upload could not be converted. Null otherwise. |
| `created_at` | string (date-time) |  |
| `request_id` | string or null | `req_…` — the call that produced this |
| `end_user_id` | string or null |  |
| `meta` | map of any or null |  |
| `url` | string or null | Time-limited link to the bytes. Null when object storage is unavailable. |
| `url_expires_at` | string (date-time) or null | When `url`, the variants' links and `preview_url` stop working; null when there is no `url`. Fetch the asset again for fresh ones. |
| `preview_url` | string or null | A displayable copy, when `url` is not displayable. Set for skyboxes, whose `url` is a Radiance `.hdr`; null otherwise. |
| `variants` | array of VariantInfo | Smaller copies of the same image, largest first. Empty for audio and for anything that has none. |
| `variants[].name` | string | `50`, `25` or `12.5` — the percentage of the original |
| `variants[].width` | integer |  |
| `variants[].height` | integer |  |
| `variants[].size_bytes` | integer |  |
| `variants[].url` | string or null | Time-limited link to this copy, good until the asset's `url_expires_at`. |

## Delete an asset

`DELETE /v1/assets/{asset_id}`

Needs `assets:write`. Removes the asset from the library, and its bytes — every variant, the thumbnail — from storage shortly after.

```bash title="curl"
curl -X DELETE "https://api.lumavox.ai/v1/assets/$ASSET_ID" \
  -H "Authorization: Bearer $API_KEY"
```

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `asset_id` | string | yes |  |

**Responses**

| Status | Description |
|---|---|
| `204` | Deleted |
| `400` | The request could not be read: malformed JSON |
| `401` | Missing or invalid credentials |
| `403` | The key is valid but its scopes do not cover this capability — a key limited to `tts` gets this from `/v1/images` rather than a 401 |
| `404` | No such asset or folder |
| `422` | The request failed validation |
| `429` | Rate limit exceeded |

## List folders at one level

`GET /v1/asset-folders`

One level of the tree — the top level unless `parent_id` names a folder — alphabetically, with what each holds. A level holds at most 500 folders, so it is always one page: `next_cursor` is null.

```bash title="curl"
curl "https://api.lumavox.ai/v1/asset-folders" \
  -H "Authorization: Bearer $API_KEY"
```

**Query parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `parent_id` | string |  | `fld_…`; omit it for the top level |

**Responses**

| Status | Description |
|---|---|
| `200` | `application/json` — The folders at that level |
| `400` | The request could not be read: malformed JSON |
| `401` | Missing or invalid credentials |
| `403` | The key is valid but its scopes do not cover this capability — a key limited to `tts` gets this from `/v1/images` rather than a 401 |
| `422` | The request failed validation |
| `429` | Rate limit exceeded |

**`200` response body**

| Field | Type | Description |
|---|---|---|
| `data` | array of FolderSummary |  |
| `data[].id` | string | `fld_…` |
| `data[].name` | string |  |
| `data[].parent_id` | string or null |  |
| `data[].created_at` | string (date-time) |  |
| `data[].asset_count` | integer or null | Live assets directly in this folder |
| `data[].folder_count` | integer or null | Subfolders directly in this folder |
| `next_cursor` | string or null | Pass as `cursor` for the next page; `null` when this is the last. |

## Create a folder

`POST /v1/asset-folders`

Needs `assets:write`. At the top level, or under `parent_id`. Two siblings cannot share a name (409).

```bash title="curl"
curl -X POST "https://api.lumavox.ai/v1/asset-folders" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"..."}'
```

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | yes | 1–120 characters. |
| `parent_id` | string or null |  | `fld_…`; omit it, or `null`, for the top level |

**Responses**

| Status | Description |
|---|---|
| `201` | `application/json` — The new folder |
| `400` | The request could not be read: malformed JSON |
| `401` | Missing or invalid credentials |
| `403` | The key is valid but its scopes do not cover this capability — a key limited to `tts` gets this from `/v1/images` rather than a 401 |
| `409` | A folder beside it already has that name |
| `422` | The request failed validation |
| `429` | Rate limit exceeded |

**`201` response body**

| Field | Type | Description |
|---|---|---|
| `id` | string | `fld_…` |
| `name` | string |  |
| `parent_id` | string or null |  |
| `created_at` | string (date-time) |  |
| `asset_count` | integer or null | Live assets directly in this folder |
| `folder_count` | integer or null | Subfolders directly in this folder |

## Get one folder and its trail

`GET /v1/asset-folders/{folder_id}`

```bash title="curl"
curl "https://api.lumavox.ai/v1/asset-folders/$FOLDER_ID" \
  -H "Authorization: Bearer $API_KEY"
```

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `folder_id` | string | yes |  |

**Responses**

| Status | Description |
|---|---|
| `200` | `application/json` — The folder and its ancestors |
| `400` | The request could not be read: malformed JSON |
| `401` | Missing or invalid credentials |
| `403` | The key is valid but its scopes do not cover this capability — a key limited to `tts` gets this from `/v1/images` rather than a 401 |
| `404` | No such asset or folder |
| `422` | The request failed validation |
| `429` | Rate limit exceeded |

**`200` response body**

| Field | Type | Description |
|---|---|---|
| `id` | string | `fld_…` |
| `name` | string |  |
| `parent_id` | string or null |  |
| `created_at` | string (date-time) |  |
| `asset_count` | integer or null | Live assets directly in this folder |
| `folder_count` | integer or null | Subfolders directly in this folder |
| `ancestors` | array of FolderSummary | The folders above it, top level first |
| `ancestors[].id` | string | `fld_…` |
| `ancestors[].name` | string |  |
| `ancestors[].parent_id` | string or null |  |
| `ancestors[].created_at` | string (date-time) |  |
| `ancestors[].asset_count` | integer or null | Live assets directly in this folder |
| `ancestors[].folder_count` | integer or null | Subfolders directly in this folder |

## Rename or move a folder

`PATCH /v1/asset-folders/{folder_id}`

Needs `assets:write`. `parent_id` is tri-state: omitted leaves it, `null` moves it to the top level.

```bash title="curl"
curl -X PATCH "https://api.lumavox.ai/v1/asset-folders/$FOLDER_ID" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
```

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `folder_id` | string | yes |  |

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string |  | 1–120 characters. |
| `parent_id` | string or null |  | `fld_…`, or null for the top level; omit it to leave it |

**Responses**

| Status | Description |
|---|---|
| `200` | `application/json` — The folder as it now stands |
| `400` | The request could not be read: malformed JSON |
| `401` | Missing or invalid credentials |
| `403` | The key is valid but its scopes do not cover this capability — a key limited to `tts` gets this from `/v1/images` rather than a 401 |
| `404` | No such asset or folder |
| `422` | The request failed validation |
| `429` | Rate limit exceeded |

**`200` response body**

| Field | Type | Description |
|---|---|---|
| `id` | string | `fld_…` |
| `name` | string |  |
| `parent_id` | string or null |  |
| `created_at` | string (date-time) |  |
| `asset_count` | integer or null | Live assets directly in this folder |
| `folder_count` | integer or null | Subfolders directly in this folder |

## Delete a folder

`DELETE /v1/asset-folders/{folder_id}`

Needs `assets:write`. Its contents move up to the parent; nothing is deleted but the folder itself.

```bash title="curl"
curl -X DELETE "https://api.lumavox.ai/v1/asset-folders/$FOLDER_ID" \
  -H "Authorization: Bearer $API_KEY"
```

**Path parameters**

| Name | Type | Required | Description |
|---|---|---|---|
| `folder_id` | string | yes |  |

**Responses**

| Status | Description |
|---|---|
| `200` | `application/json` — What moved up to the parent; the folder itself is gone |
| `400` | The request could not be read: malformed JSON |
| `401` | Missing or invalid credentials |
| `403` | The key is valid but its scopes do not cover this capability — a key limited to `tts` gets this from `/v1/images` rather than a 401 |
| `404` | No such asset or folder |
| `422` | The request failed validation |
| `429` | Rate limit exceeded |

**`200` response body**

| Field | Type | Description |
|---|---|---|
| `moved_assets` | integer | Assets that moved up to the parent rather than being deleted |
| `moved_folders` | integer | Subfolders that moved up to the parent |
