# Media

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

## GET /v1/media

List media

Return the media library: images, videos, PDFs, presentations, fonts, web links, and app-backed assets. Use limit and cursor to page through results, count=1 to include a total count, search to search by name or tag, and the comma-separated kind, state, and folderId parameters to filter. Paging is optional but recommended, since a library can hold thousands of items.

Without `limit`, every visible asset comes back unordered and without `nextCursor`. With `limit`, rows are newest first.

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

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `search` | query | string | no | Case-insensitive match on name or tags. |
| `kind` | query | string | no | Comma-separated kinds, e.g. `image,video`. |
| `state` | query | string | no | Comma-separated processing states. |
| `folderId` | query | string | no | Comma-separated folder ids. |
| `usableAt` | query | string | no | Location id: only assets that may play there (own, root library, or shared in). |
| `limit` | query | integer | no | Page size (max 500). Omit to get every row. |
| `cursor` | query | string | no | The `nextCursor` of the previous page. |
| `count` | query | "1" | no | With `limit`: also return `total`. |

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

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | array of MediaAsset |  |
| `data[].id` | string | Media asset id. |
| `data[].spaceId` | string | Workspace id. |
| `data[].name` | string |  |
| `data[].kind` | string | Media kind: image, video, audio, pdf, powerpoint, web, dashboard, weather, rss, clock, qr, menu, directory, donor-wall, hall-of-fame, birthday-board, recognition, wayfinding, check-in, emergency, touch-kiosk, package, font. Free text in storage, so a very old row may carry another value. |
| `data[].url` | string | Where the bytes live: a `/v1/media/<id>/file` path, an `https` URL, `r2://…`, or (for a web link) the page address. |
| `data[].bytes` | integer | Stored size in bytes (0 for links and apps). |
| `data[].checksum` | string \| null |  |
| `data[].active` | boolean | False when archived. |
| `data[].folderId` | string \| null |  |
| `data[].tags` | any | Tags — a JSON array of strings for every row written by the API (decoded from storage; left as the raw text if the stored value is not valid JSON). |
| `data[].altText` | string \| null |  |
| `data[].thumbnailUrl` | string \| null |  |
| `data[].width` | integer \| null |  |
| `data[].height` | integer \| null |  |
| `data[].durationSec` | number \| null | Video/audio length in seconds. |
| `data[].pdfPageCount` | integer \| null |  |
| `data[].loopDurationMs` | integer \| null | Animated image loop length. |
| `data[].fontFamily` | string \| null |  |
| `data[].fontWeights` | any \| null | Font assets: the weights in the file (decoded JSON). |
| `data[].state` | string | Processing state: `ready`, `processing`, `ready_with_warnings`, `blocked`, … |
| `data[].stateReason` | string \| null |  |
| `data[].stateCode` | string \| null |  |
| `data[].stateFault` | "user" \| "platform" \| null |  |
| `data[].stateProgress` | integer \| null |  |
| `data[].codec` | string \| null |  |
| `data[].playableRev` | integer | Bumped whenever the playable bytes change. |
| `data[].loopState` | "queued" \| "ready" \| "failed" \| null |  |
| `data[].loopCodecs` | string \| null |  |
| `data[].loopRev` | integer \| null |  |
| `data[].loopRotation` | integer \| null |  |
| `data[].loopBytes` | integer \| null |  |
| `data[].masterKey` | string \| null |  |
| `data[].masterBytes` | integer \| null |  |
| `data[].masterProbe` | string \| null |  |
| `data[].rendition4k` | string \| null |  |
| `data[].frameLumaMean` | number \| null |  |
| `data[].frameLumaVariance` | number \| null |  |
| `data[].frameEdgeDensity` | number \| null |  |
| `data[].startsAt` | string \| null | Plays only from this time. |
| `data[].expiresAt` | string \| null | Stops playing after this time. |
| `data[].autoArchiveOnExpiry` | boolean |  |
| `data[].qr` | any \| null | QR overlay settings (decoded JSON). |
| `data[].webConfig` | any \| null | Web link settings — refresh, zoom, header auth, … (decoded JSON). |
| `data[].replayState` | "healthy" \| "replay_pending" \| "replay_failed" \| null |  |
| `data[].lastReplayAt` | string \| null |  |
| `data[].lastReplaySuccessAt` | string \| null |  |
| `data[].lastFailedStep` | integer \| null |  |
| `data[].lastFailedReason` | string \| null |  |
| `data[].autoCaption` | boolean |  |
| `data[].captionTrackKey` | string \| null |  |
| `data[].captionState` | "pending" \| "ready" \| "failed" \| null |  |
| `data[].showCaptions` | boolean |  |
| `data[].audioEnabled` | boolean |  |
| `data[].focalRegion` | string \| null | Smart-fit focal region, as stored JSON text. |
| `data[].fit` | "contain" \| "cover" \| "fill" \| "blur-fill" | How the asset fills a box. |
| `data[].autoSmartFit` | boolean |  |
| `data[].rotation` | integer | Clockwise rotation in degrees. |
| `data[].originalFormat` | string \| null |  |
| `data[].packageEntry` | string \| null |  |
| `data[].packageFiles` | integer \| null |  |
| `data[].packageBytes` | integer \| null |  |
| `data[].importSourceId` | string \| null |  |
| `data[].nodeId` | string \| null | Home location; null = workspace library root. |
| `data[].recalledAt` | string \| null |  |
| `data[].recalledBy` | string \| null |  |
| `data[].createdAt` | string | ISO-8601 timestamp (UTC). |
| `data[].updatedAt` | string | ISO-8601 timestamp (UTC). |
| `data[].deletedAt` | string \| null |  |
| `data[].usageCount` | integer | Playlists, schedules, layouts, creatives, boards and screens that use this asset. |
| `data[].uploadedByName` | string \| null | Who uploaded it, from the audit log. |
| `data[].uploadSource` | "ui" \| "api" \| "mcp" \| null | How it was uploaded. |
| `nextCursor` | string \| null | Present when `?limit` was passed. Send it back as `?cursor=` for the next page; null on the last page. |
| `total` | integer | Total matching rows, when the route computes it. |

