# Layouts

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

## GET /v1/layouts

List layouts

List the workspace's multi-zone layouts, including each layout's name, resolution, and zone count.

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

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `limit` | query | integer | no | Page size. Omit to get every row; pass it to page by `cursor`. |
| `cursor` | query | string | no | The `nextCursor` of the previous page. |
| `count` | query | "1" | no | With `limit`: also return `total`, the number of matching rows. |
| `usableAt` | query | string | no | Location id: only rows usable at that location (homed there, at the workspace root, or shared to it). |

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

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | array of Layout |  |
| `data[].id` | string | Layout id. |
| `data[].spaceId` | string |  |
| `data[].name` | string |  |
| `data[].resolution` | object | Design canvas size in pixels. |
| `data[].resolution.w` | number |  |
| `data[].resolution.h` | number |  |
| `data[].zones` | array of object | Zones: geometry, bound content, frame, radius and role. `GET /v1/layouts/{id}/zones` returns the same list. |
| `data[].autoFullscreenForVideo` | boolean \| null |  |
| `data[].approvalState` | "draft" \| "pending" \| "approved" \| "rejected" | Review state. Editing an approved row returns it to `draft`. |
| `data[].approvedSnapshot` | string \| null | JSON TEXT of the last approved version (not parsed). |
| `data[].nodeId` | string \| null |  |
| `data[].importSourceId` | string \| null |  |
| `data[].theme` | any \| null | Brand Kit look (`{ source: "brand", mode?, radius? }`), or null for a bare canvas. |
| `data[].createdAt` | string | ISO-8601 timestamp (UTC). |
| `data[].updatedAt` | string | ISO-8601 timestamp (UTC). |
| `data[].deletedAt` | string \| null | Always null on these reads: deleted rows are not listed. |
| `data[].usedByScreenCount` | integer | Screens showing this layout now, directly or through a playlist or schedule. |
| `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. |

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

Create a layout

Create a multi-zone layout with a `name` and optional `resolution`, `zones`, `autoFullscreenForVideo`, `nodeId`, and `theme`. Setting `theme` to `{ source: "brand", mode?, radius? }` paints the layout using the workspace Brand Kit; omit it or set it to `null` for a plain canvas. Each zone can also carry a frame style (bare, card, accent, or glass), a corner radius in pixels, and a role of "logo" for a zone that shows the Brand Kit logo without needing its own content.

**Notes.**
- The 201 body is the row as written, not re-read from the database, so columns the create does not set (for example `lastSnapshotAt`) are absent rather than null. `GET` returns every column.
- The 201 body has no `usedByScreenCount`.

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

Request body (`application/json`):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | yes |  |
| `resolution` | object | no | Canvas size in pixels. Default 1920 x 1080. |
| `resolution.w` | number | yes |  |
| `resolution.h` | number | yes |  |
| `zones` | array of LayoutZone | no | The whole zone list. Default: one full-canvas zone named Main. |
| `zones[].id` | string | yes |  |
| `zones[].name` | string | yes |  |
| `zones[].x` | number | yes | Left edge, in layout pixels. |
| `zones[].y` | number | yes |  |
| `zones[].w` | number | yes |  |
| `zones[].h` | number | yes |  |
| `zones[].locked` | boolean | yes |  |
| `zones[].contentName` | string | no | Label of the bound content. Absent on the zone a new layout starts with. |
| `zones[].content` | object \| null | no | What the zone plays: a media file, playlist, schedule, app, web link or creative (`canvas`). |
| `zones[].zIndex` | number | no |  |
| `zones[].ownerScope` | "workspace" \| "org_unit" \| "location" | no |  |
| `zones[].ownerNodeId` | string \| null | no |  |
| `zones[].frame` | "bare" \| "card" \| "accent" \| "glass" | no | Paint style on a themed layout. |
| `zones[].radius` | number | no | Corner radius in layout pixels. |
| `zones[].role` | "logo" | no | `logo`: the zone shows the Brand Kit logo instead of content. |
| `autoFullscreenForVideo` | boolean \| null | no |  |
| `nodeId` | string \| null | no | Home location. Default: the caller's own location. |
| `theme` | object \| null | no |  |

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

Response 201: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object |  |
| `data.id` | string | Layout id. |
| `data.spaceId` | string |  |
| `data.name` | string |  |
| `data.resolution` | object | Design canvas size in pixels. |
| `data.resolution.w` | number |  |
| `data.resolution.h` | number |  |
| `data.zones` | array of object | Zones: geometry, bound content, frame, radius and role. `GET /v1/layouts/{id}/zones` returns the same list. |
| `data.autoFullscreenForVideo` | boolean \| null |  |
| `data.approvalState` | "draft" \| "pending" \| "approved" \| "rejected" | Absent: the create does not set it (it is `draft`). |
| `data.approvedSnapshot` | string \| null |  |
| `data.nodeId` | string \| null |  |
| `data.importSourceId` | string \| null |  |
| `data.theme` | any \| null |  |
| `data.createdAt` | string | ISO-8601 timestamp (UTC). |
| `data.updatedAt` | string | ISO-8601 timestamp (UTC). |
| `data.deletedAt` | string \| null | Always null on these reads: deleted rows are not listed. |

Response 401: Missing, expired or revoked bearer token.

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

Response 403: `not_shared`: zone content that is not usable at the layout's location.

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

Response 422: Missing name, invalid JSON, a bad theme or zone paint, zone content that does not exist or loops, or a location outside 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. |

## GET /v1/layouts/{id}

Get a layout

Retrieve one layout, including its zone geometry with each zone's content assignment and frame, radius, and role styling, its theme (the Brand Kit look, or null), and its video-fullscreen behaviour.

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

Parameters:

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

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

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object |  |
| `data.id` | string | Layout id. |
| `data.spaceId` | string |  |
| `data.name` | string |  |
| `data.resolution` | object | Design canvas size in pixels. |
| `data.resolution.w` | number |  |
| `data.resolution.h` | number |  |
| `data.zones` | array of object | Zones: geometry, bound content, frame, radius and role. `GET /v1/layouts/{id}/zones` returns the same list. |
| `data.autoFullscreenForVideo` | boolean \| null |  |
| `data.approvalState` | "draft" \| "pending" \| "approved" \| "rejected" | Review state. Editing an approved row returns it to `draft`. |
| `data.approvedSnapshot` | string \| null | JSON TEXT of the last approved version (not parsed). |
| `data.nodeId` | string \| null |  |
| `data.importSourceId` | string \| null |  |
| `data.theme` | any \| null | Brand Kit look (`{ source: "brand", mode?, radius? }`), or null for a bare canvas. |
| `data.createdAt` | string | ISO-8601 timestamp (UTC). |
| `data.updatedAt` | string | ISO-8601 timestamp (UTC). |
| `data.deletedAt` | string \| null | Always null on these reads: deleted rows are not listed. |
| `data.usedByScreenCount` | integer | Screens showing this layout now, directly or through a playlist or schedule. |
| `data.requiresApproval` | boolean | The home location requires approval before content airs. |

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 layout in this workspace.

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

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

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

## PATCH /v1/layouts/{id}

Update a layout

Edit a layout's name, resolution, zones, `autoFullscreenForVideo`, node, or theme. Setting `theme` to `{ source: "brand", mode?, radius? }` turns the Brand Kit look on, and `null` turns it off. Zones accept a frame style (bare, card, accent, or glass), a radius, and a role of "logo". An invalid theme, or an unrecognized frame or role, returns 422.

**Notes.**
- The response is the stored row: it has no `usedByScreenCount` or `requiresApproval` (GET has them).

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

Parameters:

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

Request body (`application/json`):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | no |  |
| `resolution` | object | no | Canvas size in pixels. Default 1920 x 1080. |
| `resolution.w` | number | yes |  |
| `resolution.h` | number | yes |  |
| `zones` | array of LayoutZone | no | The whole zone list. Default: one full-canvas zone named Main. |
| `zones[].id` | string | yes |  |
| `zones[].name` | string | yes |  |
| `zones[].x` | number | yes | Left edge, in layout pixels. |
| `zones[].y` | number | yes |  |
| `zones[].w` | number | yes |  |
| `zones[].h` | number | yes |  |
| `zones[].locked` | boolean | yes |  |
| `zones[].contentName` | string | no | Label of the bound content. Absent on the zone a new layout starts with. |
| `zones[].content` | object \| null | no | What the zone plays: a media file, playlist, schedule, app, web link or creative (`canvas`). |
| `zones[].zIndex` | number | no |  |
| `zones[].ownerScope` | "workspace" \| "org_unit" \| "location" | no |  |
| `zones[].ownerNodeId` | string \| null | no |  |
| `zones[].frame` | "bare" \| "card" \| "accent" \| "glass" | no | Paint style on a themed layout. |
| `zones[].radius` | number | no | Corner radius in layout pixels. |
| `zones[].role` | "logo" | no | `logo`: the zone shows the Brand Kit logo instead of content. |
| `autoFullscreenForVideo` | boolean \| null | no |  |
| `nodeId` | string \| null | no | Home location. Default: the caller's own location. |
| `theme` | object \| null | no |  |
| `baseUpdatedAt` | string | no | Optimistic concurrency: the `updatedAt` you read. A stale value is refused with 409 `conflict` and the `current` row. |

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

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object |  |
| `data.id` | string | Layout id. |
| `data.spaceId` | string |  |
| `data.name` | string |  |
| `data.resolution` | object | Design canvas size in pixels. |
| `data.resolution.w` | number |  |
| `data.resolution.h` | number |  |
| `data.zones` | array of object | Zones: geometry, bound content, frame, radius and role. `GET /v1/layouts/{id}/zones` returns the same list. |
| `data.autoFullscreenForVideo` | boolean \| null |  |
| `data.approvalState` | "draft" \| "pending" \| "approved" \| "rejected" | Review state. Editing an approved row returns it to `draft`. |
| `data.approvedSnapshot` | string \| null | JSON TEXT of the last approved version (not parsed). |
| `data.nodeId` | string \| null |  |
| `data.importSourceId` | string \| null |  |
| `data.theme` | any \| null | Brand Kit look (`{ source: "brand", mode?, radius? }`), or null for a bare canvas. |
| `data.createdAt` | string | ISO-8601 timestamp (UTC). |
| `data.updatedAt` | string | ISO-8601 timestamp (UTC). |
| `data.deletedAt` | string \| null | Always null on these reads: deleted rows are not listed. |

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 layout.edit, or zone content that is not usable there (`not_shared`).

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `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 layout 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`: the row changed since `baseUpdatedAt`; 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 JSON, a bad theme or zone paint, zone content that does not exist or loops back into this layout.

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

Delete a layout. This fails with 409 `content_shared` if the layout is actively shared into other spaces; pass `?force=true` to delete it anyway.

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

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Layout id. |
| `force` | query | "true" | no | Delete even when it is shared into other places; the shares go with it. |

```bash
curl -X DELETE "https://api.brixsignage.com/v1/layouts/{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 layout 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`: it 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. |

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

