Room Panels
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.
roomContext is mandatory everywhere, and the failure is unhelpfulOmit 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:
- Removal identifiers differ. Team Room removes by panel
type; Planning Room removes by theidGUID 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 — seecanInstallAnother). - 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
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:
[
{
"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
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 |
updatepanel returns 201, not 200Updating 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:
[
{
"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:
[
{
"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
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.
{
"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.
Everything Is a POST
All mutating routes are POST, including removal. There is no DELETE and no PUT. This
matches api/webhook, which also deletes via POST.