# Approvals

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

## GET /v1/approvals

List approval requests the caller is allowed to see, newest first. Use `?state=` to narrow the list to requests still awaiting a decision. The response is capped because approval history only grows over time.

Newest first. The route needs `screen.view`, but each request is listed only if the caller can also view (or approve) that content kind at its location — e.g. `playlist.view` for a playlist. A `screen.view`-only key gets an empty list.

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

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `state` | query | "pending" \| "approved" \| "rejected" \| "withdrawn" | no | Only requests in this state (`pending` = the inbox). |
| `limit` | query | integer | no | At most this many (default and max 500). |

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

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | array of ApprovalRequest |  |
| `data[].id` | string | Approval request id. |
| `data[].contentKind` | string | `playlist`, `schedule`, `creative`, `layout`, `creative-override`, `media`, `app`, … |
| `data[].contentId` | string |  |
| `data[].contentName` | string |  |
| `data[].thumbnailUrl` | string \| null |  |
| `data[].nodeId` | string | The content's home location; empty string for the workspace root. |
| `data[].nodeName` | string |  |
| `data[].requestedByName` | string |  |
| `data[].requestedAt` | string | ISO-8601 timestamp (UTC). |
| `data[].note` | string \| null |  |
| `data[].changes` | array of any | What changed since the last approved version: `{kind, label, detail, thumbnailUrl?}` items for kinds the server diffs; for other kinds, whatever the requester sent. |
| `data[].state` | "pending" \| "approved" \| "rejected" \| "withdrawn" |  |
| `data[].requestedById` | string | User id, or `apikey:<id>` for a key. |
| `data[].levels` | array of object | The approval chain, one entry per tier. |
| `data[].levels[].nodeId` | string |  |
| `data[].levels[].nodeName` | string |  |
| `data[].levels[].approverIds` | array of string |  |
| `data[].levels[].approverNames` | array of string |  |
| `data[].currentLevel` | integer | Index into `levels` awaiting a decision while pending. |
| `data[].decisions` | array of object |  |
| `data[].decisions[].level` | integer |  |
| `data[].decisions[].decidedById` | string |  |
| `data[].decisions[].decidedByName` | string \| null |  |
| `data[].decisions[].decidedAt` | string | ISO-8601 timestamp (UTC). |
| `data[].decisions[].note` | string \| null |  |
| `data[].decidedByName` | string \| null |  |
| `data[].decidedAt` | string \| null |  |
| `data[].decisionNote` | 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 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/approvals

Request approval for content

Open an approval request for a piece of content. The authenticated caller becomes the requester. Decide the request with POST /v1/approvals/:id/decide, or cancel it with POST /v1/approvals/:id/withdraw.

**Notes.**
- The request goes to the content's own location; a `nodeId` in the body is ignored. The content's review state becomes `pending`.
- The requester is the caller (`apikey:<id>` for a key).

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

Request body (`application/json`):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `contentKind` | string | yes | `playlist`, `schedule`, `creative`, `layout`, `media`, `app`, … |
| `contentId` | string | yes |  |
| `contentName` | string | yes |  |
| `note` | string | no |  |
| `thumbnailUrl` | string | no |  |
| `changes` | array of any | no | Used only for content kinds the server does not compare itself. |

```bash
curl -X POST "https://api.brixsignage.com/v1/approvals" \
  -H "Authorization: Bearer $BRIX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"contentKind":"playlist","contentId":"pl_2b3c4d5e6f7a8b9c","contentName":"Lobby Loop","note":"New spring menu"}'
```

Response 200: Success: A request for this content is already pending: that request is returned and nothing is created.

| Field | Type | Description |
| --- | --- | --- |
| `data` | ApprovalRequest | A request to approve content before it can air. |
| `data.id` | string | Approval request id. |
| `data.contentKind` | string | `playlist`, `schedule`, `creative`, `layout`, `creative-override`, `media`, `app`, … |
| `data.contentId` | string |  |
| `data.contentName` | string |  |
| `data.thumbnailUrl` | string \| null |  |
| `data.nodeId` | string | The content's home location; empty string for the workspace root. |
| `data.nodeName` | string |  |
| `data.requestedByName` | string |  |
| `data.requestedAt` | string | ISO-8601 timestamp (UTC). |
| `data.note` | string \| null |  |
| `data.changes` | array of any | What changed since the last approved version: `{kind, label, detail, thumbnailUrl?}` items for kinds the server diffs; for other kinds, whatever the requester sent. |
| `data.state` | "pending" \| "approved" \| "rejected" \| "withdrawn" |  |
| `data.requestedById` | string | User id, or `apikey:<id>` for a key. |
| `data.levels` | array of object | The approval chain, one entry per tier. |
| `data.levels[].nodeId` | string |  |
| `data.levels[].nodeName` | string |  |
| `data.levels[].approverIds` | array of string |  |
| `data.levels[].approverNames` | array of string |  |
| `data.currentLevel` | integer | Index into `levels` awaiting a decision while pending. |
| `data.decisions` | array of object |  |
| `data.decisions[].level` | integer |  |
| `data.decisions[].decidedById` | string |  |
| `data.decisions[].decidedByName` | string \| null |  |
| `data.decisions[].decidedAt` | string | ISO-8601 timestamp (UTC). |
| `data.decisions[].note` | string \| null |  |
| `data.decidedByName` | string \| null |  |
| `data.decidedAt` | string \| null |  |
| `data.decisionNote` | string \| null |  |

