# Playlists

Base URL: `https://api.brixsignage.com`. Send `Authorization: Bearer $BRIX_API_KEY` unless an operation says Auth: none.

## GET /v1/playlists

List playlists

List every playlist in the workspace, including its items and a summary of its dayparting rules.

Every playlist you can see, each with its items. Not paginated.

Auth: Bearer token. Permission: `playlist.view`.

```bash
curl "https://api.brixsignage.com/v1/playlists" \
  -H "Authorization: Bearer $BRIX_API_KEY"
```

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | array of Playlist |  |
| `data[].id` | string | Playlist id. |
| `data[].spaceId` | string |  |
| `data[].name` | string |  |
| `data[].description` | string |  |
| `data[].shuffle` | boolean |  |
| `data[].fullscreen` | boolean |  |
| `data[].startsAt` | string \| null | Plays only from this date (YYYY-MM-DD) or time, stored as sent. |
| `data[].expiresAt` | string \| null | Stops after this date (YYYY-MM-DD) or time, stored as sent. |
| `data[].approvalState` | "draft" \| "pending" \| "approved" \| "rejected" | Only `approved` content airs where the location requires approval. |
| `data[].nodeId` | string \| null | Home location; null = workspace root. |
| `data[].createdAt` | string | ISO-8601 timestamp (UTC). |
| `data[].updatedAt` | string | ISO-8601 timestamp (UTC). |
| `data[].deletedAt` | string \| null |  |
| `data[].fit` | "contain" \| "cover" \| "fill" \| "blur-fill" \| null |  |
| `data[].approvedSnapshot` | string \| null | Internal: the approved version, as stored JSON text. |
| `data[].importSourceId` | string \| null |  |
| `data[].recalledAt` | string \| null |  |
| `data[].recalledBy` | string \| null |  |
| `data[].usedByScreenCount` | integer |  |
| `data[].usedByScreens` | array of object | Up to 12 screens playing it. |
| `data[].usedByScreens[].id` | string |  |
| `data[].usedByScreens[].name` | string |  |
| `data[].allocations` | array of PlaylistAllocation |  |
| `data[].allocations[].id` | string |  |
| `data[].allocations[].label` | string |  |
| `data[].allocations[].ownerKind` | "user" \| "group" \| "org-unit" |  |
| `data[].allocations[].ownerId` | string |  |
| `data[].allocations[].ownerName` | string |  |
| `data[].allocations[].kind` | "percent" \| "every-nth" \| "daypart" |  |
| `data[].allocations[].value` | integer |  |
| `data[].allocations[].endValue` | integer | Absent (not null) when unset. |
| `data[].allocations[].colorClass` | string |  |
| `data[].allocations[].fillSpaceId` | string \| null |  |
| `data[].allocations[].fillPlaylistId` | string \| null |  |
| `data[].allocations[].unfilledBehavior` | "collapse" \| "filler" \| "holding" |  |
| `data[].allocations[].fillerRefKind` | "media" \| "playlist" \| "app" \| "layout" \| null |  |
| `data[].allocations[].fillerRefId` | string \| null |  |
| `data[].allocations[].requiresApproval` | boolean |  |
| `data[].items` | array of PlaylistItem |  |
| `data[].items[].id` | string | Playlist item id. |
| `data[].items[].refKind` | "media" \| "app" \| "layout" \| "playlist" |  |
| `data[].items[].refId` | string | The media / app instance / layout / playlist id. |
| `data[].items[].name` | string |  |
| `data[].items[].thumbnailUrl` | string | Empty string when there is none. |
| `data[].items[].kind` | string | The media kind for media items; `app`, `layout` or `playlist` otherwise. |
| `data[].items[].nestedItemCount` | integer \| null | Nested playlists only: its top-level item count. |
| `data[].items[].durationMode` | "fixed" \| "full" \| "live" \| "loop" \| "manual" |  |
| `data[].items[].durationSeconds` | integer | Dwell in seconds for `fixed`. |
| `data[].items[].loopCount` | integer |  |
| `data[].items[].loopDurationMs` | integer \| null |  |
| `data[].items[].mediaDurationSec` | number \| null |  |
| `data[].items[].fit` | "contain" \| "cover" \| "fill" \| "blur-fill" \| null | The item's own fit; null = inherit. |
| `data[].items[].assetFit` | "contain" \| "cover" \| "fill" \| "blur-fill" \| null |  |
| `data[].items[].assetStartsAt` | string \| null |  |
| `data[].items[].assetExpiresAt` | string \| null |  |
| `data[].items[].assetActive` | boolean \| null |  |
| `data[].items[].fullscreen` | boolean |  |
| `data[].items[].withheld` | "expired" \| "archived" \| "not-yet" \| null | Why a media item is not airing now, or null. |
| `data[].items[].allocationId` | string \| null |  |
| `data[].items[].position` | integer |  |
| `data[].sharedIn` | true | Present when the playlist is outside your locations and was shared in. |

Response 401: Missing, expired or revoked bearer token.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 403: The token lacks the permission this operation needs (see `x-brix-permission`).

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 5XX: Server error. The body carries a `requestId` to quote to support.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

## POST /v1/playlists

Create a playlist with a `name` and an optional `nodeId` and other fields. Safe to retry with an idempotency key, to protect against duplicate creation from a double-click.

Creates an empty playlist. Add items with POST /v1/playlists/{id}/items. Send an `Idempotency-Key` header to make a retry safe.

**Notes.**
- The create response is the new row plus empty `items` and `allocations`; it omits `fit`, `usedByScreenCount`, `usedByScreens` and the other columns that GET returns.

Auth: Bearer token. Permission: `playlist.create`.

Request body (`application/json`):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | yes |  |
| `description` | string | no |  |
| `shuffle` | boolean | no |  |
| `fullscreen` | boolean | no |  |
| `startsAt` | string | no | YYYY-MM-DD or ISO-8601. |
| `expiresAt` | string | no | YYYY-MM-DD or ISO-8601. |
| `nodeId` | string | no | Home location; defaults to your own. |

```bash
curl -X POST "https://api.brixsignage.com/v1/playlists" \
  -H "Authorization: Bearer $BRIX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Lunch menu","description":"Weekday 11:00-14:00","shuffle":false}'
```