Restore a deleted layout so it returns to the layout library.

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

Parameters:

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

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

Response 200: Success.

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

Response 401: Missing, expired or revoked bearer token.

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

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

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

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

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

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

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

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

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

## GET /v1/layouts/{id}/thumbnail

Get a layout thumbnail

Retrieve a preview image of a layout, showing each zone at its real position filled with a still of its content. The same image is used everywhere the layout is previewed. Add `?fresh=1` to regenerate it after an edit.

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

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Layout id. |
| `fresh` | query | "1" | no | Skip the cached image and draw it again. |

```bash
curl "https://api.brixsignage.com/v1/layouts/{id}/thumbnail" \
  -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 such layout 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. |

## GET /v1/layouts/{id}/zones

List a layout's zones, including the content attached to each one.

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

Parameters:

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

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

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | array of LayoutZone |  |
| `data[].id` | string |  |
| `data[].name` | string |  |
| `data[].x` | number | Left edge, in layout pixels. |
| `data[].y` | number |  |
| `data[].w` | number |  |
| `data[].h` | number |  |
| `data[].locked` | boolean |  |
| `data[].contentName` | string | Label of the bound content. Absent on the zone a new layout starts with. |
| `data[].content` | object \| null | What the zone plays: a media file, playlist, schedule, app, web link or creative (`canvas`). |
| `data[].zIndex` | number |  |
| `data[].ownerScope` | "workspace" \| "org_unit" \| "location" |  |
| `data[].ownerNodeId` | string \| null |  |
| `data[].frame` | "bare" \| "card" \| "accent" \| "glass" | Paint style on a themed layout. |
| `data[].radius` | number | Corner radius in layout pixels. |
| `data[].role` | "logo" | `logo`: the zone shows the Brand Kit logo instead of content. |

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 layout in this workspace.

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

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

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

## POST /v1/layouts/{id}/zones

Add a zone to a layout

Add one zone to a layout, specifying its geometry, the content attached to it, and, on a themed layout, its frame (bare, card, accent, or glass), radius, and role. A zone with role "logo" does not need content of its own.

**Notes.**
- Editing zones returns an approved layout to `draft` where approval is required.

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

Parameters:

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

Request body (`application/json`):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | no | Default `Zone <n>`. |
| `x` | number | no | Default 0. |
| `y` | number | no | Default 0. |
| `w` | number | no | Default 480. |
| `h` | number | no | Default 270. |
| `locked` | boolean | no |  |
| `contentName` | string | no |  |
| `content` | object \| null | no | What the zone plays. It must exist in this workspace, be usable at the layout's location, and not contain this layout. |
| `ownerScope` | "workspace" \| "org_unit" \| "location" | no |  |
| `ownerNodeId` | string \| null | no |  |
| `frame` | "bare" \| "card" \| "accent" \| "glass" | no |  |
| `radius` | number | no |  |
| `role` | "logo" | no |  |

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

Response 201: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object |  |
| `data.id` | string | Layout id. |
| `data.zones` | array of LayoutZone | Every zone, in paint order. |
| `data.zones[].id` | string |  |
| `data.zones[].name` | string |  |
| `data.zones[].x` | number | Left edge, in layout pixels. |
| `data.zones[].y` | number |  |
| `data.zones[].w` | number |  |
| `data.zones[].h` | number |  |
| `data.zones[].locked` | boolean |  |
| `data.zones[].contentName` | string | Label of the bound content. Absent on the zone a new layout starts with. |
| `data.zones[].content` | object \| null | What the zone plays: a media file, playlist, schedule, app, web link or creative (`canvas`). |
| `data.zones[].zIndex` | number |  |
| `data.zones[].ownerScope` | "workspace" \| "org_unit" \| "location" |  |
| `data.zones[].ownerNodeId` | string \| null |  |
| `data.zones[].frame` | "bare" \| "card" \| "accent" \| "glass" | Paint style on a themed layout. |
| `data.zones[].radius` | number | Corner radius in layout pixels. |
| `data.zones[].role` | "logo" | `logo`: the zone shows the Brand Kit logo instead of content. |
| `data.zone` | LayoutZone | A zone. Open: a whole-array write (`PUT /v1/layouts/{id}/zones` with `zones`, or `PATCH /v1/layouts/{id}`) stores each zone as sent, so other keys come back too. |
| `data.zone.id` | string |  |
| `data.zone.name` | string |  |
| `data.zone.x` | number | Left edge, in layout pixels. |
| `data.zone.y` | number |  |
| `data.zone.w` | number |  |
| `data.zone.h` | number |  |
| `data.zone.locked` | boolean |  |
| `data.zone.contentName` | string | Label of the bound content. Absent on the zone a new layout starts with. |
| `data.zone.content` | object \| null | What the zone plays: a media file, playlist, schedule, app, web link or creative (`canvas`). |
| `data.zone.zIndex` | number |  |
| `data.zone.ownerScope` | "workspace" \| "org_unit" \| "location" |  |
| `data.zone.ownerNodeId` | string \| null |  |
| `data.zone.frame` | "bare" \| "card" \| "accent" \| "glass" | Paint style on a themed layout. |
| `data.zone.radius` | number | Corner radius in layout pixels. |
| `data.zone.role` | "logo" | `logo`: the zone shows the Brand Kit logo instead of content. |

Response 401: Missing, expired or revoked bearer token.

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

Response 403: `not_shared`: the content is not usable at the layout's location.

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

Response 404: No such layout (or zone) 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`: the layout changed during the write three times running; retry.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `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: A bad frame, radius or role, content that does not exist, or zone content that plays this layout again (a loop).

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

Replace a layout's zones

Replace a layout's entire set of zones in one call. Send every zone you want to keep, including each zone's frame, radius, and role on a themed layout; a zone sent without those becomes a plain, unframed zone.

Send `zones` to replace the whole list, or `zoneIds` (every current zone id, once) to reorder it.

**Notes.**
- `zones` is stored as sent: keys the API does not know are kept and returned.

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

Parameters:

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

Request body (`application/json`):

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

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object |  |
| `data.id` | string | Layout id. |
| `data.zones` | array of LayoutZone | Every zone, in paint order. |
| `data.zones[].id` | string |  |
| `data.zones[].name` | string |  |
| `data.zones[].x` | number | Left edge, in layout pixels. |
| `data.zones[].y` | number |  |
| `data.zones[].w` | number |  |
| `data.zones[].h` | number |  |
| `data.zones[].locked` | boolean |  |
| `data.zones[].contentName` | string | Label of the bound content. Absent on the zone a new layout starts with. |
| `data.zones[].content` | object \| null | What the zone plays: a media file, playlist, schedule, app, web link or creative (`canvas`). |
| `data.zones[].zIndex` | number |  |
| `data.zones[].ownerScope` | "workspace" \| "org_unit" \| "location" |  |
| `data.zones[].ownerNodeId` | string \| null |  |
| `data.zones[].frame` | "bare" \| "card" \| "accent" \| "glass" | Paint style on a themed layout. |
| `data.zones[].radius` | number | Corner radius in layout pixels. |
| `data.zones[].role` | "logo" | `logo`: the zone shows the Brand Kit logo instead of content. |

Response 401: Missing, expired or revoked bearer token.

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

Response 403: `not_shared`: the content is not usable at the layout's location.

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

Response 404: No such layout (or zone) 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`: the layout changed during the write three times running; retry.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `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: `zoneIds` that are not every zone exactly once, neither `zones` nor `zoneIds`, or a bad or looping zone.

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

Update a layout zone

Edit one zone's geometry, attached content, or, on a themed layout, its frame, radius, or role. Send `null` for any of those to clear it. Rejects a change that would create a loop back into the same layout.

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

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Layout id. |
| `zoneId` | path | string | yes | Zone id. |

Request body (`application/json`):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | no | Default `Zone <n>`. |
| `x` | number | no | Default 0. |
| `y` | number | no | Default 0. |
| `w` | number | no | Default 480. |
| `h` | number | no | Default 270. |
| `locked` | boolean | no |  |
| `contentName` | string | no |  |
| `content` | object \| null | no | What the zone plays. It must exist in this workspace, be usable at the layout's location, and not contain this layout. |
| `ownerScope` | "workspace" \| "org_unit" \| "location" | no |  |
| `ownerNodeId` | string \| null | no |  |
| `frame` | "bare" \| "card" \| "accent" \| "glass" \| null | no | `null` removes it. |
| `radius` | number \| null | no | `null` removes it. |
| `role` | "logo" \| null | no | `null` removes it. |

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

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object |  |
| `data.id` | string | Layout id. |
| `data.zones` | array of LayoutZone | Every zone, in paint order. |
| `data.zones[].id` | string |  |
| `data.zones[].name` | string |  |
| `data.zones[].x` | number | Left edge, in layout pixels. |
| `data.zones[].y` | number |  |
| `data.zones[].w` | number |  |
| `data.zones[].h` | number |  |
| `data.zones[].locked` | boolean |  |
| `data.zones[].contentName` | string | Label of the bound content. Absent on the zone a new layout starts with. |
| `data.zones[].content` | object \| null | What the zone plays: a media file, playlist, schedule, app, web link or creative (`canvas`). |
| `data.zones[].zIndex` | number |  |
| `data.zones[].ownerScope` | "workspace" \| "org_unit" \| "location" |  |
| `data.zones[].ownerNodeId` | string \| null |  |
| `data.zones[].frame` | "bare" \| "card" \| "accent" \| "glass" | Paint style on a themed layout. |
| `data.zones[].radius` | number | Corner radius in layout pixels. |
| `data.zones[].role` | "logo" | `logo`: the zone shows the Brand Kit logo instead of content. |
| `data.zone` | LayoutZone | A zone. Open: a whole-array write (`PUT /v1/layouts/{id}/zones` with `zones`, or `PATCH /v1/layouts/{id}`) stores each zone as sent, so other keys come back too. |
| `data.zone.id` | string |  |
| `data.zone.name` | string |  |
| `data.zone.x` | number | Left edge, in layout pixels. |
| `data.zone.y` | number |  |
| `data.zone.w` | number |  |
| `data.zone.h` | number |  |
| `data.zone.locked` | boolean |  |
| `data.zone.contentName` | string | Label of the bound content. Absent on the zone a new layout starts with. |
| `data.zone.content` | object \| null | What the zone plays: a media file, playlist, schedule, app, web link or creative (`canvas`). |
| `data.zone.zIndex` | number |  |
| `data.zone.ownerScope` | "workspace" \| "org_unit" \| "location" |  |
| `data.zone.ownerNodeId` | string \| null |  |
| `data.zone.frame` | "bare" \| "card" \| "accent" \| "glass" | Paint style on a themed layout. |
| `data.zone.radius` | number | Corner radius in layout pixels. |
| `data.zone.role` | "logo" | `logo`: the zone shows the Brand Kit logo instead of content. |

Response 401: Missing, expired or revoked bearer token.

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

Response 403: `not_shared`: the content is not usable at the layout's location.

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

Response 404: No such layout (or zone) 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`: the layout changed during the write three times running; retry.