```json
{
  "data": [
    {
      "id": "med_0c1d2e3f4a5b6c7d",
      "spaceId": "space_1a2b3c4d5e6f7a8b",
      "name": "lunch-special.jpg",
      "kind": "image",
      "url": "/v1/media/med_0c1d2e3f4a5b6c7d/file",
      "bytes": 482113,
      "checksum": null,
      "active": true,
      "folderId": null,
      "tags": [
        "menu",
        "lunch"
      ],
      "altText": "Grilled chicken wrap with fries",
      "thumbnailUrl": null,
      "width": 1920,
      "height": 1080,
      "durationSec": null,
      "pdfPageCount": null,
      "loopDurationMs": null,
      "fontFamily": null,
      "fontWeights": null,
      "state": "ready",
      "stateReason": null,
      "stateCode": null,
      "stateFault": null,
      "stateProgress": null,
      "codec": null,
      "playableRev": 0,
      "loopState": null,
      "loopCodecs": null,
      "loopRev": null,
      "loopRotation": null,
      "loopBytes": null,
      "masterKey": null,
      "masterBytes": null,
      "masterProbe": null,
      "rendition4k": null,
      "frameLumaMean": null,
      "frameLumaVariance": null,
      "frameEdgeDensity": null,
      "startsAt": null,
      "expiresAt": null,
      "autoArchiveOnExpiry": false,
      "qr": null,
      "webConfig": null,
      "replayState": null,
      "lastReplayAt": null,
      "lastReplaySuccessAt": null,
      "lastFailedStep": null,
      "lastFailedReason": null,
      "autoCaption": false,
      "captionTrackKey": null,
      "captionState": null,
      "showCaptions": true,
      "audioEnabled": false,
      "focalRegion": null,
      "fit": "contain",
      "autoSmartFit": true,
      "rotation": 0,
      "originalFormat": null,
      "packageEntry": null,
      "packageFiles": null,
      "packageBytes": null,
      "importSourceId": null,
      "nodeId": null,
      "recalledAt": null,
      "recalledBy": null,
      "createdAt": "2026-09-20T10:15:00.000Z",
      "updatedAt": "2026-09-20T10:15:00.000Z",
      "deletedAt": null,
      "usageCount": 2,
      "uploadedByName": "Sam Rivera",
      "uploadSource": "ui"
    }
  ],
  "nextCursor": null,
  "total": 1
}
```

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/media

Create a media item

Create a media asset record for a file whose bytes are already stored elsewhere, by providing name, kind, and url. This creates only the metadata record; to upload new file bytes, use POST /v1/media/upload, PUT /v1/media/upload/:uploadId, or POST /v1/media/import-url instead.

**Notes.**
- The 201 body is the row as written, not re-read: columns the create does not set are absent rather than null. GET returns every column.
- Use this for links (web pages, streams, dashboards). To add a FILE, use POST /v1/media/upload, the multipart upload, or POST /v1/media/import-url.

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

Request body (`application/json`):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | yes |  |
| `kind` | string | yes |  |
| `url` | string | yes | An https URL, a /v1/ path, or (for a web link) the page address. Private and internal network addresses are refused. |
| `active` | boolean | no |  |
| `folderId` | string \| null | no |  |
| `tags` | array of string | no |  |
| `altText` | string \| null | no |  |
| `thumbnailUrl` | string \| null | no |  |
| `width` | integer \| null | no |  |
| `height` | integer \| null | no |  |
| `durationSec` | number \| null | no |  |
| `startsAt` | string \| null | no |  |
| `expiresAt` | string \| null | no |  |
| `autoArchiveOnExpiry` | boolean | no |  |
| `qr` | object \| null | no |  |
| `webConfig` | object \| null | no | Web link settings (refresh, zoom, …). |
| `nodeId` | string \| null | no | Home location. Default: the caller's own location. |
| `audioEnabled` | boolean | no |  |
| `fit` | "contain" \| "cover" \| "fill" \| "blur-fill" | no |  |

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

Response 201: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object |  |
| `data.id` | string | Media asset id. |
| `data.spaceId` | string | Workspace id. |
| `data.name` | string |  |
| `data.kind` | string | Media kind: image, video, audio, pdf, powerpoint, web, dashboard, weather, rss, clock, qr, menu, directory, donor-wall, hall-of-fame, birthday-board, recognition, wayfinding, check-in, emergency, touch-kiosk, package, font. Free text in storage, so a very old row may carry another value. |
| `data.url` | string | Where the bytes live: a `/v1/media/<id>/file` path, an `https` URL, `r2://…`, or (for a web link) the page address. |
| `data.bytes` | integer | Stored size in bytes (0 for links and apps). |
| `data.active` | boolean | False when archived. |
| `data.tags` | any | Tags — a JSON array of strings for every row written by the API (decoded from storage; left as the raw text if the stored value is not valid JSON). |
| `data.state` | string | Processing state: `ready`, `processing`, `ready_with_warnings`, `blocked`, … |
| `data.autoArchiveOnExpiry` | boolean |  |
| `data.nodeId` | string \| null | Home location; null = workspace library root. |
| `data.folderId` | string \| null |  |
| `data.altText` | string \| null |  |
| `data.thumbnailUrl` | string \| null |  |
| `data.width` | integer \| null |  |
| `data.height` | integer \| null |  |
| `data.durationSec` | number \| null | Video/audio length in seconds. |
| `data.startsAt` | string \| null | Plays only from this time. |
| `data.expiresAt` | string \| null | Stops playing after this time. |
| `data.qr` | any \| null | QR overlay settings (decoded JSON). |
| `data.webConfig` | any \| null | Web link settings — refresh, zoom, header auth, … (decoded JSON). |
| `data.audioEnabled` | boolean |  |
| `data.fit` | "contain" \| "cover" \| "fill" \| "blur-fill" | How the asset fills a box. |
| `data.createdAt` | string | ISO-8601 timestamp (UTC). |
| `data.updatedAt` | string | ISO-8601 timestamp (UTC). |
| `data.deletedAt` | null |  |

Response 400: `fit` is not a known value.

| 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: The folder or location does not exist.

| 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: `name`, `kind` or `url` is missing, or `kind` is unknown.

| 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/media/{id}

Get a media asset

Return one media asset with its full details, including kind, url, dimensions, duration, tags, folder, play window, and web configuration.

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