Response 201: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | PlaylistCreated | A new, empty playlist. |
| `data.id` | string | Playlist id. |
| `data.spaceId` | string |  |
| `data.name` | string |  |
| `data.description` | string |  |
| `data.shuffle` | boolean |  |
| `data.fullscreen` | boolean |  |
| `data.startsAt` | string \| null | Plays only from this date (YYYY-MM-DD) or time, stored as sent. |
| `data.expiresAt` | string \| null | Stops after this date (YYYY-MM-DD) or time, stored as sent. |
| `data.approvalState` | "draft" \| "pending" \| "approved" \| "rejected" | Only `approved` content airs where the location requires approval. |
| `data.nodeId` | string \| null | Home location; null = workspace root. |
| `data.createdAt` | string | ISO-8601 timestamp (UTC). |
| `data.updatedAt` | string | ISO-8601 timestamp (UTC). |
| `data.deletedAt` | string \| null |  |
| `data.items` | array of PlaylistItem |  |
| `data.items[].id` | string | Playlist item id. |
| `data.items[].refKind` | "media" \| "app" \| "layout" \| "playlist" |  |
| `data.items[].refId` | string | The media / app instance / layout / playlist id. |
| `data.items[].name` | string |  |
| `data.items[].thumbnailUrl` | string | Empty string when there is none. |
| `data.items[].kind` | string | The media kind for media items; `app`, `layout` or `playlist` otherwise. |
| `data.items[].nestedItemCount` | integer \| null | Nested playlists only: its top-level item count. |
| `data.items[].durationMode` | "fixed" \| "full" \| "live" \| "loop" \| "manual" |  |
| `data.items[].durationSeconds` | integer | Dwell in seconds for `fixed`. |
| `data.items[].loopCount` | integer |  |
| `data.items[].loopDurationMs` | integer \| null |  |
| `data.items[].mediaDurationSec` | number \| null |  |
| `data.items[].fit` | "contain" \| "cover" \| "fill" \| "blur-fill" \| null | The item's own fit; null = inherit. |
| `data.items[].assetFit` | "contain" \| "cover" \| "fill" \| "blur-fill" \| null |  |
| `data.items[].assetStartsAt` | string \| null |  |
| `data.items[].assetExpiresAt` | string \| null |  |
| `data.items[].assetActive` | boolean \| null |  |
| `data.items[].fullscreen` | boolean |  |
| `data.items[].withheld` | "expired" \| "archived" \| "not-yet" \| null | Why a media item is not airing now, or null. |
| `data.items[].allocationId` | string \| null |  |
| `data.items[].position` | integer |  |
| `data.allocations` | array of PlaylistAllocation |  |
| `data.allocations[].id` | string |  |
| `data.allocations[].label` | string |  |
| `data.allocations[].ownerKind` | "user" \| "group" \| "org-unit" |  |
| `data.allocations[].ownerId` | string |  |
| `data.allocations[].ownerName` | string |  |
| `data.allocations[].kind` | "percent" \| "every-nth" \| "daypart" |  |
| `data.allocations[].value` | integer |  |
| `data.allocations[].endValue` | integer | Absent (not null) when unset. |
| `data.allocations[].colorClass` | string |  |
| `data.allocations[].fillSpaceId` | string \| null |  |
| `data.allocations[].fillPlaylistId` | string \| null |  |
| `data.allocations[].unfilledBehavior` | "collapse" \| "filler" \| "holding" |  |
| `data.allocations[].fillerRefKind` | "media" \| "playlist" \| "app" \| "layout" \| null |  |
| `data.allocations[].fillerRefId` | string \| null |  |
| `data.allocations[].requiresApproval` | boolean |  |

```json
{
  "data": {
    "id": "pl_4d5e6f7a8b9c0d1e",
    "spaceId": "space_1a2b3c4d5e6f7a8b",
    "name": "Lunch menu",
    "description": "Weekday 11:00-14:00",
    "shuffle": false,
    "fullscreen": false,
    "startsAt": null,
    "expiresAt": null,
    "approvalState": "approved",
    "nodeId": null,
    "createdAt": "2026-09-28T09:00:00.000Z",
    "updatedAt": "2026-09-28T09:00:00.000Z",
    "deletedAt": null,
    "items": [],
    "allocations": []
  }
}
```

Response 401: Missing, expired or revoked bearer token.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 403: The token lacks the permission this operation needs (see `x-brix-permission`).

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 422: Missing name, an unparsable date, or an unknown location.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 5XX: Server error. The body carries a `requestId` to quote to support.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

## GET /v1/playlists/{id}

Get a playlist

Retrieve one playlist with its ordered items, including each item's reference kind and id, duration, and rules.

Auth: Bearer token. Permission: `playlist.view`.

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Playlist id. |

```bash
curl "https://api.brixsignage.com/v1/playlists/{id}" \
  -H "Authorization: Bearer $BRIX_API_KEY"
```

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | PlaylistDetail |  |
| `data.id` | string | Playlist id. |
| `data.spaceId` | string |  |
| `data.name` | string |  |
| `data.description` | string |  |
| `data.shuffle` | boolean |  |
| `data.fullscreen` | boolean |  |
| `data.startsAt` | string \| null | Plays only from this date (YYYY-MM-DD) or time, stored as sent. |
| `data.expiresAt` | string \| null | Stops after this date (YYYY-MM-DD) or time, stored as sent. |
| `data.approvalState` | "draft" \| "pending" \| "approved" \| "rejected" | Only `approved` content airs where the location requires approval. |
| `data.nodeId` | string \| null | Home location; null = workspace root. |
| `data.createdAt` | string | ISO-8601 timestamp (UTC). |
| `data.updatedAt` | string | ISO-8601 timestamp (UTC). |
| `data.deletedAt` | string \| null |  |
| `data.fit` | "contain" \| "cover" \| "fill" \| "blur-fill" \| null |  |
| `data.approvedSnapshot` | string \| null | Internal: the approved version, as stored JSON text. |
| `data.importSourceId` | string \| null |  |
| `data.recalledAt` | string \| null |  |
| `data.recalledBy` | string \| null |  |
| `data.usedByScreenCount` | integer |  |
| `data.usedByScreens` | array of object | Up to 12 screens playing it. |
| `data.usedByScreens[].id` | string |  |
| `data.usedByScreens[].name` | string |  |
| `data.allocations` | array of PlaylistAllocation |  |
| `data.allocations[].id` | string |  |
| `data.allocations[].label` | string |  |
| `data.allocations[].ownerKind` | "user" \| "group" \| "org-unit" |  |
| `data.allocations[].ownerId` | string |  |
| `data.allocations[].ownerName` | string |  |
| `data.allocations[].kind` | "percent" \| "every-nth" \| "daypart" |  |
| `data.allocations[].value` | integer |  |
| `data.allocations[].endValue` | integer | Absent (not null) when unset. |
| `data.allocations[].colorClass` | string |  |
| `data.allocations[].fillSpaceId` | string \| null |  |
| `data.allocations[].fillPlaylistId` | string \| null |  |
| `data.allocations[].unfilledBehavior` | "collapse" \| "filler" \| "holding" |  |
| `data.allocations[].fillerRefKind` | "media" \| "playlist" \| "app" \| "layout" \| null |  |
| `data.allocations[].fillerRefId` | string \| null |  |
| `data.allocations[].requiresApproval` | boolean |  |
| `data.items` | array of PlaylistItem |  |
| `data.items[].id` | string | Playlist item id. |
| `data.items[].refKind` | "media" \| "app" \| "layout" \| "playlist" |  |
| `data.items[].refId` | string | The media / app instance / layout / playlist id. |
| `data.items[].name` | string |  |
| `data.items[].thumbnailUrl` | string | Empty string when there is none. |
| `data.items[].kind` | string | The media kind for media items; `app`, `layout` or `playlist` otherwise. |
| `data.items[].nestedItemCount` | integer \| null | Nested playlists only: its top-level item count. |
| `data.items[].durationMode` | "fixed" \| "full" \| "live" \| "loop" \| "manual" |  |
| `data.items[].durationSeconds` | integer | Dwell in seconds for `fixed`. |
| `data.items[].loopCount` | integer |  |
| `data.items[].loopDurationMs` | integer \| null |  |
| `data.items[].mediaDurationSec` | number \| null |  |
| `data.items[].fit` | "contain" \| "cover" \| "fill" \| "blur-fill" \| null | The item's own fit; null = inherit. |
| `data.items[].assetFit` | "contain" \| "cover" \| "fill" \| "blur-fill" \| null |  |
| `data.items[].assetStartsAt` | string \| null |  |
| `data.items[].assetExpiresAt` | string \| null |  |
| `data.items[].assetActive` | boolean \| null |  |
| `data.items[].fullscreen` | boolean |  |
| `data.items[].withheld` | "expired" \| "archived" \| "not-yet" \| null | Why a media item is not airing now, or null. |
| `data.items[].allocationId` | string \| null |  |
| `data.items[].position` | integer |  |
| `data.sharedIn` | true | Present when the playlist is outside your locations and was shared in. |
| `data.requiresApproval` | boolean | The home location requires approval before content airs. |
| `data.resolvedItemCount` | integer | Item count with nested playlists expanded. |
| `data.resolvedDurationSec` | number | Runtime in seconds with nested playlists expanded. |

