# Playlists (part 2 of 2)

> For AI agents: the full docs index is at https://brixsignage.com/llms.txt, and every page has a markdown twin (add .md to the path).

Playlists is split into 2 pages: [part 1](https://brixsignage.com/developers/api/playlists.md) (`/v1/playlists` to `/v1/playlists/{id}/allocations`), [part 2](https://brixsignage.com/developers/api/playlists-2.md) (`/v1/playlists/{id}/duplicate` to `/v1/playlists/bulk-delete`).

Base URL `https://api.brixsignage.com`. Send `Authorization: Bearer $BRIX_API_KEY` unless an operation says Auth: none or Auth: no API key. The Permission line names what the key must hold. See [Authentication and scopes](https://brixsignage.com/developers/authentication.md), [Errors and rate limits](https://brixsignage.com/developers/errors-and-rate-limits.md) and [Pagination](https://brixsignage.com/developers/pagination.md).

- [POST /v1/playlists/{id}/duplicate](#post-v1-playlists-id-duplicate)
- [POST /v1/playlists/{id}/items](#post-v1-playlists-id-items)
- [PUT /v1/playlists/{id}/items](#put-v1-playlists-id-items)
- [PATCH /v1/playlists/{id}/items/{itemId}](#patch-v1-playlists-id-items-itemid)
- [DELETE /v1/playlists/{id}/items/{itemId}](#delete-v1-playlists-id-items-itemid)
- [POST /v1/playlists/{id}/restore](#post-v1-playlists-id-restore)
- [GET /v1/playlists/{id}/share-readiness](#get-v1-playlists-id-share-readiness)
- [POST /v1/playlists/bulk-delete](#post-v1-playlists-bulk-delete)

Every 4xx and 5xx response below returns this body:

| 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.transition` | "none" \| "fade" \| "slide-left" \| "slide-right" \| "slide-up" \| "slide-down" \| "zoom" \| null | Change between items: `fade` crossfades, `slide-*` pushes the next item in from that side, `zoom` fades it in from slightly larger; null cuts (the default). |
| `data.transitionMs` | integer \| null | How long the transition runs, in ms; null = the default 450. |
| `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.copySourceId` | string \| null | The partner library item this was copied from (Copy to client workspace), or 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" \| "ad-slot" \| "creative" \| 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" \| "playlist" \| "app" \| "layout" \| "ad-slot" \| "creative" |  |
| `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[].nestedShuffle` | boolean \| null | Nested playlists only: whether that playlist plays its items in shuffled order. |
| `data.items[].sharedFrom` | string \| null | The workspace that shares this item in, when it is not this workspace's own: the item plays that workspace's latest version and cannot be changed here. Empty string when its name is not readable; null for this workspace's own items. |
| `data.items[].repeatLoop` | object \| null | A design that repeats for each row: what it really plays. `durationSeconds` does not apply to it. Null otherwise. |
| `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[].playOnlyWhenTapped` | boolean | The rotation skips this item; a design's `show-design` tap action shows it. |
| `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.

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

Response 404: No such playlist in this workspace.

Response 5XX: Server error. The body carries a `requestId` to quote 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" \| "playlist" \| "app" \| "layout" \| "ad-slot" \| "creative" | 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 |  |
| `playOnlyWhenTapped` | boolean | no | Skip it in the rotation; show it only from a `show-design` tap. Default false. |
| `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.transition` | "none" \| "fade" \| "slide-left" \| "slide-right" \| "slide-up" \| "slide-down" \| "zoom" \| null | Change between items: `fade` crossfades, `slide-*` pushes the next item in from that side, `zoom` fades it in from slightly larger; null cuts (the default). |
| `data.transitionMs` | integer \| null | How long the transition runs, in ms; null = the default 450. |
| `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.copySourceId` | string \| null | The partner library item this was copied from (Copy to client workspace), or 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" \| "ad-slot" \| "creative" \| 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" \| "playlist" \| "app" \| "layout" \| "ad-slot" \| "creative" |  |
| `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[].nestedShuffle` | boolean \| null | Nested playlists only: whether that playlist plays its items in shuffled order. |
| `data.items[].sharedFrom` | string \| null | The workspace that shares this item in, when it is not this workspace's own: the item plays that workspace's latest version and cannot be changed here. Empty string when its name is not readable; null for this workspace's own items. |
| `data.items[].repeatLoop` | object \| null | A design that repeats for each row: what it really plays. `durationSeconds` does not apply to it. Null otherwise. |
| `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[].playOnlyWhenTapped` | boolean | The rotation skips this item; a design's `show-design` tap action shows it. |
| `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.

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

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

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

Response 5XX: Server error. The body carries a `requestId` to quote 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.transition` | "none" \| "fade" \| "slide-left" \| "slide-right" \| "slide-up" \| "slide-down" \| "zoom" \| null | Change between items: `fade` crossfades, `slide-*` pushes the next item in from that side, `zoom` fades it in from slightly larger; null cuts (the default). |
| `data.transitionMs` | integer \| null | How long the transition runs, in ms; null = the default 450. |
| `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.copySourceId` | string \| null | The partner library item this was copied from (Copy to client workspace), or 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" \| "ad-slot" \| "creative" \| 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" \| "playlist" \| "app" \| "layout" \| "ad-slot" \| "creative" |  |
| `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[].nestedShuffle` | boolean \| null | Nested playlists only: whether that playlist plays its items in shuffled order. |
| `data.items[].sharedFrom` | string \| null | The workspace that shares this item in, when it is not this workspace's own: the item plays that workspace's latest version and cannot be changed here. Empty string when its name is not readable; null for this workspace's own items. |
| `data.items[].repeatLoop` | object \| null | A design that repeats for each row: what it really plays. `durationSeconds` does not apply to it. Null otherwise. |
| `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[].playOnlyWhenTapped` | boolean | The rotation skips this item; a design's `show-design` tap action shows it. |
| `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.

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

Response 404: No such playlist in this workspace.

Response 5XX: Server error. The body carries a `requestId` to quote 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. |
| `playOnlyWhenTapped` | boolean | no | Skip it in the rotation; show it only from a `show-design` tap. |
| `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.transition` | "none" \| "fade" \| "slide-left" \| "slide-right" \| "slide-up" \| "slide-down" \| "zoom" \| null | Change between items: `fade` crossfades, `slide-*` pushes the next item in from that side, `zoom` fades it in from slightly larger; null cuts (the default). |
| `data.transitionMs` | integer \| null | How long the transition runs, in ms; null = the default 450. |
| `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.copySourceId` | string \| null | The partner library item this was copied from (Copy to client workspace), or 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" \| "ad-slot" \| "creative" \| 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" \| "playlist" \| "app" \| "layout" \| "ad-slot" \| "creative" |  |
| `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[].nestedShuffle` | boolean \| null | Nested playlists only: whether that playlist plays its items in shuffled order. |
| `data.items[].sharedFrom` | string \| null | The workspace that shares this item in, when it is not this workspace's own: the item plays that workspace's latest version and cannot be changed here. Empty string when its name is not readable; null for this workspace's own items. |
| `data.items[].repeatLoop` | object \| null | A design that repeats for each row: what it really plays. `durationSeconds` does not apply to it. Null otherwise. |
| `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[].playOnlyWhenTapped` | boolean | The rotation skips this item; a design's `show-design` tap action shows it. |
| `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.

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

Response 404: No such playlist in this workspace.

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

Response 5XX: Server error. The body carries a `requestId` to quote 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.transition` | "none" \| "fade" \| "slide-left" \| "slide-right" \| "slide-up" \| "slide-down" \| "zoom" \| null | Change between items: `fade` crossfades, `slide-*` pushes the next item in from that side, `zoom` fades it in from slightly larger; null cuts (the default). |
| `data.transitionMs` | integer \| null | How long the transition runs, in ms; null = the default 450. |
| `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.copySourceId` | string \| null | The partner library item this was copied from (Copy to client workspace), or 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" \| "ad-slot" \| "creative" \| 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" \| "playlist" \| "app" \| "layout" \| "ad-slot" \| "creative" |  |
| `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[].nestedShuffle` | boolean \| null | Nested playlists only: whether that playlist plays its items in shuffled order. |
| `data.items[].sharedFrom` | string \| null | The workspace that shares this item in, when it is not this workspace's own: the item plays that workspace's latest version and cannot be changed here. Empty string when its name is not readable; null for this workspace's own items. |
| `data.items[].repeatLoop` | object \| null | A design that repeats for each row: what it really plays. `durationSeconds` does not apply to it. Null otherwise. |
| `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[].playOnlyWhenTapped` | boolean | The rotation skips this item; a design's `show-design` tap action shows it. |
| `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.

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

Response 404: No such playlist in this workspace.

Response 5XX: Server error. The body carries a `requestId` to quote 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.

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

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

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

Response 5XX: Server error. The body carries a `requestId` to quote 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.locations[].fillApprovalState` | "draft" \| "pending" \| "approved" \| "rejected" \| null | Where the location's own content stands in review; null when nothing is added yet. |
| `data.locations[].requiresApproval` | boolean | The owner checks this location's content before it airs. |
| `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.

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

Response 404: No such playlist in this workspace.

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

## POST /v1/playlists/bulk-delete

Delete playlists in bulk

Move up to 500 playlists to the recycle bin in one call. Permission is checked for each one. The answer lists the deleted IDs in `ok` and the rest in `failed`, so one failure does not stop the rest.

Auth: Bearer token.

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

Response 4XX: Client error. 404 rather than 403 for another tenant's resource, so account existence is not leaked.

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