Parameters:

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

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

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | MediaAssetDetail | One media asset, as GET /v1/media/{id} returns it. |
| `data.id` | string | Media asset id. |
| `data.spaceId` | string | Workspace id. |
| `data.name` | string |  |
| `data.kind` | string | Media kind: image, video, audio, pdf, powerpoint, web, dashboard, weather, rss, clock, qr, menu, directory, donor-wall, hall-of-fame, birthday-board, recognition, wayfinding, check-in, emergency, touch-kiosk, package, font. Free text in storage, so a very old row may carry another value. |
| `data.url` | string | Where the bytes live: a `/v1/media/<id>/file` path, an `https` URL, `r2://…`, or (for a web link) the page address. |
| `data.bytes` | integer | Stored size in bytes (0 for links and apps). |
| `data.checksum` | string \| null |  |
| `data.active` | boolean | False when archived. |
| `data.folderId` | string \| null |  |
| `data.tags` | any | Tags — a JSON array of strings for every row written by the API (decoded from storage; left as the raw text if the stored value is not valid JSON). |
| `data.altText` | string \| null |  |
| `data.thumbnailUrl` | string \| null |  |
| `data.width` | integer \| null |  |
| `data.height` | integer \| null |  |
| `data.durationSec` | number \| null | Video/audio length in seconds. |
| `data.pdfPageCount` | integer \| null |  |
| `data.loopDurationMs` | integer \| null | Animated image loop length. |
| `data.fontFamily` | string \| null |  |
| `data.fontWeights` | any \| null | Font assets: the weights in the file (decoded JSON). |
| `data.state` | string | Processing state: `ready`, `processing`, `ready_with_warnings`, `blocked`, … |
| `data.stateReason` | string \| null |  |
| `data.stateCode` | string \| null |  |
| `data.stateFault` | "user" \| "platform" \| null |  |
| `data.stateProgress` | integer \| null |  |
| `data.codec` | string \| null |  |
| `data.playableRev` | integer | Bumped whenever the playable bytes change. |
| `data.loopState` | "queued" \| "ready" \| "failed" \| null |  |
| `data.loopCodecs` | string \| null |  |
| `data.loopRev` | integer \| null |  |
| `data.loopRotation` | integer \| null |  |
| `data.loopBytes` | integer \| null |  |
| `data.masterKey` | string \| null |  |
| `data.masterBytes` | integer \| null |  |
| `data.masterProbe` | string \| null |  |
| `data.rendition4k` | string \| null |  |
| `data.frameLumaMean` | number \| null |  |
| `data.frameLumaVariance` | number \| null |  |
| `data.frameEdgeDensity` | number \| null |  |
| `data.startsAt` | string \| null | Plays only from this time. |
| `data.expiresAt` | string \| null | Stops playing after this time. |
| `data.autoArchiveOnExpiry` | boolean |  |
| `data.qr` | any \| null | QR overlay settings (decoded JSON). |
| `data.webConfig` | any \| null | Web link settings — refresh, zoom, header auth, … (decoded JSON). |
| `data.replayState` | "healthy" \| "replay_pending" \| "replay_failed" \| null |  |
| `data.lastReplayAt` | string \| null |  |
| `data.lastReplaySuccessAt` | string \| null |  |
| `data.lastFailedStep` | integer \| null |  |
| `data.lastFailedReason` | string \| null |  |
| `data.autoCaption` | boolean |  |
| `data.captionTrackKey` | string \| null |  |
| `data.captionState` | "pending" \| "ready" \| "failed" \| null |  |
| `data.showCaptions` | boolean |  |
| `data.audioEnabled` | boolean |  |
| `data.focalRegion` | string \| null | Smart-fit focal region, as stored JSON text. |
| `data.fit` | "contain" \| "cover" \| "fill" \| "blur-fill" | How the asset fills a box. |
| `data.autoSmartFit` | boolean |  |
| `data.rotation` | integer | Clockwise rotation in degrees. |
| `data.originalFormat` | string \| null |  |
| `data.packageEntry` | string \| null |  |
| `data.packageFiles` | integer \| null |  |
| `data.packageBytes` | integer \| null |  |
| `data.importSourceId` | string \| null |  |
| `data.nodeId` | string \| null | Home location; null = workspace library root. |
| `data.recalledAt` | string \| null |  |
| `data.recalledBy` | string \| null |  |
| `data.createdAt` | string | ISO-8601 timestamp (UTC). |
| `data.updatedAt` | string | ISO-8601 timestamp (UTC). |
| `data.deletedAt` | string \| null |  |
| `data.usageCount` | integer | Playlists, schedules, layouts, creatives, boards and screens that use this asset. |
| `data.uploadedByName` | string \| null | Who uploaded it, from the audit log. |
| `data.uploadSource` | "ui" \| "api" \| "mcp" \| null | How it was uploaded. |
| `data.format` | string \| null | File format read from the stored bytes (`PNG`, `MP4 (hevc)`), only when the name has no usable extension; otherwise null. |

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 asset in this workspace (or it is 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. |

## PATCH /v1/media/{id}

Update a media asset

Edit a media asset. Accepts name, altText, tags, folderId, node, play-window fields (startsAt, expiresAt, autoArchiveOnExpiry), audioEnabled, a QR code overlay, and web configuration. Changing the URL of a web-kind asset re-checks it to make sure it is safe to fetch.

Send only the fields to change. `state` and `bytes` are set by processing and ignored here.

**Notes.**
- The response is the stored row, not the GET shape: it has no `usageCount`, `uploadedByName` or `uploadSource`.
- An invalid `fit` answers 400 with only `error` (no `message`); every other validation failure is 422 `validation_error`.

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

Parameters:

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

Request body (`application/json`):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | no |  |
| `kind` | string | no |  |
| `url` | string | no | An https URL, a /v1/ path or a data:image URI; for a web link, the page address (private and internal network addresses are refused). |
| `active` | boolean | no | False archives the asset. |
| `folderId` | string \| null | no |  |
| `tags` | array of string | no |  |
| `altText` | string \| null | no |  |
| `thumbnailUrl` | string \| null | no |  |
| `width` | integer \| null | no |  |
| `height` | integer \| null | no |  |
| `durationSec` | number \| null | no |  |
| `startsAt` | string \| null | no |  |
| `expiresAt` | string \| null | no |  |
| `autoArchiveOnExpiry` | boolean | no |  |
| `qr` | object \| null | no |  |
| `webConfig` | object \| null | no |  |
| `nodeId` | string \| null | no | Move to another location (needs media.edit there). |
| `audioEnabled` | boolean | no |  |
| `fit` | "contain" \| "cover" \| "fill" \| "blur-fill" | no |  |
| `baseUpdatedAt` | string | no | The `updatedAt` your edit is based on. When set and the row has moved since, the write is refused with 409 and the current row. |

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

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | MediaAssetUpdated | The stored row after a PATCH, without the list-only fields. |
| `data.id` | string | Media asset id. |
| `data.spaceId` | string | Workspace id. |
| `data.name` | string |  |
| `data.kind` | string | Media kind: image, video, audio, pdf, powerpoint, web, dashboard, weather, rss, clock, qr, menu, directory, donor-wall, hall-of-fame, birthday-board, recognition, wayfinding, check-in, emergency, touch-kiosk, package, font. Free text in storage, so a very old row may carry another value. |
| `data.url` | string | Where the bytes live: a `/v1/media/<id>/file` path, an `https` URL, `r2://…`, or (for a web link) the page address. |
| `data.bytes` | integer | Stored size in bytes (0 for links and apps). |
| `data.checksum` | string \| null |  |
| `data.active` | boolean | False when archived. |
| `data.folderId` | string \| null |  |
| `data.tags` | any | Tags — a JSON array of strings for every row written by the API (decoded from storage; left as the raw text if the stored value is not valid JSON). |
| `data.altText` | string \| null |  |
| `data.thumbnailUrl` | string \| null |  |
| `data.width` | integer \| null |  |
| `data.height` | integer \| null |  |
| `data.durationSec` | number \| null | Video/audio length in seconds. |
| `data.pdfPageCount` | integer \| null |  |
| `data.loopDurationMs` | integer \| null | Animated image loop length. |
| `data.fontFamily` | string \| null |  |
| `data.fontWeights` | any \| null | Font assets: the weights in the file (decoded JSON). |
| `data.state` | string | Processing state: `ready`, `processing`, `ready_with_warnings`, `blocked`, … |
| `data.stateReason` | string \| null |  |
| `data.stateCode` | string \| null |  |
| `data.stateFault` | "user" \| "platform" \| null |  |
| `data.stateProgress` | integer \| null |  |
| `data.codec` | string \| null |  |
| `data.playableRev` | integer | Bumped whenever the playable bytes change. |
| `data.loopState` | "queued" \| "ready" \| "failed" \| null |  |
| `data.loopCodecs` | string \| null |  |
| `data.loopRev` | integer \| null |  |
| `data.loopRotation` | integer \| null |  |
| `data.loopBytes` | integer \| null |  |
| `data.masterKey` | string \| null |  |
| `data.masterBytes` | integer \| null |  |
| `data.masterProbe` | string \| null |  |
| `data.rendition4k` | string \| null |  |
| `data.frameLumaMean` | number \| null |  |
| `data.frameLumaVariance` | number \| null |  |
| `data.frameEdgeDensity` | number \| null |  |
| `data.startsAt` | string \| null | Plays only from this time. |
| `data.expiresAt` | string \| null | Stops playing after this time. |
| `data.autoArchiveOnExpiry` | boolean |  |
| `data.qr` | any \| null | QR overlay settings (decoded JSON). |
| `data.webConfig` | any \| null | Web link settings — refresh, zoom, header auth, … (decoded JSON). |
| `data.replayState` | "healthy" \| "replay_pending" \| "replay_failed" \| null |  |
| `data.lastReplayAt` | string \| null |  |
| `data.lastReplaySuccessAt` | string \| null |  |
| `data.lastFailedStep` | integer \| null |  |
| `data.lastFailedReason` | string \| null |  |
| `data.autoCaption` | boolean |  |
| `data.captionTrackKey` | string \| null |  |
| `data.captionState` | "pending" \| "ready" \| "failed" \| null |  |
| `data.showCaptions` | boolean |  |
| `data.audioEnabled` | boolean |  |
| `data.focalRegion` | string \| null | Smart-fit focal region, as stored JSON text. |
| `data.fit` | "contain" \| "cover" \| "fill" \| "blur-fill" | How the asset fills a box. |
| `data.autoSmartFit` | boolean |  |
| `data.rotation` | integer | Clockwise rotation in degrees. |
| `data.originalFormat` | string \| null |  |
| `data.packageEntry` | string \| null |  |
| `data.packageFiles` | integer \| null |  |
| `data.packageBytes` | integer \| null |  |
| `data.importSourceId` | string \| null |  |
| `data.nodeId` | string \| null | Home location; null = workspace library root. |
| `data.recalledAt` | string \| null |  |
| `data.recalledBy` | string \| null |  |
| `data.createdAt` | string | ISO-8601 timestamp (UTC). |
| `data.updatedAt` | string | ISO-8601 timestamp (UTC). |
| `data.deletedAt` | string \| null |  |

Response 400: `fit` is not one of contain, cover, fill, blur-fill.

| 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: Moving it to a location where you lack media.edit, or attaching an SSO connection without settings.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 asset 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: Invalid kind, URL, folder or 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/media/{id}

Delete a media asset

Move a media asset to the recycle bin. Playlists and screens that reference it show a gap where the asset was until it is restored or replaced.

Moves the asset to the recycle bin (restorable for 30 days) and removes it from every playlist. Screens casting it return to their schedule.

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

Parameters:

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

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

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object |  |
| `data.id` | string |  |
| `data.deleted` | true |  |
| `data.sharesRemoved` | integer | Shares removed with it. |

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 asset 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. |

## GET /v1/media/{id}/captions

Get a video's captions

Return a video's caption track as WebVTT. Add ?lang= to request a machine-translated version, for example ?lang=es for Spanish. This URL is stable, so it can be referenced directly by a player.

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

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Media asset id. |
| `lang` | query | string | no | Two- or three-letter language code: a machine translation of the track (default: the original). |

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

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 media with this id, or it has no captions.

| 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 429: More than 60 reads a minute.

| 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/media/{id}/captions

Generate captions for a video

Automatically generate a caption track for a video from its audio and attach it to the asset. To upload a caption file instead, use the caption upload operation. To show or hide an existing caption track, use the caption visibility operation.

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

Parameters:

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

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

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object |  |
| `data.ok` | true |  |
| `data.key` | string | Storage path of the caption track. |
| `data.state` | "ready" |  |

Response 400: `wrong_kind`: not a video.

| 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 media with this id, or no bytes.

| 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 413: Over 40 MB: send the audio to /captions/transcribe instead.

| 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 502: `transcription_failed`.

| 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/media/{id}/captions/transcribe

Generate captions from a video's audio

Convert a short audio clip, extracted from a video as 16 kHz mono WAV, into a caption track. Use this instead of the standard caption-generation operation when the full video file is too large to process directly.

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

Parameters:

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

Request body (`application/octet-stream`):

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

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object |  |
| `data.ok` | true |  |
| `data.key` | string | Storage path of the caption track. |
| `data.state` | "ready" |  |

Response 400: `wrong_kind`: not a video.

| 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 media with this id.

| 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 413: Over 20 MB.

| 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: Empty 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. |

Response 502: `transcription_failed`.

| 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/media/{id}/captions/upload

Upload a caption file

Attach a caption file to a video. SRT files are converted to WebVTT; WebVTT files are stored as-is. This is the alternative to automatic caption generation.

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

Parameters:

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

Request body (`text/vtt`):

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

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object |  |
| `data.ok` | true |  |
| `data.key` | string | Storage path of the caption track. |
| `data.state` | "ready" |  |

Response 400: `wrong_kind`: not a video.

| 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 media with this id.

| 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 413: Over 5 MB.

| 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 415: Not a .vtt or .srt track.

| 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: Empty 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. |

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/media/{id}/captions/visibility

Show or hide a video's captions

Turn a video's caption track on or off for playback. Hiding the track keeps it in storage so it can be shown again later. If no caption track exists yet, the response indicates that one must be generated first.

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

Parameters:

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

Request body (`application/json`):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `show` | boolean | no | True shows captions; anything else hides them. |

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

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object |  |
| `data.state` | "hidden" \| "ready" \| "needs_audio" | `needs_audio`: shown, but the video has no caption track yet. |

Response 400: `wrong_kind`: not a video.

| 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 media with this id.

| 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/media/{id}/delta

Check for a media delta download

Used by playback devices to check whether their cached copy of a file is current. The caller sends the checksum of its cached copy and receives either the data needed to update it, or a flag indicating no partial update is available along with a URL to download the full file.

**Notes.**
- Reserved: delta downloads are not offered yet, so this always answers `deltaAvailable: false`.

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

Parameters:

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

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

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object |  |
| `data.deltaAvailable` | false |  |
| `data.downloadUrl` | null |  |

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. |

## GET /v1/media/{id}/file

Download a media file

Stream the media asset's file bytes. Supports HTTP Range requests for partial downloads.

**Notes.**
- HTML, SVG and script files are served as `application/octet-stream` attachments, never inline.

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

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Media asset id. |
| `w` | query | string | no | Images: resize to this width (a thumbnail). |
| `h` | query | string | no | Images: resize to this height. |
| `fit` | query | "cover" \| "contain" \| "scale-down" \| "crop" \| "pad" | no | How a resized copy fits `w` × `h` (default `cover`). |

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

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 media with this id, or the file is not in storage.

| 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/media/{id}/focal-region

Detect an image's focal region

Detect the main subject of an image and cache its position as a normalized x, y, width, and height. This lets the image be cropped around its subject when shown in a differently shaped area, instead of being cropped from the center.

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

Parameters:

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

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

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object |  |
| `data.region` | object \| null | Fractions (0–1) of the image. Null when nothing stood out; the stored region is then unchanged. |

Response 400: `wrong_kind`: not an image.

| 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 media with this id, or no bytes.

| 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 413: Over 32 MB.

| 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/media/{id}/playback-quality

Get a video's playback quality

Return dropped-frame data reported by screens that have played this video, used to flag videos that are not playing smoothly. A null health value means the asset has not been measured yet, not that it is playing well.

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

Parameters:

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

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

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object |  |
| `data.health` | object \| null | Null until a screen reports on this video. |
| `data.stutterDropPct` | number | Dropped-frame percentage above which a screen counts as struggling. |
| `data.minSamplesToWarn` | integer |  |
| `data.screens` | array of object |  |
| `data.screens[].screenId` | string |  |
| `data.screens[].worstDropPct` | number |  |
| `data.screens[].meanDropPct` | number |  |
| `data.screens[].samples` | integer |  |
| `data.screens[].videoWidth` | integer \| null |  |
| `data.screens[].videoHeight` | integer \| null |  |
| `data.screens[].lastSeenAt` | string | ISO-8601 timestamp (UTC). |

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. |

## GET /v1/media/{id}/poster

Get a media poster image

Return the poster image attached to a media asset, used as its thumbnail for videos and PDFs.

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

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Media asset id. |
| `w` | query | string | no | Resize to this width. |
| `h` | query | string | no | Resize to this height. |
| `fit` | query | "cover" \| "contain" \| "scale-down" \| "crop" \| "pad" | no |  |

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

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 media with this id, or it has no poster.

| 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/media/{id}/poster

Set or generate a media poster

Attach a poster image to a media asset, used as its thumbnail. The file must be an image and no larger than 4 MB.

**Notes.**
- With `?generate=1` the body is `{ thumbnailUrl, generated }` and no request body is read.

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

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Media asset id. |
| `generate` | query | "1" | no | `1`: generate the poster from the media itself (send no body). |

Request body (`image/*`):

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

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object \| object |  |

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 media with this id.

| 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 413: Over 4 MB.

| 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 415: Not a recognised image.

| 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: Empty 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. |

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/media/{id}/render

Render a web link now

Capture and store a screenshot of a web or link asset. If screenshot rendering is not available for this workspace, the response reports that it is not configured rather than failing.

**Notes.**
- Usually queued (`queued: true`); the frame is then at GET /v1/media/{id}/render-frame.

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

Parameters:

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

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

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object \| object |  |

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 media with this id.

| 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 429: More than 60 renders a minute.

| 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 502: The render failed.

| 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/media/{id}/render-frame

Get a web link's latest render

Return the most recent screenshot generated for a web or link asset by the render operation.

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

Parameters:

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

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

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 media with this id, or the link has not been rendered yet (a render is then queued).

| 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/media/{id}/render-source

Stream source bytes for poster rendering

Read-only byte stream used to generate a video's poster image, with support for partial (Range) reads. Access is scoped to a single media asset and workspace and expires after two minutes. This is not a general-purpose way to download or stream media; use the media file operation for that.

Auth: Bearer token.

Parameters:

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

```bash
curl "https://api.brixsignage.com/v1/media/{id}/render-source" \
  -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.

| 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/media/{id}/restore

Restore a deleted media item

Bring back a deleted media asset. Any playlist or screen still referencing it resumes rendering it.

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

Parameters:

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

```bash
curl -X POST "https://api.brixsignage.com/v1/media/{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 media with this id.

| 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`: it 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. |

## POST /v1/media/{id}/rotate

Rotate a photo or video 90, 180, or 270 degrees clockwise. This rewrites the stored file rather than only changing how it is displayed, since playback devices cannot rotate video at display time. Images are rotated immediately; video rotation is queued, and the asset keeps playing its current version until the rotated file is ready.

**Notes.**
- Photos rotate now (`rotated`); videos are re-encoded in the background (`queued`).

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

Parameters:

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

Request body (`application/json`):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `degrees` | 90 \| 180 \| 270 | yes | Clockwise. |

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

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object \| object |  |

Response 400: `degrees` is not 90, 180 or 270, or the item is not a photo or video.

| 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 media with this id, or its bytes are gone.

| 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: A rotation is already running.

| 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 502: The image service failed.

| 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 503: Rotation is not available.

| 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/media/{id}/suggest

Suggest alt text and tags for an image

Analyze an image asset and return suggested altText and tags. This only returns suggestions; it does not save them, so use the update-media-asset operation to accept them. Only images are supported. If this feature is temporarily unavailable, the response returns empty values rather than failing.

**Notes.**
- Suggestions only: nothing is saved.

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

Parameters:

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

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

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object |  |
| `data.altText` | string \| null |  |
| `data.tags` | array of string |  |

Response 400: `wrong_kind`: not an image.

| 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 media with this id, or no bytes.

| 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 413: Over 32 MB.

| 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/media/{id}/usage

Show where a media asset is used

Return where a media asset is used, across playlists, creatives, and layouts, along with when it was last played.

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

Parameters:

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

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

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | MediaUsage |  |
| `data.containers` | array of object |  |
| `data.containers[].id` | string |  |
| `data.containers[].name` | string |  |
| `data.containers[].kind` | "playlist" \| "schedule" \| "layout" \| "creative" \| "signage" | `signage` = a signage template (board). |
| `data.containers[].itemCount` | integer | Playlists only: total items in the playlist. |
| `data.screens` | array of object | Active screens showing it now, directly or through one of the containers. |
| `data.screens[].id` | string |  |
| `data.screens[].name` | string |  |
| `data.screens[].location` | string \| null |  |
| `data.screens[].status` | string | `online`, `offline` or `pairing`. |
| `data.lastPlayedAt` | string \| null | Last completed play reported by a screen; null if never played. |

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 asset 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. |

## PUT /v1/media/{id}/web-secrets

Set a web link's secrets

Store credentials, such as authentication tokens or login passwords, for a web asset. Values are encrypted at rest and write-only: send an object mapping reference names to values, where a null value deletes that reference. Only the reference names, never the values, can be read back later. This merges with any existing secrets rather than replacing them.

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

Parameters:

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

Request body (`application/json`):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `secrets` | object | yes | Secret name → value (at most 50, values ≤ 8192 characters). `null` or `""` removes one; names not sent are kept. |

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

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object |  |
| `data.ok` | true |  |
| `data.refs` | array of string | The secret NAMES now stored. Values are never returned. |

Response 400: Too many secrets, or a name or value too long.

| 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 media with this id.

| 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/media/backfill-avif

Convert AVIF images

Convert this workspace's existing AVIF image assets to JPEG.

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

Request body (`application/json`):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer | no |  |
| `after` | string | no | `nextAfter` of the previous run. |

```bash
curl -X POST "https://api.brixsignage.com/v1/media/backfill-avif" \
  -H "Authorization: Bearer $BRIX_API_KEY" \
  -H "Content-Type: application/json"
```

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object |  |
| `data.scanned` | integer |  |
| `data.transcoded` | integer |  |
| `data.rows` | array of object |  |
| `data.rows[].id` | string |  |
| `data.rows[].name` | string \| null |  |
| `data.rows[].result` | "source_gone" \| "undecodable" \| "service_error" \| "binding_absent" \| "transcoded" \| "already_done" \| "not_avif" |  |
| `data.nextAfter` | string \| null | Pass as `after` to continue; null when done. |

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 429: More than 10 runs a minute.

| 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 503: `binding_absent`: image conversion is not available.

| 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/media/backfill-codec-warnings

Re-check codec warnings

Recompute codec-compatibility warnings for this workspace's video assets under the current compatibility rules, and clear any outdated re-export warnings. Safe to run more than once.

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

```bash
curl -X POST "https://api.brixsignage.com/v1/media/backfill-codec-warnings" \
  -H "Authorization: Bearer $BRIX_API_KEY"
```

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object |  |
| `data.scanned` | integer |  |
| `data.cleared` | 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 429: More than 10 runs a minute.

| 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/media/backfill-heic

Convert HEIC images

Convert this workspace's existing HEIC image assets to JPEG so they display correctly on devices that cannot decode HEIC. Safe to run more than once, and applies only to your own workspace. Returns a 503 error if the conversion feature is temporarily unavailable.

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

Request body (`application/json`):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer | no |  |
| `after` | string | no | `nextAfter` of the previous run. |

```bash
curl -X POST "https://api.brixsignage.com/v1/media/backfill-heic" \
  -H "Authorization: Bearer $BRIX_API_KEY" \
  -H "Content-Type: application/json"
```

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object |  |
| `data.scanned` | integer |  |
| `data.transcoded` | integer |  |
| `data.rows` | array of object |  |
| `data.rows[].id` | string |  |
| `data.rows[].name` | string \| null |  |
| `data.rows[].result` | "source_gone" \| "undecodable" \| "service_error" \| "binding_absent" \| "transcoded" \| "already_done" \| "not_heic" |  |
| `data.nextAfter` | string \| null |  |

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 429: More than 10 runs a minute.

| 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 503: `binding_absent`: HEIC conversion is not available.

| 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/media/backfill-video-metadata

Fill in missing video details

Scan this workspace's existing video files to fill in missing width, height, and duration values, and correct codec information that was guessed from the filename. Also updates the codec-compatibility warning where applicable.

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

Request body (`application/json`):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer | no | Most videos to read this run. |

```bash
curl -X POST "https://api.brixsignage.com/v1/media/backfill-video-metadata" \
  -H "Authorization: Bearer $BRIX_API_KEY" \
  -H "Content-Type: application/json"
```

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object |  |
| `data.scanned` | integer |  |
| `data.updated` | integer |  |
| `data.reclassified` | 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 429: More than 10 runs a minute.

| 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/media/import-url

Import media from a URL

Import a file from a remote URL into the media library. The API fetches the file server-side, checking that the URL does not point to a disallowed destination, stores its bytes, and creates the media asset record. This is the same path used by the upload_media_from_url tool.

Fetches a public http(s) URL into the library. SVG is refused.

**Notes.**
- The response is the row as built for insert, not the GET shape: `tags` is a JSON string (not an array), `sourceUrl` is present but not stored, and most columns (state, fit, width, …) are absent. Read GET /v1/media/{id} for the settled asset.

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

Request body (`application/json`):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `url` | string | yes | Public http(s) address of the file. |
| `name` | string | no | Display name; defaults to the file name in the URL. |
| `folderId` | string | no |  |
| `nodeId` | string | no | Home location; defaults to your own. |
| `tags` | array of string | no |  |
| `autoCaption` | boolean | no | Videos: generate captions. |
| `autoSmartFit` | boolean | no | Images: detect a focal region (default true). |

```bash
curl -X POST "https://api.brixsignage.com/v1/media/import-url" \
  -H "Authorization: Bearer $BRIX_API_KEY" \
  -H "Content-Type: application/json"
```

Response 201: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | ImportedMedia | The row created by an import, before processing settles. |
| `data.id` | string | Media asset id. |
| `data.spaceId` | string |  |
| `data.name` | string |  |
| `data.kind` | string |  |
| `data.url` | string | `/v1/media/<id>/file`. |
| `data.bytes` | integer |  |
| `data.checksum` | string \| null |  |
| `data.active` | true |  |
| `data.folderId` | string \| null |  |
| `data.nodeId` | string \| null |  |
| `data.tags` | string | JSON-encoded array of strings — NOT decoded here, unlike GET /v1/media. |
| `data.sourceUrl` | string | The URL that was imported. Not stored; returned by this route only. |
| `data.autoCaption` | boolean |  |
| `data.captionState` | "pending" \| null |  |
| `data.autoSmartFit` | boolean |  |
| `data.createdAt` | string | ISO-8601 timestamp (UTC). |
| `data.updatedAt` | string | ISO-8601 timestamp (UTC). |
| `data.deletedAt` | null |  |

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 413: `too_large`: the file is over the size limit for its kind.

| 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 415: `unsupported_type`: SVG or an unknown content type.

| 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 or non-http URL, a blocked address (`url_blocked`), or an unknown folder or 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 502: `fetch_failed` / `source_status`: the source did not answer with the file.

| 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/media/moderate

Check text for unsafe content

Run a safety classifier over a piece of user-submitted text, such as banner copy or a message board post, and return its verdict. This operation only classifies the text; it does not store it.

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

Request body (`application/json`):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `text` | string | yes |  |

```bash
curl -X POST "https://api.brixsignage.com/v1/media/moderate" \
  -H "Authorization: Bearer $BRIX_API_KEY" \
  -H "Content-Type: application/json"
```

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object |  |
| `data.flagged` | boolean |  |
| `data.reason` | string | Present when flagged. |
| `data.checked` | boolean | False when the check could not run (the text is then not flagged). |

Response 400: `text` is missing.

| 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 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/media/stats

Get media library statistics

Return summary counts for the media library in one response: total assets, unused assets, and a count per folder. Use this instead of listing all media when only summary counts are needed.

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

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

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object |  |
| `data.total` | integer |  |
| `data.unused` | integer | Items used nowhere. |
| `data.folders` | array of object | Items per folder; `folderId` null = not in a folder. |
| `data.folders[].folderId` | string \| null |  |
| `data.folders[].count` | 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 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/media/upload

Upload a media file

Upload a file in a single request using multipart form data. Accepts form fields file (required), and optional name, nodeId, and folderId. This stores the file and creates the media asset record in one step, and is safe to retry with the same Idempotency-Key header. For large files or programmatic clients, minting an upload URL and then uploading with PUT is usually simpler.

**Notes.**
- Form fields other than `file` are strings, as multipart sends them.
- For files over ~100 MB use the multipart upload.

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

Request body (`multipart/form-data`):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `file` | string (binary) | yes | The file. SVG is refused. |
| `name` | string | no | Display name; default the file name. |
| `nodeId` | string | no | Home location; default the caller's own. |
| `folderId` | string | no |  |
| `alt` | string | no | Alt text. |
| `decorative` | "1" | no | `1`: the image is decorative (empty alt text). |
| `autoCaption` | "1" | no | Videos: `1` generates captions. |
| `autoSmartFit` | "0" \| "1" | no | Images: `0` skips focal-region detection (default on). |
| `durationSec` | string | no | Videos: length in seconds, when the client measured it. |
| `loopDurationMs` | string | no | Animated images: loop length. |
| `width` | string | no |  |
| `height` | string | no |  |
| `codec` | string | no |  |
| `codecString` | string | no |  |
| `fps` | string | no |  |

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

Response 201: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | UploadedMedia | The media row an upload creates, before processing settles. |
| `data.id` | string | Media asset id. |
| `data.spaceId` | string | Workspace id. |
| `data.nodeId` | string \| null | Home location; null = workspace library root. |
| `data.folderId` | string \| null |  |
| `data.name` | string |  |
| `data.kind` | string | Media kind: image, video, audio, pdf, powerpoint, web, dashboard, weather, rss, clock, qr, menu, directory, donor-wall, hall-of-fame, birthday-board, recognition, wayfinding, check-in, emergency, touch-kiosk, package, font. Free text in storage, so a very old row may carry another value. |
| `data.url` | string | Where the bytes live: a `/v1/media/<id>/file` path, an `https` URL, `r2://…`, or (for a web link) the page address. |
| `data.bytes` | integer | Stored size in bytes (0 for links and apps). |
| `data.checksum` | string | SHA-256 of the bytes. |
| `data.active` | boolean | False when archived. |
| `data.altText` | string \| null |  |
| `data.width` | integer \| null |  |
| `data.height` | integer \| null |  |
| `data.durationSec` | number \| null | Video/audio length in seconds. |
| `data.loopDurationMs` | integer \| null | Animated image loop length. |
| `data.fontFamily` | string \| null |  |
| `data.fontWeights` | array of integer \| null | Font files: the weights in the file. |
| `data.state` | "ready" \| "failed" \| "processing" \| "ready_with_warnings" \| "needs_action" |  |
| `data.stateReason` | string \| null |  |
| `data.stateCode` | string \| null |  |
| `data.stateFault` | "user" \| "platform" \| null |  |
| `data.codec` | string \| null |  |
| `data.frameLumaMean` | number \| null |  |
| `data.frameLumaVariance` | number \| null |  |
| `data.frameEdgeDensity` | number \| null |  |
| `data.autoCaption` | boolean |  |
| `data.captionState` | "pending" \| null |  |
| `data.autoSmartFit` | boolean |  |
| `data.createdAt` | string | ISO-8601 timestamp (UTC). |
| `data.updatedAt` | string | ISO-8601 timestamp (UTC). |
| `data.deletedAt` | null |  |

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: The folder or location does not exist.

| 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 413: The file is over the limit for its kind (images 250 MB, video 2 GB, documents 100 MB, fonts 10 MB).

| 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 415: SVG, or a type the library does not take.

| 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: `file` is missing.

| 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/media/upload/{uploadId}

Upload bytes to a minted upload URL

Upload the raw file bytes for a previously created upload slot. Send the file as the request body and set the Content-Type header to match the file's type. After uploading, finalize the upload to create the media asset record.

**Notes.**
- This stores the bytes for a bulk import; the import itself creates the media rows.

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

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `uploadId` | path | string | yes | An `upl_…` id from POST /v1/import/upload-urls. |

Request body (`application/octet-stream`):

```bash
curl -X PUT "https://api.brixsignage.com/v1/media/upload/{uploadId}" \
  -H "Authorization: Bearer $BRIX_API_KEY"
```

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object |  |
| `data.uploadId` | string |  |
| `data.bytes` | 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 mint with this id 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 413: Over 500 MB.

| 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 415: SVG.

| 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: Empty body or a malformed id.

| 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/media/upload/multipart/{id}

Get a multipart upload's progress

Return the state of an in-progress multipart upload, including the file's name, size, kind, part size, and every part already received, each with its part number, ETag, and size. Use this to resume an interrupted upload from the last completed part instead of starting over. Returns 404 if the upload belongs to another workspace, has already finished, or has expired.

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

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | The upload's media id, from `POST /v1/media/upload/multipart/create`. |

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

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object |  |
| `data.id` | string |  |
| `data.name` | string |  |
| `data.contentType` | string |  |
| `data.kind` | string |  |
| `data.sizeBytes` | integer |  |
| `data.partSize` | integer |  |
| `data.masterOf` | string \| null |  |
| `data.createdAt` | string | ISO-8601 timestamp (UTC). |
| `data.parts` | array of object | Parts already stored: resume from the first missing one. |
| `data.parts[].partNumber` | integer |  |
| `data.parts[].etag` | string |  |
| `data.parts[].size` | 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 upload.

| 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/media/upload/multipart/{id}/abort

Cancel a multipart upload

Cancel an in-progress multipart upload and discard any parts already received. Safe to call more than once.

**Notes.**
- Answers `aborted: true` also when there was nothing to cancel.

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

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | The upload's media id, from `POST /v1/media/upload/multipart/create`. |

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

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object |  |
| `data.aborted` | 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 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/media/upload/multipart/{id}/complete

Complete a multipart upload

Finalize a multipart upload by sending the list of uploaded parts, each with partNumber and etag. If the list is left empty, the server completes the upload using the parts it already has on record, so a client resuming an interrupted upload does not need to know every part's ETag. This creates the media asset record; video files are automatically queued for processing.

**Notes.**
- For a master upload (`masterOf`) the body is `{ id, masterKey, masterBytes }`; otherwise it is the new media row.

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

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | The upload's media id, from `POST /v1/media/upload/multipart/create`. |

Request body (`application/json`):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `parts` | array of object | yes |  |
| `parts[].partNumber` | integer | yes |  |
| `parts[].etag` | string | yes |  |

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

Response 201: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object \| object |  |

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 upload.

| 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: A part is missing or does not match.

| 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: No parts.

| 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/media/upload/multipart/{id}/part/{partNumber}

Upload one part of a multipart upload

Upload one part of a file for an in-progress multipart upload, identified by its part number. Returns the part's ETag, which is needed to complete the upload. Returns 404 if the upload belongs to another workspace.

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

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | The upload's media id, from `POST /v1/media/upload/multipart/create`. |
| `partNumber` | path | string | yes | 1-based part number. |

Request body (`application/octet-stream`):

```bash
curl -X PUT "https://api.brixsignage.com/v1/media/upload/multipart/{id}/part/{partNumber}" \
  -H "Authorization: Bearer $BRIX_API_KEY"
```

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object |  |
| `data.partNumber` | integer |  |
| `data.etag` | string | Send it back in `complete`. |

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 upload.

| 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 413: The part is larger than `partSize`.

| 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: Bad part number or empty part.

| 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/media/upload/multipart/create

Start a multipart upload

Begin a multipart upload for large files, such as multi-gigabyte videos, that are too large for a single request. Validates the file kind and checks its size against the limit allowed for that kind. Returns an upload id and the part size to use for subsequent part uploads.

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

Request body (`application/json`):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | no |  |
| `contentType` | string | no | The file's media type; decides the kind. |
| `sizeBytes` | integer | yes |  |
| `folderId` | string \| null | no |  |
| `nodeId` | string \| null | no |  |
| `alt` | string \| null | no |  |
| `decorative` | boolean | no |  |
| `loopDurationMs` | number \| null | no |  |
| `durationSec` | number \| null | no |  |
| `width` | integer \| null | no |  |
| `height` | integer \| null | no |  |
| `codec` | string \| null | no |  |
| `codecString` | string \| null | no |  |
| `fps` | number \| null | no |  |
| `masterOf` | string \| null | no | An existing video's id: upload a high-resolution master for it instead of a new asset. |
| `importSourceId` | string \| null | no | Your own id for the file. If a live asset already has it, the answer is 200 with `exists: true`. |

```bash
curl -X POST "https://api.brixsignage.com/v1/media/upload/multipart/create" \
  -H "Authorization: Bearer $BRIX_API_KEY" \
  -H "Content-Type: application/json"
```

Response 200: Success: `importSourceId` matched a live asset: `{ id, exists: true }`, nothing started.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object \| object |  |

Response 201: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object \| object |  |

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 413: `sizeBytes` is over the limit for the kind.

| 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 415: SVG.

| 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: Unknown file type, or `sizeBytes` missing.

| 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/media/upload/package

Upload an HTML5 package

Upload a zipped HTML5 package as a media asset, using multipart form data with fields file (required), and optional name, nodeId, and folderId. The archive is validated, expanded into storage, and its entry page is identified automatically.

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

Request body (`multipart/form-data`):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `file` | string (binary) | yes | The .zip. It must hold an entry page (`index.html`). |
| `name` | string | no | Display name; default the file name. |
| `nodeId` | string | no | Home location; default the caller's own. |
| `folderId` | string | no |  |

```bash
curl -X POST "https://api.brixsignage.com/v1/media/upload/package" \
  -H "Authorization: Bearer $BRIX_API_KEY"
```

Response 201: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object |  |
| `data.id` | string | Media asset id. |
| `data.spaceId` | string | Workspace id. |
| `data.nodeId` | string \| null | Home location; null = workspace library root. |
| `data.folderId` | string \| null |  |
| `data.name` | string |  |
| `data.kind` | "package" |  |
| `data.url` | string | Where the bytes live: a `/v1/media/<id>/file` path, an `https` URL, `r2://…`, or (for a web link) the page address. |
| `data.bytes` | integer | Stored size in bytes (0 for links and apps). |
| `data.checksum` | string | SHA-256 of the .zip. |
| `data.active` | boolean | False when archived. |
| `data.packageEntry` | string | The entry page found in the archive, e.g. `index.html`. |
| `data.packageFiles` | integer | Files in the archive. |
| `data.packageBytes` | integer | Expanded size of the archive, in bytes. |
| `data.state` | "ready" |  |
| `data.createdAt` | string | ISO-8601 timestamp (UTC). |
| `data.updatedAt` | string | ISO-8601 timestamp (UTC). |
| `data.deletedAt` | null |  |

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: The folder or location does not exist.

| 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 413: The .zip, or its expanded contents, is over the limit.

| 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: `file` is missing, the .zip cannot be read, or a file in it is refused (unsafe path, type not allowed, no entry page).

| 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. |