Response 401: Missing, expired or revoked bearer token.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 403: The token lacks the permission this operation needs (see `x-brix-permission`).

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 404: No such playlist in this workspace.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 5XX: Server error. The body carries a `requestId` to quote to support.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

## PATCH /v1/playlists/{id}

Update a playlist

Edit a playlist's own fields, such as name, node, shuffle, and transition settings. Items are managed through the separate /items routes.

Send only the fields to change. A playback change to an approved playlist returns it to `draft` where approval is required.

**Notes.**
- Fields of the wrong type are ignored rather than refused (for example `name: 5`).

Auth: Bearer token. Permission: `playlist.edit`.

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Playlist id. |

Request body (`application/json`):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | no |  |
| `description` | string | no |  |
| `shuffle` | boolean | no |  |
| `fullscreen` | boolean | no |  |
| `fit` | "contain" \| "cover" \| "fill" \| "blur-fill" \| null | no |  |
| `startsAt` | string \| null | no |  |
| `expiresAt` | string \| null | no |  |
| `nodeId` | string \| null | no | Move to another location (needs playlist.edit there). |
| `baseUpdatedAt` | string | no | The `updatedAt` your edit is based on; a stale value answers 409 with `current`. |

```bash
curl -X PATCH "https://api.brixsignage.com/v1/playlists/{id}" \
  -H "Authorization: Bearer $BRIX_API_KEY" \
  -H "Content-Type: application/json"
```

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | Playlist | A playlist with its ordered items. |
| `data.id` | string | Playlist id. |
| `data.spaceId` | string |  |
| `data.name` | string |  |
| `data.description` | string |  |
| `data.shuffle` | boolean |  |
| `data.fullscreen` | boolean |  |
| `data.startsAt` | string \| null | Plays only from this date (YYYY-MM-DD) or time, stored as sent. |
| `data.expiresAt` | string \| null | Stops after this date (YYYY-MM-DD) or time, stored as sent. |
| `data.approvalState` | "draft" \| "pending" \| "approved" \| "rejected" | Only `approved` content airs where the location requires approval. |
| `data.nodeId` | string \| null | Home location; null = workspace root. |
| `data.createdAt` | string | ISO-8601 timestamp (UTC). |
| `data.updatedAt` | string | ISO-8601 timestamp (UTC). |
| `data.deletedAt` | string \| null |  |
| `data.fit` | "contain" \| "cover" \| "fill" \| "blur-fill" \| null |  |
| `data.approvedSnapshot` | string \| null | Internal: the approved version, as stored JSON text. |
| `data.importSourceId` | string \| null |  |
| `data.recalledAt` | string \| null |  |
| `data.recalledBy` | string \| null |  |
| `data.usedByScreenCount` | integer |  |
| `data.usedByScreens` | array of object | Up to 12 screens playing it. |
| `data.usedByScreens[].id` | string |  |
| `data.usedByScreens[].name` | string |  |
| `data.allocations` | array of PlaylistAllocation |  |
| `data.allocations[].id` | string |  |
| `data.allocations[].label` | string |  |
| `data.allocations[].ownerKind` | "user" \| "group" \| "org-unit" |  |
| `data.allocations[].ownerId` | string |  |
| `data.allocations[].ownerName` | string |  |
| `data.allocations[].kind` | "percent" \| "every-nth" \| "daypart" |  |
| `data.allocations[].value` | integer |  |
| `data.allocations[].endValue` | integer | Absent (not null) when unset. |
| `data.allocations[].colorClass` | string |  |
| `data.allocations[].fillSpaceId` | string \| null |  |
| `data.allocations[].fillPlaylistId` | string \| null |  |
| `data.allocations[].unfilledBehavior` | "collapse" \| "filler" \| "holding" |  |
| `data.allocations[].fillerRefKind` | "media" \| "playlist" \| "app" \| "layout" \| null |  |
| `data.allocations[].fillerRefId` | string \| null |  |
| `data.allocations[].requiresApproval` | boolean |  |
| `data.items` | array of PlaylistItem |  |
| `data.items[].id` | string | Playlist item id. |
| `data.items[].refKind` | "media" \| "app" \| "layout" \| "playlist" |  |
| `data.items[].refId` | string | The media / app instance / layout / playlist id. |
| `data.items[].name` | string |  |
| `data.items[].thumbnailUrl` | string | Empty string when there is none. |
| `data.items[].kind` | string | The media kind for media items; `app`, `layout` or `playlist` otherwise. |
| `data.items[].nestedItemCount` | integer \| null | Nested playlists only: its top-level item count. |
| `data.items[].durationMode` | "fixed" \| "full" \| "live" \| "loop" \| "manual" |  |
| `data.items[].durationSeconds` | integer | Dwell in seconds for `fixed`. |
| `data.items[].loopCount` | integer |  |
| `data.items[].loopDurationMs` | integer \| null |  |
| `data.items[].mediaDurationSec` | number \| null |  |
| `data.items[].fit` | "contain" \| "cover" \| "fill" \| "blur-fill" \| null | The item's own fit; null = inherit. |
| `data.items[].assetFit` | "contain" \| "cover" \| "fill" \| "blur-fill" \| null |  |
| `data.items[].assetStartsAt` | string \| null |  |
| `data.items[].assetExpiresAt` | string \| null |  |
| `data.items[].assetActive` | boolean \| null |  |
| `data.items[].fullscreen` | boolean |  |
| `data.items[].withheld` | "expired" \| "archived" \| "not-yet" \| null | Why a media item is not airing now, or null. |
| `data.items[].allocationId` | string \| null |  |
| `data.items[].position` | integer |  |
| `data.sharedIn` | true | Present when the playlist is outside your locations and was shared in. |

Response 401: Missing, expired or revoked bearer token.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 403: Moving it to a location where you lack playlist.edit.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 404: No such playlist in this workspace.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 409: `conflict`: `baseUpdatedAt` is stale; the body carries `current`.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 422: An unparsable date or an unknown location.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 5XX: Server error. The body carries a `requestId` to quote to support.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

## DELETE /v1/playlists/{id}

Delete a playlist to the recycle bin. Screens assigned to it fall back to their default content. Fails with 409 if the playlist is shared into other spaces, unless the deletion is forced.

Moves the playlist to the recycle bin (restorable for 30 days).

Auth: Bearer token. Permission: `playlist.delete`.

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Playlist id. |
| `force` | query | "true" | no | Delete even when it is shared into other places. |

```bash
curl -X DELETE "https://api.brixsignage.com/v1/playlists/{id}" \
  -H "Authorization: Bearer $BRIX_API_KEY"
```

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object |  |
| `data.id` | string |  |
| `data.deleted` | true |  |
| `data.sharesRemoved` | integer |  |

Response 401: Missing, expired or revoked bearer token.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 403: The token lacks the permission this operation needs (see `x-brix-permission`).

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 404: No such playlist in this workspace.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 409: `content_shared`: the item is shared into other places; `shareCount`, `crossSpaceShares`, `contentShares` say where. Repeat with `?force=true` to delete it and those shares.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 5XX: Server error. The body carries a `requestId` to quote to support.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

## PUT /v1/playlists/{id}/allocations

Set playlist allocations