Response 201: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | ApprovalRequest | A request to approve content before it can air. |
| `data.id` | string | Approval request id. |
| `data.contentKind` | string | `playlist`, `schedule`, `creative`, `layout`, `creative-override`, `media`, `app`, … |
| `data.contentId` | string |  |
| `data.contentName` | string |  |
| `data.thumbnailUrl` | string \| null |  |
| `data.nodeId` | string | The content's home location; empty string for the workspace root. |
| `data.nodeName` | string |  |
| `data.requestedByName` | string |  |
| `data.requestedAt` | string | ISO-8601 timestamp (UTC). |
| `data.note` | string \| null |  |
| `data.changes` | array of any | What changed since the last approved version: `{kind, label, detail, thumbnailUrl?}` items for kinds the server diffs; for other kinds, whatever the requester sent. |
| `data.state` | "pending" \| "approved" \| "rejected" \| "withdrawn" |  |
| `data.requestedById` | string | User id, or `apikey:<id>` for a key. |
| `data.levels` | array of object | The approval chain, one entry per tier. |
| `data.levels[].nodeId` | string |  |
| `data.levels[].nodeName` | string |  |
| `data.levels[].approverIds` | array of string |  |
| `data.levels[].approverNames` | array of string |  |
| `data.currentLevel` | integer | Index into `levels` awaiting a decision while pending. |
| `data.decisions` | array of object |  |
| `data.decisions[].level` | integer |  |
| `data.decisions[].decidedById` | string |  |
| `data.decisions[].decidedByName` | string \| null |  |
| `data.decisions[].decidedAt` | string | ISO-8601 timestamp (UTC). |
| `data.decisions[].note` | string \| null |  |
| `data.decidedByName` | string \| null |  |
| `data.decidedAt` | string \| null |  |
| `data.decisionNote` | 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 content is approved, and you do not hold its approve permission (`playlist.approve`, …) at its location.

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

