---
id: panels
title: 'Room Panels'
description: 'api/teamroom and api/planningroom — list, install, remove and update the panels in a room, including the planning-room panel configuration payload and its perspective override.'
sidebar_label: 'Room Panels'
sidebar_position: 2
tags: ['api', 'rooms', 'panels', 'team-room', 'planning-room']
---

Two APIs managing the panels shown in a room: `api/teamroom` for Team Rooms and
`api/planningroom` for Planning Rooms. They exist to let panels be provisioned
programmatically — spinning up rooms at scale — and are a separate surface from
the wiring the product UI itself uses.

Every route on both requires authentication, a **`roomContext`** parameter, and
the **`EditRoom`** right — including the read routes.

:::warning `roomContext` is mandatory everywhere, and the failure is unhelpful
Omit it and neither API says "missing roomContext". `api/teamroom` routes return
`400` with `{"Exceptions": ["Bad Request", "Reference: <guid>"]}` — the GUID is
an error-report reference, not data. `api/planningroom` routes return a bare
`404`, because the action's required parameter cannot bind. Both mean the same
thing: supply `roomContext=<TeamRoom:id or PlanningRoom:id>`.

This applies to `api/teamroom/panels` as well, even though the list it returns
is not room-specific — the parameter is demanded before the action runs.
:::

## They Are Not Symmetric

The two are close enough to assume they are the same API with a different prefix. They are not.

| | `api/teamroom` | `api/planningroom` |
| --- | --- | --- |
| Available panels | `panels?roomContext=…` — list is not room-specific | `panels?roomContext=…` — per-room flags |
| Installed panels | `installedpanels?roomContext=…` | `installedpanels?roomContext=…` |
| Install | `installpanel` — no body | `installpanel` — optional configuration body |
| Remove identifier | **`type`** (string) | **`panelId`** (GUID) |
| Update | — **not available** | `updatepanel` |

The differences that break a client assuming symmetry:

1. **Removal identifiers differ.** Team Room removes by panel `type`; Planning Room removes by
   the `id` GUID returned when the panel was installed. A Team Room holds at most
   one panel of a given type, while a Planning Room addresses each instance individually
   (and can hold several of the same type — see `canInstallAnother`).
2. **Only Planning Room panels carry configuration.** Team Room panels have none to
   update — which is why its install takes no body and there is no `updatepanel`.

## `api/teamroom`

```http
GET  ~/api/teamroom/panels?roomContext=<TeamRoom:id>
GET  ~/api/teamroom/installedpanels?roomContext=<TeamRoom:id>
POST ~/api/teamroom/installpanel?roomContext=<TeamRoom:id>&type=<panelType>
POST ~/api/teamroom/removepanel?roomContext=<TeamRoom:id>&type=<panelType>
```

Available and installed panels return the same shape; `type` is the value
`installpanel` and `removepanel` take:

```json title="GET panels — response (abridged)"
[
  {
    "type": "Backlog",
    "name": "Backlog",
    "description": "Planning and prioritization of your agile Backlog in a list view.",
    "isRemovable": false
  }
]
```

`isRemovable: false` marks the panels every Team Room carries; `removepanel`
is for the removable ones. `installpanel` returns **201 Created** with the
single installed panel object (not the full list). `removepanel` returns
**204 No Content**.

## `api/planningroom`

```http
GET  ~/api/planningroom/panels?roomContext=<PlanningRoom:id>
GET  ~/api/planningroom/installedpanels?roomContext=<PlanningRoom:id>
POST ~/api/planningroom/installpanel?roomContext=<PlanningRoom:id>&type=<panelType>
POST ~/api/planningroom/removepanel?roomContext=<PlanningRoom:id>&panelId=<guid>
POST ~/api/planningroom/updatepanel?roomContext=<PlanningRoom:id>&panelId=<guid>
```

| Route | Success status |
| --- | --- |
| `installpanel` | 201 Created |
| `updatepanel` | **201 Created** |
| `removepanel` | 204 No Content |

:::note `updatepanel` returns 201, not 200

Updating an existing panel answers **201 Created** rather than 200 OK. That is unusual — 201
normally signals that a new resource was created — and a client that treats 201 as "a new panel
now exists" will double-count. Read the response body rather than inferring from the status.

:::

### Listing Panels

`panels` describes what could be installed, with per-room flags:

```json title="GET panels — response (abridged)"
[
  {
    "name": "EpicBoard",
    "type": "epic-board",
    "description": "The panel displays a Portfolio Kanban configurable by panel-specific settings.",
    "canConfigure": true,
    "canInstallAnother": true,
    "alreadyInstalled": true
  }
]
```

`installedpanels` describes the instances actually in the room, each with its
`id` GUID — the identifier `updatepanel` and `removepanel` take — and its
configuration:

```json title="GET installedpanels — response (abridged)"
[
  {
    "id": "c2a77134-63ab-4282-85ca-94ebe2aa013d",
    "title": "Epicboard 2",
    "type": "epic-board",
    "description": "The panel displays a Portfolio Kanban configurable by panel-specific settings.",
    "canInstallAnother": true,
    "canConfigure": true,
    "useRoomPerspective": false,
    "perspective": {
      "project": "Scope:1068",
      "program": "ScopeLabel:1069",
      "schedule": "Schedule:1064"
    },
    "isSingleLevel": false,
    "epicType": null,
    "url": null
  }
]
```

### The Panel Configuration Payload {#panel-configuration}

`installpanel` and `updatepanel` accept a JSON body. On install it is
**optional** — omit it and the panel takes the room's defaults; supply it and
the configuration is validated and applied in the same request.

```json
{
  "title": "Epicboard 2",
  "perspective": {
    "project": "Scope:1068",
    "program": "ScopeLabel:1069",
    "schedule": "Schedule:1064"
  },
  "epicType": "EpicCategory:246",
  "isSingleLevel": false,
  "useRoomDefaults": false,
  "url": ""
}
```

| Field | Meaning |
| --- | --- |
| `title` | Display title for this panel instance |
| `perspective` | The panel's own view slice — `project` (a `Scope` OID), and optionally `program` (a `ScopeLabel` OID) and `schedule` (a `Schedule` OID). This is how a panel looks at a **different** project, program or schedule than the room it lives in |
| `useRoomDefaults` | Boolean; use the room's perspective instead of a panel-specific one. Defaults to false. Note the asymmetry: the **request** key is `useRoomDefaults`; responses report it as `useRoomPerspective` |
| `isSingleLevel` | Boolean; when a panel-specific perspective is used, exclude items belonging to child projects of the perspective's project. Defaults to false |
| `epicType` | Optional `EpicCategory` OID narrowing Portfolio Items to one type; all types when omitted |
| `url` | Web panels only — the URL the panel shows |

The perspective is stored as room configuration, not as asset attributes, so
this page is its schema: none of these fields are discoverable through
[`meta.v1`](/apis/meta-v1).

## Everything Is a POST

All mutating routes are `POST`, including removal. There is no `DELETE` and no `PUT`. This
matches [`api/webhook`](/apis/webhooks), which also deletes via `POST`.


## Related

- [Rooms & panels overview](/apis/rooms)