Set a playlist's allocations: shares of its airtime given to a person, a group or a location (a percent, every nth play, or a time of day). This replaces the whole set; allocations you leave out are removed.

**Notes.**
- An out-of-range `value` is 400 `validation_error`; every other validation failure is 422.
- Rows with an unknown `ownerKind` or `kind` are dropped without an error.
- A missing or malformed body is treated as an empty list, which removes every allocation.
- The returned playlist row (`updatedAt`, `approvalState`) is the one read before this edit; an approved playlist is returned to `draft` in storage by this call. Re-read with GET to see it.

Auth: Bearer token. Permission: `playlist.edit`.

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Playlist id. |

Request body (`application/json`):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `allocations` | array of object | yes | The whole set, at most 200. Allocations left out are removed. |
| `allocations[].id` | string | no | Keep this id: one of the playlist's current allocations, or a new id of 6-64 URL-safe characters. Otherwise a new id is made. |
| `allocations[].label` | string | no |  |
| `allocations[].ownerKind` | "user" \| "group" \| "org-unit" | yes | Who gets the share. `org-unit` = a location (`ownerId` is its node id). |
| `allocations[].ownerId` | string | yes |  |
| `allocations[].ownerName` | string | no |  |
| `allocations[].kind` | "percent" \| "every-nth" \| "daypart" | yes |  |
| `allocations[].value` | integer | yes | `percent` 0-100, `every-nth` 1-100, `daypart` 0-1439 (the minute of the day it opens). |
| `allocations[].endValue` | number | no | `daypart`: the minute of the day it closes. |
| `allocations[].colorClass` | string | no |  |
| `allocations[].fillSpaceId` | string \| null | no | A child workspace that fills the share; null = this workspace. |
| `allocations[].unfilledBehavior` | "collapse" \| "filler" \| "holding" | no | What plays while the share is not filled. Default `collapse`. |
| `allocations[].fillerRefKind` | "media" \| "playlist" \| "app" \| "layout" | no | With `filler`: the kind of the filler content. |
| `allocations[].fillerRefId` | string | no |  |
| `allocations[].requiresApproval` | boolean | no | What the recipient puts in the share must be approved first. |

```bash
curl -X PUT "https://api.brixsignage.com/v1/playlists/{id}/allocations" \
  -H "Authorization: Bearer $BRIX_API_KEY" \
  -H "Content-Type: application/json"
```

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | Playlist | A playlist with its ordered items. |
| `data.id` | string | Playlist id. |
| `data.spaceId` | string |  |
| `data.name` | string |  |
| `data.description` | string |  |
| `data.shuffle` | boolean |  |
| `data.fullscreen` | boolean |  |
| `data.startsAt` | string \| null | Plays only from this date (YYYY-MM-DD) or time, stored as sent. |
| `data.expiresAt` | string \| null | Stops after this date (YYYY-MM-DD) or time, stored as sent. |
| `data.approvalState` | "draft" \| "pending" \| "approved" \| "rejected" | Only `approved` content airs where the location requires approval. |
| `data.nodeId` | string \| null | Home location; null = workspace root. |
| `data.createdAt` | string | ISO-8601 timestamp (UTC). |
| `data.updatedAt` | string | ISO-8601 timestamp (UTC). |
| `data.deletedAt` | string \| null |  |
| `data.fit` | "contain" \| "cover" \| "fill" \| "blur-fill" \| null |  |
| `data.approvedSnapshot` | string \| null | Internal: the approved version, as stored JSON text. |
| `data.importSourceId` | string \| null |  |
| `data.recalledAt` | string \| null |  |
| `data.recalledBy` | string \| null |  |
| `data.usedByScreenCount` | integer |  |
| `data.usedByScreens` | array of object | Up to 12 screens playing it. |
| `data.usedByScreens[].id` | string |  |
| `data.usedByScreens[].name` | string |  |
| `data.allocations` | array of PlaylistAllocation |  |
| `data.allocations[].id` | string |  |
| `data.allocations[].label` | string |  |
| `data.allocations[].ownerKind` | "user" \| "group" \| "org-unit" |  |
| `data.allocations[].ownerId` | string |  |
| `data.allocations[].ownerName` | string |  |
| `data.allocations[].kind` | "percent" \| "every-nth" \| "daypart" |  |
| `data.allocations[].value` | integer |  |
| `data.allocations[].endValue` | integer | Absent (not null) when unset. |
| `data.allocations[].colorClass` | string |  |
| `data.allocations[].fillSpaceId` | string \| null |  |
| `data.allocations[].fillPlaylistId` | string \| null |  |
| `data.allocations[].unfilledBehavior` | "collapse" \| "filler" \| "holding" |  |
| `data.allocations[].fillerRefKind` | "media" \| "playlist" \| "app" \| "layout" \| null |  |
| `data.allocations[].fillerRefId` | string \| null |  |
| `data.allocations[].requiresApproval` | boolean |  |
| `data.items` | array of PlaylistItem |  |
| `data.items[].id` | string | Playlist item id. |
| `data.items[].refKind` | "media" \| "app" \| "layout" \| "playlist" |  |
| `data.items[].refId` | string | The media / app instance / layout / playlist id. |
| `data.items[].name` | string |  |
| `data.items[].thumbnailUrl` | string | Empty string when there is none. |
| `data.items[].kind` | string | The media kind for media items; `app`, `layout` or `playlist` otherwise. |
| `data.items[].nestedItemCount` | integer \| null | Nested playlists only: its top-level item count. |
| `data.items[].durationMode` | "fixed" \| "full" \| "live" \| "loop" \| "manual" |  |
| `data.items[].durationSeconds` | integer | Dwell in seconds for `fixed`. |
| `data.items[].loopCount` | integer |  |
| `data.items[].loopDurationMs` | integer \| null |  |
| `data.items[].mediaDurationSec` | number \| null |  |
| `data.items[].fit` | "contain" \| "cover" \| "fill" \| "blur-fill" \| null | The item's own fit; null = inherit. |
| `data.items[].assetFit` | "contain" \| "cover" \| "fill" \| "blur-fill" \| null |  |
| `data.items[].assetStartsAt` | string \| null |  |
| `data.items[].assetExpiresAt` | string \| null |  |
| `data.items[].assetActive` | boolean \| null |  |
| `data.items[].fullscreen` | boolean |  |
| `data.items[].withheld` | "expired" \| "archived" \| "not-yet" \| null | Why a media item is not airing now, or null. |
| `data.items[].allocationId` | string \| null |  |
| `data.items[].position` | integer |  |
| `data.sharedIn` | true | Present when the playlist is outside your locations and was shared in. |

Response 400: `validation_error`: a `value` outside its kind's range or not a whole number.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 401: Missing, expired or revoked bearer token.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 403: The token lacks the permission this operation needs (see `x-brix-permission`).

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 404: No such playlist in this workspace.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 422: More than 200 allocations, a location that is not in this workspace (`invalid_node`), or percent shares that would give away more than 99% on some screen.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 5XX: Server error. The body carries a `requestId` to quote to support.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

## POST /v1/playlists/{id}/duplicate

Duplicate a playlist

Create a deep copy of a playlist, including its settings, share-of-voice allocations (fill assignments reset), and ordered items, named "<name> copy" and unique within the space. The copy starts as a draft with no assignment. Requires permission to create playlists at the source playlist's home node.

Auth: Bearer token. Permission: `playlist.create`.

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Playlist id. |

```bash
curl -X POST "https://api.brixsignage.com/v1/playlists/{id}/duplicate" \
  -H "Authorization: Bearer $BRIX_API_KEY"
```