Response 404: No such content, or it is at a location where you do not hold `screen.view`.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `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: `contentKind`, `contentId` or `contentName` 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. |

## GET /v1/approvals/{id}

Get an approval request

Retrieve one approval request. If the request falls outside what you are allowed to see, this returns 404 rather than 403, so its existence is not revealed.

**Notes.**
- Besides `screen.view`, the caller must be able to view or approve the content kind at its location (e.g. `playlist.view`); otherwise 404.

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

Parameters:

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

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

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | ApprovalRequest | A request to approve content before it can air. |
| `data.id` | string | Approval request id. |
| `data.contentKind` | string | `playlist`, `schedule`, `creative`, `layout`, `creative-override`, `media`, `app`, … |
| `data.contentId` | string |  |
| `data.contentName` | string |  |
| `data.thumbnailUrl` | string \| null |  |
| `data.nodeId` | string | The content's home location; empty string for the workspace root. |
| `data.nodeName` | string |  |
| `data.requestedByName` | string |  |
| `data.requestedAt` | string | ISO-8601 timestamp (UTC). |
| `data.note` | string \| null |  |
| `data.changes` | array of any | What changed since the last approved version: `{kind, label, detail, thumbnailUrl?}` items for kinds the server diffs; for other kinds, whatever the requester sent. |
| `data.state` | "pending" \| "approved" \| "rejected" \| "withdrawn" |  |
| `data.requestedById` | string | User id, or `apikey:<id>` for a key. |
| `data.levels` | array of object | The approval chain, one entry per tier. |
| `data.levels[].nodeId` | string |  |
| `data.levels[].nodeName` | string |  |
| `data.levels[].approverIds` | array of string |  |
| `data.levels[].approverNames` | array of string |  |
| `data.currentLevel` | integer | Index into `levels` awaiting a decision while pending. |
| `data.decisions` | array of object |  |
| `data.decisions[].level` | integer |  |
| `data.decisions[].decidedById` | string |  |
| `data.decisions[].decidedByName` | string \| null |  |
| `data.decisions[].decidedAt` | string | ISO-8601 timestamp (UTC). |
| `data.decisions[].note` | string \| null |  |
| `data.decidedByName` | string \| null |  |
| `data.decidedAt` | string \| null |  |
| `data.decisionNote` | 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 404: No such request, or you cannot view (or approve) that kind of content at its 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. |

## POST /v1/approvals/{id}/decide

Approve or reject content

Approve or reject a pending approval request. A reason is required when rejecting. The decision is applied to the underlying content's approval status.

Decides the CURRENT level of the chain. Approving the last level lets the content air; approving an earlier level passes it to the next. Besides `screen.view`, the caller must be a named approver for the level or hold the content kind's approve permission (`playlist.approve`, `schedule.approve`, …) at the level's location.

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

Parameters:

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

Request body (`application/json`):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `decision` | "approved" \| "rejected" | yes |  |
| `decisionNote` | string | no | Required to reject. |

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

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | ApprovalRequest | A request to approve content before it can air. |
| `data.id` | string | Approval request id. |
| `data.contentKind` | string | `playlist`, `schedule`, `creative`, `layout`, `creative-override`, `media`, `app`, … |
| `data.contentId` | string |  |
| `data.contentName` | string |  |
| `data.thumbnailUrl` | string \| null |  |
| `data.nodeId` | string | The content's home location; empty string for the workspace root. |
| `data.nodeName` | string |  |
| `data.requestedByName` | string |  |
| `data.requestedAt` | string | ISO-8601 timestamp (UTC). |
| `data.note` | string \| null |  |
| `data.changes` | array of any | What changed since the last approved version: `{kind, label, detail, thumbnailUrl?}` items for kinds the server diffs; for other kinds, whatever the requester sent. |
| `data.state` | "pending" \| "approved" \| "rejected" \| "withdrawn" |  |
| `data.requestedById` | string | User id, or `apikey:<id>` for a key. |
| `data.levels` | array of object | The approval chain, one entry per tier. |
| `data.levels[].nodeId` | string |  |
| `data.levels[].nodeName` | string |  |
| `data.levels[].approverIds` | array of string |  |
| `data.levels[].approverNames` | array of string |  |
| `data.currentLevel` | integer | Index into `levels` awaiting a decision while pending. |
| `data.decisions` | array of object |  |
| `data.decisions[].level` | integer |  |
| `data.decisions[].decidedById` | string |  |
| `data.decisions[].decidedByName` | string \| null |  |
| `data.decisions[].decidedAt` | string | ISO-8601 timestamp (UTC). |
| `data.decisions[].note` | string \| null |  |
| `data.decidedByName` | string \| null |  |
| `data.decidedAt` | string \| null |  |
| `data.decisionNote` | 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: Not eligible to decide this level, or `self_approval` (another approver exists).

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `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 request.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `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: `already_decided`.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `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 `decision`, or a rejection without `decisionNote`.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `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/approvals/{id}/withdraw

Withdraw an approval request

Cancel your own pending approval request. The content it was attached to returns to draft status.

**Notes.**
- The content's review state goes back to `draft`.

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

Parameters:

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

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

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | ApprovalRequest | A request to approve content before it can air. |
| `data.id` | string | Approval request id. |
| `data.contentKind` | string | `playlist`, `schedule`, `creative`, `layout`, `creative-override`, `media`, `app`, … |
| `data.contentId` | string |  |
| `data.contentName` | string |  |
| `data.thumbnailUrl` | string \| null |  |
| `data.nodeId` | string | The content's home location; empty string for the workspace root. |
| `data.nodeName` | string |  |
| `data.requestedByName` | string |  |
| `data.requestedAt` | string | ISO-8601 timestamp (UTC). |
| `data.note` | string \| null |  |
| `data.changes` | array of any | What changed since the last approved version: `{kind, label, detail, thumbnailUrl?}` items for kinds the server diffs; for other kinds, whatever the requester sent. |
| `data.state` | "pending" \| "approved" \| "rejected" \| "withdrawn" |  |
| `data.requestedById` | string | User id, or `apikey:<id>` for a key. |
| `data.levels` | array of object | The approval chain, one entry per tier. |
| `data.levels[].nodeId` | string |  |
| `data.levels[].nodeName` | string |  |
| `data.levels[].approverIds` | array of string |  |
| `data.levels[].approverNames` | array of string |  |
| `data.currentLevel` | integer | Index into `levels` awaiting a decision while pending. |
| `data.decisions` | array of object |  |
| `data.decisions[].level` | integer |  |
| `data.decisions[].decidedById` | string |  |
| `data.decisions[].decidedByName` | string \| null |  |
| `data.decisions[].decidedAt` | string | ISO-8601 timestamp (UTC). |
| `data.decisions[].note` | string \| null |  |
| `data.decidedByName` | string \| null |  |
| `data.decidedAt` | string \| null |  |
| `data.decisionNote` | 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: You are not the requester, and not an approver for the current level.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `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 request.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `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: `already_decided`: the request is no longer pending.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `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. |