| Field | Type | Description |
| --- | --- | --- |
| `error` | string | Machine-readable code: `unauthorized`, `forbidden`, `not_found`, `validation_error`, `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: A bad frame, radius or role, content that does not exist, or zone content that plays this layout again (a loop).

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

Delete a layout zone

Remove one zone from a layout.

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

Parameters:

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Layout id. |
| `zoneId` | path | string | yes | Zone id. |

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

Response 200: Success.

| Field | Type | Description |
| --- | --- | --- |
| `data` | object |  |
| `data.id` | string | Layout id. |
| `data.zones` | array of LayoutZone | Every zone, in paint order. |
| `data.zones[].id` | string |  |
| `data.zones[].name` | string |  |
| `data.zones[].x` | number | Left edge, in layout pixels. |
| `data.zones[].y` | number |  |
| `data.zones[].w` | number |  |
| `data.zones[].h` | number |  |
| `data.zones[].locked` | boolean |  |
| `data.zones[].contentName` | string | Label of the bound content. Absent on the zone a new layout starts with. |
| `data.zones[].content` | object \| null | What the zone plays: a media file, playlist, schedule, app, web link or creative (`canvas`). |
| `data.zones[].zIndex` | number |  |
| `data.zones[].ownerScope` | "workspace" \| "org_unit" \| "location" |  |
| `data.zones[].ownerNodeId` | string \| null |  |
| `data.zones[].frame` | "bare" \| "card" \| "accent" \| "glass" | Paint style on a themed layout. |
| `data.zones[].radius` | number | Corner radius in layout pixels. |
| `data.zones[].role` | "logo" | `logo`: the zone shows the Brand Kit logo instead of content. |

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 layout or zone 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`: the layout changed during the write three times running; retry.

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