Response 201: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | Playlist | A playlist with its ordered items. |
| `data.id` | string | Playlist id. |
| `data.spaceId` | string |  |
| `data.name` | string |  |
| `data.description` | string |  |
| `data.shuffle` | boolean |  |
| `data.fullscreen` | boolean |  |
| `data.startsAt` | string \| null | Plays only from this date (YYYY-MM-DD) or time, stored as sent. |
| `data.expiresAt` | string \| null | Stops after this date (YYYY-MM-DD) or time, stored as sent. |
| `data.approvalState` | "draft" \| "pending" \| "approved" \| "rejected" | Only `approved` content airs where the location requires approval. |
| `data.nodeId` | string \| null | Home location; null = workspace root. |
| `data.createdAt` | string | ISO-8601 timestamp (UTC). |
| `data.updatedAt` | string | ISO-8601 timestamp (UTC). |
| `data.deletedAt` | string \| null |  |
| `data.fit` | "contain" \| "cover" \| "fill" \| "blur-fill" \| null |  |
| `data.approvedSnapshot` | string \| null | Internal: the approved version, as stored JSON text. |
| `data.importSourceId` | string \| null |  |
| `data.recalledAt` | string \| null |  |
| `data.recalledBy` | string \| null |  |
| `data.usedByScreenCount` | integer |  |
| `data.usedByScreens` | array of object | Up to 12 screens playing it. |
| `data.usedByScreens[].id` | string |  |
| `data.usedByScreens[].name` | string |  |
| `data.allocations` | array of PlaylistAllocation |  |
| `data.allocations[].id` | string |  |
| `data.allocations[].label` | string |  |
| `data.allocations[].ownerKind` | "user" \| "group" \| "org-unit" |  |
| `data.allocations[].ownerId` | string |  |
| `data.allocations[].ownerName` | string |  |
| `data.allocations[].kind` | "percent" \| "every-nth" \| "daypart" |  |
| `data.allocations[].value` | integer |  |
| `data.allocations[].endValue` | integer | Absent (not null) when unset. |
| `data.allocations[].colorClass` | string |  |
| `data.allocations[].fillSpaceId` | string \| null |  |
| `data.allocations[].fillPlaylistId` | string \| null |  |
| `data.allocations[].unfilledBehavior` | "collapse" \| "filler" \| "holding" |  |
| `data.allocations[].fillerRefKind` | "media" \| "playlist" \| "app" \| "layout" \| null |  |
| `data.allocations[].fillerRefId` | string \| null |  |
| `data.allocations[].requiresApproval` | boolean |  |
| `data.items` | array of PlaylistItem |  |
| `data.items[].id` | string | Playlist item id. |
| `data.items[].refKind` | "media" \| "app" \| "layout" \| "playlist" |  |
| `data.items[].refId` | string | The media / app instance / layout / playlist id. |
| `data.items[].name` | string |  |
| `data.items[].thumbnailUrl` | string | Empty string when there is none. |
| `data.items[].kind` | string | The media kind for media items; `app`, `layout` or `playlist` otherwise. |
| `data.items[].nestedItemCount` | integer \| null | Nested playlists only: its top-level item count. |
| `data.items[].durationMode` | "fixed" \| "full" \| "live" \| "loop" \| "manual" |  |
| `data.items[].durationSeconds` | integer | Dwell in seconds for `fixed`. |
| `data.items[].loopCount` | integer |  |
| `data.items[].loopDurationMs` | integer \| null |  |
| `data.items[].mediaDurationSec` | number \| null |  |
| `data.items[].fit` | "contain" \| "cover" \| "fill" \| "blur-fill" \| null | The item's own fit; null = inherit. |
| `data.items[].assetFit` | "contain" \| "cover" \| "fill" \| "blur-fill" \| null |  |
| `data.items[].assetStartsAt` | string \| null |  |
| `data.items[].assetExpiresAt` | string \| null |  |
| `data.items[].assetActive` | boolean \| null |  |
| `data.items[].fullscreen` | boolean |  |
| `data.items[].withheld` | "expired" \| "archived" \| "not-yet" \| null | Why a media item is not airing now, or null. |
| `data.items[].allocationId` | string \| null |  |
| `data.items[].position` | integer |  |
| `data.sharedIn` | true | Present when the playlist is outside your locations and was shared in. |

Response 401: Missing, expired or revoked bearer token.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 403: The token lacks the permission this operation needs (see `x-brix-permission`).

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 404: No such playlist in this workspace.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 5XX: Server error. The body carries a `requestId` to quote to support.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

## POST /v1/playlists/{id}/items

Add an item to a playlist

Append one item to a playlist with `refKind`, `refId`, and an optional `durationSec` and other fields. Rejects references from another space, and rejects any nesting that would create a loop, such as a playlist referencing a layout zone that contains the same playlist, with a clear error rather than failing silently.

Appends one item. A video or audio file defaults to playing its full length, an animated image to one loop, anything else to 10 seconds. Returns the whole playlist.

**Notes.**
- `position` is accepted and ignored.
- The returned `approvalState` is the value from before the edit; an approved playlist is returned to `draft` in storage by this call. Re-read with GET to see it.

Auth: Bearer token. Permission: `playlist.edit`.

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Playlist id. |

Request body (`application/json`):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `refKind` | "media" \| "app" \| "layout" \| "playlist" | yes |  |
| `refId` | string | yes |  |
| `durationMode` | "fixed" \| "full" \| "live" \| "loop" \| "manual" | no |  |
| `durationSeconds` | integer | no |  |
| `loopCount` | number | no |  |
| `fit` | string | no | contain, cover, fill or blur-fill; anything else means inherit. |
| `fullscreen` | boolean | no |  |
| `position` | number | no | Accepted but ignored: the item is always appended. Reorder with PUT /v1/playlists/{id}/items. |
| `allocationId` | string | no |  |

```bash
curl -X POST "https://api.brixsignage.com/v1/playlists/{id}/items" \
  -H "Authorization: Bearer $BRIX_API_KEY" \
  -H "Content-Type: application/json"
```

Response 201: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | Playlist | A playlist with its ordered items. |
| `data.id` | string | Playlist id. |
| `data.spaceId` | string |  |
| `data.name` | string |  |
| `data.description` | string |  |
| `data.shuffle` | boolean |  |
| `data.fullscreen` | boolean |  |
| `data.startsAt` | string \| null | Plays only from this date (YYYY-MM-DD) or time, stored as sent. |
| `data.expiresAt` | string \| null | Stops after this date (YYYY-MM-DD) or time, stored as sent. |
| `data.approvalState` | "draft" \| "pending" \| "approved" \| "rejected" | Only `approved` content airs where the location requires approval. |
| `data.nodeId` | string \| null | Home location; null = workspace root. |
| `data.createdAt` | string | ISO-8601 timestamp (UTC). |
| `data.updatedAt` | string | ISO-8601 timestamp (UTC). |
| `data.deletedAt` | string \| null |  |
| `data.fit` | "contain" \| "cover" \| "fill" \| "blur-fill" \| null |  |
| `data.approvedSnapshot` | string \| null | Internal: the approved version, as stored JSON text. |
| `data.importSourceId` | string \| null |  |
| `data.recalledAt` | string \| null |  |
| `data.recalledBy` | string \| null |  |
| `data.usedByScreenCount` | integer |  |
| `data.usedByScreens` | array of object | Up to 12 screens playing it. |
| `data.usedByScreens[].id` | string |  |
| `data.usedByScreens[].name` | string |  |
| `data.allocations` | array of PlaylistAllocation |  |
| `data.allocations[].id` | string |  |
| `data.allocations[].label` | string |  |
| `data.allocations[].ownerKind` | "user" \| "group" \| "org-unit" |  |
| `data.allocations[].ownerId` | string |  |
| `data.allocations[].ownerName` | string |  |
| `data.allocations[].kind` | "percent" \| "every-nth" \| "daypart" |  |
| `data.allocations[].value` | integer |  |
| `data.allocations[].endValue` | integer | Absent (not null) when unset. |
| `data.allocations[].colorClass` | string |  |
| `data.allocations[].fillSpaceId` | string \| null |  |
| `data.allocations[].fillPlaylistId` | string \| null |  |
| `data.allocations[].unfilledBehavior` | "collapse" \| "filler" \| "holding" |  |
| `data.allocations[].fillerRefKind` | "media" \| "playlist" \| "app" \| "layout" \| null |  |
| `data.allocations[].fillerRefId` | string \| null |  |
| `data.allocations[].requiresApproval` | boolean |  |
| `data.items` | array of PlaylistItem |  |
| `data.items[].id` | string | Playlist item id. |
| `data.items[].refKind` | "media" \| "app" \| "layout" \| "playlist" |  |
| `data.items[].refId` | string | The media / app instance / layout / playlist id. |
| `data.items[].name` | string |  |
| `data.items[].thumbnailUrl` | string | Empty string when there is none. |
| `data.items[].kind` | string | The media kind for media items; `app`, `layout` or `playlist` otherwise. |
| `data.items[].nestedItemCount` | integer \| null | Nested playlists only: its top-level item count. |
| `data.items[].durationMode` | "fixed" \| "full" \| "live" \| "loop" \| "manual" |  |
| `data.items[].durationSeconds` | integer | Dwell in seconds for `fixed`. |
| `data.items[].loopCount` | integer |  |
| `data.items[].loopDurationMs` | integer \| null |  |
| `data.items[].mediaDurationSec` | number \| null |  |
| `data.items[].fit` | "contain" \| "cover" \| "fill" \| "blur-fill" \| null | The item's own fit; null = inherit. |
| `data.items[].assetFit` | "contain" \| "cover" \| "fill" \| "blur-fill" \| null |  |
| `data.items[].assetStartsAt` | string \| null |  |
| `data.items[].assetExpiresAt` | string \| null |  |
| `data.items[].assetActive` | boolean \| null |  |
| `data.items[].fullscreen` | boolean |  |
| `data.items[].withheld` | "expired" \| "archived" \| "not-yet" \| null | Why a media item is not airing now, or null. |
| `data.items[].allocationId` | string \| null |  |
| `data.items[].position` | integer |  |
| `data.sharedIn` | true | Present when the playlist is outside your locations and was shared in. |

Response 401: Missing, expired or revoked bearer token.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 403: `not_shared`: the content is not available at the playlist's location.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 404: No such playlist, or the referenced content is not in this workspace.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 422: Invalid body, a playlist loop, a layout with unbound zones, or an unknown allocation.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 5XX: Server error. The body carries a `requestId` to quote to support.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

## PUT /v1/playlists/{id}/items

Reorder a playlist's items

Replace a playlist's entire item list in one call. Send the full array of items in the order you want; the same reference and loop validation applies as when adding a single item.

Sets the order of the items: `itemIds[0]` plays first. Unknown ids are skipped. It does not add or remove items. Returns the whole playlist.

**Notes.**
- Items left out of `itemIds` keep their old position number, so two items can share a position. Send every item id.
- A missing or malformed body is treated as an empty list, not refused.

Auth: Bearer token. Permission: `playlist.edit`.

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Playlist id. |

Request body (`application/json`):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `itemIds` | array of string | yes | Item ids in the new order. |

```bash
curl -X PUT "https://api.brixsignage.com/v1/playlists/{id}/items" \
  -H "Authorization: Bearer $BRIX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"itemIds":["pli_7a8b9c0d1e2f3a4b","pli_1e2f3a4b5c6d7e8f"]}'
```

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | Playlist | A playlist with its ordered items. |
| `data.id` | string | Playlist id. |
| `data.spaceId` | string |  |
| `data.name` | string |  |
| `data.description` | string |  |
| `data.shuffle` | boolean |  |
| `data.fullscreen` | boolean |  |
| `data.startsAt` | string \| null | Plays only from this date (YYYY-MM-DD) or time, stored as sent. |
| `data.expiresAt` | string \| null | Stops after this date (YYYY-MM-DD) or time, stored as sent. |
| `data.approvalState` | "draft" \| "pending" \| "approved" \| "rejected" | Only `approved` content airs where the location requires approval. |
| `data.nodeId` | string \| null | Home location; null = workspace root. |
| `data.createdAt` | string | ISO-8601 timestamp (UTC). |
| `data.updatedAt` | string | ISO-8601 timestamp (UTC). |
| `data.deletedAt` | string \| null |  |
| `data.fit` | "contain" \| "cover" \| "fill" \| "blur-fill" \| null |  |
| `data.approvedSnapshot` | string \| null | Internal: the approved version, as stored JSON text. |
| `data.importSourceId` | string \| null |  |
| `data.recalledAt` | string \| null |  |
| `data.recalledBy` | string \| null |  |
| `data.usedByScreenCount` | integer |  |
| `data.usedByScreens` | array of object | Up to 12 screens playing it. |
| `data.usedByScreens[].id` | string |  |
| `data.usedByScreens[].name` | string |  |
| `data.allocations` | array of PlaylistAllocation |  |
| `data.allocations[].id` | string |  |
| `data.allocations[].label` | string |  |
| `data.allocations[].ownerKind` | "user" \| "group" \| "org-unit" |  |
| `data.allocations[].ownerId` | string |  |
| `data.allocations[].ownerName` | string |  |
| `data.allocations[].kind` | "percent" \| "every-nth" \| "daypart" |  |
| `data.allocations[].value` | integer |  |
| `data.allocations[].endValue` | integer | Absent (not null) when unset. |
| `data.allocations[].colorClass` | string |  |
| `data.allocations[].fillSpaceId` | string \| null |  |
| `data.allocations[].fillPlaylistId` | string \| null |  |
| `data.allocations[].unfilledBehavior` | "collapse" \| "filler" \| "holding" |  |
| `data.allocations[].fillerRefKind` | "media" \| "playlist" \| "app" \| "layout" \| null |  |
| `data.allocations[].fillerRefId` | string \| null |  |
| `data.allocations[].requiresApproval` | boolean |  |
| `data.items` | array of PlaylistItem |  |
| `data.items[].id` | string | Playlist item id. |
| `data.items[].refKind` | "media" \| "app" \| "layout" \| "playlist" |  |
| `data.items[].refId` | string | The media / app instance / layout / playlist id. |
| `data.items[].name` | string |  |
| `data.items[].thumbnailUrl` | string | Empty string when there is none. |
| `data.items[].kind` | string | The media kind for media items; `app`, `layout` or `playlist` otherwise. |
| `data.items[].nestedItemCount` | integer \| null | Nested playlists only: its top-level item count. |
| `data.items[].durationMode` | "fixed" \| "full" \| "live" \| "loop" \| "manual" |  |
| `data.items[].durationSeconds` | integer | Dwell in seconds for `fixed`. |
| `data.items[].loopCount` | integer |  |
| `data.items[].loopDurationMs` | integer \| null |  |
| `data.items[].mediaDurationSec` | number \| null |  |
| `data.items[].fit` | "contain" \| "cover" \| "fill" \| "blur-fill" \| null | The item's own fit; null = inherit. |
| `data.items[].assetFit` | "contain" \| "cover" \| "fill" \| "blur-fill" \| null |  |
| `data.items[].assetStartsAt` | string \| null |  |
| `data.items[].assetExpiresAt` | string \| null |  |
| `data.items[].assetActive` | boolean \| null |  |
| `data.items[].fullscreen` | boolean |  |
| `data.items[].withheld` | "expired" \| "archived" \| "not-yet" \| null | Why a media item is not airing now, or null. |
| `data.items[].allocationId` | string \| null |  |
| `data.items[].position` | integer |  |
| `data.sharedIn` | true | Present when the playlist is outside your locations and was shared in. |

Response 401: Missing, expired or revoked bearer token.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 403: The token lacks the permission this operation needs (see `x-brix-permission`).

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 404: No such playlist in this workspace.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 5XX: Server error. The body carries a `requestId` to quote to support.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

## PATCH /v1/playlists/{id}/items/{itemId}

Update a playlist item

Edit one playlist item's duration, loop count, fit, full-screen setting, screen tag rules or allocation. `refKind` and `refId` cannot be changed on an existing item; delete it and add a new item to point at different content.

**Notes.**
- An unknown `itemId` is not refused: nothing changes and the answer is 200 with the playlist.
- `targetTags` and `excludeTags` are stored but not returned on the items.
- Fields of the wrong type are ignored rather than refused.
- The returned playlist row (`updatedAt`, `approvalState`) is the one read before this edit; an approved playlist is returned to `draft` in storage by this call. Re-read with GET to see it.

Auth: Bearer token. Permission: `playlist.edit`.

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Playlist id. |
| `itemId` | path | string | yes | Playlist item id. |

Request body (`application/json`):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `durationMode` | "fixed" \| "full" \| "live" \| "loop" \| "manual" | no |  |
| `durationSeconds` | number | no | Dwell for `fixed`, rounded and clamped to 1 second through 24 hours. |
| `loopCount` | number | no | Plays per turn for `loop`; rounded. |
| `fit` | "contain" \| "cover" \| "fill" \| "blur-fill" \| null | no | The item's own fit; `null` = follow the file's fit. |
| `fullscreen` | boolean | no | Take over the whole screen, over any layout. |
| `targetTags` | array of string \| null | no | Play only on screens with one of these tags. `null` or `[]` clears the rule. |
| `excludeTags` | array of string \| null | no | Never play on screens with one of these tags. `null` or `[]` clears the rule. |
| `allocationId` | string \| null | no | Put the item in one of this playlist's allocations; `null` = the base rotation. |

```bash
curl -X PATCH "https://api.brixsignage.com/v1/playlists/{id}/items/{itemId}" \
  -H "Authorization: Bearer $BRIX_API_KEY" \
  -H "Content-Type: application/json"
```

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | Playlist | A playlist with its ordered items. |
| `data.id` | string | Playlist id. |
| `data.spaceId` | string |  |
| `data.name` | string |  |
| `data.description` | string |  |
| `data.shuffle` | boolean |  |
| `data.fullscreen` | boolean |  |
| `data.startsAt` | string \| null | Plays only from this date (YYYY-MM-DD) or time, stored as sent. |
| `data.expiresAt` | string \| null | Stops after this date (YYYY-MM-DD) or time, stored as sent. |
| `data.approvalState` | "draft" \| "pending" \| "approved" \| "rejected" | Only `approved` content airs where the location requires approval. |
| `data.nodeId` | string \| null | Home location; null = workspace root. |
| `data.createdAt` | string | ISO-8601 timestamp (UTC). |
| `data.updatedAt` | string | ISO-8601 timestamp (UTC). |
| `data.deletedAt` | string \| null |  |
| `data.fit` | "contain" \| "cover" \| "fill" \| "blur-fill" \| null |  |
| `data.approvedSnapshot` | string \| null | Internal: the approved version, as stored JSON text. |
| `data.importSourceId` | string \| null |  |
| `data.recalledAt` | string \| null |  |
| `data.recalledBy` | string \| null |  |
| `data.usedByScreenCount` | integer |  |
| `data.usedByScreens` | array of object | Up to 12 screens playing it. |
| `data.usedByScreens[].id` | string |  |
| `data.usedByScreens[].name` | string |  |
| `data.allocations` | array of PlaylistAllocation |  |
| `data.allocations[].id` | string |  |
| `data.allocations[].label` | string |  |
| `data.allocations[].ownerKind` | "user" \| "group" \| "org-unit" |  |
| `data.allocations[].ownerId` | string |  |
| `data.allocations[].ownerName` | string |  |
| `data.allocations[].kind` | "percent" \| "every-nth" \| "daypart" |  |
| `data.allocations[].value` | integer |  |
| `data.allocations[].endValue` | integer | Absent (not null) when unset. |
| `data.allocations[].colorClass` | string |  |
| `data.allocations[].fillSpaceId` | string \| null |  |
| `data.allocations[].fillPlaylistId` | string \| null |  |
| `data.allocations[].unfilledBehavior` | "collapse" \| "filler" \| "holding" |  |
| `data.allocations[].fillerRefKind` | "media" \| "playlist" \| "app" \| "layout" \| null |  |
| `data.allocations[].fillerRefId` | string \| null |  |
| `data.allocations[].requiresApproval` | boolean |  |
| `data.items` | array of PlaylistItem |  |
| `data.items[].id` | string | Playlist item id. |
| `data.items[].refKind` | "media" \| "app" \| "layout" \| "playlist" |  |
| `data.items[].refId` | string | The media / app instance / layout / playlist id. |
| `data.items[].name` | string |  |
| `data.items[].thumbnailUrl` | string | Empty string when there is none. |
| `data.items[].kind` | string | The media kind for media items; `app`, `layout` or `playlist` otherwise. |
| `data.items[].nestedItemCount` | integer \| null | Nested playlists only: its top-level item count. |
| `data.items[].durationMode` | "fixed" \| "full" \| "live" \| "loop" \| "manual" |  |
| `data.items[].durationSeconds` | integer | Dwell in seconds for `fixed`. |
| `data.items[].loopCount` | integer |  |
| `data.items[].loopDurationMs` | integer \| null |  |
| `data.items[].mediaDurationSec` | number \| null |  |
| `data.items[].fit` | "contain" \| "cover" \| "fill" \| "blur-fill" \| null | The item's own fit; null = inherit. |
| `data.items[].assetFit` | "contain" \| "cover" \| "fill" \| "blur-fill" \| null |  |
| `data.items[].assetStartsAt` | string \| null |  |
| `data.items[].assetExpiresAt` | string \| null |  |
| `data.items[].assetActive` | boolean \| null |  |
| `data.items[].fullscreen` | boolean |  |
| `data.items[].withheld` | "expired" \| "archived" \| "not-yet" \| null | Why a media item is not airing now, or null. |
| `data.items[].allocationId` | string \| null |  |
| `data.items[].position` | integer |  |
| `data.sharedIn` | true | Present when the playlist is outside your locations and was shared in. |

Response 401: Missing, expired or revoked bearer token.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 403: The token lacks the permission this operation needs (see `x-brix-permission`).

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 404: No such playlist in this workspace.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 422: `allocationId` is not one of this playlist's allocations, or nothing to update.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 5XX: Server error. The body carries a `requestId` to quote to support.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

## DELETE /v1/playlists/{id}/items/{itemId}

Remove a playlist item

Remove one item from a playlist. The remaining items keep their existing order.

Removes the item and closes the gap in the positions. Returns the whole playlist.

**Notes.**
- An unknown `itemId` is not refused: nothing is removed and the answer is 200 with the playlist.
- The returned playlist row (`updatedAt`, `approvalState`) is the one read before this edit; an approved playlist is returned to `draft` in storage by this call. Re-read with GET to see it.

Auth: Bearer token. Permission: `playlist.edit`.

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Playlist id. |
| `itemId` | path | string | yes | Playlist item id. |

```bash
curl -X DELETE "https://api.brixsignage.com/v1/playlists/{id}/items/{itemId}" \
  -H "Authorization: Bearer $BRIX_API_KEY"
```

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | Playlist | A playlist with its ordered items. |
| `data.id` | string | Playlist id. |
| `data.spaceId` | string |  |
| `data.name` | string |  |
| `data.description` | string |  |
| `data.shuffle` | boolean |  |
| `data.fullscreen` | boolean |  |
| `data.startsAt` | string \| null | Plays only from this date (YYYY-MM-DD) or time, stored as sent. |
| `data.expiresAt` | string \| null | Stops after this date (YYYY-MM-DD) or time, stored as sent. |
| `data.approvalState` | "draft" \| "pending" \| "approved" \| "rejected" | Only `approved` content airs where the location requires approval. |
| `data.nodeId` | string \| null | Home location; null = workspace root. |
| `data.createdAt` | string | ISO-8601 timestamp (UTC). |
| `data.updatedAt` | string | ISO-8601 timestamp (UTC). |
| `data.deletedAt` | string \| null |  |
| `data.fit` | "contain" \| "cover" \| "fill" \| "blur-fill" \| null |  |
| `data.approvedSnapshot` | string \| null | Internal: the approved version, as stored JSON text. |
| `data.importSourceId` | string \| null |  |
| `data.recalledAt` | string \| null |  |
| `data.recalledBy` | string \| null |  |
| `data.usedByScreenCount` | integer |  |
| `data.usedByScreens` | array of object | Up to 12 screens playing it. |
| `data.usedByScreens[].id` | string |  |
| `data.usedByScreens[].name` | string |  |
| `data.allocations` | array of PlaylistAllocation |  |
| `data.allocations[].id` | string |  |
| `data.allocations[].label` | string |  |
| `data.allocations[].ownerKind` | "user" \| "group" \| "org-unit" |  |
| `data.allocations[].ownerId` | string |  |
| `data.allocations[].ownerName` | string |  |
| `data.allocations[].kind` | "percent" \| "every-nth" \| "daypart" |  |
| `data.allocations[].value` | integer |  |
| `data.allocations[].endValue` | integer | Absent (not null) when unset. |
| `data.allocations[].colorClass` | string |  |
| `data.allocations[].fillSpaceId` | string \| null |  |
| `data.allocations[].fillPlaylistId` | string \| null |  |
| `data.allocations[].unfilledBehavior` | "collapse" \| "filler" \| "holding" |  |
| `data.allocations[].fillerRefKind` | "media" \| "playlist" \| "app" \| "layout" \| null |  |
| `data.allocations[].fillerRefId` | string \| null |  |
| `data.allocations[].requiresApproval` | boolean |  |
| `data.items` | array of PlaylistItem |  |
| `data.items[].id` | string | Playlist item id. |
| `data.items[].refKind` | "media" \| "app" \| "layout" \| "playlist" |  |
| `data.items[].refId` | string | The media / app instance / layout / playlist id. |
| `data.items[].name` | string |  |
| `data.items[].thumbnailUrl` | string | Empty string when there is none. |
| `data.items[].kind` | string | The media kind for media items; `app`, `layout` or `playlist` otherwise. |
| `data.items[].nestedItemCount` | integer \| null | Nested playlists only: its top-level item count. |
| `data.items[].durationMode` | "fixed" \| "full" \| "live" \| "loop" \| "manual" |  |
| `data.items[].durationSeconds` | integer | Dwell in seconds for `fixed`. |
| `data.items[].loopCount` | integer |  |
| `data.items[].loopDurationMs` | integer \| null |  |
| `data.items[].mediaDurationSec` | number \| null |  |
| `data.items[].fit` | "contain" \| "cover" \| "fill" \| "blur-fill" \| null | The item's own fit; null = inherit. |
| `data.items[].assetFit` | "contain" \| "cover" \| "fill" \| "blur-fill" \| null |  |
| `data.items[].assetStartsAt` | string \| null |  |
| `data.items[].assetExpiresAt` | string \| null |  |
| `data.items[].assetActive` | boolean \| null |  |
| `data.items[].fullscreen` | boolean |  |
| `data.items[].withheld` | "expired" \| "archived" \| "not-yet" \| null | Why a media item is not airing now, or null. |
| `data.items[].allocationId` | string \| null |  |
| `data.items[].position` | integer |  |
| `data.sharedIn` | true | Present when the playlist is outside your locations and was shared in. |

Response 401: Missing, expired or revoked bearer token.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 403: The token lacks the permission this operation needs (see `x-brix-permission`).

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 404: No such playlist in this workspace.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 5XX: Server error. The body carries a `requestId` to quote to support.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

## POST /v1/playlists/{id}/restore

Restore a deleted playlist so its items and ordering return to the playlist library.

Auth: Bearer token. Permission: `playlist.delete`.

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Playlist id. |

```bash
curl -X POST "https://api.brixsignage.com/v1/playlists/{id}/restore" \
  -H "Authorization: Bearer $BRIX_API_KEY"
```

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object |  |
| `data.id` | string |  |
| `data.restored` | true |  |

Response 401: Missing, expired or revoked bearer token.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 403: The token lacks the permission this operation needs (see `x-brix-permission`).

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 404: No such playlist in this workspace, or it was purged.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 409: `not_deleted`: the playlist is not in the recycle bin.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 5XX: Server error. The body carries a `requestId` to quote to support.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

## GET /v1/playlists/{id}/share-readiness

Get playlist share readiness

For a playlist's airtime reserved for other teams, list who at each location can fill their share, and the narrowest role that would let them do so.

**Notes.**
- `requiredPermissions` is absent when `locations` is empty.

Auth: Bearer token. Permission: `playlist.view`.

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Playlist id. |

```bash
curl "https://api.brixsignage.com/v1/playlists/{id}/share-readiness" \
  -H "Authorization: Bearer $BRIX_API_KEY"
```

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object |  |
| `data.locations` | array of object | One per location that holds a percent share of this playlist. |
| `data.locations[].nodeId` | string |  |
| `data.locations[].nodeName` | string |  |
| `data.locations[].canFill` | boolean | Somebody at the location can fill the share now. |
| `data.locations[].fillerCount` | integer |  |
| `data.locations[].candidates` | array of object | People at the location who cannot fill it yet. |
| `data.locations[].candidates[].userId` | string |  |
| `data.locations[].candidates[].name` | string |  |
| `data.locations[].candidates[].email` | string |  |
| `data.locations[].roleId` | string \| null | The narrowest existing role that would let them fill it. |
| `data.locations[].roleName` | string \| null |  |
| `data.locations[].mayGrant` | boolean | The caller may give that role at that location. |
| `data.requiredPermissions` | array of string | The permissions filling a share needs. Absent when no location holds a share. |

Response 401: Missing, expired or revoked bearer token.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 403: The token lacks the permission this operation needs (see `x-brix-permission`).

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 404: No such playlist in this workspace.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |

Response 5XX: Server error. The body carries a `requestId` to quote to support.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `conflict`, `rate_limited`, `internal_error`, … |
| `message` | string | Human-readable explanation. Safe to show an operator. |
| `requestId` | string | Present on 5xx: quote it to support. |
