<!-- generated by bin/openapi-markdown from scripts/openapi/openapi.yaml sha256:d9ce1799cf6eaada6f3857475525ee0f759891c4ced7ac8ac2c7969e4c275b44 -->
# TRMNL API

- **OpenAPI Version:** `3.0.1`
- **API Version:** `1`

Send an account API key, or the access token of an app a user connected with OAuth, as a bearer
token. Create a key on your account settings page and pick what it may do: read,
content, devices, delete, profile or apps; an app asks for the same scopes and the user
picks them (see <https://trmnl.com/auth.md>). An operation outside
them answers 403 naming the capability it needs. A key can also be limited to some
devices and plugin settings, and then answers 403 naming what it was not granted. The
legacy account API key reaches only the endpoints it always had, and answers 403 on the
rest.

This is a plain OpenAPI 3 document, so a client can be generated from it, or read
straight off it at runtime. The TRMNL CLI (<https://github.com/usetrmnl/cli>) takes the second
route, so every operation here is a command. It signs in through your browser, no key needed:

```
brew install usetrmnl/tap/trmnl
trmnl list-devices
```

To build a plugin, use trmnlp instead: <https://github.com/usetrmnl/trmnlp>. It serves
your markup locally with live reload and syncs it with your account.

Over MCP (<https://trmnl.com/mcp>) an agent runs these same operations as account
tools. An OAuth connection holds the capabilities its user ticked at consent (read,
content, devices, delete, profile) and may be limited to some devices and plugin
settings; an operation outside them answers 403 naming what is missing. See
<https://trmnl.com/auth.md>.

## Contents

**Operations**

- [GET /api/display](#scalar-operation-get-apidisplay)
- [GET /api/display/current](#scalar-operation-get-apidisplaycurrent)
- [POST /api/log](#scalar-operation-post-apilog)
- [GET /api/setup](#scalar-operation-get-apisetup)
- [GET /api/categories](#scalar-operation-get-apicategories)
- [GET /api/firmware/flash](#scalar-operation-get-apifirmwareflash)
- [GET /api/ips](#scalar-operation-get-apiips)
- [GET /api/models](#scalar-operation-get-apimodels)
- [GET /api/palettes](#scalar-operation-get-apipalettes)
- [GET /api/book/{token}/events](#scalar-operation-get-apibooktokenevents)
- [POST /api/book/{token}/bookings](#scalar-operation-post-apibooktokenbookings)
- [DELETE /api/book/{token}/bookings/{id}](#scalar-operation-delete-apibooktokenbookingsid)
- [PATCH /api/book/{token}/bookings/{id}/end](#scalar-operation-patch-apibooktokenbookingsidend)
- [GET /api/analytics](#scalar-operation-get-apianalytics)
- [GET /api/analytics/errors](#scalar-operation-get-apianalyticserrors)
- [POST /api/analytics/hidden\_errors](#scalar-operation-post-apianalyticshidden-errors)
- [DELETE /api/analytics/hidden\_errors](#scalar-operation-delete-apianalyticshidden-errors)
- [GET /api/analytics/uninstall\_feedback](#scalar-operation-get-apianalyticsuninstall-feedback)
- [GET /api/apps/fleet/{installation\_id}](#scalar-operation-get-apiappsfleetinstallation-id)
- [GET /api/apps/fleet/{installation\_id}/devices](#scalar-operation-get-apiappsfleetinstallation-iddevices)
- [POST /api/apps/fleet/{installation\_id}/devices](#scalar-operation-post-apiappsfleetinstallation-iddevices)
- [DELETE /api/apps/fleet/{installation\_id}/devices/{device\_id}](#scalar-operation-delete-apiappsfleetinstallation-iddevicesdevice-id)
- [POST /api/apps/fleet/{installation\_id}/devices/{device\_id}/pushes](#scalar-operation-post-apiappsfleetinstallation-iddevicesdevice-idpushes)
- [POST /api/apps/fleet/{installation\_id}/pushes](#scalar-operation-post-apiappsfleetinstallation-idpushes)
- [PATCH /api/apps/fleet/{installation\_id}/settings](#scalar-operation-patch-apiappsfleetinstallation-idsettings)
- [PATCH /api/apps/fleet/{installation\_id}/alerts](#scalar-operation-patch-apiappsfleetinstallation-idalerts)
- [PUT /api/apps/fleet/{installation\_id}/master](#scalar-operation-put-apiappsfleetinstallation-idmaster)
- [DELETE /api/apps/fleet/{installation\_id}/master](#scalar-operation-delete-apiappsfleetinstallation-idmaster)
- [GET /api/apps](#scalar-operation-get-apiapps)
- [GET /api/apps/installations](#scalar-operation-get-apiappsinstallations)
- [POST /api/apps/installations](#scalar-operation-post-apiappsinstallations)
- [DELETE /api/apps/installations/{id}](#scalar-operation-delete-apiappsinstallationsid)
- [GET /api/apps/room\_booking/{installation\_id}/integrations](#scalar-operation-get-apiappsroom-bookinginstallation-idintegrations)
- [GET /api/apps/room\_booking/{installation\_id}/integrations/{integration\_type}/connect\_url](#scalar-operation-get-apiappsroom-bookinginstallation-idintegrationsintegration-typeconnect-url)
- [POST /api/apps/room\_booking/{installation\_id}/integrations/{integration\_type}/{id}/syncs](#scalar-operation-post-apiappsroom-bookinginstallation-idintegrationsintegration-typeidsyncs)
- [PATCH /api/apps/room\_booking/{installation\_id}/integrations/{integration\_type}/{id}](#scalar-operation-patch-apiappsroom-bookinginstallation-idintegrationsintegration-typeid)
- [DELETE /api/apps/room\_booking/{installation\_id}/integrations/{integration\_type}/{id}](#scalar-operation-delete-apiappsroom-bookinginstallation-idintegrationsintegration-typeid)
- [GET /api/apps/room\_booking/{installation\_id}](#scalar-operation-get-apiappsroom-bookinginstallation-id)
- [GET /api/apps/room\_booking/{installation\_id}/billing](#scalar-operation-get-apiappsroom-bookinginstallation-idbilling)
- [GET /api/apps/room\_booking/{installation\_id}/settings](#scalar-operation-get-apiappsroom-bookinginstallation-idsettings)
- [PATCH /api/apps/room\_booking/{installation\_id}/settings](#scalar-operation-patch-apiappsroom-bookinginstallation-idsettings)
- [PUT /api/apps/room\_booking/{installation\_id}/settings/company\_logo](#scalar-operation-put-apiappsroom-bookinginstallation-idsettingscompany-logo)
- [DELETE /api/apps/room\_booking/{installation\_id}/settings/company\_logo](#scalar-operation-delete-apiappsroom-bookinginstallation-idsettingscompany-logo)
- [PUT /api/apps/room\_booking/{installation\_id}/settings/color\_company\_logo](#scalar-operation-put-apiappsroom-bookinginstallation-idsettingscolor-company-logo)
- [DELETE /api/apps/room\_booking/{installation\_id}/settings/color\_company\_logo](#scalar-operation-delete-apiappsroom-bookinginstallation-idsettingscolor-company-logo)
- [GET /api/apps/room\_booking/{installation\_id}/calendars](#scalar-operation-get-apiappsroom-bookinginstallation-idcalendars)
- [POST /api/apps/room\_booking/{installation\_id}/calendars](#scalar-operation-post-apiappsroom-bookinginstallation-idcalendars)
- [GET /api/apps/room\_booking/{installation\_id}/calendars/{id}](#scalar-operation-get-apiappsroom-bookinginstallation-idcalendarsid)
- [PATCH /api/apps/room\_booking/{installation\_id}/calendars/{id}](#scalar-operation-patch-apiappsroom-bookinginstallation-idcalendarsid)
- [DELETE /api/apps/room\_booking/{installation\_id}/calendars/{id}](#scalar-operation-delete-apiappsroom-bookinginstallation-idcalendarsid)
- [POST /api/apps/room\_booking/{installation\_id}/calendars/{id}/refetches](#scalar-operation-post-apiappsroom-bookinginstallation-idcalendarsidrefetches)
- [PUT /api/apps/room\_booking/{installation\_id}/calendars/{id}/devices/{device\_id}](#scalar-operation-put-apiappsroom-bookinginstallation-idcalendarsiddevicesdevice-id)
- [DELETE /api/apps/room\_booking/{installation\_id}/calendars/{id}/devices/{device\_id}](#scalar-operation-delete-apiappsroom-bookinginstallation-idcalendarsiddevicesdevice-id)
- [GET /api/apps/room\_booking/{installation\_id}/calendars/{id}/screens](#scalar-operation-get-apiappsroom-bookinginstallation-idcalendarsidscreens)
- [GET /api/apps/room\_booking/{installation\_id}/calendars/{id}/bookings](#scalar-operation-get-apiappsroom-bookinginstallation-idcalendarsidbookings)
- [POST /api/apps/room\_booking/{installation\_id}/calendars/{id}/bookings](#scalar-operation-post-apiappsroom-bookinginstallation-idcalendarsidbookings)
- [DELETE /api/apps/room\_booking/{installation\_id}/calendars/{id}/bookings/{booking\_id}](#scalar-operation-delete-apiappsroom-bookinginstallation-idcalendarsidbookingsbooking-id)
- [PATCH /api/apps/room\_booking/{installation\_id}/calendars/{id}/bookings/{booking\_id}/end](#scalar-operation-patch-apiappsroom-bookinginstallation-idcalendarsidbookingsbooking-idend)
- [GET /api/apps/room\_booking/{installation\_id}/collections](#scalar-operation-get-apiappsroom-bookinginstallation-idcollections)
- [POST /api/apps/room\_booking/{installation\_id}/collections](#scalar-operation-post-apiappsroom-bookinginstallation-idcollections)
- [PATCH /api/apps/room\_booking/{installation\_id}/collections/{id}](#scalar-operation-patch-apiappsroom-bookinginstallation-idcollectionsid)
- [DELETE /api/apps/room\_booking/{installation\_id}/collections/{id}](#scalar-operation-delete-apiappsroom-bookinginstallation-idcollectionsid)
- [PUT /api/apps/room\_booking/{installation\_id}/collections/{id}/devices/{device\_id}](#scalar-operation-put-apiappsroom-bookinginstallation-idcollectionsiddevicesdevice-id)
- [DELETE /api/apps/room\_booking/{installation\_id}/collections/{id}/devices/{device\_id}](#scalar-operation-delete-apiappsroom-bookinginstallation-idcollectionsiddevicesdevice-id)
- [GET /api/apps/room\_booking/{installation\_id}/collections/{id}/screens](#scalar-operation-get-apiappsroom-bookinginstallation-idcollectionsidscreens)
- [POST /api/plugin\_installations](#scalar-operation-post-apiplugin-installations)
- [GET /api/plugin\_installations/{id}](#scalar-operation-get-apiplugin-installationsid)
- [DELETE /api/plugin\_installations/{id}](#scalar-operation-delete-apiplugin-installationsid)
- [POST /api/plugin\_installations/{plugin\_installation\_id}/completion](#scalar-operation-post-apiplugin-installationsplugin-installation-idcompletion)
- [GET /api/plugin\_installations/{plugin\_installation\_id}/configuration](#scalar-operation-get-apiplugin-installationsplugin-installation-idconfiguration)
- [PATCH /api/plugin\_installations/{plugin\_installation\_id}/configuration](#scalar-operation-patch-apiplugin-installationsplugin-installation-idconfiguration)
- [POST /api/plugin\_installations/{plugin\_installation\_id}/configuration/evaluation](#scalar-operation-post-apiplugin-installationsplugin-installation-idconfigurationevaluation)
- [POST /api/plugin\_installations/{plugin\_installation\_id}/configuration/choices/{resolver\_id}](#scalar-operation-post-apiplugin-installationsplugin-installation-idconfigurationchoicesresolver-id)
- [GET /api/plugin\_settings/{plugin\_setting\_id}/removal\_preview](#scalar-operation-get-apiplugin-settingsplugin-setting-idremoval-preview)
- [GET /api/capabilities](#scalar-operation-get-apicapabilities)
- [DELETE /api/devices/{device\_id}/association](#scalar-operation-delete-apidevicesdevice-idassociation)
- [GET /api/devices/{device\_id}/mashups/options](#scalar-operation-get-apidevicesdevice-idmashupsoptions)
- [GET /api/playlists/items/{item\_id}/preview](#scalar-operation-get-apiplaylistsitemsitem-idpreview)
- [POST /api/plugin\_settings/{plugin\_setting\_id}/connections/{connection\_id}/attempts](#scalar-operation-post-apiplugin-settingsplugin-setting-idconnectionsconnection-idattempts)
- [DELETE /api/plugin\_settings/{plugin\_setting\_id}/connections/{connection\_id}](#scalar-operation-delete-apiplugin-settingsplugin-setting-idconnectionsconnection-id)
- [POST /api/plugin\_installations/{plugin\_installation\_id}/connections/{connection\_id}/attempts](#scalar-operation-post-apiplugin-installationsplugin-installation-idconnectionsconnection-idattempts)
- [DELETE /api/plugin\_installations/{plugin\_installation\_id}/connections/{connection\_id}](#scalar-operation-delete-apiplugin-installationsplugin-installation-idconnectionsconnection-id)
- [GET /api/plugin\_connection\_attempts/{id}](#scalar-operation-get-apiplugin-connection-attemptsid)
- [DELETE /api/plugin\_connection\_attempts/{id}](#scalar-operation-delete-apiplugin-connection-attemptsid)
- [POST /api/plugin\_connection\_attempts/{id}/confirmation](#scalar-operation-post-apiplugin-connection-attemptsidconfirmation)
- [POST /api/devices](#scalar-operation-post-apidevices)
- [GET /api/devices](#scalar-operation-get-apidevices)
- [GET /api/devices/{device\_id}/coverage](#scalar-operation-get-apidevicesdevice-idcoverage)
- [POST /api/devices/{device\_id}/firmware\_update\_retries](#scalar-operation-post-apidevicesdevice-idfirmware-update-retries)
- [GET /api/devices/{device\_id}/forecast](#scalar-operation-get-apidevicesdevice-idforecast)
- [POST /api/devices/{device\_id}/identification](#scalar-operation-post-apidevicesdevice-ididentification)
- [POST /api/devices/{device\_id}/mirror](#scalar-operation-post-apidevicesdevice-idmirror)
- [DELETE /api/devices/{device\_id}/mirror](#scalar-operation-delete-apidevicesdevice-idmirror)
- [POST /api/devices/{device\_id}/mirror/resyncs](#scalar-operation-post-apidevicesdevice-idmirrorresyncs)
- [POST /api/devices/{device\_id}/playlist\_copies](#scalar-operation-post-apidevicesdevice-idplaylist-copies)
- [GET /api/devices/{device\_id}/playlist\_items](#scalar-operation-get-apidevicesdevice-idplaylist-items)
- [POST /api/devices/{device\_id}/playlist\_items](#scalar-operation-post-apidevicesdevice-idplaylist-items)
- [PUT /api/devices/{device\_id}/playlist\_items/order](#scalar-operation-put-apidevicesdevice-idplaylist-itemsorder)
- [GET /api/devices/{id}](#scalar-operation-get-apidevicesid)
- [PATCH /api/devices/{id}](#scalar-operation-patch-apidevicesid)
- [DELETE /api/devices/{device\_id}/playlist](#scalar-operation-delete-apidevicesdevice-idplaylist)
- [GET /api/devices/{device\_id}/logs](#scalar-operation-get-apidevicesdevice-idlogs)
- [POST /api/markup](#scalar-operation-post-apimarkup)
- [POST /api/devices/{device\_id}/mashups](#scalar-operation-post-apidevicesdevice-idmashups)
- [GET /api/mashups/{id}](#scalar-operation-get-apimashupsid)
- [PATCH /api/mashups/{id}](#scalar-operation-patch-apimashupsid)
- [POST /api/mashups/{id}/health\_resets](#scalar-operation-post-apimashupsidhealth-resets)
- [GET /api/me](#scalar-operation-get-apime)
- [PATCH /api/me](#scalar-operation-patch-apime)
- [GET /api/my\_plugins](#scalar-operation-get-apimy-plugins)
- [POST /api/my\_plugins](#scalar-operation-post-apimy-plugins)
- [PATCH /api/my\_plugins/{id}](#scalar-operation-patch-apimy-pluginsid)
- [POST /api/plugin\_settings/{plugin\_setting\_id}/configuration/photo\_selection](#scalar-operation-post-apiplugin-settingsplugin-setting-idconfigurationphoto-selection)
- [PATCH /api/plugin\_settings/{plugin\_setting\_id}/configuration/photo\_selection](#scalar-operation-patch-apiplugin-settingsplugin-setting-idconfigurationphoto-selection)
- [POST /api/devices/{device\_id}/playlist\_items/bulk](#scalar-operation-post-apidevicesdevice-idplaylist-itemsbulk)
- [GET /api/playlists/items/{item\_id}/schedule](#scalar-operation-get-apiplaylistsitemsitem-idschedule)
- [PUT /api/playlists/items/{item\_id}/schedule](#scalar-operation-put-apiplaylistsitemsitem-idschedule)
- [GET /api/playlists/items](#scalar-operation-get-apiplaylistsitems)
- [PATCH /api/playlists/items/{id}](#scalar-operation-patch-apiplaylistsitemsid)
- [DELETE /api/playlists/items/{id}](#scalar-operation-delete-apiplaylistsitemsid)
- [POST /api/playlists/items/{id}/duplicates](#scalar-operation-post-apiplaylistsitemsidduplicates)
- [GET /api/plugin\_settings/{id}/archive](#scalar-operation-get-apiplugin-settingsidarchive)
- [POST /api/plugin\_settings/{id}/archive](#scalar-operation-post-apiplugin-settingsidarchive)
- [POST /api/plugin\_settings/{plugin\_setting\_id}/configuration/evaluation](#scalar-operation-post-apiplugin-settingsplugin-setting-idconfigurationevaluation)
- [POST /api/plugin\_settings/{plugin\_setting\_id}/configuration/choices/{resolver\_id}](#scalar-operation-post-apiplugin-settingsplugin-setting-idconfigurationchoicesresolver-id)
- [GET /api/plugin\_settings/{plugin\_setting\_id}/configuration](#scalar-operation-get-apiplugin-settingsplugin-setting-idconfiguration)
- [PATCH /api/plugin\_settings/{plugin\_setting\_id}/configuration](#scalar-operation-patch-apiplugin-settingsplugin-setting-idconfiguration)
- [POST /api/plugin\_settings/custom\_fields/verifications](#scalar-operation-post-apiplugin-settingscustom-fieldsverifications)
- [GET /api/plugin\_settings/{id}/data](#scalar-operation-get-apiplugin-settingsiddata)
- [POST /api/plugin\_settings/{id}/data](#scalar-operation-post-apiplugin-settingsiddata)
- [PUT /api/plugin\_settings/{id}/featured\_image](#scalar-operation-put-apiplugin-settingsidfeatured-image)
- [DELETE /api/plugin\_settings/{id}/featured\_image](#scalar-operation-delete-apiplugin-settingsidfeatured-image)
- [GET /api/plugin\_settings/{plugin\_setting\_id}/files](#scalar-operation-get-apiplugin-settingsplugin-setting-idfiles)
- [PUT /api/plugin\_settings/{plugin\_setting\_id}/files](#scalar-operation-put-apiplugin-settingsplugin-setting-idfiles)
- [POST /api/plugin\_settings/{id}/image](#scalar-operation-post-apiplugin-settingsidimage)
- [GET /api/plugin\_settings/{plugin\_setting\_id}/logs](#scalar-operation-get-apiplugin-settingsplugin-setting-idlogs)
- [GET /api/plugin\_settings/{plugin\_setting\_id}/markup/{size}](#scalar-operation-get-apiplugin-settingsplugin-setting-idmarkupsize)
- [PUT /api/plugin\_settings/{plugin\_setting\_id}/markup/{size}](#scalar-operation-put-apiplugin-settingsplugin-setting-idmarkupsize)
- [GET /api/plugin\_settings/{plugin\_setting\_id}/merge\_variables](#scalar-operation-get-apiplugin-settingsplugin-setting-idmerge-variables)
- [PATCH /api/plugin\_settings/{id}](#scalar-operation-patch-apiplugin-settingsid)
- [DELETE /api/plugin\_settings/{id}](#scalar-operation-delete-apiplugin-settingsid)
- [POST /api/plugin\_settings/{id}/copies](#scalar-operation-post-apiplugin-settingsidcopies)
- [POST /api/plugin\_settings/{id}/state\_clears](#scalar-operation-post-apiplugin-settingsidstate-clears)
- [DELETE /api/plugin\_settings/{id}/credentials](#scalar-operation-delete-apiplugin-settingsidcredentials)
- [POST /api/plugin\_settings/{id}/debug\_logs](#scalar-operation-post-apiplugin-settingsiddebug-logs)
- [POST /api/plugin\_settings/{id}/health\_resets](#scalar-operation-post-apiplugin-settingsidhealth-resets)
- [DELETE /api/plugin\_settings/{id}/transform](#scalar-operation-delete-apiplugin-settingsidtransform)
- [POST /api/plugin\_settings/{plugin\_setting\_id}/refreshes](#scalar-operation-post-apiplugin-settingsplugin-setting-idrefreshes)
- [GET /api/plugin\_settings/{plugin\_setting\_id}/refreshes/{id}](#scalar-operation-get-apiplugin-settingsplugin-setting-idrefreshesid)
- [POST /api/plugin\_settings/{plugin\_setting\_id}/screenshots](#scalar-operation-post-apiplugin-settingsplugin-setting-idscreenshots)
- [GET /api/plugin\_settings/{plugin\_setting\_id}/screenshots/{id}](#scalar-operation-get-apiplugin-settingsplugin-setting-idscreenshotsid)
- [PATCH /api/plugin\_settings/{plugin\_setting\_id}/settings](#scalar-operation-patch-apiplugin-settingsplugin-setting-idsettings)
- [GET /api/plugin\_settings](#scalar-operation-get-apiplugin-settings)
- [POST /api/plugin\_settings](#scalar-operation-post-apiplugin-settings)
- [GET /api/plugin\_settings/{id}/details](#scalar-operation-get-apiplugin-settingsiddetails)
- [GET /api/plugins](#scalar-operation-get-apiplugins)
- [GET /api/plugins/{id}](#scalar-operation-get-apipluginsid)
- [GET /api/recipes](#scalar-operation-get-apirecipes)
- [GET /api/recipes/{id}](#scalar-operation-get-apirecipesid)
- [GET /api/recipes/{id}/markup](#scalar-operation-get-apirecipesidmarkup)
- [POST /api/recipes/{id}/installs](#scalar-operation-post-apirecipesidinstalls)
- [GET /api/devices/{device\_id}/timeline](#scalar-operation-get-apidevicesdevice-idtimeline)
- [GET /api/plugin\_settings/{plugin\_setting\_id}/timeline](#scalar-operation-get-apiplugin-settingsplugin-setting-idtimeline)
- [GET /api/mashups/{mashup\_id}/timeline](#scalar-operation-get-apimashupsmashup-idtimeline)
- [GET /api/user\_themes](#scalar-operation-get-apiuser-themes)
- [POST /api/user\_themes](#scalar-operation-post-apiuser-themes)
- [GET /api/user\_themes/{id}](#scalar-operation-get-apiuser-themesid)
- [PATCH /api/user\_themes/{id}](#scalar-operation-patch-apiuser-themesid)
- [DELETE /api/user\_themes/{id}](#scalar-operation-delete-apiuser-themesid)
- [POST /api/user\_themes/imports](#scalar-operation-post-apiuser-themesimports)

**Schemas**

- [PluginConnectionAttempt](#scalar-schema-pluginconnectionattempt)
- [CatalogEntry](#scalar-schema-catalogentry)
- [PluginInstallation](#scalar-schema-plugininstallation)
- [ConfigurationField](#scalar-schema-configurationfield)
- [PluginSync](#scalar-schema-pluginsync)
- [PluginConfiguration](#scalar-schema-pluginconfiguration)
- [ConfigurationChange](#scalar-schema-configurationchange)
- [ConfigurationWrite](#scalar-schema-configurationwrite)
- [ConfigurationError](#scalar-schema-configurationerror)
- [MashupOptions](#scalar-schema-mashupoptions)
- [Error](#scalar-schema-error)
- [Device](#scalar-schema-device)
- [Model](#scalar-schema-model)
- [Palette](#scalar-schema-palette)
- [PlaylistItem](#scalar-schema-playlistitem)
- [Mashup](#scalar-schema-mashup)
- [Recipe](#scalar-schema-recipe)
- [Screen](#scalar-schema-screen)
- [PlaylistItemParams](#scalar-schema-playlistitemparams)
- [Plugin](#scalar-schema-plugin)
- [PluginSetting](#scalar-schema-pluginsetting)
- [PluginSettingArchive](#scalar-schema-pluginsettingarchive)
- [PluginSettingParams](#scalar-schema-pluginsettingparams)
- [PluginSettingDataParams](#scalar-schema-pluginsettingdataparams)
- [UserTheme](#scalar-schema-usertheme)
- [UserThemeSettings](#scalar-schema-userthemesettings)
- [MyPluginParams](#scalar-schema-mypluginparams)
- [MyPlugin](#scalar-schema-myplugin)
- [User](#scalar-schema-user)
- [TimelineStep](#scalar-schema-timelinestep)
- [Timeline](#scalar-schema-timeline)
- [App](#scalar-schema-app)
- [AppInstallation](#scalar-schema-appinstallation)
- [FleetMember](#scalar-schema-fleetmember)
- [Fleet](#scalar-schema-fleet)
- [RoomBookingCalendar](#scalar-schema-roombookingcalendar)
- [RoomBooking](#scalar-schema-roombooking)
- [RoomBookingCollection](#scalar-schema-roombookingcollection)
- [RoomBookingSummary](#scalar-schema-roombookingsummary)
- [RoomBookingSettings](#scalar-schema-roombookingsettings)
- [RoomBookingIntegration](#scalar-schema-roombookingintegration)
- [PublicBookingEvent](#scalar-schema-publicbookingevent)
- [RoomBookingBilling](#scalar-schema-roombookingbilling)
- [RoomBookingDeviceScreen](#scalar-schema-roombookingdevicescreen)
- [RoomBookingCalendarDetails](#scalar-schema-roombookingcalendardetails)

<a id="scalar-context-global-servers"></a>

## Servers

- **URL:** `https://{defaultHost}`
  - **Variables:**
    - `defaultHost` (default: `trmnl.com`)

## Operations

<a id="scalar-operation-get-apidisplay"></a>

### Fetch the next screen

- **Method:** `GET`
- **Path:** `/api/display`
- **Operation ID:** `getDisplay`
- **Tags:** Device API

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Header parameters

- **`Access-Token` (required)**: `string`

  Device API Key (eg. abc-123)
- **`Battery-Voltage`**: `number`

  Device battery voltage (eg. 3.7)
- **`Percent-Charged`**: `number`

  Device percent charged (eg. 69.4)
- **`Battery-Count`**: `number`

  Number of batteries in device (e.g. 1-2)
- **`Battery-Charging`**: `number`

  Whether device is plugged into power (eg. true)
- **`Battery-Health`**: `number`

  Quality of battery (eg. -1-100)
- **`Battery-Current`**: `number`

  Battery current (eg. -1-N)
- **`Battery-Temp`**: `number`

  Battery temperature in celsius (eg. -1-N)
- **`Battery-Capacity`**: `number`

  Ratio of current / max capacity (eg. -1/-1 in fault case)
- **`USB-Connected`**: `string`

  Whether device is plugged into power via USB ("true" or "false")
- **`WiFi-Band`**: `string`

  WiFi band the device is connected on ("2.4" or "5")
- **`Panel-Rev`**: `string`

  Identifier of the ePaper panel fitted to the device
- **`FW-Version`**: `string`

  Device firmware version (eg. 0.0.1)
- **`FW-Commit`**: `string`

  Device firmware commit hash, for the development channel (eg. a14914c)
- **`RSSI`**: `number`

  Device RSSI (eg. -69)
- **`Height`**: `string`

  Device screen height (eg. 480)
- **`Width`**: `string`

  Device screen width (eg. 800)
- **`Special-Function`**: `boolean`

  Device special function (eg. true)
- **`BASE64`**: `boolean`

  Encode image function (eg. true)
- **`Sensors`**: `string`

  Environmental sensor data
- **`Image-Cached`**: `string`

  Whether the device served the previous image from cache (eg. true / false)
- **`Wake-Time`**: `integer`

  Seconds the device was awake during the last cycle (eg. 12)

#### Responses

##### 200 Success

**Content type:** `application/json`

- **`action`**: `string | null`
- **`filename`**: `string | null`
- **`firmware_url`**: `string | null`
- **`image_url`**: `string | null`
- **`refresh_rate`**: `integer`
- **`reset_firmware`**: `boolean`
- **`special_function`**: `string`
- **`status`**: `integer`
- **`update_firmware`**: `boolean`

<a id="scalar-example-1"></a>

**Generated example:**

```json
{
  "status": 200,
  "image_url": "https://trmnl.com/images/system_screens/setup_logo/og_plus.png",
  "filename": "setup-logo.bmp",
  "refresh_rate": 300,
  "reset_firmware": false,
  "update_firmware": false,
  "firmware_url": "https://trmnl.com/firmware/1.0.0.bin",
  "special_function": "identify",
  "action": "identify"
}
```

<a id="scalar-operation-get-apidisplaycurrent"></a>

### Fetch the current screen

- **Method:** `GET`
- **Path:** `/api/display/current`
- **Operation ID:** `getCurrentScreen`
- **Tags:** Device API

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Header parameters

- **`Access-Token` (required)**: `string`

  Device API Key (eg. abc-123)

#### Responses

##### 200 Success

**Content type:** `application/json`

- **`filename`**: `string | null`
- **`image_url`**: `string | null`
- **`refresh_rate`**: `integer`
- **`rendered_at`**: `string | null`
- **`status`**: `integer`

<a id="scalar-example-2"></a>

**Generated example:**

```json
{
  "status": 200,
  "refresh_rate": 300,
  "image_url": "https://trmnl.com/images/system_screens/setup_logo/og_plus.png",
  "filename": "setup-logo.bmp",
  "rendered_at": "2023-01-01T00:00:00Z"
}
```

<a id="scalar-operation-post-apilog"></a>

### Log with logs\[] (array)

- **Method:** `POST`
- **Path:** `/api/log`
- **Operation ID:** `createDeviceLog`
- **Tags:** Device API

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Header parameters

- **`Access-Token` (required)**: `string`

  Device API Key (eg. abc-123)

#### Request body

An array of log entries. Each entry can be any JSON type: string, object, etc.

**Required:** `true`

**Content type:** `application/json`

no additional properties

- **`logs` (required)**: `array of any`

<a id="scalar-example-3"></a>

**Generated example:**

```json
{
  "logs": []
}
```

#### Responses

##### 204 Logs created when ignore\_log\_messages is not set

<a id="scalar-operation-get-apisetup"></a>

### Set up device

- **Method:** `GET`
- **Path:** `/api/setup`
- **Operation ID:** `setupDevice`
- **Tags:** Device API

Please note that the returned `status` JSON value may NOT always equal the HTTP status code. Notably, if a device MAC address is not found,
then the HTTP status code will be 200 but the `status` code in the response will be 404.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Header parameters

- **`ID` (required)**: `string`

  Device MAC Address (eg. 41:B4:10:39:A1:24)
- **`Model` (required)**: `string`

  DEVICE\_MODEL from firmware definitions
- **`Panel-Rev`**: `string`

  Identifier of the ePaper panel fitted to the device

#### Responses

##### 200 Success

**Content type:** `application/json`

- **`api_key`**: `string | null`
- **`friendly_id`**: `string | null`
- **`image_url`**: `string | null`
- **`message`**: `string`
- **`status`**: `integer`

<a id="scalar-example-4"></a>

**Generated example:**

```json
{
  "status": 200,
  "api_key": "abc-123",
  "friendly_id": "ABC-123",
  "image_url": "https://trmnl.com/images/system_screens/setup_logo/og_plus.png",
  "message": "Register at trmnl.com/start with Device ID 'ABC-123'"
}
```

<a id="scalar-operation-get-apicategories"></a>

### List all plugin categories

- **Method:** `GET`
- **Path:** `/api/categories`
- **Operation ID:** `listCategories`
- **Tags:** Categories

Returns a list of approved plugin categories.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Responses

##### 200 Success

**Content type:** `application/json`

- **`data`**: `array of string`

<a id="scalar-example-5"></a>

**Generated example:**

```json
{
  "data": [
    "life",
    "marketing",
    "ecommerce"
  ]
}
```

<a id="scalar-operation-get-apifirmwareflash"></a>

### List flashable firmware versions per device model

- **Method:** `GET`
- **Path:** `/api/firmware/flash`
- **Operation ID:** `listFlashFirmwares`
- **Tags:** Flash Firmwares

Firmware builds flashable via the TRMNL web flasher (/flash).

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Responses

##### 200 Success

**Content type:** `application/json`

- **`data` (required)**: `object`
  - **`models` (required)**: `array`

    **Items:**
    - **`chipFamily` (required)**: `string`
    - **`keyname` (required)**: `string`
    - **`label` (required)**: `string`
    - **`versions` (required)**: `array`

      **Items:**
      - **`url` (required)**: `string`
      - **`version` (required)**: `string`

<a id="scalar-example-6"></a>

**Generated example:**

```json
{
  "data": {
    "models": [
      {
        "keyname": "",
        "chipFamily": "",
        "label": "",
        "versions": [
          {
            "version": "",
            "url": ""
          }
        ]
      }
    ]
  }
}
```

<a id="scalar-operation-get-apiips"></a>

### List all TRMNL server IP addresses

- **Method:** `GET`
- **Path:** `/api/ips`
- **Operation ID:** `listServerIps`
- **Tags:** Server IPs

Returns a list of public IP addresses for all TRMNL core servers.

Plugin poll requests will only originate from these IPs.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Responses

##### 200 Success

**Content type:** `application/json`

- **`data`**: `object`
  - **`ipv4`**: `array of string`
  - **`ipv6`**: `array of string`

<a id="scalar-example-7"></a>

**Generated example:**

```json
{
  "data": {
    "ipv4": [
      ""
    ],
    "ipv6": [
      ""
    ]
  }
}
```

<a id="scalar-operation-get-apimodels"></a>

### List all device models

- **Method:** `GET`
- **Path:** `/api/models`
- **Operation ID:** `listModels`
- **Tags:** Models

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Responses

##### 200 Success

**Content type:** `application/json`

- **`data`**: array of [Model](#scalar-schema-model)

<a id="scalar-example-8"></a>

**Generated example:**

```json
{
  "data": [
    {
      "name": "trmnl_original",
      "label": "TRMNL",
      "description": "Original TRMNL model",
      "width": 800,
      "height": 480,
      "colors": 2,
      "bit_depth": 1,
      "scale_factor": 1,
      "rotation": 90,
      "mime_type": "image/png",
      "offset_x": 10,
      "offset_y": 20,
      "kind": "trmnl",
      "palette_ids": [
        "bw",
        "gray-4",
        "gray-16"
      ],
      "preview_white_point": "true_white",
      "image_size_limit": 90000,
      "image_upload_supported": true,
      "css": {
        "classes": {
          "device": "screen--og_plus",
          "size": "screen--md",
          "density": "screen--density-1x"
        },
        "variables": [
          [
            "--screen-w",
            "800px"
          ]
        ]
      }
    }
  ]
}
```

<a id="scalar-operation-get-apipalettes"></a>

### List all palettes

- **Method:** `GET`
- **Path:** `/api/palettes`
- **Operation ID:** `listPalettes`
- **Tags:** Palettes

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Responses

##### 200 Success

**Content type:** `application/json`

- **`data`**: array of [Palette](#scalar-schema-palette)

<a id="scalar-example-9"></a>

**Generated example:**

```json
{
  "data": [
    {
      "id": "gray-16",
      "name": "16-Gray",
      "grays": 16,
      "colors": [
        "#FF0000",
        "#00FF00",
        "#0000FF",
        "#FFFF00",
        "#000000",
        "#FFFFFF"
      ],
      "framework_class": "screen--4bit",
      "grayscale_bit_depth": 1
    }
  ]
}
```

<a id="scalar-operation-get-apibooktokenevents"></a>

### List the bookings a visitor sees

- **Method:** `GET`
- **Path:** `/api/book/{token}/events`
- **Operation ID:** `listPublicBookingEvents`
- **Tags:** Public booking

Public: no bearer token, so it is not an account operation and no agent tool offers it. The rolling week from start\_at, as the public booking page shows it, with times in the room time zone. A collection token also takes resource\_type and resource\_id to pick one of its rooms.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Path parameters

- **`token` (required)**: `string`

  The room public booking token, from its QR code or public\_booking\_token

#### Query parameters

- **`start_at` (required)**: `string`

  Window start, ISO 8601 or YYYY-MM-DDTHH:MM in the room time zone
- **`resource_type`**: `string`

  Collection tokens only:
  - `ical`
  - `google`
  - `microsoft`
- **`resource_id`**: `integer`

  Collection tokens only

#### Responses

##### 200 Success

**Content type:** `application/json`

- **`bookingLimit`**: `integer`

  How many bookings may overlap; more than one for a desk pool
- **`events`**: array of [PublicBookingEvent](#scalar-schema-publicbookingevent)
- **`fetched_at`**: `string | null`, format: `date_time`

<a id="scalar-example-10"></a>

**Generated example:**

```json
{
  "events": [
    {
      "id": 1,
      "title": "",
      "startsAt": "2026-06-12T10:00",
      "endsAt": "2026-06-12T11:00",
      "actionLabel": "",
      "actionMethod": "delete",
      "actionPath": "/api/book/{token}/bookings/12",
      "actionConfirm": ""
    }
  ],
  "bookingLimit": 1,
  "fetched_at": null
}
```

##### 400 start\_at is missing

##### 410 Unknown token, Booking with TRMNL off, or a room that cannot be booked: all look the same

<a id="scalar-operation-post-apibooktokenbookings"></a>

### Book a room as a visitor

- **Method:** `POST`
- **Path:** `/api/book/{token}/bookings`
- **Operation ID:** `createPublicBooking`
- **Tags:** Public booking

Public: no bearer token. The same rules as the public booking page: within the room booking window, ending before midnight, and on an on-demand room duration\_minutes (15, 30 or 60) from now instead of times. The title is capped and defaults when blank. Limited per token.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Path parameters

- **`token` (required)**: `string`

  The room public booking token

#### Query parameters

- **`resource_type`**: `string`

  Collection tokens only
- **`resource_id`**: `integer`

  Collection tokens only

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`duration_minutes`**: `integer`, possible values: `15, 30, 60`

  On-demand rooms only
- **`ends_at`**: `string`
- **`starts_at`**: `string`

  ISO 8601, or YYYY-MM-DDTHH:MM in the room time zone
- **`title`**: `string | null`

<a id="scalar-example-11"></a>

**Generated example:**

```json
{
  "title": null,
  "starts_at": "",
  "ends_at": "",
  "duration_minutes": 15
}
```

#### Responses

##### 200 Booked

**Content type:** `application/json`

- **`data`**: [PublicBookingEvent](#scalar-schema-publicbookingevent)

  The shape the public booking page uses: times are local to the room, without a zone

<a id="scalar-example-12"></a>

**Generated example:**

```json
{
  "data": {
    "id": 1,
    "title": "",
    "startsAt": "2026-06-12T10:00",
    "endsAt": "2026-06-12T11:00",
    "actionLabel": "",
    "actionMethod": "delete",
    "actionPath": "/api/book/{token}/bookings/12",
    "actionConfirm": ""
  }
}
```

##### 409 The slot was just taken

##### 410 Unknown token, Booking with TRMNL off, or a room that cannot be booked: all look the same

##### 422 Outside the booking window

##### 429 Too many bookings from one token

<a id="scalar-operation-delete-apibooktokenbookingsid"></a>

### Cancel an upcoming public booking

- **Method:** `DELETE`
- **Path:** `/api/book/{token}/bookings/{id}`
- **Operation ID:** `cancelPublicBooking`
- **Tags:** Public booking

Public: no bearer token. Only a booking made through TRMNL that has not started.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Path parameters

- **`token` (required)**: `string`

  The room public booking token
- **`id` (required)**: `integer`

  Booking id

#### Responses

##### 204 Canceled

##### 404 A booking made outside TRMNL, or one on another room

##### 410 Unknown token, Booking with TRMNL off, or a room that cannot be booked: all look the same

<a id="scalar-operation-patch-apibooktokenbookingsidend"></a>

### End a public booking in progress

- **Method:** `PATCH`
- **Path:** `/api/book/{token}/bookings/{id}/end`
- **Operation ID:** `endPublicBooking`
- **Tags:** Public booking

Public: no bearer token. Frees the room now; only a booking made through TRMNL that is in progress.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Path parameters

- **`token` (required)**: `string`

  The room public booking token
- **`id` (required)**: `integer`

  Booking id

#### Responses

##### 200 Ended

**Content type:** `application/json`

- **`data`**: [PublicBookingEvent](#scalar-schema-publicbookingevent)

  The shape the public booking page uses: times are local to the room, without a zone

<a id="scalar-example-13"></a>

**Generated example:**

```json
{
  "data": {
    "id": 1,
    "title": "",
    "startsAt": "2026-06-12T10:00",
    "endsAt": "2026-06-12T11:00",
    "actionLabel": "",
    "actionMethod": "delete",
    "actionPath": "/api/book/{token}/bookings/12",
    "actionConfirm": ""
  }
}
```

##### 404 A booking that has not started

##### 410 Unknown token, Booking with TRMNL off, or a room that cannot be booked: all look the same

<a id="scalar-operation-get-apianalytics"></a>

### Read the analytics of the plugins you published

- **Method:** `GET`
- **Path:** `/api/analytics`
- **Operation ID:** `getAuthorAnalytics`
- **Tags:** Analytics

Each recipe and third-party plugin you published with its installs, forks and health, the totals, the share of live installs in each health state and a 90-day install growth series.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Responses

##### 200 Success

**Content type:** `application/json`

- **`data`**: `object`
  - **`growth`**: `array`

    **Items:**

    `array of any`

    \[date, installs to date]
  - **`health`**: `object`

    healthy, degraded and erroring as a share of live installs

    **Additional properties:**
    - **`percent`**: `number | null`
  - **`plugins`**: `array`

    **Items:**
    - **`forks`**: `integer`
    - **`installs`**: `integer`
    - **`name`**: `string`
    - **`state`**: `string | null`, possible values: `"healthy", "degraded", "erroring", null`
  - **`stats`**: `object`
    - **`connections`**: `integer`
    - **`pageviews`**: `integer`
    - **`plugins`**: `integer`

<a id="scalar-example-14"></a>

**Generated example:**

```json
{
  "data": {
    "plugins": [
      {
        "name": "",
        "state": "healthy",
        "installs": 1,
        "forks": 1
      }
    ],
    "stats": {
      "plugins": 1,
      "connections": 1,
      "pageviews": 1
    },
    "health": {
      "additionalProperty": {
        "percent": null
      }
    },
    "growth": [
      []
    ]
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

<a id="scalar-example-15"></a>

**Generated example:**

```json
{
  "error": "An error occurred"
}
```

<a id="scalar-operation-get-apianalyticserrors"></a>

### List the failing installs of the plugins you published

- **Method:** `GET`
- **Path:** `/api/analytics/errors`
- **Operation ID:** `listAuthorPluginErrors`
- **Tags:** Analytics

One row per distinct error message still failing in the last week, with who can act on it: author (your plugin), upstream (the data source), installer\_connection or installer\_setup. Rows you hid stay listed with hidden true.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Responses

##### 200 Success

**Content type:** `application/json`

- **`data`**: `array`

  **Items:**
  - **`count`**: `integer`
  - **`hidden`**: `boolean`
  - **`kind`**: `string`, possible values: `"recipe", "third_party"`
  - **`last_seen`**: `string`, format: `date_time`
  - **`message`**: `string`
  - **`owner`**: `string`, possible values: `"author", "upstream", "installer_connection", "installer_setup"`
  - **`plugin_name`**: `string | null`
  - **`subject_id`**: `integer`

<a id="scalar-example-16"></a>

**Generated example:**

```json
{
  "data": [
    {
      "kind": "recipe",
      "subject_id": 1,
      "plugin_name": null,
      "owner": "author",
      "message": "",
      "count": 1,
      "last_seen": "",
      "hidden": true
    }
  ]
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

<a id="scalar-operation-post-apianalyticshidden-errors"></a>

### Hide an error row from your analytics

- **Method:** `POST`
- **Path:** `/api/analytics/hidden_errors`
- **Operation ID:** `hideAuthorPluginError`
- **Tags:** Analytics

Takes the kind, subject\_id, owner and message of a listAuthorPluginErrors row. The row stops showing on the dashboard and in the emails about it; unhideAuthorPluginError puts it back.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`kind` (required)**: `string`, possible values: `"recipe", "third_party"`
- **`message` (required)**: `string`

  The row message; it is matched by shape, so numbers and urls in it may differ
- **`owner` (required)**: `string`, possible values: `"author", "upstream", "installer_connection", "installer_setup"`
- **`subject_id` (required)**: `integer`

  The recipe or third-party plugin id of the row

<a id="scalar-example-17"></a>

**Generated example:**

```json
{
  "kind": "recipe",
  "subject_id": 1,
  "owner": "author",
  "message": ""
}
```

#### Responses

##### 200 Hidden

**Content type:** `application/json`

- **`data`**: `object`
  - **`hidden`**: `boolean`

<a id="scalar-example-18"></a>

**Generated example:**

```json
{
  "data": {
    "hidden": true
  }
}
```

##### 400 A field of the key is missing

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 422 A kind that is not recipe or third\_party

<a id="scalar-operation-delete-apianalyticshidden-errors"></a>

### Show a hidden error row again

- **Method:** `DELETE`
- **Path:** `/api/analytics/hidden_errors`
- **Operation ID:** `unhideAuthorPluginError`
- **Tags:** Analytics

Takes the same kind, subject\_id, owner and message that hid the row.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Query parameters

- **`kind` (required)**: `string`

  :
  - `recipe`
  - `third_party`
- **`subject_id` (required)**: `integer`
- **`owner` (required)**: `string`
- **`message` (required)**: `string`

#### Responses

##### 200 Shown again

**Content type:** `application/json`

- **`data`**: `object`
  - **`hidden`**: `boolean`

<a id="scalar-example-19"></a>

**Generated example:**

```json
{
  "data": {
    "hidden": false
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

<a id="scalar-operation-get-apianalyticsuninstall-feedback"></a>

### Read why installers removed the plugins you published

- **Method:** `GET`
- **Path:** `/api/analytics/uninstall_feedback`
- **Operation ID:** `listUninstallFeedback`
- **Tags:** Analytics

The last 30 days of uninstall feedback across everything you published: the total, a count per reason, the latest written comments and the typical days installed. Never names the installer.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Responses

##### 200 Success

**Content type:** `application/json`

- **`data`**: `object`
  - **`reason_breakdown`**: `array`

    **Items:**

    `array of any`

    \[reason, count]
  - **`recent_comments`**: `array`

    **Items:**
    - **`created_at`**: `string`, format: `date_time`
    - **`detail`**: `string`
    - **`kind_label`**: `string | null`
    - **`plugin_name`**: `string`
    - **`reason_label`**: `string | null`
  - **`total`**: `integer`
  - **`typical_days_installed`**: `integer | null`
  - **`window_days`**: `integer`

<a id="scalar-example-20"></a>

**Generated example:**

```json
{
  "data": {
    "window_days": 30,
    "total": 1,
    "reason_breakdown": [
      []
    ],
    "recent_comments": [
      {
        "detail": "",
        "plugin_name": "",
        "reason_label": null,
        "kind_label": null,
        "created_at": ""
      }
    ],
    "typical_days_installed": null
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

<a id="scalar-operation-get-apiappsfleetinstallation-id"></a>

### Read a fleet

- **Method:** `GET`
- **Path:** `/api/apps/fleet/{installation_id}`
- **Operation ID:** `getFleet`
- **Tags:** Apps - Fleet

The master device, how many mirrors need a push, the settings mirrors inherit and the overdue alert. listFleetDevices has each member.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Fleet installation id, from listAppInstallations

#### Responses

##### 200 Success

**Content type:** `application/json`

- **`data`**: [Fleet](#scalar-schema-fleet)

<a id="scalar-example-21"></a>

**Generated example:**

```json
{
  "data": {
    "id": "",
    "name": "Fleet",
    "master": {
      "device_id": 123,
      "name": "Lobby",
      "role": "master",
      "check_in_state": "overdue",
      "expected_check_in_at": null,
      "last_pushed_at": null,
      "in_sync": true
    },
    "mirror_count": 3,
    "needs_push_count": 1,
    "overdue_count": 0,
    "last_pushed_at": null,
    "inherited_settings": [
      "refresh_interval"
    ],
    "alerts": {
      "enabled": true,
      "threshold_minutes": 60,
      "email": null
    }
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Installation belongs to another user

<a id="scalar-operation-get-apiappsfleetinstallation-iddevices"></a>

### List the devices in a fleet

- **Method:** `GET`
- **Path:** `/api/apps/fleet/{installation_id}/devices`
- **Operation ID:** `listFleetDevices`
- **Tags:** Apps - Fleet

The master first, then the mirrors by urgency: overdue, on time, asleep, never seen.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Fleet installation id

#### Responses

##### 200 Success

**Content type:** `application/json`

- **`data`**: array of [FleetMember](#scalar-schema-fleetmember)

<a id="scalar-example-22"></a>

**Generated example:**

```json
{
  "data": [
    {
      "device_id": 123,
      "name": "Lobby",
      "role": "master",
      "check_in_state": "overdue",
      "expected_check_in_at": null,
      "last_pushed_at": null,
      "in_sync": true
    }
  ]
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Installation belongs to another user

<a id="scalar-operation-post-apiappsfleetinstallation-iddevices"></a>

### Add a device to a fleet as a mirror

- **Method:** `POST`
- **Path:** `/api/apps/fleet/{installation_id}/devices`
- **Operation ID:** `addFleetDevice`
- **Tags:** Apps - Fleet

The device joins as a mirror; setFleetMaster promotes one. A device can be in one fleet only.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Fleet installation id

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`device_id` (required)**: `integer`

<a id="scalar-example-23"></a>

**Generated example:**

```json
{
  "device_id": 1
}
```

#### Responses

##### 200 Added

**Content type:** `application/json`

- **`data`**: [FleetMember](#scalar-schema-fleetmember)

<a id="scalar-example-24"></a>

**Generated example:**

```json
{
  "data": {
    "device_id": 123,
    "name": "Lobby",
    "role": "master",
    "check_in_state": "overdue",
    "expected_check_in_at": null,
    "last_pushed_at": null,
    "in_sync": true
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Device belongs to another user

##### 422 The device is already in a fleet

<a id="scalar-operation-delete-apiappsfleetinstallation-iddevicesdevice-id"></a>

### Remove a device from a fleet

- **Method:** `DELETE`
- **Path:** `/api/apps/fleet/{installation_id}/devices/{device_id}`
- **Operation ID:** `removeFleetDevice`
- **Tags:** Apps - Fleet

Leaves the last pushed playlist on the device.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Fleet installation id
- **`device_id` (required)**: `integer`

  Device id

#### Responses

##### 204 Removed

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Device is not in this fleet

<a id="scalar-operation-post-apiappsfleetinstallation-iddevicesdevice-idpushes"></a>

### Push the master playlist to one mirror

- **Method:** `POST`
- **Path:** `/api/apps/fleet/{installation_id}/devices/{device_id}/pushes`
- **Operation ID:** `pushFleetDevice`
- **Tags:** Apps - Fleet

Queues the push; the mirror shows the master playlist and inherited settings once it runs.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Fleet installation id
- **`device_id` (required)**: `integer`

  Device id of a mirror

#### Responses

##### 202 Queued

##### 401, 429

- `401` Unauthorized
- `429` The mirror has had its hourly pushes

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Not a mirror of this fleet

<a id="scalar-operation-post-apiappsfleetinstallation-idpushes"></a>

### Push the master playlist to every mirror

- **Method:** `POST`
- **Path:** `/api/apps/fleet/{installation_id}/pushes`
- **Operation ID:** `pushFleet`
- **Tags:** Apps - Fleet

Queues one push for the whole fleet. Each mirror gets a copy of the master playlist and the inherited settings.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Fleet installation id

#### Responses

##### 202 Queued

##### 401, 429

- `401` Unauthorized
- `429` The fleet has had its hourly pushes

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Installation belongs to another user

##### 422 No master device

<a id="scalar-operation-patch-apiappsfleetinstallation-idsettings"></a>

### Choose the settings mirrors inherit from the master

- **Method:** `PATCH`
- **Path:** `/api/apps/fleet/{installation_id}/settings`
- **Operation ID:** `updateFleetSettings`
- **Tags:** Apps - Fleet

Replaces the list. Each push copies these settings from the master; an empty list copies the playlist only.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Fleet installation id

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`inherited_settings` (required)**: `array`

  **Items:**

  `string`, possible values: `"refresh_interval", "orientation", "sleep", "palette"`

<a id="scalar-example-25"></a>

**Generated example:**

```json
{
  "inherited_settings": [
    "refresh_interval"
  ]
}
```

#### Responses

##### 200 Updated

**Content type:** `application/json`

- **`data`**: [Fleet](#scalar-schema-fleet)

<a id="scalar-example-26"></a>

**Generated example:**

```json
{
  "data": {
    "id": "",
    "name": "Fleet",
    "master": {
      "device_id": 123,
      "name": "Lobby",
      "role": "master",
      "check_in_state": "overdue",
      "expected_check_in_at": null,
      "last_pushed_at": null,
      "in_sync": true
    },
    "mirror_count": 3,
    "needs_push_count": 1,
    "overdue_count": 0,
    "last_pushed_at": null,
    "inherited_settings": [
      "refresh_interval"
    ],
    "alerts": {
      "enabled": true,
      "threshold_minutes": 60,
      "email": null
    }
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Installation belongs to another user

##### 422 A setting that cannot be inherited

<a id="scalar-operation-patch-apiappsfleetinstallation-idalerts"></a>

### Change the overdue check-in alert

- **Method:** `PATCH`
- **Path:** `/api/apps/fleet/{installation_id}/alerts`
- **Operation ID:** `updateFleetAlerts`
- **Tags:** Apps - Fleet

One email digest when a member is overdue by the threshold. Keys left out keep their value; a null email sends the digest to the account owner.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Fleet installation id

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`overdue_alert_email`**: `string | null`
- **`overdue_alert_enabled`**: `boolean`
- **`overdue_alert_threshold_minutes`**: `integer`, possible values: `30, 60, 240`

<a id="scalar-example-27"></a>

**Generated example:**

```json
{
  "overdue_alert_enabled": true,
  "overdue_alert_threshold_minutes": 30,
  "overdue_alert_email": null
}
```

#### Responses

##### 200 Updated

**Content type:** `application/json`

- **`data`**: [Fleet](#scalar-schema-fleet)

<a id="scalar-example-28"></a>

**Generated example:**

```json
{
  "data": {
    "id": "",
    "name": "Fleet",
    "master": {
      "device_id": 123,
      "name": "Lobby",
      "role": "master",
      "check_in_state": "overdue",
      "expected_check_in_at": null,
      "last_pushed_at": null,
      "in_sync": true
    },
    "mirror_count": 3,
    "needs_push_count": 1,
    "overdue_count": 0,
    "last_pushed_at": null,
    "inherited_settings": [
      "refresh_interval"
    ],
    "alerts": {
      "enabled": true,
      "threshold_minutes": 60,
      "email": null
    }
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Installation belongs to another user

##### 422 A threshold the alert does not offer

<a id="scalar-operation-put-apiappsfleetinstallation-idmaster"></a>

### Set the master device

- **Method:** `PUT`
- **Path:** `/api/apps/fleet/{installation_id}/master`
- **Operation ID:** `setFleetMaster`
- **Tags:** Apps - Fleet

The device whose playlist and settings the mirrors copy. A mirror of this fleet is promoted; the previous master leaves the fleet.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Fleet installation id

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`device_id` (required)**: `integer`

<a id="scalar-example-29"></a>

**Generated example:**

```json
{
  "device_id": 1
}
```

#### Responses

##### 200 Set

**Content type:** `application/json`

- **`data`**: [Fleet](#scalar-schema-fleet)

<a id="scalar-example-30"></a>

**Generated example:**

```json
{
  "data": {
    "id": "",
    "name": "Fleet",
    "master": {
      "device_id": 123,
      "name": "Lobby",
      "role": "master",
      "check_in_state": "overdue",
      "expected_check_in_at": null,
      "last_pushed_at": null,
      "in_sync": true
    },
    "mirror_count": 3,
    "needs_push_count": 1,
    "overdue_count": 0,
    "last_pushed_at": null,
    "inherited_settings": [
      "refresh_interval"
    ],
    "alerts": {
      "enabled": true,
      "threshold_minutes": 60,
      "email": null
    }
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Device belongs to another user

##### 422 The device is in another fleet

<a id="scalar-operation-delete-apiappsfleetinstallation-idmaster"></a>

### Remove the master device

- **Method:** `DELETE`
- **Path:** `/api/apps/fleet/{installation_id}/master`
- **Operation ID:** `removeFleetMaster`
- **Tags:** Apps - Fleet

The fleet keeps its mirrors but cannot push until a new master is set.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Fleet installation id

#### Responses

##### 204 Removed

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Installation belongs to another user

<a id="scalar-operation-get-apiapps"></a>

### List the apps that can be installed

- **Method:** `GET`
- **Path:** `/api/apps`
- **Operation ID:** `listApps`
- **Tags:** Apps

Every app the platform offers, with whether this account holds an active installation of it.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Responses

##### 200 Success

**Content type:** `application/json`

- **`data`**: array of [App](#scalar-schema-app)

<a id="scalar-example-31"></a>

**Generated example:**

```json
{
  "data": [
    {
      "app_key": "room_booking",
      "name": "Booking",
      "tagline": "Rooms and desks on your screens",
      "installed": false
    }
  ]
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

<a id="scalar-operation-get-apiappsinstallations"></a>

### List my app installations

- **Method:** `GET`
- **Path:** `/api/apps/installations`
- **Operation ID:** `listAppInstallations`
- **Tags:** Apps

The active installations of this account. Their ids are the installation\_id the fleet and room booking operations take.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Responses

##### 200 Success

**Content type:** `application/json`

- **`data`**: array of [AppInstallation](#scalar-schema-appinstallation)

<a id="scalar-example-32"></a>

**Generated example:**

```json
{
  "data": [
    {
      "id": "5e2d0a3c-8f1b-4c8a-9c1e-2f6a7b8c9d0e",
      "app_key": "fleet",
      "name": "Fleet",
      "summary": "3 mirrors",
      "created_at": "2026-09-22T12:00:00Z"
    }
  ]
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

<a id="scalar-operation-post-apiappsinstallations"></a>

### Install an app

- **Method:** `POST`
- **Path:** `/api/apps/installations`
- **Operation ID:** `installApp`
- **Tags:** Apps

Installs the app for this account. An app already installed answers its existing installation with 200.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`app_key` (required)**: `string`

  One of the app\_key values listApps answers

<a id="scalar-example-33"></a>

**Generated example:**

```json
{
  "app_key": ""
}
```

#### Responses

##### 200 Installed, or already was

**Content type:** `application/json`

- **`data`**: [AppInstallation](#scalar-schema-appinstallation)

<a id="scalar-example-34"></a>

**Generated example:**

```json
{
  "data": {
    "id": "5e2d0a3c-8f1b-4c8a-9c1e-2f6a7b8c9d0e",
    "app_key": "fleet",
    "name": "Fleet",
    "summary": "3 mirrors",
    "created_at": "2026-09-22T12:00:00Z"
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 No such app

<a id="scalar-operation-delete-apiappsinstallationsid"></a>

### Uninstall an app

- **Method:** `DELETE`
- **Path:** `/api/apps/installations/{id}`
- **Operation ID:** `uninstallApp`
- **Tags:** Apps

Retires the installation. Its calendars, fleet memberships and device bindings go with it.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`id` (required)**: `string`

  Installation id

#### Responses

##### 204 Uninstalled

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Not an installation id

##### 502 Stripe refused to cancel the subscription, so nothing was uninstalled

<a id="scalar-operation-get-apiappsroom-bookinginstallation-idintegrations"></a>

### List the connected calendar accounts

- **Method:** `GET`
- **Path:** `/api/apps/room_booking/{installation_id}/integrations`
- **Operation ID:** `listRoomBookingIntegrations`
- **Tags:** Apps - Room Booking

The Google and Microsoft accounts (personal) and workspaces (admin) connected to the installation, with how many of their calendars are selected for sync. integration\_type and id address one in the other integration operations.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Room booking installation id

#### Responses

##### 200 Success

**Content type:** `application/json`

- **`data`**: array of [RoomBookingIntegration](#scalar-schema-roombookingintegration)

<a id="scalar-example-35"></a>

**Generated example:**

```json
{
  "data": [
    {
      "id": 1,
      "integration_type": "google_user",
      "provider": "google",
      "workspace": true,
      "label": "owner@gmail.com",
      "admin_email": null,
      "booking_organizer_email": null,
      "parking_resource_group_id": null,
      "calendar_count": 1,
      "selected_calendar_count": 1,
      "last_synced_at": null,
      "last_attempted_at": null
    }
  ]
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Installation belongs to another user

<a id="scalar-operation-get-apiappsroom-bookinginstallation-idintegrationsintegration-typeconnect-url"></a>

### Where to send the owner to connect an account

- **Method:** `GET`
- **Path:** `/api/apps/room_booking/{installation_id}/integrations/{integration_type}/connect_url`
- **Operation ID:** `getRoomBookingIntegrationConnectUrl`
- **Tags:** Apps - Room Booking

Connecting needs the owner to consent at Google or Microsoft in a browser, signed in to TRMNL. The answer is the form action that starts it: an HTML form the owner submits with the given method, from a TRMNL page (it is CSRF-protected).

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Room booking installation id
- **`integration_type` (required)**: `string`

#### Responses

##### 200 Success

**Content type:** `application/json`

- **`data`**: `object`
  - **`integrations_url`**: `string`
  - **`method`**: `string`
  - **`url`**: `string`

<a id="scalar-example-36"></a>

**Generated example:**

```json
{
  "data": {
    "url": "",
    "method": "",
    "integrations_url": ""
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Not an integration type

<a id="scalar-operation-post-apiappsroom-bookinginstallation-idintegrationsintegration-typeidsyncs"></a>

### Refresh the calendar list of a connected account

- **Method:** `POST`
- **Path:** `/api/apps/room_booking/{installation_id}/integrations/{integration_type}/{id}/syncs`
- **Operation ID:** `syncRoomBookingIntegration`
- **Tags:** Apps - Room Booking

Reads the calendars the account can see from the provider, so a new room shows up in listRoomBookingCalendars once selected.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Room booking installation id
- **`integration_type` (required)**: `string`
- **`id` (required)**: `integer`

  Integration id

#### Responses

##### 200 Synchronized

**Content type:** `application/json`

- **`data`**: [RoomBookingIntegration](#scalar-schema-roombookingintegration)

<a id="scalar-example-37"></a>

**Generated example:**

```json
{
  "data": {
    "id": 1,
    "integration_type": "google_user",
    "provider": "google",
    "workspace": true,
    "label": "owner@gmail.com",
    "admin_email": null,
    "booking_organizer_email": null,
    "parking_resource_group_id": null,
    "calendar_count": 1,
    "selected_calendar_count": 1,
    "last_synced_at": null,
    "last_attempted_at": null
  }
}
```

##### 401, 429

- `401` Unauthorized
- `429` The account has had its hourly syncs

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Integration of another type

##### 422 The provider could not be read

<a id="scalar-operation-patch-apiappsroom-bookinginstallation-idintegrationsintegration-typeid"></a>

### Choose the calendars of an account to sync, and its workspace settings

- **Method:** `PATCH`
- **Path:** `/api/apps/room_booking/{installation_id}/integrations/{integration_type}/{id}`
- **Operation ID:** `updateRoomBookingIntegration`
- **Tags:** Apps - Room Booking

calendar\_ids, when given, is the whole set to sync: a calendar left out stops syncing and leaves every screen and collection. The ids are the numbers of the account's calendars, from listRoomBookingCalendars without the kind prefix. booking\_organizer\_email and parking\_resource\_group\_id apply to a Microsoft workspace and are verified against Graph.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Room booking installation id
- **`integration_type` (required)**: `string`
- **`id` (required)**: `integer`

  Integration id

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`booking_organizer_email`**: `string | null`
- **`calendar_ids`**: `array of integer`
- **`parking_resource_group_id`**: `string | null`

<a id="scalar-example-38"></a>

**Generated example:**

```json
{
  "calendar_ids": [
    1
  ],
  "booking_organizer_email": null,
  "parking_resource_group_id": null
}
```

#### Responses

##### 200 Updated

**Content type:** `application/json`

- **`data`**: [RoomBookingIntegration](#scalar-schema-roombookingintegration)

<a id="scalar-example-39"></a>

**Generated example:**

```json
{
  "data": {
    "id": 1,
    "integration_type": "google_user",
    "provider": "google",
    "workspace": true,
    "label": "owner@gmail.com",
    "admin_email": null,
    "booking_organizer_email": null,
    "parking_resource_group_id": null,
    "calendar_count": 1,
    "selected_calendar_count": 1,
    "last_synced_at": null,
    "last_attempted_at": null
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Installation belongs to another user

##### 422 Graph cannot verify the organizer mailbox

<a id="scalar-operation-delete-apiappsroom-bookinginstallation-idintegrationsintegration-typeid"></a>

### Disconnect an account

- **Method:** `DELETE`
- **Path:** `/api/apps/room_booking/{installation_id}/integrations/{integration_type}/{id}`
- **Operation ID:** `disconnectRoomBookingIntegration`
- **Tags:** Apps - Room Booking

Forgets the credentials and removes the account's calendars from every screen and collection.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Room booking installation id
- **`integration_type` (required)**: `string`
- **`id` (required)**: `integer`

  Integration id

#### Responses

##### 204 Disconnected

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Integration belongs to another installation

<a id="scalar-operation-get-apiappsroom-bookinginstallation-id"></a>

### Read a room booking installation

- **Method:** `GET`
- **Path:** `/api/apps/room_booking/{installation_id}`
- **Operation ID:** `getRoomBooking`
- **Tags:** Apps - Room Booking

What is connected, how many calendars and collections there are, which devices show one, and the billing state. Google and Microsoft accounts are connected in the browser.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Room booking installation id, from listAppInstallations

#### Responses

##### 200 Success

**Content type:** `application/json`

- **`data`**: [RoomBookingSummary](#scalar-schema-roombookingsummary)

<a id="scalar-example-40"></a>

**Generated example:**

```json
{
  "data": {
    "id": "",
    "name": "Booking",
    "timezone": null,
    "configured": true,
    "connected_accounts": {
      "google_user": 1,
      "google_workspace": 1,
      "microsoft_user": 1,
      "microsoft_workspace": 1
    },
    "calendar_count": 1,
    "collection_count": 1,
    "sync_error_count": 1,
    "device_ids": [
      1
    ],
    "billing": {
      "locked": true,
      "subscription_required": true,
      "subscription_status": "active",
      "billing_url": ""
    }
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Installation belongs to another user

<a id="scalar-operation-get-apiappsroom-bookinginstallation-idbilling"></a>

### Read the billing state of a room booking installation

- **Method:** `GET`
- **Path:** `/api/apps/room_booking/{installation_id}/billing`
- **Operation ID:** `getRoomBookingBilling`
- **Tags:** Apps - Room Booking

Each calendar on a screen is billed monthly or yearly after a trial. Subscribing and managing the subscription happen in the browser on the billing page (checkout\_url and portal\_url), because each opens a Stripe session for the owner; the API never starts one.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Room booking installation id

#### Responses

##### 200 The billing state

**Content type:** `application/json`

- **`data`**: [RoomBookingBilling](#scalar-schema-roombookingbilling)

<a id="scalar-example-41"></a>

**Generated example:**

```json
{
  "data": {
    "locked": true,
    "subscription_required": true,
    "billable_resource_count": 1,
    "amount_in_dollars": {
      "month": 8,
      "year": 80
    },
    "trial_days": 14,
    "grace_until": null,
    "subscription": {
      "id": "",
      "status": "active",
      "interval": "month",
      "billed_quantity": 1
    },
    "checkout_url": "",
    "portal_url": null
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Installation belongs to another user

<a id="scalar-operation-get-apiappsroom-bookinginstallation-idsettings"></a>

### Read the settings of a room booking installation

- **Method:** `GET`
- **Path:** `/api/apps/room_booking/{installation_id}/settings`
- **Operation ID:** `getRoomBookingSettings`
- **Tags:** Apps - Room Booking

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Room booking installation id

#### Responses

##### 200 Success

**Content type:** `application/json`

- **`data`**: [RoomBookingSettings](#scalar-schema-roombookingsettings)

<a id="scalar-example-42"></a>

**Generated example:**

```json
{
  "data": {
    "timezone": "Europe/Amsterdam",
    "public_bookings": true,
    "dark_mode": true,
    "show_company_logo": true,
    "daily_public_booking_url_rotation": true,
    "time_format": null,
    "walk_up_calendar": true,
    "walk_up_booking_window_days": 1,
    "company_logo_attached": true,
    "color_company_logo_attached": true
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Installation belongs to another user

<a id="scalar-operation-patch-apiappsroom-bookinginstallation-idsettings"></a>

### Change the settings of a room booking installation

- **Method:** `PATCH`
- **Path:** `/api/apps/room_booking/{installation_id}/settings`
- **Operation ID:** `updateRoomBookingSettings`
- **Tags:** Apps - Room Booking

Keys left out keep their value. The company logo images are uploaded in the browser; show\_company\_logo only switches them on.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Room booking installation id

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`daily_public_booking_url_rotation`**: `boolean`
- **`dark_mode`**: `boolean`
- **`public_bookings`**: `boolean`

  Booking with TRMNL
- **`show_company_logo`**: `boolean`
- **`time_format`**: `string | null`, possible values: `null, "24_hour", "12_hour"`

  null detects it from the time zone
- **`timezone`**: `string | null`

  IANA name for every calendar without its own; null follows the owner
- **`walk_up_booking_window_days`**: `integer`, possible values: `1, 2, 7, 30`
- **`walk_up_calendar`**: `boolean`

  Upcoming bookings on the public booking page

<a id="scalar-example-43"></a>

**Generated example:**

```json
{
  "timezone": null,
  "public_bookings": true,
  "dark_mode": true,
  "show_company_logo": true,
  "daily_public_booking_url_rotation": true,
  "time_format": null,
  "walk_up_calendar": true,
  "walk_up_booking_window_days": 1
}
```

#### Responses

##### 200 Updated

**Content type:** `application/json`

- **`data`**: [RoomBookingSettings](#scalar-schema-roombookingsettings)

<a id="scalar-example-44"></a>

**Generated example:**

```json
{
  "data": {
    "timezone": "Europe/Amsterdam",
    "public_bookings": true,
    "dark_mode": true,
    "show_company_logo": true,
    "daily_public_booking_url_rotation": true,
    "time_format": null,
    "walk_up_calendar": true,
    "walk_up_booking_window_days": 1,
    "company_logo_attached": true,
    "color_company_logo_attached": true
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Installation belongs to another user

##### 422 A time zone that does not exist

<a id="scalar-operation-put-apiappsroom-bookinginstallation-idsettingscompany-logo"></a>

### Upload the company logo shown on the screens

- **Method:** `PUT`
- **Path:** `/api/apps/room_booking/{installation_id}/settings/company_logo`
- **Operation ID:** `setRoomBookingCompanyLogo`
- **Tags:** Apps - Room Booking

PNG, JPEG or SVG up to 2 MB, as JSON with the bytes in base64. Shown once show\_company\_logo is on.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Room booking installation id

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`image_base64` (required)**: `string`

  The image bytes, base64 encoded

<a id="scalar-example-45"></a>

**Generated example:**

```json
{
  "image_base64": ""
}
```

#### Responses

##### 200 Uploaded

**Content type:** `application/json`

- **`data`**: [RoomBookingSettings](#scalar-schema-roombookingsettings)

<a id="scalar-example-46"></a>

**Generated example:**

```json
{
  "data": {
    "timezone": "Europe/Amsterdam",
    "public_bookings": true,
    "dark_mode": true,
    "show_company_logo": true,
    "daily_public_booking_url_rotation": true,
    "time_format": null,
    "walk_up_calendar": true,
    "walk_up_booking_window_days": 1,
    "company_logo_attached": true,
    "color_company_logo_attached": true
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Installation belongs to another user

##### 422 Not base64

<a id="scalar-operation-delete-apiappsroom-bookinginstallation-idsettingscompany-logo"></a>

### Remove the company logo

- **Method:** `DELETE`
- **Path:** `/api/apps/room_booking/{installation_id}/settings/company_logo`
- **Operation ID:** `removeRoomBookingCompanyLogo`
- **Tags:** Apps - Room Booking

Removes the color logo with it, since the color one is only ever shown in its place.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Room booking installation id

#### Responses

##### 204 Removed

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Installation belongs to another user

<a id="scalar-operation-put-apiappsroom-bookinginstallation-idsettingscolor-company-logo"></a>

### Upload the color company logo for color screens

- **Method:** `PUT`
- **Path:** `/api/apps/room_booking/{installation_id}/settings/color_company_logo`
- **Operation ID:** `setRoomBookingColorCompanyLogo`
- **Tags:** Apps - Room Booking

Takes the place of the company logo on a color device. Only accepted while the account has a color device or a color logo already uploaded.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Room booking installation id

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`image_base64` (required)**: `string`

<a id="scalar-example-47"></a>

**Generated example:**

```json
{
  "image_base64": ""
}
```

#### Responses

##### 200 Uploaded

**Content type:** `application/json`

- **`data`**: [RoomBookingSettings](#scalar-schema-roombookingsettings)

<a id="scalar-example-48"></a>

**Generated example:**

```json
{
  "data": {
    "timezone": "Europe/Amsterdam",
    "public_bookings": true,
    "dark_mode": true,
    "show_company_logo": true,
    "daily_public_booking_url_rotation": true,
    "time_format": null,
    "walk_up_calendar": true,
    "walk_up_booking_window_days": 1,
    "company_logo_attached": true,
    "color_company_logo_attached": true
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Installation belongs to another user

##### 422 No color device on the account

<a id="scalar-operation-delete-apiappsroom-bookinginstallation-idsettingscolor-company-logo"></a>

### Remove the color company logo

- **Method:** `DELETE`
- **Path:** `/api/apps/room_booking/{installation_id}/settings/color_company_logo`
- **Operation ID:** `removeRoomBookingColorCompanyLogo`
- **Tags:** Apps - Room Booking

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Room booking installation id

#### Responses

##### 204 Removed

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Installation belongs to another user

<a id="scalar-operation-get-apiappsroom-bookinginstallation-idcalendars"></a>

### List the calendars of a room booking installation

- **Method:** `GET`
- **Path:** `/api/apps/room_booking/{installation_id}/calendars`
- **Operation ID:** `listRoomBookingCalendars`
- **Tags:** Apps - Room Booking

Every iCal calendar and every Google or Microsoft calendar selected for sync. A calendar id is ":", kind being ical, google or microsoft, and is what the other calendar operations take.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Room booking installation id, from listAppInstallations

#### Responses

##### 200 Success

**Content type:** `application/json`

- **`data`**: array of [RoomBookingCalendar](#scalar-schema-roombookingcalendar)

<a id="scalar-example-49"></a>

**Generated example:**

```json
{
  "data": [
    {
      "id": "ical:12",
      "kind": "ical",
      "name": "Boardroom",
      "timezone": "Europe/Amsterdam",
      "capacity": null,
      "show_booking_qr": true,
      "show_company_logo": true,
      "feed_backed": true,
      "ics_url": null,
      "resource_kind": "room",
      "booking_mode": null,
      "last_synced_at": null,
      "last_sync_error": null,
      "bound_device_ids": [
        1
      ],
      "url_display_url": null,
      "public_booking_token": null,
      "public_booking_url": null
    }
  ]
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Installation belongs to another user

<a id="scalar-operation-post-apiappsroom-bookinginstallation-idcalendars"></a>

### Add an iCal calendar

- **Method:** `POST`
- **Path:** `/api/apps/room_booking/{installation_id}/calendars`
- **Operation ID:** `createRoomBookingCalendar`
- **Tags:** Apps - Room Booking

An iCal feed is read-only: its events show on the screen but bookings are made in the source calendar. Google and Microsoft calendars are connected in the browser, since they need the owner to sign in.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Room booking installation id, from listAppInstallations

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`ics_url` (required)**: `string`

  http, https or webcal
- **`name` (required)**: `string`
- **`capacity`**: `integer | null`
- **`show_booking_qr`**: `boolean`
- **`show_company_logo`**: `boolean`
- **`timezone`**: `string | null`

  IANA name; the installation time zone when left out

<a id="scalar-example-50"></a>

**Generated example:**

```json
{
  "name": "",
  "ics_url": "",
  "timezone": null,
  "capacity": null,
  "show_booking_qr": true,
  "show_company_logo": true
}
```

#### Responses

##### 200 Created

**Content type:** `application/json`

- **`data`**: [RoomBookingCalendar](#scalar-schema-roombookingcalendar)

<a id="scalar-example-51"></a>

**Generated example:**

```json
{
  "data": {
    "id": "ical:12",
    "kind": "ical",
    "name": "Boardroom",
    "timezone": "Europe/Amsterdam",
    "capacity": null,
    "show_booking_qr": true,
    "show_company_logo": true,
    "feed_backed": true,
    "ics_url": null,
    "resource_kind": "room",
    "booking_mode": null,
    "last_synced_at": null,
    "last_sync_error": null,
    "bound_device_ids": [
      1
    ],
    "url_display_url": null,
    "public_booking_token": null,
    "public_booking_url": null
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 402 Billing is locked until the subscription is settled

##### 404 Installation belongs to another user

##### 422 Refused

<a id="scalar-operation-get-apiappsroom-bookinginstallation-idcalendarsid"></a>

### Read a calendar with its current and upcoming bookings

- **Method:** `GET`
- **Path:** `/api/apps/room_booking/{installation_id}/calendars/{id}`
- **Operation ID:** `getRoomBookingCalendar`
- **Tags:** Apps - Room Booking

Answers the last-synced mirror of the source calendar (last\_synced\_at says how old); refetchRoomBookingCalendar reads the source again.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Room booking installation id
- **`id` (required)**: `string`

  Calendar id, as listRoomBookingCalendars answers it

#### Responses

##### 200 Success

**Content type:** `application/json`

- **`data`**: [RoomBookingCalendarDetails](#scalar-schema-roombookingcalendardetails)

<a id="scalar-example-52"></a>

**Generated example:**

```json
{
  "data": {
    "id": "ical:12",
    "kind": "ical",
    "name": "Boardroom",
    "timezone": "Europe/Amsterdam",
    "capacity": null,
    "show_booking_qr": true,
    "show_company_logo": true,
    "feed_backed": true,
    "ics_url": null,
    "resource_kind": "room",
    "booking_mode": null,
    "last_synced_at": null,
    "last_sync_error": null,
    "bound_device_ids": [
      1
    ],
    "url_display_url": null,
    "public_booking_token": null,
    "public_booking_url": null,
    "current_booking": {
      "id": 1,
      "title": null,
      "starts_at": "",
      "ends_at": "",
      "all_day": true,
      "source": "owner",
      "status": "confirmed",
      "booked_with_trmnl": true
    },
    "upcoming_bookings": [
      {
        "id": 1,
        "title": null,
        "starts_at": "",
        "ends_at": "",
        "all_day": true,
        "source": "owner",
        "status": "confirmed",
        "booked_with_trmnl": true
      }
    ]
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Not a calendar id

<a id="scalar-operation-patch-apiappsroom-bookinginstallation-idcalendarsid"></a>

### Change a calendar

- **Method:** `PATCH`
- **Path:** `/api/apps/room_booking/{installation_id}/calendars/{id}`
- **Operation ID:** `updateRoomBookingCalendar`
- **Tags:** Apps - Room Booking

An iCal calendar takes name, ics\_url, timezone, capacity, show\_booking\_qr and show\_company\_logo. A Google or Microsoft calendar keeps its provider name and takes override\_name, show\_booking\_qr and show\_company\_logo.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Room booking installation id
- **`id` (required)**: `string`

  Calendar id, as listRoomBookingCalendars answers it

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`capacity`**: `integer | null`
- **`ics_url`**: `string`
- **`name`**: `string`
- **`override_name`**: `string | null`

  Google and Microsoft calendars only
- **`show_booking_qr`**: `boolean`
- **`show_company_logo`**: `boolean`
- **`timezone`**: `string | null`

<a id="scalar-example-53"></a>

**Generated example:**

```json
{
  "name": "",
  "ics_url": "",
  "timezone": null,
  "capacity": null,
  "override_name": null,
  "show_booking_qr": true,
  "show_company_logo": true
}
```

#### Responses

##### 200 Updated

**Content type:** `application/json`

- **`data`**: [RoomBookingCalendar](#scalar-schema-roombookingcalendar)

<a id="scalar-example-54"></a>

**Generated example:**

```json
{
  "data": {
    "id": "ical:12",
    "kind": "ical",
    "name": "Boardroom",
    "timezone": "Europe/Amsterdam",
    "capacity": null,
    "show_booking_qr": true,
    "show_company_logo": true,
    "feed_backed": true,
    "ics_url": null,
    "resource_kind": "room",
    "booking_mode": null,
    "last_synced_at": null,
    "last_sync_error": null,
    "bound_device_ids": [
      1
    ],
    "url_display_url": null,
    "public_booking_token": null,
    "public_booking_url": null
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Installation belongs to another user

##### 422 Refused

<a id="scalar-operation-delete-apiappsroom-bookinginstallation-idcalendarsid"></a>

### Remove an iCal calendar

- **Method:** `DELETE`
- **Path:** `/api/apps/room_booking/{installation_id}/calendars/{id}`
- **Operation ID:** `deleteRoomBookingCalendar`
- **Tags:** Apps - Room Booking

Takes it off every device. A Google or Microsoft calendar is not deleted here: deselect it from its account in the browser.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Room booking installation id
- **`id` (required)**: `string`

  Calendar id, as listRoomBookingCalendars answers it

#### Responses

##### 204 Removed

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Installation belongs to another user

##### 422 A provider calendar

<a id="scalar-operation-post-apiappsroom-bookinginstallation-idcalendarsidrefetches"></a>

### Refetch a calendar from its source now

- **Method:** `POST`
- **Path:** `/api/apps/room_booking/{installation_id}/calendars/{id}/refetches`
- **Operation ID:** `refetchRoomBookingCalendar`
- **Tags:** Apps - Room Booking

Reads the source calendar and updates the mirror the screens show.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Room booking installation id
- **`id` (required)**: `string`

  Calendar id

#### Responses

##### 200 Refetched

**Content type:** `application/json`

- **`data`**: [RoomBookingCalendar](#scalar-schema-roombookingcalendar)

<a id="scalar-example-55"></a>

**Generated example:**

```json
{
  "data": {
    "id": "ical:12",
    "kind": "ical",
    "name": "Boardroom",
    "timezone": "Europe/Amsterdam",
    "capacity": null,
    "show_booking_qr": true,
    "show_company_logo": true,
    "feed_backed": true,
    "ics_url": null,
    "resource_kind": "room",
    "booking_mode": null,
    "last_synced_at": null,
    "last_sync_error": null,
    "bound_device_ids": [
      1
    ],
    "url_display_url": null,
    "public_booking_token": null,
    "public_booking_url": null
  }
}
```

##### 401, 429

- `401` Unauthorized
- `429` The calendar has had its hourly refetches

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Installation belongs to another user

##### 422 The source could not be read

<a id="scalar-operation-put-apiappsroom-bookinginstallation-idcalendarsiddevicesdevice-id"></a>

### Show a calendar on a device

- **Method:** `PUT`
- **Path:** `/api/apps/room_booking/{installation_id}/calendars/{id}/devices/{device_id}`
- **Operation ID:** `bindRoomBookingCalendarDevice`
- **Tags:** Apps - Room Booking

Adds the room screen to the device playlist once; a device already showing it is left as it is. The first room to reach a screen starts billing: subscription\_required is then true and billing\_url is where the owner subscribes, and until they do the other changes answer 402.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Room booking installation id
- **`id` (required)**: `string`

  Calendar id
- **`device_id` (required)**

  Device ID, or url for the browser display

  **One of:**
  - `integer`
  - `string`, possible values: `"url"`

#### Responses

##### 200 Showing

**Content type:** `application/json`

- **`billing_url`**: `string | null`
- **`data`**: [RoomBookingCalendar](#scalar-schema-roombookingcalendar)
- **`subscription_required`**: `boolean`

<a id="scalar-example-56"></a>

**Generated example:**

```json
{
  "data": {
    "id": "ical:12",
    "kind": "ical",
    "name": "Boardroom",
    "timezone": "Europe/Amsterdam",
    "capacity": null,
    "show_booking_qr": true,
    "show_company_logo": true,
    "feed_backed": true,
    "ics_url": null,
    "resource_kind": "room",
    "booking_mode": null,
    "last_synced_at": null,
    "last_sync_error": null,
    "bound_device_ids": [
      1
    ],
    "url_display_url": null,
    "public_booking_token": null,
    "public_booking_url": null
  },
  "subscription_required": true,
  "billing_url": null
}
```

##### 401, 422

- `401` Unauthorized
- `422` The account holds as many plugin settings as it may

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 402 Billing is locked until the subscription is settled

##### 404 Device belongs to another user

<a id="scalar-operation-delete-apiappsroom-bookinginstallation-idcalendarsiddevicesdevice-id"></a>

### Stop showing a calendar on a device

- **Method:** `DELETE`
- **Path:** `/api/apps/room_booking/{installation_id}/calendars/{id}/devices/{device_id}`
- **Operation ID:** `unbindRoomBookingCalendarDevice`
- **Tags:** Apps - Room Booking

Removes the room screen from that device playlist only.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Room booking installation id
- **`id` (required)**: `string`

  Calendar id
- **`device_id` (required)**

  Device ID, or url for the browser display

  **One of:**
  - `integer`
  - `string`, possible values: `"url"`

#### Responses

##### 204 Removed

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Installation belongs to another user

<a id="scalar-operation-get-apiappsroom-bookinginstallation-idcalendarsidscreens"></a>

### Read the screens a calendar is showing on

- **Method:** `GET`
- **Path:** `/api/apps/room_booking/{installation_id}/calendars/{id}/screens`
- **Operation ID:** `listRoomBookingCalendarScreens`
- **Tags:** Apps - Room Booking

One entry per device showing this calendar, with the image that device is showing. A calendar on no device answers an empty list. screen is null while a device has not rendered the calendar yet, so a caller can tell that apart from not showing it at all. image\_url is a presigned link (expires after ActiveStorage.service\_urls\_expire\_in, 5 minutes by default); fetch it promptly.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Room booking installation id
- **`id` (required)**: `string`

  Calendar id

#### Responses

##### 200 The devices showing it

**Content type:** `application/json`

- **`data`**: array of [RoomBookingDeviceScreen](#scalar-schema-roombookingdevicescreen)

<a id="scalar-example-57"></a>

**Generated example:**

```json
{
  "data": [
    {
      "device_id": 1,
      "device_name": "Boardroom display",
      "screen": {
        "image_url": "https://trmnl-screens.s3.amazonaws.com/...",
        "rendered_at": "2023-10-01T12:00:00Z",
        "playlist_item_id": 1,
        "plugin_setting_id": 1,
        "mashup_id": 1,
        "filename": "weather-1696161600"
      }
    }
  ]
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Not a calendar of this installation

<a id="scalar-operation-get-apiappsroom-bookinginstallation-idcalendarsidbookings"></a>

### List the bookings of a calendar

- **Method:** `GET`
- **Path:** `/api/apps/room_booking/{installation_id}/calendars/{id}/bookings`
- **Operation ID:** `listRoomBookings`
- **Tags:** Apps - Room Booking

Confirmed bookings in a window, soonest first. The window defaults to the next seven days.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Room booking installation id
- **`id` (required)**: `string`

  Calendar id

#### Query parameters

- **`from`**: `string`

  Window start; now when left out
- **`to`**: `string`

  Window end; seven days after from when left out
- **`status`**: `string`

  confirmed when left out:
  - `confirmed`
  - `cancelled`

#### Responses

##### 200 Success

**Content type:** `application/json`

- **`data`**: array of [RoomBooking](#scalar-schema-roombooking)

<a id="scalar-example-58"></a>

**Generated example:**

```json
{
  "data": [
    {
      "id": 1,
      "title": null,
      "starts_at": "",
      "ends_at": "",
      "all_day": true,
      "source": "owner",
      "status": "confirmed",
      "booked_with_trmnl": true
    }
  ]
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Installation belongs to another user

##### 422 A window that ends before it starts

<a id="scalar-operation-post-apiappsroom-bookinginstallation-idcalendarsidbookings"></a>

### Book a room

- **Method:** `POST`
- **Path:** `/api/apps/room_booking/{installation_id}/calendars/{id}/bookings`
- **Operation ID:** `createRoomBooking`
- **Tags:** Apps - Room Booking

Books the slot on the room calendar as the owner. Times are ISO 8601; a time without a zone is read in the calendar time zone. An iCal calendar is read-only and refuses; an overlap answers 409.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Room booking installation id
- **`id` (required)**: `string`

  Calendar id

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`ends_at` (required)**: `string`, format: `date-time`
- **`starts_at` (required)**: `string`, format: `date-time`
- **`title`**: `string | null`

<a id="scalar-example-59"></a>

**Generated example:**

```json
{
  "title": null,
  "starts_at": "",
  "ends_at": ""
}
```

#### Responses

##### 200 Booked

**Content type:** `application/json`

- **`data`**: [RoomBooking](#scalar-schema-roombooking)

<a id="scalar-example-60"></a>

**Generated example:**

```json
{
  "data": {
    "id": 1,
    "title": null,
    "starts_at": "",
    "ends_at": "",
    "all_day": true,
    "source": "owner",
    "status": "confirmed",
    "booked_with_trmnl": true
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 402 Billing is locked until the subscription is settled

##### 404 Installation belongs to another user

##### 409 The slot overlaps a confirmed booking

##### 422 Ends before it starts

<a id="scalar-operation-delete-apiappsroom-bookinginstallation-idcalendarsidbookingsbooking-id"></a>

### Cancel an upcoming booking

- **Method:** `DELETE`
- **Path:** `/api/apps/room_booking/{installation_id}/calendars/{id}/bookings/{booking_id}`
- **Operation ID:** `cancelRoomBooking`
- **Tags:** Apps - Room Booking

Only a booking made through TRMNL that has not started yet; endRoomBooking is for one in progress.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Room booking installation id
- **`id` (required)**: `string`

  Calendar id
- **`booking_id` (required)**: `integer`

  Booking id

#### Responses

##### 204 Canceled

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 402 Billing is locked until the subscription is settled

##### 404 A booking already in progress

<a id="scalar-operation-patch-apiappsroom-bookinginstallation-idcalendarsidbookingsbooking-idend"></a>

### End a booking in progress now

- **Method:** `PATCH`
- **Path:** `/api/apps/room_booking/{installation_id}/calendars/{id}/bookings/{booking_id}/end`
- **Operation ID:** `endRoomBooking`
- **Tags:** Apps - Room Booking

Frees the room: the booking ends at the current time. Only a booking made through TRMNL that is in progress.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Room booking installation id
- **`id` (required)**: `string`

  Calendar id
- **`booking_id` (required)**: `integer`

  Booking id

#### Responses

##### 200 Ended

**Content type:** `application/json`

- **`data`**: [RoomBooking](#scalar-schema-roombooking)

<a id="scalar-example-61"></a>

**Generated example:**

```json
{
  "data": {
    "id": 1,
    "title": null,
    "starts_at": "",
    "ends_at": "",
    "all_day": true,
    "source": "owner",
    "status": "confirmed",
    "booked_with_trmnl": true
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 402 Billing is locked until the subscription is settled

##### 404 A booking that has not started

<a id="scalar-operation-get-apiappsroom-bookinginstallation-idcollections"></a>

### List the calendar collections

- **Method:** `GET`
- **Path:** `/api/apps/room_booking/{installation_id}/collections`
- **Operation ID:** `listRoomBookingCollections`
- **Tags:** Apps - Room Booking

A collection puts several calendars on one screen, as a list or a grid.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Room booking installation id

#### Responses

##### 200 Success

**Content type:** `application/json`

- **`data`**: array of [RoomBookingCollection](#scalar-schema-roombookingcollection)

<a id="scalar-example-62"></a>

**Generated example:**

```json
{
  "data": [
    {
      "id": 1,
      "name": "First Floor",
      "collection_type": "any",
      "display_layout": "list",
      "show_booking_qr": true,
      "show_company_logo": true,
      "resource_ids": [
        "ical:12",
        "google:3"
      ],
      "bound_device_ids": [
        1
      ],
      "url_display_url": null,
      "public_booking_token": null,
      "public_booking_url": null
    }
  ]
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Installation belongs to another user

<a id="scalar-operation-post-apiappsroom-bookinginstallation-idcollections"></a>

### Add a calendar collection

- **Method:** `POST`
- **Path:** `/api/apps/room_booking/{installation_id}/collections`
- **Operation ID:** `createRoomBookingCollection`
- **Tags:** Apps - Room Booking

resource\_ids are calendar ids from listRoomBookingCalendars; at least one is needed.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Room booking installation id

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`name` (required)**: `string`
- **`resource_ids` (required)**: `array of string`

  Calendar ids, as listRoomBookingCalendars answers them
- **`collection_type`**: `string`, possible values: `"any", "rooms", "spaces", "desks", "desk_pools", "parking_spaces", "vehicles", "equipment", "people", "other"`

  rooms when left out
- **`display_layout`**: `string`, possible values: `"list", "grid"`

  list when left out
- **`show_booking_qr`**: `boolean`
- **`show_company_logo`**: `boolean`

<a id="scalar-example-63"></a>

**Generated example:**

```json
{
  "name": "",
  "collection_type": "any",
  "display_layout": "list",
  "show_booking_qr": true,
  "show_company_logo": true,
  "resource_ids": [
    ""
  ]
}
```

#### Responses

##### 200 Created

**Content type:** `application/json`

- **`data`**: [RoomBookingCollection](#scalar-schema-roombookingcollection)

<a id="scalar-example-64"></a>

**Generated example:**

```json
{
  "data": {
    "id": 1,
    "name": "First Floor",
    "collection_type": "any",
    "display_layout": "list",
    "show_booking_qr": true,
    "show_company_logo": true,
    "resource_ids": [
      "ical:12",
      "google:3"
    ],
    "bound_device_ids": [
      1
    ],
    "url_display_url": null,
    "public_booking_token": null,
    "public_booking_url": null
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 402 Billing is locked until the subscription is settled

##### 404 Installation belongs to another user

##### 422 A layout the collection does not have

<a id="scalar-operation-patch-apiappsroom-bookinginstallation-idcollectionsid"></a>

### Change a calendar collection

- **Method:** `PATCH`
- **Path:** `/api/apps/room_booking/{installation_id}/collections/{id}`
- **Operation ID:** `updateRoomBookingCollection`
- **Tags:** Apps - Room Booking

Keys left out keep their value; resource\_ids, when given, replaces the whole set.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Room booking installation id
- **`id` (required)**: `integer`

  Collection id

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`collection_type`**: `string`, possible values: `"any", "rooms", "spaces", "desks", "desk_pools", "parking_spaces", "vehicles", "equipment", "people", "other"`
- **`display_layout`**: `string`, possible values: `"list", "grid"`
- **`name`**: `string`
- **`resource_ids`**: `array of string`
- **`show_booking_qr`**: `boolean`
- **`show_company_logo`**: `boolean`

<a id="scalar-example-65"></a>

**Generated example:**

```json
{
  "name": "",
  "collection_type": "any",
  "display_layout": "list",
  "show_booking_qr": true,
  "show_company_logo": true,
  "resource_ids": [
    ""
  ]
}
```

#### Responses

##### 200 Updated

**Content type:** `application/json`

- **`data`**: [RoomBookingCollection](#scalar-schema-roombookingcollection)

<a id="scalar-example-66"></a>

**Generated example:**

```json
{
  "data": {
    "id": 1,
    "name": "First Floor",
    "collection_type": "any",
    "display_layout": "list",
    "show_booking_qr": true,
    "show_company_logo": true,
    "resource_ids": [
      "ical:12",
      "google:3"
    ],
    "bound_device_ids": [
      1
    ],
    "url_display_url": null,
    "public_booking_token": null,
    "public_booking_url": null
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 402 Billing is locked until the subscription is settled

##### 404 Collection belongs to another installation

##### 422 Emptying the calendars is refused, and the current ones stay

<a id="scalar-operation-delete-apiappsroom-bookinginstallation-idcollectionsid"></a>

### Remove a calendar collection

- **Method:** `DELETE`
- **Path:** `/api/apps/room_booking/{installation_id}/collections/{id}`
- **Operation ID:** `deleteRoomBookingCollection`
- **Tags:** Apps - Room Booking

Takes it off every device. The calendars in it stay.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Room booking installation id
- **`id` (required)**: `integer`

  Collection id

#### Responses

##### 204 Removed

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Installation belongs to another user

<a id="scalar-operation-put-apiappsroom-bookinginstallation-idcollectionsiddevicesdevice-id"></a>

### Show a collection on a device

- **Method:** `PUT`
- **Path:** `/api/apps/room_booking/{installation_id}/collections/{id}/devices/{device_id}`
- **Operation ID:** `bindRoomBookingCollectionDevice`
- **Tags:** Apps - Room Booking

Adds the collection screen to the device playlist once. Billing works as for bindRoomBookingCalendarDevice.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Room booking installation id
- **`id` (required)**: `integer`

  Collection id
- **`device_id` (required)**

  Device ID, or url for the browser display

  **One of:**
  - `integer`
  - `string`, possible values: `"url"`

#### Responses

##### 200 Showing

**Content type:** `application/json`

- **`billing_url`**: `string | null`
- **`data`**: [RoomBookingCollection](#scalar-schema-roombookingcollection)
- **`subscription_required`**: `boolean`

<a id="scalar-example-67"></a>

**Generated example:**

```json
{
  "data": {
    "id": 1,
    "name": "First Floor",
    "collection_type": "any",
    "display_layout": "list",
    "show_booking_qr": true,
    "show_company_logo": true,
    "resource_ids": [
      "ical:12",
      "google:3"
    ],
    "bound_device_ids": [
      1
    ],
    "url_display_url": null,
    "public_booking_token": null,
    "public_booking_url": null
  },
  "subscription_required": true,
  "billing_url": null
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 402 Billing is locked until the subscription is settled

##### 404 Device belongs to another user

<a id="scalar-operation-delete-apiappsroom-bookinginstallation-idcollectionsiddevicesdevice-id"></a>

### Stop showing a collection on a device

- **Method:** `DELETE`
- **Path:** `/api/apps/room_booking/{installation_id}/collections/{id}/devices/{device_id}`
- **Operation ID:** `unbindRoomBookingCollectionDevice`
- **Tags:** Apps - Room Booking

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Room booking installation id
- **`id` (required)**: `integer`

  Collection id
- **`device_id` (required)**

  Device ID, or url for the browser display

  **One of:**
  - `integer`
  - `string`, possible values: `"url"`

#### Responses

##### 204 Removed

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Installation belongs to another user

<a id="scalar-operation-get-apiappsroom-bookinginstallation-idcollectionsidscreens"></a>

### Read the screens a collection is showing on

- **Method:** `GET`
- **Path:** `/api/apps/room_booking/{installation_id}/collections/{id}/screens`
- **Operation ID:** `listRoomBookingCollectionScreens`
- **Tags:** Apps - Room Booking

The same answer as listRoomBookingCalendarScreens, for a collection.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`installation_id` (required)**: `string`

  Room booking installation id
- **`id` (required)**: `integer`

  Collection id

#### Responses

##### 200 The devices showing it

**Content type:** `application/json`

- **`data`**: array of [RoomBookingDeviceScreen](#scalar-schema-roombookingdevicescreen)

<a id="scalar-example-68"></a>

**Generated example:**

```json
{
  "data": [
    {
      "device_id": 1,
      "device_name": "Boardroom display",
      "screen": {
        "image_url": "https://trmnl-screens.s3.amazonaws.com/...",
        "rendered_at": "2023-10-01T12:00:00Z",
        "playlist_item_id": 1,
        "plugin_setting_id": 1,
        "mashup_id": 1,
        "filename": "weather-1696161600"
      }
    }
  ]
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Collection belongs to another installation

<a id="scalar-operation-post-apiplugin-installations"></a>

### Prepare an installation

- **Method:** `POST`
- **Path:** `/api/plugin_installations`
- **Operation ID:** `prepareInstallation`
- **Tags:** Installations

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Header parameters

- **`Idempotency-Key` (required)**: `string`, format: `uuid`
- **`X-TRMNL-Configuration-Capabilities`**: `string`, maxLength: `4096`

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`choice_id` (required)**: `string`
- **`kind` (required)**: `string`, possible values: `"official", "recipe"`
- **`source_id` (required)**: `integer`
- **`source_revision` (required)**: `string`
- **`device_id`**: `integer`

  Device whose playlist gets the plugin once the installation completes

<a id="scalar-example-69"></a>

**Generated example:**

```json
{
  "kind": "official",
  "source_id": 1,
  "source_revision": "",
  "choice_id": "",
  "device_id": 1
}
```

#### Responses

##### 200 Retained installation replay

**Content type:** `application/json`

- **`data`**: [PluginInstallation](#scalar-schema-plugininstallation)

<a id="scalar-example-70"></a>

**Generated example:**

```json
{
  "data": {
    "id": "",
    "state": "setup_required",
    "source": {
      "kind": "",
      "id": 1,
      "revision": "",
      "choice_id": "",
      "device_id": 1
    },
    "plugin_setting_id": null,
    "expires_at": ""
  }
}
```

##### 201 Created installation

**Content type:** `application/json`

- **`data`**: [PluginInstallation](#scalar-schema-plugininstallation)

<a id="scalar-example-71"></a>

**Generated example:**

```json
{
  "data": {
    "id": "",
    "state": "setup_required",
    "source": {
      "kind": "",
      "id": 1,
      "revision": "",
      "choice_id": "",
      "device_id": 1
    },
    "plugin_setting_id": null,
    "expires_at": ""
  }
}
```

##### 409 Idempotency key conflict

**Content type:** `application/json`

[ConfigurationError](#scalar-schema-configurationerror)

<a id="scalar-example-72"></a>

**Generated example:**

```json
{
  "error": {
    "code": "",
    "message": "",
    "field_errors": [
      {
        "path": "",
        "code": "",
        "message": ""
      }
    ]
  }
}
```

<a id="scalar-operation-get-apiplugin-installationsid"></a>

### Read a retained installation

- **Method:** `GET`
- **Path:** `/api/plugin_installations/{id}`
- **Operation ID:** `getInstallation`
- **Tags:** Installations

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`id` (required)**: `string`, format: `uuid`

#### Responses

##### 200 Installation

**Content type:** `application/json`

- **`data`**: [PluginInstallation](#scalar-schema-plugininstallation)

<a id="scalar-example-73"></a>

**Generated example:**

```json
{
  "data": {
    "id": "",
    "state": "setup_required",
    "source": {
      "kind": "",
      "id": 1,
      "revision": "",
      "choice_id": "",
      "device_id": 1
    },
    "plugin_setting_id": null,
    "expires_at": ""
  }
}
```

<a id="scalar-operation-delete-apiplugin-installationsid"></a>

### Cancel a pending installation

- **Method:** `DELETE`
- **Path:** `/api/plugin_installations/{id}`
- **Operation ID:** `cancelInstallation`
- **Tags:** Installations

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`id` (required)**: `string`, format: `uuid`

#### Responses

##### 204 Cancelled

<a id="scalar-operation-post-apiplugin-installationsplugin-installation-idcompletion"></a>

### Complete an installation once

- **Method:** `POST`
- **Path:** `/api/plugin_installations/{plugin_installation_id}/completion`
- **Operation ID:** `completeInstallation`
- **Tags:** Installations

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`plugin_installation_id` (required)**: `string`, format: `uuid`

#### Responses

##### 200 Completed installation

**Content type:** `application/json`

- **`data`**: [PluginInstallation](#scalar-schema-plugininstallation)

<a id="scalar-example-74"></a>

**Generated example:**

```json
{
  "data": {
    "id": "",
    "state": "setup_required",
    "source": {
      "kind": "",
      "id": 1,
      "revision": "",
      "choice_id": "",
      "device_id": 1
    },
    "plugin_setting_id": null,
    "expires_at": ""
  }
}
```

##### 422 Required setup is incomplete

<a id="scalar-operation-get-apiplugin-installationsplugin-installation-idconfiguration"></a>

### Read pending configuration

- **Method:** `GET`
- **Path:** `/api/plugin_installations/{plugin_installation_id}/configuration`
- **Operation ID:** `getInstallationConfiguration`
- **Tags:** Installations

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`plugin_installation_id` (required)**: `string`, format: `uuid`

#### Responses

##### 200 Pending configuration

**Content type:** `application/json`

- **`data`**: [PluginConfiguration](#scalar-schema-pluginconfiguration) | [PluginInstallation](#scalar-schema-plugininstallation)

<a id="scalar-example-75"></a>

**Generated example:**

```json
{
  "data": {
    "schema_version": 1,
    "revision": "",
    "required_capabilities": [
      ""
    ],
    "sections": [
      {
        "id": "",
        "title": "",
        "fields": [
          {
            "path": "",
            "type": "",
            "label": "",
            "help": "",
            "required": true,
            "read_only": true,
            "nullable": true,
            "constraints": {},
            "choices": [
              {
                "label": "",
                "value": null
              }
            ]
          }
        ]
      }
    ],
    "values": {},
    "secrets": {
      "additionalProperty": {
        "present": true
      }
    },
    "connections": [
      {}
    ],
    "actions": [
      {}
    ],
    "readiness": {
      "ready": true,
      "blocking_paths": [
        ""
      ],
      "reason": null
    },
    "sync": {
      "source_kinds": [
        ""
      ],
      "upload_allowed": true,
      "reason": null,
      "action_id": null
    }
  }
}
```

<a id="scalar-operation-patch-apiplugin-installationsplugin-installation-idconfiguration"></a>

### Save pending configuration

- **Method:** `PATCH`
- **Path:** `/api/plugin_installations/{plugin_installation_id}/configuration`
- **Operation ID:** `updateInstallationConfiguration`
- **Tags:** Installations

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`plugin_installation_id` (required)**: `string`, format: `uuid`

#### Request body

**Required:** `true`

**Content type:** `application/json`

[ConfigurationWrite](#scalar-schema-configurationwrite)

<a id="scalar-example-76"></a>

**Generated example:**

```json
{
  "revision": "",
  "changes": [
    {
      "op": "set",
      "path": "",
      "value": null
    }
  ]
}
```

#### Responses

##### 200 Updated pending configuration

**Content type:** `application/json`

- **`data`**: [PluginConfiguration](#scalar-schema-pluginconfiguration)

<a id="scalar-example-77"></a>

**Generated example:**

```json
{
  "data": {
    "schema_version": 1,
    "revision": "",
    "required_capabilities": [
      ""
    ],
    "sections": [
      {
        "id": "",
        "title": "",
        "fields": [
          {
            "path": "",
            "type": "",
            "label": "",
            "help": "",
            "required": true,
            "read_only": true,
            "nullable": true,
            "constraints": {},
            "choices": [
              {
                "label": "",
                "value": null
              }
            ]
          }
        ]
      }
    ],
    "values": {},
    "secrets": {
      "additionalProperty": {
        "present": true
      }
    },
    "connections": [
      {}
    ],
    "actions": [
      {}
    ],
    "readiness": {
      "ready": true,
      "blocking_paths": [
        ""
      ],
      "reason": null
    },
    "sync": {
      "source_kinds": [
        ""
      ],
      "upload_allowed": true,
      "reason": null,
      "action_id": null
    }
  }
}
```

<a id="scalar-operation-post-apiplugin-installationsplugin-installation-idconfigurationevaluation"></a>

### Evaluate pending configuration without saving

- **Method:** `POST`
- **Path:** `/api/plugin_installations/{plugin_installation_id}/configuration/evaluation`
- **Operation ID:** `evaluateInstallationConfiguration`
- **Tags:** Installations

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`plugin_installation_id` (required)**: `string`, format: `uuid`

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`draft` (required)**: array of [ConfigurationChange](#scalar-schema-configurationchange), maxItems: `200`
- **`revision` (required)**: `string`

<a id="scalar-example-78"></a>

**Generated example:**

```json
{
  "revision": "",
  "draft": [
    {
      "op": "set",
      "path": "",
      "value": null
    }
  ]
}
```

#### Responses

##### 200 Evaluated configuration

**Content type:** `application/json`

- **`data`**: [PluginConfiguration](#scalar-schema-pluginconfiguration)

<a id="scalar-example-79"></a>

**Generated example:**

```json
{
  "data": {
    "schema_version": 1,
    "revision": "",
    "required_capabilities": [
      ""
    ],
    "sections": [
      {
        "id": "",
        "title": "",
        "fields": [
          {
            "path": "",
            "type": "",
            "label": "",
            "help": "",
            "required": true,
            "read_only": true,
            "nullable": true,
            "constraints": {},
            "choices": [
              {
                "label": "",
                "value": null
              }
            ]
          }
        ]
      }
    ],
    "values": {},
    "secrets": {
      "additionalProperty": {
        "present": true
      }
    },
    "connections": [
      {}
    ],
    "actions": [
      {}
    ],
    "readiness": {
      "ready": true,
      "blocking_paths": [
        ""
      ],
      "reason": null
    },
    "sync": {
      "source_kinds": [
        ""
      ],
      "upload_allowed": true,
      "reason": null,
      "action_id": null
    }
  }
}
```

<a id="scalar-operation-post-apiplugin-installationsplugin-installation-idconfigurationchoicesresolver-id"></a>

### Resolve pending configuration choices

- **Method:** `POST`
- **Path:** `/api/plugin_installations/{plugin_installation_id}/configuration/choices/{resolver_id}`
- **Operation ID:** `getInstallationConfigurationChoices`
- **Tags:** Installations

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`plugin_installation_id` (required)**: `string`, format: `uuid`
- **`resolver_id` (required)**: `string`

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`draft` (required)**: array of [ConfigurationChange](#scalar-schema-configurationchange), maxItems: `200`
- **`revision` (required)**: `string`
- **`cursor`**: `string`
- **`query`**: `string`, maxLength: `200`

<a id="scalar-example-80"></a>

**Generated example:**

```json
{
  "revision": "",
  "draft": [
    {
      "op": "set",
      "path": "",
      "value": null
    }
  ],
  "query": "",
  "cursor": ""
}
```

#### Responses

##### 404 Unknown resolver

<a id="scalar-operation-get-apiplugin-settingsplugin-setting-idremoval-preview"></a>

### Preview installation removal

- **Method:** `GET`
- **Path:** `/api/plugin_settings/{plugin_setting_id}/removal_preview`
- **Operation ID:** `getPluginSettingRemovalPreview`
- **Tags:** Plugin Settings

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`plugin_setting_id` (required)**: `integer`

#### Responses

##### 200 Affected placements

**Content type:** `application/json`

- **`data`**: `object`
  - **`placements` (required)**: `array`

    **Items:**
    - **`device_id` (required)**: `integer`
    - **`device_name` (required)**: `string`
    - **`id` (required)**: `integer`
    - **`impact`**: `string`, possible values: `"removed", "source_removed"`
  - **`revision` (required)**: `string`

<a id="scalar-example-81"></a>

**Generated example:**

```json
{
  "data": {
    "revision": "",
    "placements": [
      {
        "id": 1,
        "device_id": 1,
        "device_name": "",
        "impact": "removed"
      }
    ]
  }
}
```

<a id="scalar-operation-get-apicapabilities"></a>

### Read supported Companion contract versions

- **Method:** `GET`
- **Path:** `/api/capabilities`
- **Operation ID:** `getCompanionCapabilities`
- **Tags:** Companion

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Responses

##### 200 Exact supported versions; missing features are unavailable

**Content type:** `application/json`

- **`data`**: `object`

  **Additional properties:**

  `integer`

<a id="scalar-example-82"></a>

**Generated example:**

```json
{
  "data": {
    "additionalProperty": 1
  }
}
```

<a id="scalar-operation-delete-apidevicesdevice-idassociation"></a>

### Unlink an owned physical device

- **Method:** `DELETE`
- **Path:** `/api/devices/{device_id}/association`
- **Operation ID:** `deleteDeviceAssociation`
- **Tags:** Devices

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`device_id` (required)**: `integer`

#### Responses

##### 204 Device unlinked; the device record remains

<a id="scalar-operation-get-apidevicesdevice-idmashupsoptions"></a>

### Read preset layouts and compatible plugin settings

- **Method:** `GET`
- **Path:** `/api/devices/{device_id}/mashups/options`
- **Operation ID:** `getMashupOptions`
- **Tags:** Mashups

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`device_id` (required)**: `integer`

#### Query parameters

- **`mashup_id`**: `integer`

#### Responses

##### 200 Options for this device

**Content type:** `application/json`

- **`data`**: [MashupOptions](#scalar-schema-mashupoptions)

<a id="scalar-example-83"></a>

**Generated example:**

```json
{
  "data": {
    "layouts": [
      {
        "id": "",
        "name": "",
        "columns": 1,
        "rows": 1,
        "sections": [
          {
            "position": "",
            "column": 1,
            "row": 1,
            "column_span": 1,
            "row_span": 1
          }
        ]
      }
    ],
    "plugins": [
      {
        "id": 1,
        "name": "",
        "plugin_name": "",
        "compatible_layouts": [
          ""
        ]
      }
    ]
  }
}
```

<a id="scalar-operation-get-apiplaylistsitemsitem-idpreview"></a>

### Read the current rendered playlist image

- **Method:** `GET`
- **Path:** `/api/playlists/items/{item_id}/preview`
- **Operation ID:** `getPlaylistItemPreview`
- **Tags:** Playlists

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`item_id` (required)**: `integer`

#### Responses

##### 200 Private image; no storage redirect or device check-in

<a id="scalar-operation-post-apiplugin-settingsplugin-setting-idconnectionsconnection-idattempts"></a>

### Start provider authorization for the exact target

- **Method:** `POST`
- **Path:** `/api/plugin_settings/{plugin_setting_id}/connections/{connection_id}/attempts`
- **Operation ID:** `startPluginSettingsConnection`
- **Tags:** Provider authorization

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`plugin_setting_id` (required)**: `integer`
- **`connection_id` (required)**: `string`

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`action_id` (required)**: `string`
- **`callback_id` (required)**: `string`, possible values: `"companion"`
- **`revision` (required)**: `string`

<a id="scalar-example-84"></a>

**Generated example:**

```json
{
  "action_id": "",
  "revision": "",
  "callback_id": "companion"
}
```

#### Responses

##### 201 Created; launch\_url is returned only on creation

**Content type:** `application/json`

- **`data`**: [PluginConnectionAttempt](#scalar-schema-pluginconnectionattempt)

<a id="scalar-example-85"></a>

**Generated example:**

```json
{
  "data": {
    "id": "",
    "state": "pending",
    "expires_at": "",
    "connection_id": "",
    "launch_url": "",
    "target": {
      "kind": "instance",
      "id": 1
    }
  }
}
```

<a id="scalar-operation-delete-apiplugin-settingsplugin-setting-idconnectionsconnection-id"></a>

### Disconnect only this target provider grant

- **Method:** `DELETE`
- **Path:** `/api/plugin_settings/{plugin_setting_id}/connections/{connection_id}`
- **Operation ID:** `disconnectPluginSettingsConnection`
- **Tags:** Provider authorization

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`plugin_setting_id` (required)**: `integer`
- **`connection_id` (required)**: `string`

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`revision` (required)**: `string`

<a id="scalar-example-86"></a>

**Generated example:**

```json
{
  "revision": ""
}
```

#### Responses

##### 200 Configuration after disconnect

**Content type:** `application/json`

- **`data`**: [PluginConfiguration](#scalar-schema-pluginconfiguration)

<a id="scalar-example-87"></a>

**Generated example:**

```json
{
  "data": {
    "schema_version": 1,
    "revision": "",
    "required_capabilities": [
      ""
    ],
    "sections": [
      {
        "id": "",
        "title": "",
        "fields": [
          {
            "path": "",
            "type": "",
            "label": "",
            "help": "",
            "required": true,
            "read_only": true,
            "nullable": true,
            "constraints": {},
            "choices": [
              {
                "label": "",
                "value": null
              }
            ]
          }
        ]
      }
    ],
    "values": {},
    "secrets": {
      "additionalProperty": {
        "present": true
      }
    },
    "connections": [
      {}
    ],
    "actions": [
      {}
    ],
    "readiness": {
      "ready": true,
      "blocking_paths": [
        ""
      ],
      "reason": null
    },
    "sync": {
      "source_kinds": [
        ""
      ],
      "upload_allowed": true,
      "reason": null,
      "action_id": null
    }
  }
}
```

<a id="scalar-operation-post-apiplugin-installationsplugin-installation-idconnectionsconnection-idattempts"></a>

### Start provider authorization for the exact target

- **Method:** `POST`
- **Path:** `/api/plugin_installations/{plugin_installation_id}/connections/{connection_id}/attempts`
- **Operation ID:** `startPluginInstallationsConnection`
- **Tags:** Provider authorization

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`plugin_installation_id` (required)**: `string`
- **`connection_id` (required)**: `string`

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`action_id` (required)**: `string`
- **`callback_id` (required)**: `string`, possible values: `"companion"`
- **`revision` (required)**: `string`

<a id="scalar-example-88"></a>

**Generated example:**

```json
{
  "action_id": "",
  "revision": "",
  "callback_id": "companion"
}
```

#### Responses

##### 201 Created; launch\_url is returned only on creation

**Content type:** `application/json`

- **`data`**: [PluginConnectionAttempt](#scalar-schema-pluginconnectionattempt)

<a id="scalar-example-89"></a>

**Generated example:**

```json
{
  "data": {
    "id": "",
    "state": "pending",
    "expires_at": "",
    "connection_id": "",
    "launch_url": "",
    "target": {
      "kind": "instance",
      "id": 1
    }
  }
}
```

<a id="scalar-operation-delete-apiplugin-installationsplugin-installation-idconnectionsconnection-id"></a>

### Disconnect only this target provider grant

- **Method:** `DELETE`
- **Path:** `/api/plugin_installations/{plugin_installation_id}/connections/{connection_id}`
- **Operation ID:** `disconnectPluginInstallationsConnection`
- **Tags:** Provider authorization

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`plugin_installation_id` (required)**: `string`
- **`connection_id` (required)**: `string`

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`revision` (required)**: `string`

<a id="scalar-example-90"></a>

**Generated example:**

```json
{
  "revision": ""
}
```

#### Responses

##### 200 Configuration after disconnect

**Content type:** `application/json`

- **`data`**: [PluginConfiguration](#scalar-schema-pluginconfiguration)

<a id="scalar-example-91"></a>

**Generated example:**

```json
{
  "data": {
    "schema_version": 1,
    "revision": "",
    "required_capabilities": [
      ""
    ],
    "sections": [
      {
        "id": "",
        "title": "",
        "fields": [
          {
            "path": "",
            "type": "",
            "label": "",
            "help": "",
            "required": true,
            "read_only": true,
            "nullable": true,
            "constraints": {},
            "choices": [
              {
                "label": "",
                "value": null
              }
            ]
          }
        ]
      }
    ],
    "values": {},
    "secrets": {
      "additionalProperty": {
        "present": true
      }
    },
    "connections": [
      {}
    ],
    "actions": [
      {}
    ],
    "readiness": {
      "ready": true,
      "blocking_paths": [
        ""
      ],
      "reason": null
    },
    "sync": {
      "source_kinds": [
        ""
      ],
      "upload_allowed": true,
      "reason": null,
      "action_id": null
    }
  }
}
```

<a id="scalar-operation-get-apiplugin-connection-attemptsid"></a>

### Read authoritative owned attempt state

- **Method:** `GET`
- **Path:** `/api/plugin_connection_attempts/{id}`
- **Operation ID:** `getConnectionAttempt`
- **Tags:** Provider authorization

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`id` (required)**: `string`, format: `uuid`

#### Responses

##### 200 Attempt state without launch ticket or provider data

**Content type:** `application/json`

- **`data`**: [PluginConnectionAttempt](#scalar-schema-pluginconnectionattempt)

<a id="scalar-example-92"></a>

**Generated example:**

```json
{
  "data": {
    "id": "",
    "state": "pending",
    "expires_at": "",
    "connection_id": "",
    "launch_url": "",
    "target": {
      "kind": "instance",
      "id": 1
    }
  }
}
```

<a id="scalar-operation-delete-apiplugin-connection-attemptsid"></a>

### Cancel an owned pending or exchanging attempt

- **Method:** `DELETE`
- **Path:** `/api/plugin_connection_attempts/{id}`
- **Operation ID:** `cancelConnectionAttempt`
- **Tags:** Provider authorization

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`id` (required)**: `string`, format: `uuid`

#### Responses

##### 200 Authoritative state; an already connected attempt remains connected

**Content type:** `application/json`

- **`data`**: [PluginConnectionAttempt](#scalar-schema-pluginconnectionattempt)

<a id="scalar-example-93"></a>

**Generated example:**

```json
{
  "data": {
    "id": "",
    "state": "pending",
    "expires_at": "",
    "connection_id": "",
    "launch_url": "",
    "target": {
      "kind": "instance",
      "id": 1
    }
  }
}
```

<a id="scalar-operation-post-apiplugin-connection-attemptsidconfirmation"></a>

### Attach the exchanged grant with the confirmation from the provider callback

- **Method:** `POST`
- **Path:** `/api/plugin_connection_attempts/{id}/confirmation`
- **Operation ID:** `confirmConnectionAttempt`
- **Tags:** Provider authorization

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`id` (required)**: `string`, format: `uuid`

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`confirmation` (required)**: `string`

<a id="scalar-example-94"></a>

**Generated example:**

```json
{
  "confirmation": ""
}
```

#### Responses

##### 200 Connected; the callback confirmation works once and only for the user who started the attempt

**Content type:** `application/json`

- **`data`**: [PluginConnectionAttempt](#scalar-schema-pluginconnectionattempt)

<a id="scalar-example-95"></a>

**Generated example:**

```json
{
  "data": {
    "id": "",
    "state": "pending",
    "expires_at": "",
    "connection_id": "",
    "launch_url": "",
    "target": {
      "kind": "instance",
      "id": 1
    }
  }
}
```

<a id="scalar-operation-post-apidevices"></a>

### Claim an unclaimed device

- **Method:** `POST`
- **Path:** `/api/devices`
- **Operation ID:** `claimDevice`
- **Tags:** Devices

Links a device that has not been claimed yet (a new device, or a QR code scan) to this account. Sets up the starter playlist item the first time. friendly\_id is printed on the device screen during setup.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`friendly_id` (required)**: `string`

  The code shown on the device during setup

<a id="scalar-example-96"></a>

**Generated example:**

```json
{
  "friendly_id": ""
}
```

#### Responses

##### 200 Claimed

**Content type:** `application/json`

- **`data`**: [Device](#scalar-schema-device)

<a id="scalar-example-97"></a>

**Generated example:**

```json
{
  "data": {
    "management": {},
    "id": 123,
    "name": "My TRMNL",
    "friendly_id": "ABC-123",
    "mac_address": "••:••:••:••:9A:BC",
    "firmware_version": "1.8.14",
    "firmware_channel": "production",
    "ota_enabled": true,
    "pinned_firmware_version": "1.8.14",
    "battery_voltage": 3.7,
    "rssi": -70,
    "wifi_band": "5",
    "refresh_interval": 900,
    "orientation": 0,
    "sleep_screen_enabled": true,
    "low_battery_notification_enabled": true,
    "auto_advance": true,
    "sleep_mode_enabled": false,
    "sleep_start_time": 1320,
    "sleep_end_time": 480,
    "sleep_until": "2026-10-01T15:00:00.000Z",
    "last_ping_at": "2026-03-31T14:30:00.000Z",
    "hardware_last_ping_at": "2026-03-31T14:30:00.000Z",
    "percent_charged": 85,
    "wifi_strength": 75,
    "mashup_layouts": {
      "1Lx1R": [
        "a",
        "b"
      ],
      "2x2": [
        "a",
        "b",
        "c",
        "d"
      ]
    }
  }
}
```

##### 400, 401, 422, 429

- `400` friendly\_id is missing
- `401` Unauthorized
- `422` A concurrent claim already took the device
- `429` Too many failed claims from this account or address

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

<a id="scalar-operation-get-apidevices"></a>

### List my devices

- **Method:** `GET`
- **Path:** `/api/devices`
- **Operation ID:** `listDevices`
- **Tags:** Devices

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Responses

##### 200 Success

**Content type:** `application/json`

- **`data`**: array of [Device](#scalar-schema-device)

<a id="scalar-example-98"></a>

**Generated example:**

```json
{
  "data": [
    {
      "management": {},
      "id": 123,
      "name": "My TRMNL",
      "friendly_id": "ABC-123",
      "mac_address": "••:••:••:••:9A:BC",
      "firmware_version": "1.8.14",
      "firmware_channel": "production",
      "ota_enabled": true,
      "pinned_firmware_version": "1.8.14",
      "battery_voltage": 3.7,
      "rssi": -70,
      "wifi_band": "5",
      "refresh_interval": 900,
      "orientation": 0,
      "sleep_screen_enabled": true,
      "low_battery_notification_enabled": true,
      "auto_advance": true,
      "sleep_mode_enabled": false,
      "sleep_start_time": 1320,
      "sleep_end_time": 480,
      "sleep_until": "2026-10-01T15:00:00.000Z",
      "last_ping_at": "2026-03-31T14:30:00.000Z",
      "hardware_last_ping_at": "2026-03-31T14:30:00.000Z",
      "percent_charged": 85,
      "wifi_strength": 75,
      "mashup_layouts": {
        "1Lx1R": [
          "a",
          "b"
        ],
        "2x2": [
          "a",
          "b",
          "c",
          "d"
        ]
      }
    }
  ]
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

<a id="scalar-operation-get-apidevicesdevice-idcoverage"></a>

### Times of the week a device's playlist shows nothing scheduled

- **Method:** `GET`
- **Path:** `/api/devices/{device_id}/coverage`
- **Operation ID:** `getDevicePlaylistCoverage`
- **Tags:** Devices

Windows carry minutes since midnight in the account timezone, end inclusive.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`device_id` (required)**: `integer`

  Device id

#### Responses

##### 200 The gaps in the schedule

**Content type:** `application/json`

- **`data`**: `object`
  - **`gaps`**: `array`

    **Items:**
    - **`covered`**: `array of array of integer`

      \[start\_minute, end\_minute] windows the playlist shows
    - **`uncovered`**: `array of array of integer`

      \[start\_minute, end\_minute] windows nothing is scheduled
    - **`week_days`**: `array of integer`

      0 is Sunday through 6 is Saturday
  - **`period_count`**: `integer`

    How many uncovered windows across the week

<a id="scalar-example-99"></a>

**Generated example:**

```json
{
  "data": {
    "gaps": [
      {
        "week_days": [
          1
        ],
        "covered": [
          [
            1
          ]
        ],
        "uncovered": [
          [
            1
          ]
        ]
      }
    ],
    "period_count": 1
  }
}
```

##### 401, 404

- `401` Unauthorized
- `404` Device belongs to another user

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

<a id="scalar-operation-post-apidevicesdevice-idfirmware-update-retries"></a>

### Retry a stalled firmware update

- **Method:** `POST`
- **Path:** `/api/devices/{device_id}/firmware_update_retries`
- **Operation ID:** `retryDeviceFirmwareUpdate`
- **Tags:** Devices

Clears the backoff on a device stuck on an available firmware update, so the next check-in offers it again.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`device_id` (required)**: `integer`

  Device id

#### Responses

##### 200 Backoff cleared

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Device belongs to another user

<a id="scalar-operation-get-apidevicesdevice-idforecast"></a>

### Simulate a device's coming check-ins, deciding each one the way a real check-in would

- **Method:** `GET`
- **Path:** `/api/devices/{device_id}/forecast`
- **Operation ID:** `getDeviceForecast`
- **Tags:** Devices

Simulates upcoming check-ins without writing anything. reason explains a step's wait (e.g. asleep, or an extended refresh rate).

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`device_id` (required)**: `integer`

  Device id

#### Query parameters

- **`hours`**: `integer`

  How far ahead to simulate, 1 to 48. Defaults to 24

#### Responses

##### 200 The simulated steps

**Content type:** `application/json`

- **`data`**: `object`
  - **`steps`**: `array`

    **Items:**
    - **`at`**: `string`, format: `date_time`

      When this simulated check-in happens
    - **`name`**: `string | null`

      The plugin setting or mashup showing at this step
    - **`playlist_item_id`**: `integer | null`
    - **`plugin_setting_id`**: `integer | null`
    - **`reason`**: `string | null`

      Why the wait is what it is, e.g. asleep or an extended refresh rate
    - **`refresh_rate_seconds`**: `integer`

      The item's own wake interval, before extension
    - **`refresh_seconds`**: `integer`

      Seconds until the following step, after any extension
    - **`render_at`**: `string | null`, format: `date_time`

      When the next render is predicted to land, if any
    - **`render_reason`**: `string | null`

      Why no render is predicted, when render\_at is null

<a id="scalar-example-100"></a>

**Generated example:**

```json
{
  "data": {
    "steps": [
      {
        "at": "",
        "playlist_item_id": null,
        "plugin_setting_id": null,
        "name": null,
        "refresh_seconds": 1,
        "refresh_rate_seconds": 1,
        "reason": null,
        "render_at": null,
        "render_reason": null
      }
    ]
  }
}
```

##### 401, 404

- `401` Unauthorized
- `404` Device belongs to another user

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

<a id="scalar-operation-post-apidevicesdevice-ididentification"></a>

### Identify a device

- **Method:** `POST`
- **Path:** `/api/devices/{device_id}/identification`
- **Operation ID:** `identifyDevice`
- **Tags:** Devices

Requests a screen showing the device friendly ID. Rendering runs in the background. The device receives the screen when it next checks in; this does not wake a sleeping device.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`device_id` (required)**: `integer`

  Device id

#### Responses

##### 202 Identification requested

**Content type:** `application/json`

- **`data` (required)**: `object`
  - **`success` (required)**: `boolean`

<a id="scalar-example-101"></a>

**Generated example:**

```json
{
  "data": {
    "success": true
  }
}
```

##### 401, 404, 422

- `401` Unauthorized
- `404` Device not found in your account
- `422` Device could not be updated

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

<a id="scalar-operation-post-apidevicesdevice-idmirror"></a>

### Point a device at another device's shared playlist

- **Method:** `POST`
- **Path:** `/api/devices/{device_id}/mirror`
- **Operation ID:** `mirrorDevice`
- **Tags:** Devices

Mirroring copies the master device's sharable playlist onto this device. The master must have visibility set to sharable.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`device_id` (required)**: `integer`

  Device id

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`master_friendly_id` (required)**: `string`

  friendly\_id of the sharable device to mirror

<a id="scalar-example-102"></a>

**Generated example:**

```json
{
  "master_friendly_id": ""
}
```

#### Responses

##### 200 Mirroring the master

##### 400, 401, 422

- `400` master\_friendly\_id is missing
- `401` Unauthorized
- `422` The friendly ID is not one this device can mirror

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Device belongs to another user

<a id="scalar-operation-delete-apidevicesdevice-idmirror"></a>

### Stop mirroring

- **Method:** `DELETE`
- **Path:** `/api/devices/{device_id}/mirror`
- **Operation ID:** `stopMirroringDevice`
- **Tags:** Devices

Clears the device's mirror. This is a DELETE on the mirror link, not on the device — the device keeps its own playlist history.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`device_id` (required)**: `integer`

  Device id

#### Responses

##### 204 Mirroring removed

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Device belongs to another user

<a id="scalar-operation-post-apidevicesdevice-idmirrorresyncs"></a>

### Resync a mirrored device with its master

- **Method:** `POST`
- **Path:** `/api/devices/{device_id}/mirror/resyncs`
- **Operation ID:** `resyncDeviceMirror`
- **Tags:** Devices

Copies playlist items the master has added since the device last synced. Only playlist items are copied automatically otherwise; use this after adding new content to the master.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`device_id` (required)**: `integer`

  Device id

#### Responses

##### 200 Resynced

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Device belongs to another user

<a id="scalar-operation-post-apidevicesdevice-idplaylist-copies"></a>

### Copy a playlist to another device

- **Method:** `POST`
- **Path:** `/api/devices/{device_id}/playlist_copies`
- **Operation ID:** `copyDevicePlaylist`
- **Tags:** Playlists

Appends items in source order without removing existing target items. Preserves appearance, priority, and schedules. Plugin instances are shared; mashups are copied. Existing setup placeholders for the same plugin are skipped. Both devices must belong to your account. Repeating this request adds the configured items again. Refreshes are queued after copying.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`device_id` (required)**: `integer`

  Source device id

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`target_device_id` (required)**: `integer`

  Destination device id

<a id="scalar-example-103"></a>

**Generated example:**

```json
{
  "target_device_id": 1
}
```

#### Responses

##### 200 Playlist copied and refreshes queued

**Content type:** `application/json`

- **`data` (required)**: `object`
  - **`success` (required)**: `boolean`

<a id="scalar-example-104"></a>

**Generated example:**

```json
{
  "data": {
    "success": true
  }
}
```

##### 400, 401, 404, 422

- `400` Destination device id is missing or is not a scalar
- `401` Unauthorized
- `404` Source or destination device not found in your account
- `422` Source and destination are the same, or an item is invalid

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

<a id="scalar-operation-get-apidevicesdevice-idplaylist-items"></a>

### List the playlist for one device

- **Method:** `GET`
- **Path:** `/api/devices/{device_id}/playlist_items`
- **Operation ID:** `listDevicePlaylist`
- **Tags:** Playlists

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`device_id` (required)**: `integer`

  Device id

#### Responses

##### 200 Reads as empty for a device belonging to someone else

**Content type:** `application/json`

- **`data`**: array of [PlaylistItem](#scalar-schema-playlistitem)

<a id="scalar-example-105"></a>

**Generated example:**

```json
{
  "data": [
    {
      "presentation": {},
      "created_at": "2023-10-01T12:00:00Z",
      "configuration_state": "configured",
      "device_id": 1,
      "id": 1,
      "mashup_id": 1,
      "mirror": true,
      "plugin": {
        "id": 1,
        "name": "Weather",
        "keyname": "weather",
        "description": null,
        "image": null,
        "image_dark": null
      },
      "plugin_id": 1,
      "plugin_setting": {
        "sync": {
          "source_kinds": [
            ""
          ],
          "upload_allowed": true,
          "reason": null,
          "action_id": null
        },
        "id": 1,
        "name": "My Plugin Setting",
        "description": "Upcoming train departures",
        "plugin_id": 1,
        "refresh_interval": 60,
        "health_notification_enabled": false,
        "icon_url": "https://trmnl-public.s3.com/qwertyasdf",
        "icon_content_type": "image/png",
        "read_only?": false,
        "strategy": "webhook"
      },
      "plugin_setting_id": 1,
      "rendered_at": "2023-10-01T12:00:00Z",
      "row_order": 1,
      "updated_at": "2023-10-01T12:00:00Z",
      "visible": true,
      "palette_id": "bw",
      "font_family": "classic",
      "text_scale": "large",
      "theme": "dark"
    }
  ]
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

<a id="scalar-operation-post-apidevicesdevice-idplaylist-items"></a>

### Add a plugin instance to a device playlist

- **Method:** `POST`
- **Path:** `/api/devices/{device_id}/playlist_items`
- **Operation ID:** `addDevicePlaylistItem`
- **Tags:** Playlists

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`device_id` (required)**: `integer`

  Device id

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`plugin_setting_id` (required)**: `integer`

  Plugin setting id

<a id="scalar-example-106"></a>

**Generated example:**

```json
{
  "plugin_setting_id": 1
}
```

#### Responses

##### 200 Added to the playlist

**Content type:** `application/json`

- **`data`**: [PlaylistItem](#scalar-schema-playlistitem)

<a id="scalar-example-107"></a>

**Generated example:**

```json
{
  "data": {
    "presentation": {},
    "created_at": "2023-10-01T12:00:00Z",
    "configuration_state": "configured",
    "device_id": 1,
    "id": 1,
    "mashup_id": 1,
    "mirror": true,
    "plugin": {
      "id": 1,
      "name": "Weather",
      "keyname": "weather",
      "description": null,
      "image": null,
      "image_dark": null
    },
    "plugin_id": 1,
    "plugin_setting": {
      "sync": {
        "source_kinds": [
          ""
        ],
        "upload_allowed": true,
        "reason": null,
        "action_id": null
      },
      "id": 1,
      "name": "My Plugin Setting",
      "description": "Upcoming train departures",
      "plugin_id": 1,
      "refresh_interval": 60,
      "health_notification_enabled": false,
      "icon_url": "https://trmnl-public.s3.com/qwertyasdf",
      "icon_content_type": "image/png",
      "read_only?": false,
      "strategy": "webhook"
    },
    "plugin_setting_id": 1,
    "rendered_at": "2023-10-01T12:00:00Z",
    "row_order": 1,
    "updated_at": "2023-10-01T12:00:00Z",
    "visible": true,
    "palette_id": "bw",
    "font_family": "classic",
    "text_scale": "large",
    "theme": "dark"
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Device belongs to another user

<a id="scalar-operation-put-apidevicesdevice-idplaylist-itemsorder"></a>

### Reorder a device playlist

- **Method:** `PUT`
- **Path:** `/api/devices/{device_id}/playlist_items/order`
- **Operation ID:** `reorderDevicePlaylist`
- **Tags:** Playlists

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`device_id` (required)**: `integer`

  Device id

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`playlist_item_ids` (required)**: `array of integer`

<a id="scalar-example-108"></a>

**Generated example:**

```json
{
  "playlist_item_ids": [
    1
  ]
}
```

#### Responses

##### 200 Reordered

**Content type:** `application/json`

- **`data`**: `object`
  - **`success`**: `boolean`

<a id="scalar-example-109"></a>

**Generated example:**

```json
{
  "data": {
    "success": true
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Device belongs to another user

##### 422 Ids do not name every item on the device

<a id="scalar-operation-get-apidevicesid"></a>

### Get the data of a device

- **Method:** `GET`
- **Path:** `/api/devices/{id}`
- **Operation ID:** `getDevice`
- **Tags:** Devices

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`id` (required)**: `integer`

  Device ID

#### Responses

##### 200 Success

**Content type:** `application/json`

- **`data`**: [Device](#scalar-schema-device)

<a id="scalar-example-110"></a>

**Generated example:**

```json
{
  "data": {
    "management": {},
    "id": 123,
    "name": "My TRMNL",
    "friendly_id": "ABC-123",
    "mac_address": "••:••:••:••:9A:BC",
    "firmware_version": "1.8.14",
    "firmware_channel": "production",
    "ota_enabled": true,
    "pinned_firmware_version": "1.8.14",
    "battery_voltage": 3.7,
    "rssi": -70,
    "wifi_band": "5",
    "refresh_interval": 900,
    "orientation": 0,
    "sleep_screen_enabled": true,
    "low_battery_notification_enabled": true,
    "auto_advance": true,
    "sleep_mode_enabled": false,
    "sleep_start_time": 1320,
    "sleep_end_time": 480,
    "sleep_until": "2026-10-01T15:00:00.000Z",
    "last_ping_at": "2026-03-31T14:30:00.000Z",
    "hardware_last_ping_at": "2026-03-31T14:30:00.000Z",
    "percent_charged": 85,
    "wifi_strength": 75,
    "mashup_layouts": {
      "1Lx1R": [
        "a",
        "b"
      ],
      "2x2": [
        "a",
        "b",
        "c",
        "d"
      ]
    }
  }
}
```

##### 401, 404

- `401` Unauthorized
- `404` Not found

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

<a id="scalar-operation-patch-apidevicesid"></a>

### Update a device

- **Method:** `PATCH`
- **Path:** `/api/devices/{id}`
- **Operation ID:** `updateDevice`
- **Tags:** Devices

The same settings as the device page. Keys the device cannot take (custom dimensions on a fixed-size model, appearance on a native device) are dropped; a native device can only switch between the OG models.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`id` (required)**: `integer`

  Device ID

#### Request body

**Content type:** `application/json`

- **`auto_advance`**: `boolean`

  Beta: may change or go away. false keeps the current screen until a button press; taken only when management.writable\_fields lists it
- **`custom_format`**: `string`

  byod\_custom model only
- **`custom_height`**: `integer`

  byod\_custom model only
- **`custom_width`**: `integer`

  byod\_custom model only
- **`dither_pixel_ratio`**: `number`

  byod\_custom model only
- **`firmware_channel`**: `string`, possible values: `"development", "test", "staging", "production"`
- **`font_family`**: `string`
- **`framework_size`**: `string`, possible values: `"sm", "md", "lg"`

  byod\_custom model only
- **`guest_mode_item_duration`**: `integer | null`
- **`guest_mode_item_id`**: `integer | null`
- **`low_battery_notification_email`**: `string | null`
- **`low_battery_notification_enabled`**: `boolean`
- **`low_battery_screen_disabled`**: `boolean`
- **`maximum_compatibility`**: `boolean`
- **`maximum_image_bytes`**: `integer`

  byod\_custom model only
- **`model_id`**: `integer`

  listModels; a BYOD device can take any model
- **`name`**: `string`
- **`orientation`**: `integer`
- **`ota_enabled`**: `boolean`
- **`palette_id`**: `string`

  listPalettes
- **`percent_charged`**: `number`
- **`playlist_item_ttl`**: `integer | null`, possible values: `24, 48, 72, 96, 120, 144, 168`

  Hours a playlist item can go without a new screen before it is skipped; null never skips
- **`refresh_interval`**: `integer`

  Seconds between check-ins, 300 to 86400
- **`refreshing_screen_disabled`**: `boolean`
- **`rotate`**: `integer`
- **`scale_factor`**: `number`

  BYOD only
- **`sleep_end_time`**: `integer`

  Minutes after midnight, 0 to 1439
- **`sleep_mode_enabled`**: `boolean`
- **`sleep_screen_enabled`**: `boolean`
- **`sleep_start_time`**: `integer`

  Minutes after midnight, 0 to 1439
- **`sleep_until`**: `string | null`

  Sleep once, from the next check-in until this ISO 8601 time (at most a year out); no offset means the account time zone, null cancels. Any later check-in, such as a button press, wakes the device
- **`special_function`**: `string | null`
- **`temperature_profile`**: `string`, possible values: `"default", "a", "b"`
- **`text_scale`**: `string`, possible values: `"small", "regular", "large", "xlarge"`
- **`theme`**: `string`
- **`touchbar_mode`**: `string`, possible values: `"tap", "swipe"`
- **`ui_scale`**: `number`

  BYOD only
- **`visibility`**: `string`, possible values: `"standalone", "sharable"`
- **`waits_for_next_render`**: `boolean`

<a id="scalar-example-111"></a>

**Generated example:**

```json
{
  "name": "Kitchen TRMNL",
  "refresh_interval": 900,
  "orientation": 0,
  "sleep_mode_enabled": true,
  "sleep_screen_enabled": true,
  "sleep_start_time": 1320,
  "sleep_end_time": 480,
  "sleep_until": "2026-10-01T17:00",
  "ota_enabled": true,
  "low_battery_notification_enabled": true,
  "low_battery_notification_email": null,
  "low_battery_screen_disabled": true,
  "percent_charged": 69,
  "visibility": "standalone",
  "model_id": 1,
  "firmware_channel": "development",
  "special_function": null,
  "touchbar_mode": "tap",
  "temperature_profile": "default",
  "waits_for_next_render": true,
  "refreshing_screen_disabled": true,
  "maximum_compatibility": true,
  "guest_mode_item_id": null,
  "guest_mode_item_duration": null,
  "palette_id": "bw",
  "font_family": "classic",
  "text_scale": "small",
  "theme": "",
  "auto_advance": true,
  "playlist_item_ttl": 24,
  "custom_width": 1,
  "custom_height": 1,
  "custom_format": "",
  "framework_size": "sm",
  "maximum_image_bytes": 1,
  "dither_pixel_ratio": 1,
  "scale_factor": 1,
  "ui_scale": 1,
  "rotate": 1
}
```

#### Responses

##### 200 Updated

**Content type:** `application/json`

- **`data`**: [Device](#scalar-schema-device)

<a id="scalar-example-112"></a>

**Generated example:**

```json
{
  "data": {
    "management": {},
    "id": 123,
    "name": "My TRMNL",
    "friendly_id": "ABC-123",
    "mac_address": "••:••:••:••:9A:BC",
    "firmware_version": "1.8.14",
    "firmware_channel": "production",
    "ota_enabled": true,
    "pinned_firmware_version": "1.8.14",
    "battery_voltage": 3.7,
    "rssi": -70,
    "wifi_band": "5",
    "refresh_interval": 900,
    "orientation": 0,
    "sleep_screen_enabled": true,
    "low_battery_notification_enabled": true,
    "auto_advance": true,
    "sleep_mode_enabled": false,
    "sleep_start_time": 1320,
    "sleep_end_time": 480,
    "sleep_until": "2026-10-01T15:00:00.000Z",
    "last_ping_at": "2026-03-31T14:30:00.000Z",
    "hardware_last_ping_at": "2026-03-31T14:30:00.000Z",
    "percent_charged": 85,
    "wifi_strength": 75,
    "mashup_layouts": {
      "1Lx1R": [
        "a",
        "b"
      ],
      "2x2": [
        "a",
        "b",
        "c",
        "d"
      ]
    }
  }
}
```

##### 401, 422, 429

- `401` Unauthorized
- `422` Unprocessable Entity
- `429` An appearance change renders every playlist item again, past the hourly render allowance

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

<a id="scalar-operation-delete-apidevicesdevice-idplaylist"></a>

### Clear a device playlist

- **Method:** `DELETE`
- **Path:** `/api/devices/{device_id}/playlist`
- **Operation ID:** `clearDevicePlaylist`
- **Tags:** Playlists

Removes every item from the device playlist. The plugin settings stay in the account.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`device_id` (required)**: `integer`

  Device id

#### Responses

##### 200 Cleared

**Content type:** `application/json`

- **`data`**: `object`
  - **`success`**: `boolean`

<a id="scalar-example-113"></a>

**Generated example:**

```json
{
  "data": {
    "success": true
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Device belongs to another user

<a id="scalar-operation-get-apidevicesdevice-idlogs"></a>

### Read the logs of a device

- **Method:** `GET`
- **Path:** `/api/devices/{device_id}/logs`
- **Operation ID:** `getDeviceLogs`
- **Tags:** Devices

What the device reported and what rendering for it logged, newest first. before\_ts and before\_event\_id from the last row page further back.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`device_id` (required)**: `integer`

  Device id

#### Query parameters

- **`level`**: `string`

  :
  - `debug`
  - `info`
  - `warn`
  - `error`
- **`source`**: `string`
- **`limit`**: `integer`

  1 to 100, default 20
- **`before_ts`**: `string`
- **`before_event_id`**: `string`

#### Responses

##### 200 Scopes the query to the device within the account

**Content type:** `application/json`

- **`data`**: `object`
  - **`logs`**: `array of object`
  - **`more`**: `boolean`
  - **`oldest_cursor`**: `object | null`
    - **`event_id`**: `string`
    - **`ts`**: `string`

<a id="scalar-example-114"></a>

**Generated example:**

```json
{
  "data": {
    "logs": [
      {}
    ],
    "more": true,
    "oldest_cursor": {
      "ts": "",
      "event_id": ""
    }
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Device belongs to another user

##### 422 A level the logs do not have

<a id="scalar-operation-post-apimarkup"></a>

### Render Liquid template

- **Method:** `POST`
- **Path:** `/api/markup`
- **Operation ID:** `renderMarkup`
- **Tags:** Markup

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Request body

The Liquid markup(s) to render and an optional set of variables to use in the rendering process.

**Required:** `true`

**Content type:** `application/json`

no additional properties

- **`markup` (required)**: `string | (array of string)`
- **`variables` (required)**: `object`

<a id="scalar-example-115"></a>

**Generated example:**

```json
{
  "markup": "Hello, {{ name }}!",
  "variables": {
    "name": "World"
  }
}
```

#### Responses

##### 200 Success

**Content type:** `application/json`

- **`data`**: `string | (array of string)`

<a id="scalar-example-116"></a>

**Generated example:**

```json
{
  "data": ""
}
```

##### 401 No API key or session

##### 403 API key without the read capability

<a id="scalar-operation-post-apidevicesdevice-idmashups"></a>

### Add a mashup to a device playlist

- **Method:** `POST`
- **Path:** `/api/devices/{device_id}/mashups`
- **Operation ID:** `createDeviceMashup`
- **Tags:** Mashups

Splits one screen between plugin settings. getDevice lists the layouts the device can show and the positions each layout has. The 1x1 layout adds a plain playlist item instead of a mashup.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`device_id` (required)**: `integer`

  Device id

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`contents` (required)**: `object`

  Plugin setting id by position; a position left out renders empty

  **Additional properties:**

  `integer`
- **`layout` (required)**: `string`, possible values: `"1x1", "1Tx1B", "1Lx1R", "1Tx2B", "2Tx1B", "1Lx2R", "2Lx1R", "2x2", "3x3"`

  One of the layouts getDevice lists for the device
- **`grid_config`**: `array | null`

  The cells of a 3x3 layout only

  **Items:**
  - **`col`**: `integer`
  - **`cs`**: `integer`
  - **`pos`**: `string`
  - **`row`**: `integer`
  - **`rs`**: `integer`

<a id="scalar-example-117"></a>

**Generated example:**

```json
{
  "layout": "1x1",
  "grid_config": [
    {
      "pos": "",
      "col": 1,
      "row": 1,
      "cs": 1,
      "rs": 1
    }
  ],
  "contents": {
    "additionalProperty": 1
  }
}
```

#### Responses

##### 200 Added to the playlist

**Content type:** `application/json`

- **`data`**: [PlaylistItem](#scalar-schema-playlistitem)

<a id="scalar-example-118"></a>

**Generated example:**

```json
{
  "data": {
    "presentation": {},
    "created_at": "2023-10-01T12:00:00Z",
    "configuration_state": "configured",
    "device_id": 1,
    "id": 1,
    "mashup_id": 1,
    "mirror": true,
    "plugin": {
      "id": 1,
      "name": "Weather",
      "keyname": "weather",
      "description": null,
      "image": null,
      "image_dark": null
    },
    "plugin_id": 1,
    "plugin_setting": {
      "sync": {
        "source_kinds": [
          ""
        ],
        "upload_allowed": true,
        "reason": null,
        "action_id": null
      },
      "id": 1,
      "name": "My Plugin Setting",
      "description": "Upcoming train departures",
      "plugin_id": 1,
      "refresh_interval": 60,
      "health_notification_enabled": false,
      "icon_url": "https://trmnl-public.s3.com/qwertyasdf",
      "icon_content_type": "image/png",
      "read_only?": false,
      "strategy": "webhook"
    },
    "plugin_setting_id": 1,
    "rendered_at": "2023-10-01T12:00:00Z",
    "row_order": 1,
    "updated_at": "2023-10-01T12:00:00Z",
    "visible": true,
    "palette_id": "bw",
    "font_family": "classic",
    "text_scale": "large",
    "theme": "dark"
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Device belongs to another user

##### 422 A layout that does not exist

<a id="scalar-operation-get-apimashupsid"></a>

### Read a mashup and its sections

- **Method:** `GET`
- **Path:** `/api/mashups/{id}`
- **Operation ID:** `getMashup`
- **Tags:** Mashups

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`id` (required)**: `integer`

  Mashup id

#### Responses

##### 200 Success

**Content type:** `application/json`

- **`data`**: [Mashup](#scalar-schema-mashup)

<a id="scalar-example-119"></a>

**Generated example:**

```json
{
  "data": {
    "version": "",
    "id": 1,
    "layout": "1Lx1R",
    "grid_config": [
      {
        "pos": "",
        "col": 1,
        "row": 1,
        "cs": 1,
        "rs": 1
      }
    ],
    "positions": [
      "a",
      "b"
    ],
    "contents": {
      "a": 1,
      "b": 2
    },
    "health_notification_enabled": true,
    "created_at": "2023-10-01T12:00:00Z",
    "updated_at": "2023-10-01T12:00:00Z"
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Mashup belongs to another user

<a id="scalar-operation-patch-apimashupsid"></a>

### Change the sections of a mashup

- **Method:** `PATCH`
- **Path:** `/api/mashups/{id}`
- **Operation ID:** `updateMashup`
- **Tags:** Mashups

Positions left out keep their plugin setting.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`id` (required)**: `integer`

  Mashup id

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`contents`**: `object`

  Plugin setting id by position

  **Additional properties:**

  `integer`
- **`health_notification_enabled`**: `boolean`

<a id="scalar-example-120"></a>

**Generated example:**

```json
{
  "contents": {
    "additionalProperty": 1
  },
  "health_notification_enabled": true
}
```

#### Responses

##### 200 Updated

**Content type:** `application/json`

- **`data`**: [Mashup](#scalar-schema-mashup)

<a id="scalar-example-121"></a>

**Generated example:**

```json
{
  "data": {
    "version": "",
    "id": 1,
    "layout": "1Lx1R",
    "grid_config": [
      {
        "pos": "",
        "col": 1,
        "row": 1,
        "cs": 1,
        "rs": 1
      }
    ],
    "positions": [
      "a",
      "b"
    ],
    "contents": {
      "a": 1,
      "b": 2
    },
    "health_notification_enabled": true,
    "created_at": "2023-10-01T12:00:00Z",
    "updated_at": "2023-10-01T12:00:00Z"
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Mashup belongs to another user

##### 422 A position the layout does not have

<a id="scalar-operation-post-apimashupsidhealth-resets"></a>

### Reset a mashup past its health breaker

- **Method:** `POST`
- **Path:** `/api/mashups/{id}/health_resets`
- **Operation ID:** `resetMashupHealth`
- **Tags:** Mashups

Returns an erroring mashup to healthy and re-schedules its renders, the same as the reset link on the mashup page.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`id` (required)**: `integer`

  Mashup id

#### Responses

##### 200 Healthy again

**Content type:** `application/json`

- **`data`**: `object`
  - **`success`**: `boolean`

<a id="scalar-example-122"></a>

**Generated example:**

```json
{
  "data": {
    "success": true
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Mashup belongs to another user

<a id="scalar-operation-get-apime"></a>

### Get my user data

- **Method:** `GET`
- **Path:** `/api/me`
- **Operation ID:** `getMe`
- **Tags:** Users

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Responses

##### 200 Success

**Content type:** `application/json`

- **`data`**: [User](#scalar-schema-user)

<a id="scalar-example-123"></a>

**Generated example:**

```json
{
  "data": {
    "id": 42,
    "name": "Jim Bob",
    "email": "jimbob@gmail.net",
    "first_name": "Jim",
    "last_name": "Bob",
    "locale": "en",
    "time_zone": "Eastern Time (US & Canada)",
    "time_zone_iana": "America/New_York",
    "utc_offset": -14400,
    "title_bar_enabled": true,
    "account_name": null,
    "low_battery_notification_email": null,
    "api_key": "user_xxxxxx"
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

<a id="scalar-operation-patch-apime"></a>

### Update my profile

- **Method:** `PATCH`
- **Path:** `/api/me`
- **Operation ID:** `updateMe`
- **Tags:** Users

Name, time zone, locale and the account-wide display settings. Email, password and keys stay on the account page.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`account_name`**: `string | null`
- **`first_name`**: `string`
- **`last_name`**: `string`
- **`locale`**: `string`
- **`low_battery_notification_email`**: `string | null`
- **`time_zone`**: `string`

  An IANA name or a Rails zone name
- **`title_bar_enabled`**: `boolean`

  Show the title bar on every screen

<a id="scalar-example-124"></a>

**Generated example:**

```json
{
  "first_name": "",
  "last_name": "",
  "time_zone": "Europe/London",
  "locale": "en",
  "title_bar_enabled": true,
  "low_battery_notification_email": null,
  "account_name": null
}
```

#### Responses

##### 200 Takes a Rails zone name as well

**Content type:** `application/json`

- **`data`**: [User](#scalar-schema-user)

<a id="scalar-example-125"></a>

**Generated example:**

```json
{
  "data": {
    "id": 42,
    "name": "Jim Bob",
    "email": "jimbob@gmail.net",
    "first_name": "Jim",
    "last_name": "Bob",
    "locale": "en",
    "time_zone": "Eastern Time (US & Canada)",
    "time_zone_iana": "America/New_York",
    "utc_offset": -14400,
    "title_bar_enabled": true,
    "account_name": null,
    "low_battery_notification_email": null,
    "api_key": "user_xxxxxx"
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 422 A locale the app does not have

<a id="scalar-operation-get-apimy-plugins"></a>

### List the third-party plugins you are building

- **Method:** `GET`
- **Path:** `/api/my_plugins`
- **Operation ID:** `listMyPlugins`
- **Tags:** My Plugins

Plugins you author against the third-party plugin API, whatever their status: development, in\_review or published. Recipes are plugin settings, not listed here.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Responses

##### 200 Success

**Content type:** `application/json`

- **`data`**: array of [MyPlugin](#scalar-schema-myplugin)

<a id="scalar-example-126"></a>

**Generated example:**

```json
{
  "data": [
    {
      "id": 1,
      "name": "Metro Schedule",
      "keyname": "metro_schedule",
      "description": null,
      "plugin_type": "third_party",
      "status": "development",
      "category": [
        ""
      ],
      "refresh_every": 1,
      "no_screen_padding": true,
      "installation_url": null,
      "plugin_management_url": null,
      "plugin_markup_url": null,
      "installation_success_webhook_url": null,
      "uninstallation_webhook_url": null,
      "knowledge_base_url": null,
      "client_id": "",
      "created_at": "",
      "updated_at": ""
    }
  ]
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

<a id="scalar-operation-post-apimy-plugins"></a>

### Start a third-party plugin

- **Method:** `POST`
- **Path:** `/api/my_plugins`
- **Operation ID:** `createMyPlugin`
- **Tags:** My Plugins

Creates the plugin in development status; it publishes from the dashboard after review, not from here. The icon is an upload the dashboard takes. Needs a developer edition device on the account.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Request body

**Required:** `true`

**Content type:** `application/json`

[MyPluginParams](#scalar-schema-mypluginparams)

<a id="scalar-example-127"></a>

**Generated example:**

```json
{
  "name": "Metro Schedule",
  "description": "Local train and bus times",
  "category": [
    "album"
  ],
  "refresh_every": 1440,
  "no_screen_padding": true,
  "installation_url": "",
  "plugin_management_url": null,
  "plugin_markup_url": "",
  "installation_success_webhook_url": null,
  "uninstallation_webhook_url": null,
  "knowledge_base_url": null
}
```

#### Responses

##### 200 Created

**Content type:** `application/json`

- **`data`**: [MyPlugin](#scalar-schema-myplugin)

<a id="scalar-example-128"></a>

**Generated example:**

```json
{
  "data": {
    "id": 1,
    "name": "Metro Schedule",
    "keyname": "metro_schedule",
    "description": null,
    "plugin_type": "third_party",
    "status": "development",
    "category": [
      ""
    ],
    "refresh_every": 1,
    "no_screen_padding": true,
    "installation_url": null,
    "plugin_management_url": null,
    "plugin_markup_url": null,
    "installation_success_webhook_url": null,
    "uninstallation_webhook_url": null,
    "knowledge_base_url": null,
    "client_id": "",
    "created_at": "",
    "updated_at": ""
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 403 No developer edition device on the account

##### 422 A refresh rate the plugin API does not offer

<a id="scalar-operation-patch-apimy-pluginsid"></a>

### Change a third-party plugin

- **Method:** `PATCH`
- **Path:** `/api/my_plugins/{id}`
- **Operation ID:** `updateMyPlugin`
- **Tags:** My Plugins

Fields left out keep their value. Status cannot change here.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`id` (required)**: `integer`

  Plugin id

#### Request body

**Required:** `true`

**Content type:** `application/json`

[MyPluginParams](#scalar-schema-mypluginparams)

[Generated example](#scalar-example-127)

#### Responses

##### 200 Updated

**Content type:** `application/json`

- **`data`**: [MyPlugin](#scalar-schema-myplugin)

<a id="scalar-example-129"></a>

**Generated example:**

```json
{
  "data": {
    "id": 1,
    "name": "Metro Schedule",
    "keyname": "metro_schedule",
    "description": null,
    "plugin_type": "third_party",
    "status": "development",
    "category": [
      ""
    ],
    "refresh_every": 1,
    "no_screen_padding": true,
    "installation_url": null,
    "plugin_management_url": null,
    "plugin_markup_url": null,
    "installation_success_webhook_url": null,
    "uninstallation_webhook_url": null,
    "knowledge_base_url": null,
    "client_id": "",
    "created_at": "",
    "updated_at": ""
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Plugin belongs to another user

##### 422 A required field is blanked

<a id="scalar-operation-post-apiplugin-settingsplugin-setting-idconfigurationphoto-selection"></a>

### Start Google Photos selection

- **Method:** `POST`
- **Path:** `/api/plugin_settings/{plugin_setting_id}/configuration/photo_selection`
- **Operation ID:** `startPhotoSelection`
- **Tags:** Plugin Configuration

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`plugin_setting_id` (required)**: `integer`

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`revision` (required)**: `string`

<a id="scalar-example-130"></a>

**Generated example:**

```json
{
  "revision": ""
}
```

#### Responses

##### 200 Picker URI and account-bound ticket

**Content type:** `application/json`

- **`data`**: `object`
  - **`picker_uri` (required)**: `string`
  - **`revision` (required)**: `string`
  - **`ticket` (required)**: `string`

<a id="scalar-example-131"></a>

**Generated example:**

```json
{
  "data": {
    "picker_uri": "",
    "ticket": "",
    "revision": ""
  }
}
```

<a id="scalar-operation-patch-apiplugin-settingsplugin-setting-idconfigurationphoto-selection"></a>

### Confirm Google Photos selection

- **Method:** `PATCH`
- **Path:** `/api/plugin_settings/{plugin_setting_id}/configuration/photo_selection`
- **Operation ID:** `completePhotoSelection`
- **Tags:** Plugin Configuration

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`plugin_setting_id` (required)**: `integer`

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`revision` (required)**: `string`
- **`ticket` (required)**: `string`

<a id="scalar-example-132"></a>

**Generated example:**

```json
{
  "revision": "",
  "ticket": ""
}
```

#### Responses

##### 200 Updated configuration

**Content type:** `application/json`

- **`data`**: [PluginConfiguration](#scalar-schema-pluginconfiguration)

<a id="scalar-example-133"></a>

**Generated example:**

```json
{
  "data": {
    "schema_version": 1,
    "revision": "",
    "required_capabilities": [
      ""
    ],
    "sections": [
      {
        "id": "",
        "title": "",
        "fields": [
          {
            "path": "",
            "type": "",
            "label": "",
            "help": "",
            "required": true,
            "read_only": true,
            "nullable": true,
            "constraints": {},
            "choices": [
              {
                "label": "",
                "value": null
              }
            ]
          }
        ]
      }
    ],
    "values": {},
    "secrets": {
      "additionalProperty": {
        "present": true
      }
    },
    "connections": [
      {}
    ],
    "actions": [
      {}
    ],
    "readiness": {
      "ready": true,
      "blocking_paths": [
        ""
      ],
      "reason": null
    },
    "sync": {
      "source_kinds": [
        ""
      ],
      "upload_allowed": true,
      "reason": null,
      "action_id": null
    }
  }
}
```

<a id="scalar-operation-post-apidevicesdevice-idplaylist-itemsbulk"></a>

### Show or hide several playlist items on a device at once

- **Method:** `POST`
- **Path:** `/api/devices/{device_id}/playlist_items/bulk`
- **Operation ID:** `bulkUpdateDevicePlaylist`
- **Tags:** Playlists

action\_type is hide or show. To remove items use deletePlaylistItem instead.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`device_id` (required)**: `integer`

  Device id

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`action_type` (required)**: `string`, possible values: `"hide", "show"`
- **`playlist_item_ids` (required)**: `array of integer`

<a id="scalar-example-134"></a>

**Generated example:**

```json
{
  "action_type": "hide",
  "playlist_item_ids": [
    1
  ]
}
```

#### Responses

##### 200 Applied, answering the items in the submitted order

**Content type:** `application/json`

- **`data`**: array of [PlaylistItem](#scalar-schema-playlistitem)

<a id="scalar-example-135"></a>

**Generated example:**

```json
{
  "data": [
    {
      "presentation": {},
      "created_at": "2023-10-01T12:00:00Z",
      "configuration_state": "configured",
      "device_id": 1,
      "id": 1,
      "mashup_id": 1,
      "mirror": true,
      "plugin": {
        "id": 1,
        "name": "Weather",
        "keyname": "weather",
        "description": null,
        "image": null,
        "image_dark": null
      },
      "plugin_id": 1,
      "plugin_setting": {
        "sync": {
          "source_kinds": [
            ""
          ],
          "upload_allowed": true,
          "reason": null,
          "action_id": null
        },
        "id": 1,
        "name": "My Plugin Setting",
        "description": "Upcoming train departures",
        "plugin_id": 1,
        "refresh_interval": 60,
        "health_notification_enabled": false,
        "icon_url": "https://trmnl-public.s3.com/qwertyasdf",
        "icon_content_type": "image/png",
        "read_only?": false,
        "strategy": "webhook"
      },
      "plugin_setting_id": 1,
      "rendered_at": "2023-10-01T12:00:00Z",
      "row_order": 1,
      "updated_at": "2023-10-01T12:00:00Z",
      "visible": true,
      "palette_id": "bw",
      "font_family": "classic",
      "text_scale": "large",
      "theme": "dark"
    }
  ]
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Device belongs to another user

##### 422 No named id belongs to the device

<a id="scalar-operation-get-apiplaylistsitemsitem-idschedule"></a>

### Read when a playlist item is allowed to display

- **Method:** `GET`
- **Path:** `/api/playlists/items/{item_id}/schedule`
- **Operation ID:** `getPlaylistItemSchedule`
- **Tags:** Playlists

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`item_id` (required)**: `integer`

  Playlist item id

#### Responses

##### 200 Reports an item with no windows as always active

**Content type:** `application/json`

- **`data`**: `object`
  - **`always_active`**: `boolean`

    True when no window and no dates are set, so the item always displays
  - **`date_range`**: `object | null`

    Only display between these dates. Null when no dates are set
    - **`end_on`**: `string | null`, format: `date`

      Last day shown. Null for no end
    - **`repeats_yearly`**: `boolean`

      Ignore the years and repeat every year. Needs both dates, and a start after the end runs across the new year
    - **`start_on`**: `string | null`, format: `date`

      First day shown. Null for no start
  - **`week_schedules`**: `array`

    **Items:**
    - **`end_time`**: `string`
    - **`start_time`**: `string`
    - **`week_days`**: `array of integer`

      0 is Sunday through 6 is Saturday

<a id="scalar-example-136"></a>

**Generated example:**

```json
{
  "data": {
    "week_schedules": [
      {
        "week_days": [
          1
        ],
        "start_time": "09:00",
        "end_time": "17:00"
      }
    ],
    "date_range": {
      "start_on": null,
      "end_on": null,
      "repeats_yearly": true
    },
    "always_active": false
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Playlist item belonging to another user

<a id="scalar-operation-put-apiplaylistsitemsitem-idschedule"></a>

### Replace when a playlist item is allowed to display

- **Method:** `PUT`
- **Path:** `/api/playlists/items/{item_id}/schedule`
- **Operation ID:** `replacePlaylistItemSchedule`
- **Tags:** Playlists

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`item_id` (required)**: `integer`

  Playlist item id

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`week_schedules` (required)**: `array`

  **Items:**
  - **`end_time`**: `string`

    HH:MM, 00:00 to 23:59
  - **`start_time`**: `string`

    HH:MM, 00:00 to 23:59
  - **`week_days`**: `array of integer`

    0 is Sunday through 6 is Saturday
- **`date_range`**: `object | null`

  Only display between these dates. Null when no dates are set
  - **`end_on`**: `string | null`, format: `date`

    Last day shown. Null for no end
  - **`repeats_yearly`**: `boolean`

    Ignore the years and repeat every year. Needs both dates, and a start after the end runs across the new year
  - **`start_on`**: `string | null`, format: `date`

    First day shown. Null for no start

<a id="scalar-example-137"></a>

**Generated example:**

```json
{
  "week_schedules": [
    {
      "week_days": [
        1
      ],
      "start_time": "",
      "end_time": ""
    }
  ],
  "date_range": {
    "start_on": null,
    "end_on": null,
    "repeats_yearly": true
  }
}
```

#### Responses

##### 200 An empty array and no date\_range clear the schedule, so the item always displays

**Content type:** `application/json`

- **`data`**: `object`
  - **`always_active`**: `boolean`

    True when no window and no dates are set, so the item always displays
  - **`date_range`**: `object | null`

    Only display between these dates. Null when no dates are set
    - **`end_on`**: `string | null`, format: `date`

      Last day shown. Null for no end
    - **`repeats_yearly`**: `boolean`

      Ignore the years and repeat every year. Needs both dates, and a start after the end runs across the new year
    - **`start_on`**: `string | null`, format: `date`

      First day shown. Null for no start
  - **`week_schedules`**: `array`

    **Items:**
    - **`end_time`**: `string`
    - **`start_time`**: `string`
    - **`week_days`**: `array of integer`

      0 is Sunday through 6 is Saturday

<a id="scalar-example-138"></a>

**Generated example:**

```json
{
  "data": {
    "week_schedules": [
      {
        "week_days": [
          1
        ],
        "start_time": "09:00",
        "end_time": "17:00"
      }
    ],
    "date_range": {
      "start_on": null,
      "end_on": null,
      "repeats_yearly": true
    },
    "always_active": false
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Playlist item belonging to another user

##### 422 A time that is not HH:MM is refused

<a id="scalar-operation-get-apiplaylistsitems"></a>

### List my playlist items

- **Method:** `GET`
- **Path:** `/api/playlists/items`
- **Operation ID:** `listPlaylistItems`
- **Tags:** Playlists

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Responses

##### 200 Success

**Content type:** `application/json`

- **`data`**: array of [PlaylistItem](#scalar-schema-playlistitem)

<a id="scalar-example-139"></a>

**Generated example:**

```json
{
  "data": [
    {
      "presentation": {},
      "created_at": "2023-10-01T12:00:00Z",
      "configuration_state": "configured",
      "device_id": 1,
      "id": 1,
      "mashup_id": 1,
      "mirror": true,
      "plugin": {
        "id": 1,
        "name": "Weather",
        "keyname": "weather",
        "description": null,
        "image": null,
        "image_dark": null
      },
      "plugin_id": 1,
      "plugin_setting": {
        "sync": {
          "source_kinds": [
            ""
          ],
          "upload_allowed": true,
          "reason": null,
          "action_id": null
        },
        "id": 1,
        "name": "My Plugin Setting",
        "description": "Upcoming train departures",
        "plugin_id": 1,
        "refresh_interval": 60,
        "health_notification_enabled": false,
        "icon_url": "https://trmnl-public.s3.com/qwertyasdf",
        "icon_content_type": "image/png",
        "read_only?": false,
        "strategy": "webhook"
      },
      "plugin_setting_id": 1,
      "rendered_at": "2023-10-01T12:00:00Z",
      "row_order": 1,
      "updated_at": "2023-10-01T12:00:00Z",
      "visible": true,
      "palette_id": "bw",
      "font_family": "classic",
      "text_scale": "large",
      "theme": "dark"
    }
  ]
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

<a id="scalar-operation-patch-apiplaylistsitemsid"></a>

### Update a playlist item

- **Method:** `PATCH`
- **Path:** `/api/playlists/items/{id}`
- **Operation ID:** `updatePlaylistItem`
- **Tags:** Playlists

The appearance fields override the device's own for this item; null returns one to the device default. listPalettes names the palettes a device model has.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`id` (required)**: `integer`

  ID of the playlist item

#### Request body

**Content type:** `application/json`

- **`font_family`**: `string | null`
- **`palette_id`**: `string | null`
- **`text_scale`**: `string | null`, possible values: `"small", "regular", "large", "xlarge", null`
- **`theme`**: `string | null`
- **`visible`**: `boolean`

<a id="scalar-example-140"></a>

**Generated example:**

```json
{
  "visible": true,
  "palette_id": "bw",
  "font_family": "classic",
  "text_scale": "small",
  "theme": "dark"
}
```

#### Responses

##### 200 item that is not an object is read as no change

**Content type:** `application/json`

- **`data`**: [PlaylistItem](#scalar-schema-playlistitem)

<a id="scalar-example-141"></a>

**Generated example:**

```json
{
  "data": {
    "presentation": {},
    "created_at": "2023-10-01T12:00:00Z",
    "configuration_state": "configured",
    "device_id": 1,
    "id": 1,
    "mashup_id": 1,
    "mirror": true,
    "plugin": {
      "id": 1,
      "name": "Weather",
      "keyname": "weather",
      "description": null,
      "image": null,
      "image_dark": null
    },
    "plugin_id": 1,
    "plugin_setting": {
      "sync": {
        "source_kinds": [
          ""
        ],
        "upload_allowed": true,
        "reason": null,
        "action_id": null
      },
      "id": 1,
      "name": "My Plugin Setting",
      "description": "Upcoming train departures",
      "plugin_id": 1,
      "refresh_interval": 60,
      "health_notification_enabled": false,
      "icon_url": "https://trmnl-public.s3.com/qwertyasdf",
      "icon_content_type": "image/png",
      "read_only?": false,
      "strategy": "webhook"
    },
    "plugin_setting_id": 1,
    "rendered_at": "2023-10-01T12:00:00Z",
    "row_order": 1,
    "updated_at": "2023-10-01T12:00:00Z",
    "visible": true,
    "palette_id": "bw",
    "font_family": "classic",
    "text_scale": "large",
    "theme": "dark"
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 422 An appearance value the device cannot show

<a id="scalar-operation-delete-apiplaylistsitemsid"></a>

### Remove a playlist item

- **Method:** `DELETE`
- **Path:** `/api/playlists/items/{id}`
- **Operation ID:** `deletePlaylistItem`
- **Tags:** Playlists

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`id` (required)**: `integer`

  ID of the playlist item

#### Responses

##### 200 Removed

**Content type:** `application/json`

- **`data`**: `object`
  - **`success`**: `boolean`

<a id="scalar-example-142"></a>

**Generated example:**

```json
{
  "data": {
    "success": true
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Playlist item belonging to another user

<a id="scalar-operation-post-apiplaylistsitemsidduplicates"></a>

### Duplicate a playlist item

- **Method:** `POST`
- **Path:** `/api/playlists/items/{id}/duplicates`
- **Operation ID:** `duplicatePlaylistItem`
- **Tags:** Playlists

Copies the plugin setting or mashup and places the copy on the same device.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`id` (required)**: `integer`

  Playlist item id

#### Responses

##### 200 The new playlist item

**Content type:** `application/json`

- **`data`**: [PlaylistItem](#scalar-schema-playlistitem)

<a id="scalar-example-143"></a>

**Generated example:**

```json
{
  "data": {
    "presentation": {},
    "created_at": "2023-10-01T12:00:00Z",
    "configuration_state": "configured",
    "device_id": 1,
    "id": 1,
    "mashup_id": 1,
    "mirror": true,
    "plugin": {
      "id": 1,
      "name": "Weather",
      "keyname": "weather",
      "description": null,
      "image": null,
      "image_dark": null
    },
    "plugin_id": 1,
    "plugin_setting": {
      "sync": {
        "source_kinds": [
          ""
        ],
        "upload_allowed": true,
        "reason": null,
        "action_id": null
      },
      "id": 1,
      "name": "My Plugin Setting",
      "description": "Upcoming train departures",
      "plugin_id": 1,
      "refresh_interval": 60,
      "health_notification_enabled": false,
      "icon_url": "https://trmnl-public.s3.com/qwertyasdf",
      "icon_content_type": "image/png",
      "read_only?": false,
      "strategy": "webhook"
    },
    "plugin_setting_id": 1,
    "rendered_at": "2023-10-01T12:00:00Z",
    "row_order": 1,
    "updated_at": "2023-10-01T12:00:00Z",
    "visible": true,
    "palette_id": "bw",
    "font_family": "classic",
    "text_scale": "large",
    "theme": "dark"
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Playlist item belongs to another user

##### 422 A placeholder has nothing to copy until it is configured

<a id="scalar-operation-get-apiplugin-settingsidarchive"></a>

### Download a plugin setting archive

- **Method:** `GET`
- **Path:** `/api/plugin_settings/{id}/archive`
- **Operation ID:** `downloadPluginSettingArchive`
- **Tags:** Plugin Settings

Answers a zip, not JSON: settings.yml, one .liquid per markup size, and
shared.liquid.

This endpoint is available for unauthenticated requests.

When unauthenticated, any published recipe may be archived.

When authenticated, the requesting user's private plugins are also archivable.

An archive of another user's recipe leaves polling\_headers and polling\_body empty.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`id` (required)**: `integer`

  Plugin setting ID

#### Responses

##### 200 Success

##### 404, 422

- `404` Not Found
- `422` Unprocessable Entity

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

<a id="scalar-operation-post-apiplugin-settingsidarchive"></a>

### Upload a plugin setting archive

- **Method:** `POST`
- **Path:** `/api/plugin_settings/{id}/archive`
- **Operation ID:** `uploadPluginSettingArchive`
- **Tags:** Plugin Settings

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`id` (required)**: `integer`

  Plugin setting ID

#### Request body

Plugin setting archive file

**Required:** `true`

**Content type:** `multipart/form-data`

`null`

<a id="scalar-example-144"></a>

**Generated example:**

```json
null
```

#### Responses

##### 200 Success

**Content type:** `application/json`

- **`data`**: [PluginSettingArchive](#scalar-schema-pluginsettingarchive)

<a id="scalar-example-145"></a>

**Generated example:**

```json
{
  "data": {
    "settings_yaml": ""
  }
}
```

##### 401, 422

- `401` Unauthorized
- `422` Unprocessable Entity

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

<a id="scalar-operation-post-apiplugin-settingsplugin-setting-idconfigurationevaluation"></a>

### Evaluate a typed draft without saving

- **Method:** `POST`
- **Path:** `/api/plugin_settings/{plugin_setting_id}/configuration/evaluation`
- **Operation ID:** `evaluatePluginConfiguration`
- **Tags:** Plugin Configuration

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`plugin_setting_id` (required)**: `integer`

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`draft` (required)**: array of [ConfigurationChange](#scalar-schema-configurationchange), maxItems: `200`
- **`revision` (required)**: `string`

<a id="scalar-example-146"></a>

**Generated example:**

```json
{
  "revision": "",
  "draft": [
    {
      "op": "set",
      "path": "",
      "value": null
    }
  ]
}
```

#### Responses

##### 200 Evaluated configuration and safe field errors

**Content type:** `application/json`

- **`data`**: [PluginConfiguration](#scalar-schema-pluginconfiguration)

<a id="scalar-example-147"></a>

**Generated example:**

```json
{
  "data": {
    "schema_version": 1,
    "revision": "",
    "required_capabilities": [
      ""
    ],
    "sections": [
      {
        "id": "",
        "title": "",
        "fields": [
          {
            "path": "",
            "type": "",
            "label": "",
            "help": "",
            "required": true,
            "read_only": true,
            "nullable": true,
            "constraints": {},
            "choices": [
              {
                "label": "",
                "value": null
              }
            ]
          }
        ]
      }
    ],
    "values": {},
    "secrets": {
      "additionalProperty": {
        "present": true
      }
    },
    "connections": [
      {}
    ],
    "actions": [
      {}
    ],
    "readiness": {
      "ready": true,
      "blocking_paths": [
        ""
      ],
      "reason": null
    },
    "sync": {
      "source_kinds": [
        ""
      ],
      "upload_allowed": true,
      "reason": null,
      "action_id": null
    }
  }
}
```

##### 409 Configuration changed

<a id="scalar-operation-post-apiplugin-settingsplugin-setting-idconfigurationchoicesresolver-id"></a>

### Read owner-bound remote choices

- **Method:** `POST`
- **Path:** `/api/plugin_settings/{plugin_setting_id}/configuration/choices/{resolver_id}`
- **Operation ID:** `getPluginConfigurationChoices`
- **Tags:** Plugin Configuration

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`plugin_setting_id` (required)**: `integer`
- **`resolver_id` (required)**: `string`

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`draft` (required)**: array of [ConfigurationChange](#scalar-schema-configurationchange), maxItems: `200`
- **`revision` (required)**: `string`
- **`cursor`**: `string | null`
- **`query`**: `string | null`, maxLength: `200`

<a id="scalar-example-148"></a>

**Generated example:**

```json
{
  "revision": "",
  "draft": [
    {
      "op": "set",
      "path": "",
      "value": null
    }
  ],
  "query": null,
  "cursor": null
}
```

#### Responses

##### 404 Unknown resolver for owned target

<a id="scalar-operation-get-apiplugin-settingsplugin-setting-idconfiguration"></a>

### Read safe typed configuration

- **Method:** `GET`
- **Path:** `/api/plugin_settings/{plugin_setting_id}/configuration`
- **Operation ID:** `getPluginConfiguration`
- **Tags:** Plugin Configuration

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`plugin_setting_id` (required)**: `integer`

#### Responses

##### 200 Typed configuration with write-only secrets omitted

**Content type:** `application/json`

- **`data` (required)**: [PluginConfiguration](#scalar-schema-pluginconfiguration)

<a id="scalar-example-149"></a>

**Generated example:**

```json
{
  "data": {
    "schema_version": 1,
    "revision": "",
    "required_capabilities": [
      ""
    ],
    "sections": [
      {
        "id": "",
        "title": "",
        "fields": [
          {
            "path": "",
            "type": "",
            "label": "",
            "help": "",
            "required": true,
            "read_only": true,
            "nullable": true,
            "constraints": {},
            "choices": [
              {
                "label": "",
                "value": null
              }
            ]
          }
        ]
      }
    ],
    "values": {},
    "secrets": {
      "additionalProperty": {
        "present": true
      }
    },
    "connections": [
      {}
    ],
    "actions": [
      {}
    ],
    "readiness": {
      "ready": true,
      "blocking_paths": [
        ""
      ],
      "reason": null
    },
    "sync": {
      "source_kinds": [
        ""
      ],
      "upload_allowed": true,
      "reason": null,
      "action_id": null
    }
  }
}
```

##### 401 Account bearer token required

**Content type:** `application/json`

[ConfigurationError](#scalar-schema-configurationerror)

[Generated example](#scalar-example-72)

##### 404 Absent or unowned instance

<a id="scalar-operation-patch-apiplugin-settingsplugin-setting-idconfiguration"></a>

### Apply atomic typed changes

- **Method:** `PATCH`
- **Path:** `/api/plugin_settings/{plugin_setting_id}/configuration`
- **Operation ID:** `updatePluginConfiguration`
- **Tags:** Plugin Configuration

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`plugin_setting_id` (required)**: `integer`

#### Request body

**Required:** `true`

**Content type:** `application/json`

[ConfigurationWrite](#scalar-schema-configurationwrite)

[Generated example](#scalar-example-76)

#### Responses

##### 200 Configuration saved

##### 409 Stale revision

##### 422 Invalid typed change; no fields saved

<a id="scalar-operation-post-apiplugin-settingscustom-fieldsverifications"></a>

### Check the custom fields YAML of a recipe

- **Method:** `POST`
- **Path:** `/api/plugin_settings/custom_fields/verifications`
- **Operation ID:** `verifyCustomFields`
- **Tags:** Recipes

Validates the YAML a recipe author writes under custom\_fields, the form its installers fill in, without saving it. Each entry needs keyname, field\_type and name.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`custom_fields` (required)**: `string`

  The custom fields as a YAML list

<a id="scalar-example-150"></a>

**Generated example:**

```json
{
  "custom_fields": ""
}
```

#### Responses

##### 200 Valid

**Content type:** `application/json`

- **`data`**: `object`
  - **`errors`**: `array of string`
  - **`valid`**: `boolean`

<a id="scalar-example-151"></a>

**Generated example:**

```json
{
  "data": {
    "valid": true,
    "errors": [
      ""
    ]
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 422 custom\_fields that is not a string

<a id="scalar-operation-get-apiplugin-settingsiddata"></a>

### Get the data of a native plugin setting; a private plugin reads through getMergeVariables

- **Method:** `GET`
- **Path:** `/api/plugin_settings/{id}/data`
- **Operation ID:** `getPluginSettingData`
- **Tags:** Plugin Settings

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`id` (required)**: `string`

  Plugin setting ID or UUID

#### Responses

##### 200 Success

**Content type:** `application/json`

- **`data` (required)**: `object`

  The merge variables the instance currently renders from

<a id="scalar-example-152"></a>

**Generated example:**

```json
{
  "data": {}
}
```

##### 401, 404, 422

- `401` Unauthorized
- `404` Not found
- `422` A private or global plugin has no data endpoint

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

<a id="scalar-operation-post-apiplugin-settingsiddata"></a>

### Update data for a webhook plugin setting

- **Method:** `POST`
- **Path:** `/api/plugin_settings/{id}/data`
- **Operation ID:** `updatePluginSettingData`
- **Tags:** Plugin Settings

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`id` (required)**: `string`

  Plugin setting ID or UUID

#### Request body

The value of `merge_variables` must be a JSON object

**Required:** `true`

**Content type:** `application/json`

no additional properties

- **`merge_variables` (required)**: `any`

<a id="scalar-example-153"></a>

**Generated example:**

```json
{
  "merge_variables": null
}
```

#### Responses

##### 200 Success with UUID (no auth required)

**Content type:** `application/json`

- **`error` (required)**: `string | null`

  Null when the write succeeded
- **`merge_variables` (required)**: `object | null`

  The variables now stored
- **`processing`**: `string`

  Present when a transform runs out of band, so the variables are not final

<a id="scalar-example-154"></a>

**Generated example:**

```json
{
  "error": null,
  "merge_variables": null,
  "processing": "serverless"
}
```

##### 401, 404, 422

- `401` Unauthorized
- `404` Not found
- `422` Data cannot be modified

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

<a id="scalar-operation-put-apiplugin-settingsidfeatured-image"></a>

### Generate the marketplace preview image of a plugin setting

- **Method:** `PUT`
- **Path:** `/api/plugin_settings/{id}/featured_image`
- **Operation ID:** `setPluginSettingFeaturedImage`
- **Tags:** Plugin Settings

Queues a fresh render of the instance and attaches it as the image the recipe listing shows. Takes no body. One generation runs at a time per instance; a request while one runs is answered the same way and queues nothing.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`id` (required)**: `string`

  Plugin setting id or uuid

#### Responses

##### 202 Generation queued

**Content type:** `application/json`

- **`data`**: `object`
  - **`status`**: `string`, possible values: `"generating"`

<a id="scalar-example-155"></a>

**Generated example:**

```json
{
  "data": {
    "status": "generating"
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Plugin setting belongs to another user

<a id="scalar-operation-delete-apiplugin-settingsidfeatured-image"></a>

### Remove the marketplace preview image of a plugin setting

- **Method:** `DELETE`
- **Path:** `/api/plugin_settings/{id}/featured_image`
- **Operation ID:** `removePluginSettingFeaturedImage`
- **Tags:** Plugin Settings

Deletes the featured image; the recipe listing then shows a screenshot instead.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`id` (required)**: `string`

  Plugin setting id or uuid

#### Responses

##### 204 Featured image removed

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Plugin setting belongs to another user

##### 422 No featured image to remove

<a id="scalar-operation-get-apiplugin-settingsplugin-setting-idfiles"></a>

### Read the files of a private plugin

- **Method:** `GET`
- **Path:** `/api/plugin_settings/{plugin_setting_id}/files`
- **Operation ID:** `getPluginSettingFiles`
- **Tags:** Plugin Settings

The archive downloadPluginSettingArchive zips, as text by filename: settings.yml and one .liquid per markup size. Any published recipe can be read; another author's recipe comes without its polling headers and body.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`plugin_setting_id` (required)**: `integer`

  Plugin setting id

#### Responses

##### 200 Reads another author's published recipe without its secrets

**Content type:** `application/json`

- **`data`**: `object`
  - **`files`**: `object`

    **Additional properties:**

    `string`

<a id="scalar-example-156"></a>

**Generated example:**

```json
{
  "data": {
    "files": {
      "additionalProperty": ""
    }
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Not the caller's and not a published recipe

##### 422 A plugin type that has no files

<a id="scalar-operation-put-apiplugin-settingsplugin-setting-idfiles"></a>

### Replace the files of a private plugin

- **Method:** `PUT`
- **Path:** `/api/plugin_settings/{plugin_setting_id}/files`
- **Operation ID:** `importPluginSettingFiles`
- **Tags:** Plugin Settings

The same files uploadPluginSettingArchive takes zipped, as text by filename. settings.yml is required; a markup file left out keeps its current content.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`plugin_setting_id` (required)**: `integer`

  Plugin setting id

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`files` (required)**: `object`

  **Additional properties:**

  `string`

<a id="scalar-example-157"></a>

**Generated example:**

```json
{
  "files": {
    "settings.yml": "---\nstrategy: static\nname: Mine\n",
    "full.liquid": "<div>{{ title }}</div>"
  }
}
```

#### Responses

##### 200 Persists a settings-only import, with no markup file to save through

**Content type:** `application/json`

- **`data`**: [PluginSettingArchive](#scalar-schema-pluginsettingarchive)

<a id="scalar-example-158"></a>

**Generated example:**

```json
{
  "data": {
    "settings_yaml": ""
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Plugin setting belongs to another user

##### 422 settings.yml missing or malformed

<a id="scalar-operation-post-apiplugin-settingsidimage"></a>

### Upload an image for a webhook\_image plugin

- **Method:** `POST`
- **Path:** `/api/plugin_settings/{id}/image`
- **Operation ID:** `uploadPluginSettingImage`
- **Tags:** Plugin Settings

Send the image as JSON with the bytes in base64, or as the raw request body with its image Content-Type (not multipart form data). PNG, JPEG and WebP.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Path parameters

- **`id` (required)**: `string`

  Plugin setting id or uuid

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`image_base64` (required)**: `string`

  The image bytes, base64 encoded

<a id="scalar-example-159"></a>

**Generated example:**

```json
{
  "image_base64": ""
}
```

**Content type:** `image/png`

- **`image_base64` (required)**: `string`

  The image bytes, base64 encoded

<a id="scalar-example-160"></a>

**Generated example:**

```json
{
  "image_base64": ""
}
```

**Content type:** `image/jpeg`

- **`image_base64` (required)**: `string`

  The image bytes, base64 encoded

<a id="scalar-example-161"></a>

**Generated example:**

```json
{
  "image_base64": ""
}
```

**Content type:** `image/webp`

- **`image_base64` (required)**: `string`

  The image bytes, base64 encoded

<a id="scalar-example-162"></a>

**Generated example:**

```json
{
  "image_base64": ""
}
```

#### Responses

##### 200 Full-color image on a color device

**Content type:** `application/json`

- **`data`**: `object`
  - **`message`**: `string`

<a id="scalar-example-163"></a>

**Generated example:**

```json
{
  "data": {
    "message": ""
  }
}
```

##### 401 Numeric id without an API key

##### 404 Not found

##### 422 Image too large

##### 429 Rate limited

<a id="scalar-operation-get-apiplugin-settingsplugin-setting-idlogs"></a>

### Read plugin instance health and recent logs

- **Method:** `GET`
- **Path:** `/api/plugin_settings/{plugin_setting_id}/logs`
- **Operation ID:** `getPluginSettingLogs`
- **Tags:** Plugin Settings - Logs

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`plugin_setting_id` (required)**: `string`

  Plugin setting id or uuid

#### Query parameters

- **`level`**: `string`

  Only return logs at this level:
  - `debug`
  - `info`
  - `warn`
  - `error`
- **`limit`**: `integer`

  Maximum logs to return, 1 to 50. Defaults to 20

#### Responses

##### 200 Accepts the integer id the listing returns, as well as the uuid

**Content type:** `application/json`

- **`data`**: `object`
  - **`health`**: `object`
    - **`error_message`**: `string | null`
    - **`error_retry_count`**: `integer | null`
    - **`last_refresh`**: `string | null`, format: `date_time`
    - **`next_refresh`**: `string | null`, format: `date_time`
    - **`state`**: `string`
  - **`logs`**: `array`

    **Items:**
    - **`dump`**: `array | null`

      **Items:**

      One log line, shaped { m: ", , " }
      - **`m`**: `string`
    - **`event_id`**: `string`
    - **`level`**: `string`
    - **`source`**: `string | null`
    - **`ts`**: `string`
  - **`logs_status`**: `string`, possible values: `"available", "unavailable"`

    Unavailable when the log store cannot be read; logs is then empty

<a id="scalar-example-164"></a>

**Generated example:**

```json
{
  "data": {
    "health": {
      "state": "active",
      "error_message": null,
      "error_retry_count": 0,
      "last_refresh": null,
      "next_refresh": null
    },
    "logs_status": "available",
    "logs": [
      {
        "event_id": "11111111-1111-4111-8111-111111111111",
        "ts": "2026-09-05 12:00:00.000",
        "level": "error",
        "source": "private_plugin",
        "dump": [
          {
            "m": ""
          }
        ]
      }
    ]
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 UUID belonging to another user

##### 422 A level the logs do not have

<a id="scalar-operation-get-apiplugin-settingsplugin-setting-idmarkupsize"></a>

### Read markup for a size

- **Method:** `GET`
- **Path:** `/api/plugin_settings/{plugin_setting_id}/markup/{size}`
- **Operation ID:** `readMarkup`
- **Tags:** Plugin Settings - Markup

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`plugin_setting_id` (required)**: `string`

  Plugin setting id or uuid
- **`size` (required)**: `string`, possible values: `"markup_full", "markup_half_horizontal", "markup_half_vertical", "markup_quadrant", "markup_shared", "tidbyt_canvas", "transform_js"`

  Markup size

#### Responses

##### 200 Returns markup content

**Content type:** `application/json`

- **`data`**: `object`
  - **`markup`**: `string | null`

    Null when nothing is attached at this size
  - **`shared_markup`**: `string`

    The markup\_shared partial, returned alongside any other size that has one
  - **`size`**: `string`

<a id="scalar-example-165"></a>

**Generated example:**

```json
{
  "data": {
    "size": "markup_full",
    "markup": null,
    "shared_markup": ""
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 UUID belonging to another user

##### 422 Invalid size

<a id="scalar-operation-put-apiplugin-settingsplugin-setting-idmarkupsize"></a>

### Write markup for a size

- **Method:** `PUT`
- **Path:** `/api/plugin_settings/{plugin_setting_id}/markup/{size}`
- **Operation ID:** `writeMarkup`
- **Tags:** Plugin Settings - Markup

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`plugin_setting_id` (required)**: `string`

  Plugin setting id or uuid
- **`size` (required)**: `string`, possible values: `"markup_full", "markup_half_horizontal", "markup_half_vertical", "markup_quadrant", "markup_shared", "tidbyt_canvas", "transform_js"`

  Markup size

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`content` (required)**: `string`

<a id="scalar-example-166"></a>

**Generated example:**

```json
{
  "content": ""
}
```

#### Responses

##### 200 Markup written successfully

**Content type:** `application/json`

- **`data`**: `object`
  - **`size`**: `string`
  - **`success`**: `boolean`

<a id="scalar-example-167"></a>

**Generated example:**

```json
{
  "data": {
    "success": true,
    "size": "markup_full"
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 422 Invalid size

<a id="scalar-operation-get-apiplugin-settingsplugin-setting-idmerge-variables"></a>

### Read the merge variables a plugin instance renders from

- **Method:** `GET`
- **Path:** `/api/plugin_settings/{plugin_setting_id}/merge_variables`
- **Operation ID:** `getMergeVariables`
- **Tags:** Plugin Settings - Merge Variables

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`plugin_setting_id` (required)**: `string`

  Plugin setting id or uuid

#### Responses

##### 200 Infers the type of each variable held as static data

**Content type:** `application/json`

- **`data`**: `object`
  - **`global_variables`**: `any`

    trmnl.\* variables, with sensitive settings masked
  - **`schema`**: `any`

    Type inferred for each merge variable
  - **`sensor_readings`**: `any`

    Present only when the owner has sensor readings
  - **`strategy`**: `string | null`
  - **`transform_raw_input`**: `any`

    Pre-transform input, present only when a transform is attached
  - **`transform_raw_input_schema`**: `any`

    Type inferred for each pre-transform variable
  - **`variables`**: `any`

    The merge variables the instance renders from

<a id="scalar-example-168"></a>

**Generated example:**

```json
{
  "data": {
    "strategy": "polling",
    "variables": null,
    "schema": null,
    "global_variables": null,
    "transform_raw_input": null,
    "transform_raw_input_schema": null,
    "sensor_readings": null
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 UUID belonging to another user

<a id="scalar-operation-patch-apiplugin-settingsid"></a>

### Rename a plugin setting or change how it refreshes

- **Method:** `PATCH`
- **Path:** `/api/plugin_settings/{id}`
- **Operation ID:** `updatePluginSetting`
- **Tags:** Plugin Settings

Changes the attributes of the instance itself. Its form fields are updatePluginSettingFields, its markup writeMarkup. A refresh\_interval faster than the plan allows is raised to the nearest allowed value.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`id` (required)**: `string`

  Plugin setting id or uuid

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`description`**: `string | null`
- **`health_notification_enabled`**: `boolean`

  Email the owner when the instance stops rendering
- **`name`**: `string`
- **`refresh_interval`**: `integer`, possible values: `1440, 720, 480, 360, 240, 120, 60, 30, 15, 10, 5`

  Minutes between renders

<a id="scalar-example-169"></a>

**Generated example:**

```json
{
  "name": "",
  "description": null,
  "refresh_interval": 1440,
  "health_notification_enabled": true
}
```

#### Responses

##### 200 Updated

**Content type:** `application/json`

- **`data`**: [PluginSetting](#scalar-schema-pluginsetting)

<a id="scalar-example-170"></a>

**Generated example:**

```json
{
  "data": {
    "sync": {
      "source_kinds": [
        ""
      ],
      "upload_allowed": true,
      "reason": null,
      "action_id": null
    },
    "id": 1,
    "name": "My Plugin Setting",
    "description": "Upcoming train departures",
    "plugin_id": 1,
    "refresh_interval": 60,
    "health_notification_enabled": false,
    "icon_url": "https://trmnl-public.s3.com/qwertyasdf",
    "icon_content_type": "image/png",
    "read_only?": false,
    "strategy": "webhook"
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Plugin setting belongs to another user

##### 422 Pushes nothing to GitHub when the save is refused

<a id="scalar-operation-delete-apiplugin-settingsid"></a>

### Delete a plugin setting

- **Method:** `DELETE`
- **Path:** `/api/plugin_settings/{id}`
- **Operation ID:** `deletePluginSetting`
- **Tags:** Plugin Settings

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`id` (required)**: `integer`

  ID of the plugin setting to delete

#### Responses

##### 204 Deleted

##### 401, 404

- `401` Unauthorized
- `404` Not found

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

<a id="scalar-operation-post-apiplugin-settingsidcopies"></a>

### Copy a plugin setting

- **Method:** `POST`
- **Path:** `/api/plugin_settings/{id}/copies`
- **Operation ID:** `copyPluginSetting`
- **Tags:** Plugin Settings

Adds a new instance with the same settings and markup, named "\[Copy] ", and answers it.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`id` (required)**: `string`

  Plugin setting id or uuid

#### Responses

##### 200 The copy

**Content type:** `application/json`

- **`data`**: [PluginSetting](#scalar-schema-pluginsetting)

<a id="scalar-example-171"></a>

**Generated example:**

```json
{
  "data": {
    "sync": {
      "source_kinds": [
        ""
      ],
      "upload_allowed": true,
      "reason": null,
      "action_id": null
    },
    "id": 1,
    "name": "My Plugin Setting",
    "description": "Upcoming train departures",
    "plugin_id": 1,
    "refresh_interval": 60,
    "health_notification_enabled": false,
    "icon_url": "https://trmnl-public.s3.com/qwertyasdf",
    "icon_content_type": "image/png",
    "read_only?": false,
    "strategy": "webhook"
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Plugin setting belongs to another user

##### 422 The copy does not pass validation

<a id="scalar-operation-post-apiplugin-settingsidstate-clears"></a>

### Clear the state a plugin setting has accumulated

- **Method:** `POST`
- **Path:** `/api/plugin_settings/{id}/state_clears`
- **Operation ID:** `clearPluginSettingState`
- **Tags:** Plugin Settings

Forgets the trmnl.state a private plugin carries between renders. Its data and settings stay.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`id` (required)**: `string`

  Plugin setting id or uuid

#### Responses

##### 204 State cleared

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Plugin setting belongs to another user

<a id="scalar-operation-delete-apiplugin-settingsidcredentials"></a>

### Forget the linked account and remove the plugin setting

- **Method:** `DELETE`
- **Path:** `/api/plugin_settings/{id}/credentials`
- **Operation ID:** `resetPluginSettingCredentials`
- **Tags:** Plugin Settings

Removes the OAuth credential the account holds for this plugin AND deletes the plugin setting, so the plugin can be set up again from scratch. Destructive: the setting is gone afterwards.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`id` (required)**: `string`

  Plugin setting id or uuid

#### Responses

##### 204 Credential forgotten and setting removed

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Plugin setting belongs to another user

##### 422 A published recipe cannot be removed

<a id="scalar-operation-post-apiplugin-settingsiddebug-logs"></a>

### Turn on debug logs for a day

- **Method:** `POST`
- **Path:** `/api/plugin_settings/{id}/debug_logs`
- **Operation ID:** `enablePluginSettingDebugLogs`
- **Tags:** Plugin Settings - Logs

A private plugin then records the request and response of each fetch for 24 hours; read them with getPluginSettingLogs.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`id` (required)**: `string`

  Plugin setting id or uuid

#### Responses

##### 200 Debug logs enabled

**Content type:** `application/json`

- **`data`**: `object`
  - **`debug_logs_until`**: `string`, format: `date-time`

<a id="scalar-example-172"></a>

**Generated example:**

```json
{
  "data": {
    "debug_logs_until": ""
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Plugin setting belongs to another user

##### 422 Only a private plugin records debug logs

<a id="scalar-operation-post-apiplugin-settingsidhealth-resets"></a>

### Reset the health of a plugin setting that stopped rendering

- **Method:** `POST`
- **Path:** `/api/plugin_settings/{id}/health_resets`
- **Operation ID:** `resetPluginSettingHealth`
- **Tags:** Plugin Settings

Clears the error count and puts the instance back on its render schedule. include\_forks=true also resets every install of a recipe this setting publishes.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`id` (required)**: `string`

  Plugin setting id or uuid

#### Query parameters

- **`include_forks`**: `boolean`

  true also resets the installs of the recipe this setting publishes

#### Responses

##### 204 Health reset

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Plugin setting belongs to another user

<a id="scalar-operation-delete-apiplugin-settingsidtransform"></a>

### Remove the transform script of a private plugin

- **Method:** `DELETE`
- **Path:** `/api/plugin_settings/{id}/transform`
- **Operation ID:** `removePluginSettingTransform`
- **Tags:** Plugin Settings - Markup

Deletes the serverless transform file and forgets its language; renders then use the fetched data as is.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`id` (required)**: `string`

  Plugin setting id or uuid

#### Responses

##### 204 Transform removed

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Plugin setting belongs to another user

##### 422 The setting refuses to save

<a id="scalar-operation-post-apiplugin-settingsplugin-setting-idrefreshes"></a>

### Refresh a plugin instance

- **Method:** `POST`
- **Path:** `/api/plugin_settings/{plugin_setting_id}/refreshes`
- **Operation ID:** `startRefresh`
- **Tags:** Plugin Settings - Refresh

A polling private plugin refetches its URL and answers a job\_id to poll with getRefresh. Any other instance queues a render of its current data and answers without one.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`plugin_setting_id` (required)**: `string`

  Plugin setting id or uuid

#### Responses

##### 202 Queues a render for an instance that does not poll, such as a native plugin

**Content type:** `application/json`

- **`data`**: `object`
  - **`job_id`**: `string`

    Only for a polling instance
  - **`render_in_progress`**: `boolean`

    Only for a render: another render already holds the instance
  - **`status`**: `string`, possible values: `"pending", "queued"`

<a id="scalar-example-173"></a>

**Generated example:**

```json
{
  "data": {
    "job_id": "3f1e...",
    "status": "pending",
    "render_in_progress": true
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 UUID belonging to another user

##### 422 Instance has no polling URL

##### 429 The account has used its hourly render allowance, shared with the dashboard and MCP

<a id="scalar-operation-get-apiplugin-settingsplugin-setting-idrefreshesid"></a>

### Read the result of a refresh

- **Method:** `GET`
- **Path:** `/api/plugin_settings/{plugin_setting_id}/refreshes/{id}`
- **Operation ID:** `getRefresh`
- **Tags:** Plugin Settings - Refresh

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`plugin_setting_id` (required)**: `string`

  Plugin setting id or uuid
- **`id` (required)**: `string`

  The job\_id returned when the refresh was started

#### Responses

##### 200 Returns the freshly fetched variables

**Content type:** `application/json`

- **`data`**: `object`
  - **`job_id`**: `string`
  - **`logs`**: `array`

    **Items:**

    `any`

    A log entry from the fetch
  - **`polling_url`**: `string | null`
  - **`schema`**: `any`

    Type inferred for each merge variable
  - **`status`**: `string`, possible values: `"complete"`
  - **`success`**: `boolean`
  - **`variables`**: `any`

    The merge variables the fetch returned
  - **`warning`**: `string`

    Present when the data is stale after a transform failure

<a id="scalar-example-174"></a>

**Generated example:**

```json
{
  "data": {
    "job_id": "",
    "status": "complete",
    "success": true,
    "variables": null,
    "schema": null,
    "polling_url": null,
    "warning": "",
    "logs": []
  }
}
```

##### 202 Refresh has not finished yet

**Content type:** `application/json`

- **`data`**: `object`
  - **`job_id`**: `string`
  - **`status`**: `string`, possible values: `"pending"`

<a id="scalar-example-175"></a>

**Generated example:**

```json
{
  "data": {
    "job_id": "3f1e...",
    "status": "pending"
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 UUID belonging to another user

##### 410 Result aged out, or the job id was never issued

<a id="scalar-operation-post-apiplugin-settingsplugin-setting-idscreenshots"></a>

### Start a preview render of a plugin instance

- **Method:** `POST`
- **Path:** `/api/plugin_settings/{plugin_setting_id}/screenshots`
- **Operation ID:** `startPreview`
- **Tags:** Plugin Settings - Preview

Renders the instance so you can check its layout. The image comes back at reduced
resolution and is not sized for a panel; a device is driven through /api/display.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`plugin_setting_id` (required)**: `string`

  Plugin setting id or uuid

#### Request body

**Content type:** `application/json`

- **`device_model`**: `string | null`

  Device model keyname to render on. Omit for the standard preview appearance
- **`size`**: `string`, possible values: `"markup_full", "markup_half_horizontal", "markup_half_vertical", "markup_quadrant"`

<a id="scalar-example-176"></a>

**Generated example:**

```json
{
  "size": "markup_full",
  "device_model": "og"
}
```

#### Responses

##### 202 Renders one reduced image per request, however many the MCP tools batch at once

**Content type:** `application/json`

- **`data`**: `object`
  - **`job_id`**: `string`
  - **`status`**: `string`, possible values: `"pending"`

<a id="scalar-example-177"></a>

**Generated example:**

```json
{
  "data": {
    "job_id": "",
    "status": "pending"
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 UUID belonging to another user

##### 422 Unknown device model

##### 429 The account has used its hourly render allowance, shared with the dashboard and MCP

<a id="scalar-operation-get-apiplugin-settingsplugin-setting-idscreenshotsid"></a>

### Read the result of a preview render

- **Method:** `GET`
- **Path:** `/api/plugin_settings/{plugin_setting_id}/screenshots/{id}`
- **Operation ID:** `getPreview`
- **Tags:** Plugin Settings - Preview

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`plugin_setting_id` (required)**: `string`

  Plugin setting id or uuid
- **`id` (required)**: `string`

  The job\_id returned when the render was started

#### Responses

##### 200 Hands back the image the worker rendered

**Content type:** `application/json`

- **`data`**: `object`
  - **`job_id`**: `string`
  - **`preview_base64`**: `string`

    A reduced-resolution PNG for checking layout, not a device-ready image
  - **`status`**: `string`, possible values: `"complete"`

<a id="scalar-example-178"></a>

**Generated example:**

```json
{
  "data": {
    "job_id": "",
    "status": "complete",
    "preview_base64": ""
  }
}
```

##### 202 Render has not finished yet

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 UUID belonging to another user

##### 410 Result aged out, or the job id was never issued

##### 422 The render failed

<a id="scalar-operation-patch-apiplugin-settingsplugin-setting-idsettings"></a>

### Update plugin settings fields

- **Method:** `PATCH`
- **Path:** `/api/plugin_settings/{plugin_setting_id}/settings`
- **Operation ID:** `updatePluginSettingFields`
- **Tags:** Plugin Settings - Settings

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`plugin_setting_id` (required)**: `string`

  Plugin setting id or uuid

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`fields` (required)**: `object`

<a id="scalar-example-179"></a>

**Generated example:**

```json
{
  "fields": {}
}
```

#### Responses

##### 200 Settings updated successfully

**Content type:** `application/json`

- **`data`**: `object`
  - **`success`**: `boolean`
  - **`warnings`**: `array of string`

    Fields that were written but are hidden under the current settings

<a id="scalar-example-180"></a>

**Generated example:**

```json
{
  "data": {
    "success": true,
    "warnings": [
      ""
    ]
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Unknown UUID

##### 422 Non-string field values

<a id="scalar-operation-get-apiplugin-settings"></a>

### List my plugin settings

- **Method:** `GET`
- **Path:** `/api/plugin_settings`
- **Operation ID:** `listPluginSettings`
- **Tags:** Plugin Settings

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Query parameters

- **`plugin_id`**: `string`

  ID of a plugin or "calendars" to filter calendar plugins

#### Responses

##### 200 Returns all calendar plugin settings except Google Calendar

**Content type:** `application/json`

- **`data`**: array of [PluginSetting](#scalar-schema-pluginsetting)

<a id="scalar-example-181"></a>

**Generated example:**

```json
{
  "data": [
    {
      "sync": {
        "source_kinds": [
          ""
        ],
        "upload_allowed": true,
        "reason": null,
        "action_id": null
      },
      "id": 1,
      "name": "My Plugin Setting",
      "description": "Upcoming train departures",
      "plugin_id": 1,
      "refresh_interval": 60,
      "health_notification_enabled": false,
      "icon_url": "https://trmnl-public.s3.com/qwertyasdf",
      "icon_content_type": "image/png",
      "read_only?": false,
      "strategy": "webhook"
    }
  ]
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

<a id="scalar-operation-post-apiplugin-settings"></a>

### Create a new plugin setting

- **Method:** `POST`
- **Path:** `/api/plugin_settings`
- **Operation ID:** `createPluginSetting`
- **Tags:** Plugin Settings

listPlugins gives the plugin\_id to send here; getPlugin lists the fields to send updatePluginSettingFields next.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Request body

**Required:** `true`

**Content type:** `application/json`

[PluginSettingParams](#scalar-schema-pluginsettingparams)

<a id="scalar-example-182"></a>

**Generated example:**

```json
{
  "name": "My Plugin Setting",
  "plugin_id": 1
}
```

#### Responses

##### 200 Success

**Content type:** `application/json`

- **`data`**: [PluginSetting](#scalar-schema-pluginsetting)

<a id="scalar-example-183"></a>

**Generated example:**

```json
{
  "data": {
    "sync": {
      "source_kinds": [
        ""
      ],
      "upload_allowed": true,
      "reason": null,
      "action_id": null
    },
    "id": 1,
    "name": "My Plugin Setting",
    "description": "Upcoming train departures",
    "plugin_id": 1,
    "refresh_interval": 60,
    "health_notification_enabled": false,
    "icon_url": "https://trmnl-public.s3.com/qwertyasdf",
    "icon_content_type": "image/png",
    "read_only?": false,
    "strategy": "webhook"
  }
}
```

##### 401, 422

- `401` Unauthorized
- `422` Unprocessable Entity

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

<a id="scalar-operation-get-apiplugin-settingsiddetails"></a>

### Get plugin setting details

- **Method:** `GET`
- **Path:** `/api/plugin_settings/{id}/details`
- **Operation ID:** `getPluginSettingDetails`
- **Tags:** Plugin Settings

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`id` (required)**: `string`

  Plugin setting id or uuid

#### Responses

##### 200 Returns plugin details with sizes

**Content type:** `application/json`

- **`data`**: `object`
  - **`available_fields`**: `any`

    Fields writable under the current settings
  - **`custom_fields`**: `array`

    **Items:**

    `any`

    A user-defined form field
  - **`error_message`**: `string`

    Present only while the instance is failing
  - **`form_fields`**: `array`

    **Items:**

    `any`

    A form field definition
  - **`framework`**: `any`

    Design system version and asset URLs the markup renders against
  - **`name`**: `string`
  - **`plugin_name`**: `string`
  - **`serverless_language`**: `string | null`
  - **`settings`**: `any`

    The instance settings, with sensitive values removed
  - **`sizes`**: `object`

    Each markup size, and whether markup is attached at it

    **Additional properties:**

    `boolean`
  - **`state`**: `string`
  - **`strategy`**: `string | null`
  - **`transform_runtime`**: `string`

<a id="scalar-example-184"></a>

**Generated example:**

```json
{
  "data": {
    "name": "My Plugin Setting",
    "plugin_name": "Private Plugin",
    "state": "active",
    "strategy": "polling",
    "transform_runtime": "serverless",
    "serverless_language": "javascript",
    "error_message": "",
    "framework": null,
    "settings": null,
    "form_fields": [],
    "custom_fields": [],
    "available_fields": null,
    "sizes": {
      "additionalProperty": true
    }
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 UUID belonging to another user

<a id="scalar-operation-get-apiplugins"></a>

### List plugins available to install

- **Method:** `GET`
- **Path:** `/api/plugins`
- **Operation ID:** `listPlugins`
- **Tags:** Plugins

The published catalog (native and third-party) plus your own third-party plugins at any status. Send a plugin id to createPluginSetting.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Query parameters

- **`category`**: `string`

  One of Plugin::CATEGORIES
- **`query`**: `string`

  Words to find in the plugin name, description or categories
- **`page_size`**: `integer`, minimum: `1`, maximum: `100`

  Selects a cursor page with items and next\_cursor instead of the default array
- **`cursor`**: `string`

  Opaque cursor bound to this account and filters

#### Responses

##### 200 Success

**Content type:** `application/json`

- **`data`**: array of [Plugin](#scalar-schema-plugin)

<a id="scalar-example-185"></a>

**Generated example:**

```json
{
  "data": [
    {
      "kind": "official",
      "source_revision": "",
      "requirements": [
        ""
      ],
      "compatibility": {},
      "install_choices": [
        {}
      ],
      "categories": [
        ""
      ],
      "id": 1,
      "keyname": "weather",
      "name": "Weather",
      "description": "Current conditions and forecast",
      "category": [
        "news"
      ],
      "plugin_type": "native",
      "form_type": "form_input",
      "oauth": false,
      "image_url": "https://trmnl.com/images/plugins/weather.svg",
      "refresh_every": 30,
      "installable": true,
      "form_fields": [
        {
          "keyname": "username",
          "field_type": "string",
          "name": "User Name",
          "description": null,
          "help_text": null,
          "placeholder": null,
          "optional": null,
          "options": null,
          "default": null
        }
      ]
    }
  ]
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

<a id="scalar-operation-get-apipluginsid"></a>

### Read a plugin and its form fields

- **Method:** `GET`
- **Path:** `/api/plugins/{id}`
- **Operation ID:** `getPlugin`
- **Tags:** Plugins

form\_fields lists what to send updatePluginSettingFields, keyed by the same keyname.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`id` (required)**: `string`

  Plugin id or keyname

#### Responses

##### 200 Success

**Content type:** `application/json`

- **`data`**: [Plugin](#scalar-schema-plugin)

<a id="scalar-example-186"></a>

**Generated example:**

```json
{
  "data": {
    "kind": "official",
    "source_revision": "",
    "requirements": [
      ""
    ],
    "compatibility": {},
    "install_choices": [
      {}
    ],
    "categories": [
      ""
    ],
    "id": 1,
    "keyname": "weather",
    "name": "Weather",
    "description": "Current conditions and forecast",
    "category": [
      "news"
    ],
    "plugin_type": "native",
    "form_type": "form_input",
    "oauth": false,
    "image_url": "https://trmnl.com/images/plugins/weather.svg",
    "refresh_every": 30,
    "installable": true,
    "form_fields": [
      {
        "keyname": "username",
        "field_type": "string",
        "name": "User Name",
        "description": null,
        "help_text": null,
        "placeholder": null,
        "optional": null,
        "options": null,
        "default": null
      }
    ]
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 No plugin matches the id or keyname

<a id="scalar-operation-get-apirecipes"></a>

### Search published recipes

- **Method:** `GET`
- **Path:** `/api/recipes`
- **Operation ID:** `searchRecipes`
- **Tags:** Recipes

Recipes are plugin settings other users published. Use one or two keywords, or #category to filter by category.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Query parameters

- **`query`**: `string`

  Keywords, or #category
- **`sort_by`**: `string`

  :
  - `newest`
  - `oldest`
  - `popularity`
  - `install`
  - `fork`
- **`limit`**: `integer`

  1 to 25, default 10
- **`page_size`**: `integer`, minimum: `1`, maximum: `100`

  Selects a cursor page with items and next\_cursor instead of the default array
- **`cursor`**: `string`

  Opaque cursor bound to this account and filters

#### Responses

##### 200 Lists the newest recipes when no query is given

**Content type:** `application/json`

- **`data`**: array of [Recipe](#scalar-schema-recipe)

<a id="scalar-example-187"></a>

**Generated example:**

```json
{
  "data": [
    {
      "kind": "official",
      "source_revision": "",
      "requirements": [
        ""
      ],
      "compatibility": {},
      "install_choices": [
        {}
      ],
      "id": 1,
      "name": "Train Departures",
      "description": null,
      "author": "Ada",
      "categories": [
        ""
      ],
      "ai_tags": [
        ""
      ],
      "ai_keywords": [
        ""
      ],
      "stats": {
        "installs": 1,
        "forks": 1
      },
      "strategy": "polling",
      "custom_fields": [
        {}
      ],
      "published_at": null,
      "screenshot_url": null,
      "install_method": "simple_install",
      "share_oauth_config": true
    }
  ]
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

<a id="scalar-operation-get-apirecipesid"></a>

### Read a recipe and how it installs

- **Method:** `GET`
- **Path:** `/api/recipes/{id}`
- **Operation ID:** `getRecipe`
- **Tags:** Recipes

install\_method says what installRecipe will do: simple\_install mirrors the author's render; read\_only\_fork copies the recipe so the installer fills its custom fields; oauth\_choice is a fork that can also carry the author's OAuth connection when inherit\_oauth is true.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`id` (required)**: `integer`

  Recipe id

#### Responses

##### 200 A recipe with custom fields must be forked so the installer can fill them

**Content type:** `application/json`

- **`data`**: [Recipe](#scalar-schema-recipe)

<a id="scalar-example-188"></a>

**Generated example:**

```json
{
  "data": {
    "kind": "official",
    "source_revision": "",
    "requirements": [
      ""
    ],
    "compatibility": {},
    "install_choices": [
      {}
    ],
    "id": 1,
    "name": "Train Departures",
    "description": null,
    "author": "Ada",
    "categories": [
      ""
    ],
    "ai_tags": [
      ""
    ],
    "ai_keywords": [
      ""
    ],
    "stats": {
      "installs": 1,
      "forks": 1
    },
    "strategy": "polling",
    "custom_fields": [
      {}
    ],
    "published_at": null,
    "screenshot_url": null,
    "install_method": "simple_install",
    "share_oauth_config": true
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Not a published recipe

<a id="scalar-operation-get-apirecipesidmarkup"></a>

### Read the markup of a recipe

- **Method:** `GET`
- **Path:** `/api/recipes/{id}/markup`
- **Operation ID:** `getRecipeMarkup`
- **Tags:** Recipes

A starting point for a private plugin of your own; writeMarkup takes each size.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`id` (required)**: `integer`

  Recipe id

#### Query parameters

- **`sizes`**: `string`

  Comma-separated markup sizes to read, default all: markup\_full, markup\_half\_horizontal, markup\_half\_vertical, markup\_quadrant, shared\_markup

#### Responses

##### 200 Success

**Content type:** `application/json`

- **`data`**: `object`
  - **`custom_fields`**: `array of object`
  - **`id`**: `integer`
  - **`markups`**: `object`

    Markup by size

    **Additional properties:**

    `string`
  - **`name`**: `string`
  - **`strategy`**: `string | null`

<a id="scalar-example-189"></a>

**Generated example:**

```json
{
  "data": {
    "id": 1,
    "name": "",
    "strategy": null,
    "custom_fields": [
      {}
    ],
    "markups": {
      "additionalProperty": ""
    }
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Not a published recipe

##### 422 A size that does not exist

<a id="scalar-operation-post-apirecipesidinstalls"></a>

### Install a recipe

- **Method:** `POST`
- **Path:** `/api/recipes/{id}/installs`
- **Operation ID:** `installRecipe`
- **Tags:** Recipes

Adds the recipe to the account the way getRecipe's install\_method says, and to the device playlist when a device\_id is given.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`id` (required)**: `integer`

  Recipe id

#### Request body

**Content type:** `application/json`

- **`device_id`**: `integer`

  Device whose playlist the install joins
- **`inherit_oauth`**: `boolean`

  For an oauth\_choice recipe: carry the author's OAuth connection

<a id="scalar-example-190"></a>

**Generated example:**

```json
{
  "device_id": 1,
  "inherit_oauth": true
}
```

#### Responses

##### 200 Installs without a playlist when no device is given

**Content type:** `application/json`

- **`data`**: `object`
  - **`install_method`**: `string`, possible values: `"simple_install", "read_only_fork", "oauth_choice"`
  - **`plugin_setting`**: [PluginSetting](#scalar-schema-pluginsetting)

<a id="scalar-example-191"></a>

**Generated example:**

```json
{
  "data": {
    "plugin_setting": {
      "sync": {
        "source_kinds": [
          ""
        ],
        "upload_allowed": true,
        "reason": null,
        "action_id": null
      },
      "id": 1,
      "name": "My Plugin Setting",
      "description": "Upcoming train departures",
      "plugin_id": 1,
      "refresh_interval": 60,
      "health_notification_enabled": false,
      "icon_url": "https://trmnl-public.s3.com/qwertyasdf",
      "icon_content_type": "image/png",
      "read_only?": false,
      "strategy": "webhook"
    },
    "install_method": "simple_install"
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Not a published recipe

<a id="scalar-operation-get-apidevicesdevice-idtimeline"></a>

### Read a device timeline

- **Method:** `GET`
- **Path:** `/api/devices/{device_id}/timeline`
- **Operation ID:** `getDeviceTimeline`
- **Tags:** Devices

One day of a device: the check-ins it made (recorded, from telemetry) and, on today, the check-ins it will make (expected, simulated from its playlist and settings). source narrows both halves to one plugin setting or mashup.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`device_id` (required)**: `integer`

  Device id

#### Query parameters

- **`date`**: `string`

  ISO 8601 date in the owner’s time zone, default today; an unreadable date reads as today
- **`source`**: `string`

  PluginSetting: or Mashup:, one of the account’s own
- **`hours`**: `integer`

  How far ahead the strip looks, default 24:
  - `3`
  - `12`
  - `24`

#### Responses

##### 200 Success

**Content type:** `application/json`

- **`data`**: [Timeline](#scalar-schema-timeline)

<a id="scalar-example-192"></a>

**Generated example:**

```json
{
  "data": {
    "date": "",
    "today": "",
    "zone": "America/New_York",
    "available": true,
    "query_failed": true,
    "truncated": true,
    "check_in": {
      "state": "never_seen",
      "expected_at": null
    },
    "recorded": [
      {
        "at": "",
        "name": null,
        "outcome": null,
        "leading": {},
        "supporting": [
          {}
        ],
        "trace_entries": [
          {}
        ]
      }
    ],
    "expected": [
      {
        "at": "",
        "device_id": 1,
        "playlist_item_id": null,
        "source_type": "PluginSetting",
        "source_id": null,
        "name": null,
        "reason": null,
        "refresh_seconds": 1,
        "refresh_rate_seconds": 1,
        "render_at": null,
        "render_reason": null,
        "waits_past_refresh_rate": true
      }
    ],
    "strip": [
      {
        "at": "",
        "device_id": 1,
        "playlist_item_id": null,
        "source_type": "PluginSetting",
        "source_id": null,
        "name": null,
        "reason": null,
        "refresh_seconds": 1,
        "refresh_rate_seconds": 1,
        "render_at": null,
        "render_reason": null,
        "waits_past_refresh_rate": true
      }
    ]
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Source belongs to another user

<a id="scalar-operation-get-apiplugin-settingsplugin-setting-idtimeline"></a>

### Read a plugin setting timeline

- **Method:** `GET`
- **Path:** `/api/plugin_settings/{plugin_setting_id}/timeline`
- **Operation ID:** `getPluginSettingTimeline`
- **Tags:** Plugin Settings

One day of a plugin setting across every device that shows it: the check-ins that served it (recorded) and, on today, the ones that will (expected), each naming its device.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`plugin_setting_id` (required)**: `integer`

  Plugin setting id

#### Query parameters

- **`date`**: `string`

  ISO 8601 date in the owner’s time zone, default today

#### Responses

##### 200 Success

**Content type:** `application/json`

- **`data`**: [Timeline](#scalar-schema-timeline)

<a id="scalar-example-193"></a>

**Generated example:**

```json
{
  "data": {
    "date": "",
    "today": "",
    "zone": "America/New_York",
    "available": true,
    "query_failed": true,
    "truncated": true,
    "check_in": {
      "state": "never_seen",
      "expected_at": null
    },
    "recorded": [
      {
        "at": "",
        "name": null,
        "outcome": null,
        "leading": {},
        "supporting": [
          {}
        ],
        "trace_entries": [
          {}
        ]
      }
    ],
    "expected": [
      {
        "at": "",
        "device_id": 1,
        "playlist_item_id": null,
        "source_type": "PluginSetting",
        "source_id": null,
        "name": null,
        "reason": null,
        "refresh_seconds": 1,
        "refresh_rate_seconds": 1,
        "render_at": null,
        "render_reason": null,
        "waits_past_refresh_rate": true
      }
    ],
    "strip": [
      {
        "at": "",
        "device_id": 1,
        "playlist_item_id": null,
        "source_type": "PluginSetting",
        "source_id": null,
        "name": null,
        "reason": null,
        "refresh_seconds": 1,
        "refresh_rate_seconds": 1,
        "render_at": null,
        "render_reason": null,
        "waits_past_refresh_rate": true
      }
    ]
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Plugin setting belongs to another user

<a id="scalar-operation-get-apimashupsmashup-idtimeline"></a>

### Read a mashup timeline

- **Method:** `GET`
- **Path:** `/api/mashups/{mashup_id}/timeline`
- **Operation ID:** `getMashupTimeline`
- **Tags:** Mashups

One day of a mashup across every device that shows it: the check-ins that served it (recorded) and, on today, the ones that will (expected), each naming its device.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`mashup_id` (required)**: `integer`

  Mashup id

#### Query parameters

- **`date`**: `string`

  ISO 8601 date in the owner’s time zone, default today

#### Responses

##### 200 Success

**Content type:** `application/json`

- **`data`**: [Timeline](#scalar-schema-timeline)

<a id="scalar-example-194"></a>

**Generated example:**

```json
{
  "data": {
    "date": "",
    "today": "",
    "zone": "America/New_York",
    "available": true,
    "query_failed": true,
    "truncated": true,
    "check_in": {
      "state": "never_seen",
      "expected_at": null
    },
    "recorded": [
      {
        "at": "",
        "name": null,
        "outcome": null,
        "leading": {},
        "supporting": [
          {}
        ],
        "trace_entries": [
          {}
        ]
      }
    ],
    "expected": [
      {
        "at": "",
        "device_id": 1,
        "playlist_item_id": null,
        "source_type": "PluginSetting",
        "source_id": null,
        "name": null,
        "reason": null,
        "refresh_seconds": 1,
        "refresh_rate_seconds": 1,
        "render_at": null,
        "render_reason": null,
        "waits_past_refresh_rate": true
      }
    ],
    "strip": [
      {
        "at": "",
        "device_id": 1,
        "playlist_item_id": null,
        "source_type": "PluginSetting",
        "source_id": null,
        "name": null,
        "reason": null,
        "refresh_seconds": 1,
        "refresh_rate_seconds": 1,
        "render_at": null,
        "render_reason": null,
        "waits_past_refresh_rate": true
      }
    ]
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Mashup belongs to another user

<a id="scalar-operation-get-apiuser-themes"></a>

### List my themes

- **Method:** `GET`
- **Path:** `/api/user_themes`
- **Operation ID:** `listUserThemes`
- **Tags:** User Themes

Requires the theme builder to be enabled for the account.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Responses

##### 200 Success

**Content type:** `application/json`

- **`data`**: array of [UserTheme](#scalar-schema-usertheme)

<a id="scalar-example-195"></a>

**Generated example:**

```json
{
  "data": [
    {
      "id": 1,
      "name": "Sunny Days",
      "settings": {
        "font": null,
        "chip": "filled",
        "screen_scale": "",
        "surfaces": {
          "additionalProperty": ""
        },
        "remap": {
          "grays": "",
          "colors": {
            "additionalProperty": ""
          }
        },
        "layout": {
          "whitespace": "compact",
          "corners": "sharp",
          "title_bar_height": "slim",
          "progress": "slim"
        },
        "typography": {
          "additionalProperty": ""
        },
        "spacing": {
          "title_bar_padding": "flush",
          "item_padding": "none"
        },
        "title_bar": {
          "additionalProperty": ""
        },
        "item": {
          "fill": "",
          "border": "none",
          "ink": ""
        },
        "advanced": {
          "additionalProperty": "anything"
        }
      },
      "created_at": "2023-10-01T12:00:00Z",
      "updated_at": "2023-10-01T12:00:00Z",
      "scss": ""
    }
  ]
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 The theme builder is not enabled for the account

<a id="scalar-operation-post-apiuser-themes"></a>

### Create a theme

- **Method:** `POST`
- **Path:** `/api/user_themes`
- **Operation ID:** `createUserTheme`
- **Tags:** User Themes

Requires the theme builder to be enabled for the account. settings is validated the same way the theme builder validates it: a value outside the vocabulary (an unrecognized token, surface, hue, or advanced channel) answers 422; a key the recipe does not read is ignored. advanced is either a mapping (channels/slots restated on one token) or the same mapping as YAML text.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`name` (required)**: `string`

  Unique per account
- **`settings`**: [UserThemeSettings](#scalar-schema-userthemesettings)

  Every key is optional; an omitted key keeps the framework default. A value outside the vocabulary (an unrecognized token, surface, hue, or advanced channel) answers 422; a key the recipe does not read is ignored, not rejected.

<a id="scalar-example-196"></a>

**Generated example:**

```json
{
  "name": "",
  "settings": {
    "font": null,
    "chip": "filled",
    "screen_scale": "",
    "surfaces": {
      "additionalProperty": ""
    },
    "remap": {
      "grays": "",
      "colors": {
        "additionalProperty": ""
      }
    },
    "layout": {
      "whitespace": "compact",
      "corners": "sharp",
      "title_bar_height": "slim",
      "progress": "slim"
    },
    "typography": {
      "additionalProperty": ""
    },
    "spacing": {
      "title_bar_padding": "flush",
      "item_padding": "none"
    },
    "title_bar": {
      "additionalProperty": ""
    },
    "item": {
      "fill": "",
      "border": "none",
      "ink": ""
    },
    "advanced": {
      "additionalProperty": "anything"
    }
  }
}
```

#### Responses

##### 200 Created

**Content type:** `application/json`

- **`data`**: [UserTheme](#scalar-schema-usertheme)

<a id="scalar-example-197"></a>

**Generated example:**

```json
{
  "data": {
    "id": 1,
    "name": "Sunny Days",
    "settings": {
      "font": null,
      "chip": "filled",
      "screen_scale": "",
      "surfaces": {
        "additionalProperty": ""
      },
      "remap": {
        "grays": "",
        "colors": {
          "additionalProperty": ""
        }
      },
      "layout": {
        "whitespace": "compact",
        "corners": "sharp",
        "title_bar_height": "slim",
        "progress": "slim"
      },
      "typography": {
        "additionalProperty": ""
      },
      "spacing": {
        "title_bar_padding": "flush",
        "item_padding": "none"
      },
      "title_bar": {
        "additionalProperty": ""
      },
      "item": {
        "fill": "",
        "border": "none",
        "ink": ""
      },
      "advanced": {
        "additionalProperty": "anything"
      }
    },
    "created_at": "2023-10-01T12:00:00Z",
    "updated_at": "2023-10-01T12:00:00Z",
    "scss": ""
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 422 Rejects advanced YAML that does not parse

<a id="scalar-operation-get-apiuser-themesid"></a>

### Read a theme

- **Method:** `GET`
- **Path:** `/api/user_themes/{id}`
- **Operation ID:** `getUserTheme`
- **Tags:** User Themes

scss is the framework stylesheet this theme exports, the same file the builder downloads. Requires the theme builder to be enabled for the account.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`id` (required)**: `integer`

  Theme id

#### Responses

##### 200 Success

**Content type:** `application/json`

- **`data`**: [UserTheme](#scalar-schema-usertheme)

<a id="scalar-example-198"></a>

**Generated example:**

```json
{
  "data": {
    "id": 1,
    "name": "Sunny Days",
    "settings": {
      "font": null,
      "chip": "filled",
      "screen_scale": "",
      "surfaces": {
        "additionalProperty": ""
      },
      "remap": {
        "grays": "",
        "colors": {
          "additionalProperty": ""
        }
      },
      "layout": {
        "whitespace": "compact",
        "corners": "sharp",
        "title_bar_height": "slim",
        "progress": "slim"
      },
      "typography": {
        "additionalProperty": ""
      },
      "spacing": {
        "title_bar_padding": "flush",
        "item_padding": "none"
      },
      "title_bar": {
        "additionalProperty": ""
      },
      "item": {
        "fill": "",
        "border": "none",
        "ink": ""
      },
      "advanced": {
        "additionalProperty": "anything"
      }
    },
    "created_at": "2023-10-01T12:00:00Z",
    "updated_at": "2023-10-01T12:00:00Z",
    "scss": ""
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Theme belongs to another user

<a id="scalar-operation-patch-apiuser-themesid"></a>

### Update a theme

- **Method:** `PATCH`
- **Path:** `/api/user_themes/{id}`
- **Operation ID:** `updateUserTheme`
- **Tags:** User Themes

Requires the theme builder to be enabled for the account.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`id` (required)**: `integer`

  Theme id

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`name`**: `string`
- **`settings`**: [UserThemeSettings](#scalar-schema-userthemesettings)

  Every key is optional; an omitted key keeps the framework default. A value outside the vocabulary (an unrecognized token, surface, hue, or advanced channel) answers 422; a key the recipe does not read is ignored, not rejected.

<a id="scalar-example-199"></a>

**Generated example:**

```json
{
  "name": "",
  "settings": {
    "font": null,
    "chip": "filled",
    "screen_scale": "",
    "surfaces": {
      "additionalProperty": ""
    },
    "remap": {
      "grays": "",
      "colors": {
        "additionalProperty": ""
      }
    },
    "layout": {
      "whitespace": "compact",
      "corners": "sharp",
      "title_bar_height": "slim",
      "progress": "slim"
    },
    "typography": {
      "additionalProperty": ""
    },
    "spacing": {
      "title_bar_padding": "flush",
      "item_padding": "none"
    },
    "title_bar": {
      "additionalProperty": ""
    },
    "item": {
      "fill": "",
      "border": "none",
      "ink": ""
    },
    "advanced": {
      "additionalProperty": "anything"
    }
  }
}
```

#### Responses

##### 200 Updated

**Content type:** `application/json`

- **`data`**: [UserTheme](#scalar-schema-usertheme)

<a id="scalar-example-200"></a>

**Generated example:**

```json
{
  "data": {
    "id": 1,
    "name": "Sunny Days",
    "settings": {
      "font": null,
      "chip": "filled",
      "screen_scale": "",
      "surfaces": {
        "additionalProperty": ""
      },
      "remap": {
        "grays": "",
        "colors": {
          "additionalProperty": ""
        }
      },
      "layout": {
        "whitespace": "compact",
        "corners": "sharp",
        "title_bar_height": "slim",
        "progress": "slim"
      },
      "typography": {
        "additionalProperty": ""
      },
      "spacing": {
        "title_bar_padding": "flush",
        "item_padding": "none"
      },
      "title_bar": {
        "additionalProperty": ""
      },
      "item": {
        "fill": "",
        "border": "none",
        "ink": ""
      },
      "advanced": {
        "additionalProperty": "anything"
      }
    },
    "created_at": "2023-10-01T12:00:00Z",
    "updated_at": "2023-10-01T12:00:00Z",
    "scss": ""
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Theme belongs to another user

##### 422 Rejects settings outside the recipe vocabulary

<a id="scalar-operation-delete-apiuser-themesid"></a>

### Delete a theme

- **Method:** `DELETE`
- **Path:** `/api/user_themes/{id}`
- **Operation ID:** `deleteUserTheme`
- **Tags:** User Themes

Requires the theme builder to be enabled for the account.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Path parameters

- **`id` (required)**: `integer`

  Theme id

#### Responses

##### 204 Deleted

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 Theme belongs to another user

<a id="scalar-operation-post-apiuser-themesimports"></a>

### Create a theme from a shared theme snippet

- **Method:** `POST`
- **Path:** `/api/user_themes/imports`
- **Operation ID:** `importUserTheme`
- **Tags:** User Themes

Decodes a /t/:token share link (the "Copy share link" button on a theme) through the same settings parse createUserTheme uses, and saves it under name. Requires the theme builder to be enabled for the account.

**Servers:** [Inherited servers](#scalar-context-global-servers)

#### Authentication

- **bearer\_auth**: HTTP bearer (`API key or OAuth access token`)

#### Request body

**Required:** `true`

**Content type:** `application/json`

- **`name` (required)**: `string`

  Unique per account
- **`token` (required)**: `string`

  The token from a /t/:token share link, with or without the URL around it

<a id="scalar-example-201"></a>

**Generated example:**

```json
{
  "token": "",
  "name": ""
}
```

#### Responses

##### 200 Created

**Content type:** `application/json`

- **`data`**: [UserTheme](#scalar-schema-usertheme)

<a id="scalar-example-202"></a>

**Generated example:**

```json
{
  "data": {
    "id": 1,
    "name": "Sunny Days",
    "settings": {
      "font": null,
      "chip": "filled",
      "screen_scale": "",
      "surfaces": {
        "additionalProperty": ""
      },
      "remap": {
        "grays": "",
        "colors": {
          "additionalProperty": ""
        }
      },
      "layout": {
        "whitespace": "compact",
        "corners": "sharp",
        "title_bar_height": "slim",
        "progress": "slim"
      },
      "typography": {
        "additionalProperty": ""
      },
      "spacing": {
        "title_bar_padding": "flush",
        "item_padding": "none"
      },
      "title_bar": {
        "additionalProperty": ""
      },
      "item": {
        "fill": "",
        "border": "none",
        "ink": ""
      },
      "advanced": {
        "additionalProperty": "anything"
      }
    },
    "created_at": "2023-10-01T12:00:00Z",
    "updated_at": "2023-10-01T12:00:00Z",
    "scss": ""
  }
}
```

##### 401 Unauthorized

**Content type:** `application/json`

[Error](#scalar-schema-error)

[Generated example](#scalar-example-15)

##### 404 The theme builder is not enabled for the account

##### 422 name is missing

## Schemas

<a id="scalar-schema-pluginconnectionattempt"></a>

### PluginConnectionAttempt

**Type:** `object`, no additional properties

- **`connection_id` (required)**: `string`
- **`expires_at` (required)**: `string`, format: `date-time`
- **`id` (required)**: `string`, format: `uuid`
- **`state` (required)**: `string`, possible values: `"pending", "exchanging", "exchanged", "connected", "denied", "failed", "expired", "cancelled"`
- **`target` (required)**: `object`
  - **`id` (required)**

    **One of:**
    - `integer`
    - `string`, format: `uuid`
  - **`kind` (required)**: `string`, possible values: `"instance", "installation"`
- **`launch_url`**: `string`, format: `uri`

<a id="scalar-example-203"></a>

**Generated example:**

```json
{
  "id": "",
  "state": "pending",
  "expires_at": "",
  "connection_id": "",
  "launch_url": "",
  "target": {
    "kind": "instance",
    "id": 1
  }
}
```

<a id="scalar-schema-catalogentry"></a>

### CatalogEntry

**Type:** `object`

- **`categories` (required)**: `array of string`
- **`description` (required)**: `string | null`
- **`id` (required)**: `integer`
- **`kind` (required)**: `string`, possible values: `"official", "recipe"`
- **`name` (required)**: `string`
- **`compatibility`**: `object`
- **`install_choices`**: `array`

  **Items:**
  - **`id` (required)**: `string`
  - **`label` (required)**: `string`
  - **`requirements` (required)**: `array of string`
  - **`help`**: `string`
- **`requirements`**: `array of string`
- **`source_revision`**: `string`

<a id="scalar-example-204"></a>

**Generated example:**

```json
{
  "kind": "official",
  "id": 1,
  "name": "",
  "description": null,
  "categories": [
    ""
  ],
  "source_revision": "",
  "requirements": [
    ""
  ],
  "compatibility": {},
  "install_choices": [
    {
      "id": "",
      "label": "",
      "help": "",
      "requirements": [
        ""
      ]
    }
  ]
}
```

<a id="scalar-schema-plugininstallation"></a>

### PluginInstallation

**Type:** `object`

- **`expires_at` (required)**: `string`, format: `date_time`
- **`id` (required)**: `string`, format: `uuid`
- **`plugin_setting_id` (required)**: `integer | null`
- **`source` (required)**: `object`
  - **`choice_id` (required)**: `string`
  - **`id` (required)**: `integer`
  - **`kind` (required)**: `string`
  - **`revision` (required)**: `string`
  - **`device_id`**: `integer`
- **`state` (required)**: `string`, possible values: `"setup_required", "ready", "completed", "cancelled", "expired", "removed"`

<a id="scalar-example-205"></a>

**Generated example:**

```json
{
  "id": "",
  "state": "setup_required",
  "source": {
    "kind": "",
    "id": 1,
    "revision": "",
    "choice_id": "",
    "device_id": 1
  },
  "plugin_setting_id": null,
  "expires_at": ""
}
```

<a id="scalar-schema-configurationfield"></a>

### ConfigurationField

**Type:** `object`

- **`constraints` (required)**: `object`
- **`label` (required)**: `string`
- **`nullable` (required)**: `boolean`
- **`path` (required)**: `string`, pattern: `^/values/`
- **`read_only` (required)**: `boolean`
- **`required` (required)**: `boolean`
- **`type` (required)**: `string`
- **`choices`**

  **One of:**
  - `array`

    **Array of:**
    - **`label` (required)**: `string`
    - **`value` (required)**: `any`
  - `object`
    - **`depends_on` (required)**: `array of string`
    - **`paginated` (required)**: `boolean`
    - **`resolver_id` (required)**: `string`
    - **`searchable` (required)**: `boolean`
- **`help`**: `string`

<a id="scalar-example-206"></a>

**Generated example:**

```json
{
  "path": "",
  "type": "",
  "label": "",
  "help": "",
  "required": true,
  "read_only": true,
  "nullable": true,
  "constraints": {},
  "choices": [
    {
      "label": "",
      "value": null
    }
  ]
}
```

<a id="scalar-schema-pluginsync"></a>

### PluginSync

**Type:** `object`

- **`source_kinds` (required)**: `array of string`
- **`upload_allowed` (required)**: `boolean`
- **`action_id`**: `string | null`
- **`reason`**: `string | null`

<a id="scalar-example-207"></a>

**Generated example:**

```json
{
  "source_kinds": [
    ""
  ],
  "upload_allowed": true,
  "reason": null,
  "action_id": null
}
```

<a id="scalar-schema-pluginconfiguration"></a>

### PluginConfiguration

**Type:** `object`

- **`actions` (required)**: `array of object`
- **`connections` (required)**: `array of object`
- **`readiness` (required)**: `object`
  - **`blocking_paths` (required)**: `array of string`
  - **`ready` (required)**: `boolean`
  - **`reason`**: `string | null`
- **`required_capabilities` (required)**: `array of string`
- **`revision` (required)**: `string`
- **`schema_version` (required)**: `integer`, possible values: `1`
- **`secrets` (required)**: `object`

  **Additional properties:**
  - **`present` (required)**: `boolean`
- **`sections` (required)**: `array`

  **Items:**
  - **`fields` (required)**: array of [ConfigurationField](#scalar-schema-configurationfield)
  - **`id` (required)**: `string`
  - **`title` (required)**: `string`
- **`sync` (required)**: [PluginSync](#scalar-schema-pluginsync)
- **`values` (required)**: `object`

  Only declared nonsecret values. Secret values are never returned.

<a id="scalar-example-208"></a>

**Generated example:**

```json
{
  "schema_version": 1,
  "revision": "",
  "required_capabilities": [
    ""
  ],
  "sections": [
    {
      "id": "",
      "title": "",
      "fields": [
        {
          "path": "",
          "type": "",
          "label": "",
          "help": "",
          "required": true,
          "read_only": true,
          "nullable": true,
          "constraints": {},
          "choices": [
            {
              "label": "",
              "value": null
            }
          ]
        }
      ]
    }
  ],
  "values": {},
  "secrets": {
    "additionalProperty": {
      "present": true
    }
  },
  "connections": [
    {}
  ],
  "actions": [
    {}
  ],
  "readiness": {
    "ready": true,
    "blocking_paths": [
      ""
    ],
    "reason": null
  },
  "sync": {
    "source_kinds": [
      ""
    ],
    "upload_allowed": true,
    "reason": null,
    "action_id": null
  }
}
```

<a id="scalar-schema-configurationchange"></a>

### ConfigurationChange

**Type:** `object`, no additional properties

- **`op` (required)**: `string`, possible values: `"set", "unset", "clear_secret"`
- **`path` (required)**: `string`, pattern: `^/values/`
- **`value`**: `any`

<a id="scalar-example-209"></a>

**Generated example:**

```json
{
  "op": "set",
  "path": "",
  "value": null
}
```

<a id="scalar-schema-configurationwrite"></a>

### ConfigurationWrite

**Type:** `object`

- **`changes` (required)**: array of [ConfigurationChange](#scalar-schema-configurationchange), maxItems: `200`
- **`revision` (required)**: `string`

<a id="scalar-example-210"></a>

**Generated example:**

```json
{
  "revision": "",
  "changes": [
    {
      "op": "set",
      "path": "",
      "value": null
    }
  ]
}
```

<a id="scalar-schema-configurationerror"></a>

### ConfigurationError

**Type:** `object`

- **`error` (required)**: `object`
  - **`code` (required)**: `string`
  - **`field_errors` (required)**: `array`

    **Items:**
    - **`code` (required)**: `string`
    - **`message` (required)**: `string`
    - **`path` (required)**: `string`
  - **`message` (required)**: `string`

<a id="scalar-example-211"></a>

**Generated example:**

```json
{
  "error": {
    "code": "",
    "message": "",
    "field_errors": [
      {
        "path": "",
        "code": "",
        "message": ""
      }
    ]
  }
}
```

<a id="scalar-schema-mashupoptions"></a>

### MashupOptions

**Type:** `object`

- **`layouts` (required)**: `array`

  **Items:**
  - **`columns` (required)**: `integer`
  - **`id` (required)**: `string`
  - **`name` (required)**: `string`
  - **`rows` (required)**: `integer`
  - **`sections` (required)**: `array`

    **Items:**
    - **`column` (required)**: `integer`
    - **`column_span` (required)**: `integer`
    - **`position` (required)**: `string`
    - **`row` (required)**: `integer`
    - **`row_span` (required)**: `integer`
- **`plugins` (required)**: `array`

  **Items:**
  - **`compatible_layouts` (required)**: `array of string`
  - **`id` (required)**: `integer`
  - **`name` (required)**: `string`
  - **`plugin_name` (required)**: `string`

<a id="scalar-example-212"></a>

**Generated example:**

```json
{
  "layouts": [
    {
      "id": "",
      "name": "",
      "columns": 1,
      "rows": 1,
      "sections": [
        {
          "position": "",
          "column": 1,
          "row": 1,
          "column_span": 1,
          "row_span": 1
        }
      ]
    }
  ],
  "plugins": [
    {
      "id": 1,
      "name": "",
      "plugin_name": "",
      "compatible_layouts": [
        ""
      ]
    }
  ]
}
```

<a id="scalar-schema-error"></a>

### Error

**Type:** `object`, no additional properties

- **`error`**: `string`

<a id="scalar-example-213"></a>

**Generated example:**

```json
{
  "error": "An error occurred"
}
```

<a id="scalar-schema-device"></a>

### Device

**Type:** `object`, no additional properties

- **`auto_advance`**: `boolean`

  Beta: may change or go away. Present only when management.writable\_fields lists it
- **`battery_voltage`**: `number | null`
- **`firmware_channel`**: `string`

  Release channel the device follows
- **`firmware_version`**: `string | null`

  Version the device reported at its last check-in
- **`friendly_id`**: `string`
- **`hardware_last_ping_at`**: `string | null`, format: `date_time`
- **`id`**: `integer`
- **`last_ping_at`**: `string | null`, format: `date_time`
- **`low_battery_notification_enabled`**: `boolean`
- **`mac_address`**: `string`

  All but the last two bytes are masked for an API key or an agent; the account API key reads it whole.
- **`management`**: `object`
- **`mashup_layouts`**: `object`

  The mashup layouts the device can show and the positions to fill in each; 3x3 lists none because its grid\_config names them

  **Additional properties:**

  `array of string`
- **`name`**: `string`
- **`orientation`**: `integer`
- **`ota_enabled`**: `boolean`

  Whether the device accepts over-the-air updates
- **`percent_charged`**: `number`, minimum: `0`, maximum: `100`
- **`pinned_firmware_version`**: `string | null`

  Version the device is pinned to, if any
- **`refresh_interval`**: `integer`

  Seconds between check-ins, 300 to 86400
- **`rssi`**: `integer | null`
- **`sleep_end_time`**: `integer`
- **`sleep_mode_enabled`**: `boolean`
- **`sleep_screen_enabled`**: `boolean`
- **`sleep_start_time`**: `integer`
- **`sleep_until`**: `string | null`, format: `date_time`

  A one-time sleep the device takes at its next check-in, then clears
- **`wifi_band`**: `string | null`

  WiFi band the device is connected on ("2.4" or "5")
- **`wifi_strength`**: `number`, minimum: `0`, maximum: `100`

<a id="scalar-example-214"></a>

**Generated example:**

```json
{
  "management": {},
  "id": 123,
  "name": "My TRMNL",
  "friendly_id": "ABC-123",
  "mac_address": "••:••:••:••:9A:BC",
  "firmware_version": "1.8.14",
  "firmware_channel": "production",
  "ota_enabled": true,
  "pinned_firmware_version": "1.8.14",
  "battery_voltage": 3.7,
  "rssi": -70,
  "wifi_band": "5",
  "refresh_interval": 900,
  "orientation": 0,
  "sleep_screen_enabled": true,
  "low_battery_notification_enabled": true,
  "auto_advance": true,
  "sleep_mode_enabled": false,
  "sleep_start_time": 1320,
  "sleep_end_time": 480,
  "sleep_until": "2026-10-01T15:00:00.000Z",
  "last_ping_at": "2026-03-31T14:30:00.000Z",
  "hardware_last_ping_at": "2026-03-31T14:30:00.000Z",
  "percent_charged": 85,
  "wifi_strength": 75,
  "mashup_layouts": {
    "1Lx1R": [
      "a",
      "b"
    ],
    "2x2": [
      "a",
      "b",
      "c",
      "d"
    ]
  }
}
```

<a id="scalar-schema-model"></a>

### Model

**Type:** `object`, no additional properties

- **`bit_depth`**: `integer`

  Color bit depth
- **`colors`**: `integer`

  Number of colors supported
- **`css`**: `object | null`

  CSS classes and variables for web rendering
  - **`classes`**: `object`
    - **`density`**: `string`
    - **`device`**: `string`
    - **`size`**: `string`
  - **`variables`**: `array of array of string`
- **`description`**: `string`

  Description
- **`height`**: `integer`

  Screen height in pixels
- **`image_size_limit`**: `integer | null`

  Maximum image file size in bytes for webhook uploads; null means the model declares no limit
- **`image_upload_supported`**: `boolean`

  Whether webhook image uploads are supported for this device type
- **`kind`**: `string`, possible values: `"trmnl", "kindle", "byod", "tidbyt"`

  Device kind (e.g., trmnl, kindle, byod)
- **`label`**: `string`

  Human-readable name
- **`mime_type`**: `string`

  Image MIME type
- **`name`**: `string`

  Unique identifier
- **`offset_x`**: `integer`

  X offset for image rendering
- **`offset_y`**: `integer`

  Y offset for image rendering
- **`palette_ids`**: `array of string`

  Supported color palette IDs
- **`preview_white_point`**: `string`, possible values: `"true_white", "limited"`

  Preview white point mode for color previews
- **`rotation`**: `integer`

  Screen rotation in degrees
- **`scale_factor`**: `number`

  Display scale factor
- **`width`**: `integer`

  Screen width in pixels

<a id="scalar-example-215"></a>

**Generated example:**

```json
{
  "name": "trmnl_original",
  "label": "TRMNL",
  "description": "Original TRMNL model",
  "width": 800,
  "height": 480,
  "colors": 2,
  "bit_depth": 1,
  "scale_factor": 1,
  "rotation": 90,
  "mime_type": "image/png",
  "offset_x": 10,
  "offset_y": 20,
  "kind": "trmnl",
  "palette_ids": [
    "bw",
    "gray-4",
    "gray-16"
  ],
  "preview_white_point": "true_white",
  "image_size_limit": 90000,
  "image_upload_supported": true,
  "css": {
    "classes": {
      "device": "screen--og_plus",
      "size": "screen--md",
      "density": "screen--density-1x"
    },
    "variables": [
      [
        "--screen-w",
        "800px"
      ]
    ]
  }
}
```

<a id="scalar-schema-palette"></a>

### Palette

**Type:** `object`, no additional properties

- **`id` (required)**: `string`

  Unique identifier
- **`name` (required)**: `string`

  Human-readable name
- **`colors`**: `(array of string) | null`

  Array of hex color codes (null for grayscale palettes)
- **`framework_class`**: `string`

  Framework CSS class for this palette
- **`grays`**: `integer | null`

  Number of grayscale levels (null for color palettes)
- **`grayscale_bit_depth`**: `integer | null`

  For color palettes, bit depth for grayscale areas (1 for 3/4-color limited, 2 for 6/7-color, 4 for full-spectrum)

<a id="scalar-example-216"></a>

**Generated example:**

```json
{
  "id": "gray-16",
  "name": "16-Gray",
  "grays": 16,
  "colors": [
    "#FF0000",
    "#00FF00",
    "#0000FF",
    "#FFFF00",
    "#000000",
    "#FFFFFF"
  ],
  "framework_class": "screen--4bit",
  "grayscale_bit_depth": 1
}
```

<a id="scalar-schema-playlistitem"></a>

### PlaylistItem

**Type:** `object`, no additional properties

- **`configuration_state`**: `string`, possible values: `"configured", "needs_configuration"`
- **`created_at`**: `string`, format: `date_time`
- **`device_id`**: `integer`
- **`font_family`**: `string | null`
- **`id`**: `integer`
- **`mashup_id`**: `integer | null`
- **`mirror`**: `boolean`
- **`palette_id`**: `string | null`

  Overrides the device palette for this item
- **`plugin`**: `object | null`
  - **`description`**: `string | null`
  - **`id`**: `integer`
  - **`image`**: `string | null`
  - **`image_dark`**: `string | null`
  - **`keyname`**: `string`
  - **`name`**: `string`
- **`plugin_id`**: `integer | null`
- **`plugin_setting`**: [PluginSetting](#scalar-schema-pluginsetting)
- **`plugin_setting_id`**: `integer | null`
- **`presentation`**: `object`
- **`rendered_at`**: `string | null`, format: `date_time`
- **`row_order`**: `integer`
- **`text_scale`**: `string | null`
- **`theme`**: `string | null`
- **`updated_at`**: `string`, format: `date_time`
- **`visible`**: `boolean`

<a id="scalar-example-217"></a>

**Generated example:**

```json
{
  "presentation": {},
  "created_at": "2023-10-01T12:00:00Z",
  "configuration_state": "configured",
  "device_id": 1,
  "id": 1,
  "mashup_id": 1,
  "mirror": true,
  "plugin": {
    "id": 1,
    "name": "Weather",
    "keyname": "weather",
    "description": null,
    "image": null,
    "image_dark": null
  },
  "plugin_id": 1,
  "plugin_setting": {
    "sync": {
      "source_kinds": [
        ""
      ],
      "upload_allowed": true,
      "reason": null,
      "action_id": null
    },
    "id": 1,
    "name": "My Plugin Setting",
    "description": "Upcoming train departures",
    "plugin_id": 1,
    "refresh_interval": 60,
    "health_notification_enabled": false,
    "icon_url": "https://trmnl-public.s3.com/qwertyasdf",
    "icon_content_type": "image/png",
    "read_only?": false,
    "strategy": "webhook"
  },
  "plugin_setting_id": 1,
  "rendered_at": "2023-10-01T12:00:00Z",
  "row_order": 1,
  "updated_at": "2023-10-01T12:00:00Z",
  "visible": true,
  "palette_id": "bw",
  "font_family": "classic",
  "text_scale": "large",
  "theme": "dark"
}
```

<a id="scalar-schema-mashup"></a>

### Mashup

**Type:** `object`, no additional properties

- **`contents`**: `object`

  Plugin setting id by position

  **Additional properties:**

  `integer`
- **`created_at`**: `string`, format: `date_time`
- **`grid_config`**: `array | null`

  The cells of a 3x3 layout

  **Items:**
  - **`col`**: `integer`
  - **`cs`**: `integer`
  - **`pos`**: `string`
  - **`row`**: `integer`
  - **`rs`**: `integer`
- **`health_notification_enabled`**: `boolean`
- **`id`**: `integer`
- **`layout`**: `string`, possible values: `"1x1", "1Tx1B", "1Lx1R", "1Tx2B", "2Tx1B", "1Lx2R", "2Lx1R", "2x2", "3x3"`
- **`positions`**: `array of string`

  The sections the layout has
- **`updated_at`**: `string`, format: `date_time`
- **`version`**: `string`

<a id="scalar-example-218"></a>

**Generated example:**

```json
{
  "version": "",
  "id": 1,
  "layout": "1Lx1R",
  "grid_config": [
    {
      "pos": "",
      "col": 1,
      "row": 1,
      "cs": 1,
      "rs": 1
    }
  ],
  "positions": [
    "a",
    "b"
  ],
  "contents": {
    "a": 1,
    "b": 2
  },
  "health_notification_enabled": true,
  "created_at": "2023-10-01T12:00:00Z",
  "updated_at": "2023-10-01T12:00:00Z"
}
```

<a id="scalar-schema-recipe"></a>

### Recipe

**Type:** `object`, no additional properties

- **`ai_keywords`**: `(array of string) | null`
- **`ai_tags`**: `(array of string) | null`
- **`author`**: `string | null`
- **`categories`**: `(array of string) | null`
- **`compatibility`**: `object`
- **`custom_fields`**: `array of object`

  Fields a fork needs filled
- **`description`**: `string | null`
- **`id`**: `integer`
- **`install_choices`**: `array of object`
- **`install_method`**: `string`, possible values: `"simple_install", "read_only_fork", "oauth_choice"`

  getRecipe only
- **`kind`**: `string`, possible values: `"official", "recipe"`
- **`name`**: `string`
- **`published_at`**: `string | null`, format: `date_time`
- **`requirements`**: `array of string`
- **`screenshot_url`**: `string | null`
- **`share_oauth_config`**: `boolean`

  getRecipe only
- **`source_revision`**: `string`
- **`stats`**: `object`
  - **`forks`**: `integer`
  - **`installs`**: `integer`
- **`strategy`**: `string | null`

<a id="scalar-example-219"></a>

**Generated example:**

```json
{
  "kind": "official",
  "source_revision": "",
  "requirements": [
    ""
  ],
  "compatibility": {},
  "install_choices": [
    {}
  ],
  "id": 1,
  "name": "Train Departures",
  "description": null,
  "author": "Ada",
  "categories": [
    ""
  ],
  "ai_tags": [
    ""
  ],
  "ai_keywords": [
    ""
  ],
  "stats": {
    "installs": 1,
    "forks": 1
  },
  "strategy": "polling",
  "custom_fields": [
    {}
  ],
  "published_at": null,
  "screenshot_url": null,
  "install_method": "simple_install",
  "share_oauth_config": true
}
```

<a id="scalar-schema-screen"></a>

### Screen

**Type:** `object`, no additional properties

- **`filename`**: `string`
- **`image_url`**: `string`

  Presigned; expires after ActiveStorage.service\_urls\_expire\_in (5 minutes by default)
- **`mashup_id`**: `integer | null`
- **`playlist_item_id`**: `integer`
- **`plugin_setting_id`**: `integer | null`
- **`rendered_at`**: `string | null`, format: `date_time`

<a id="scalar-example-220"></a>

**Generated example:**

```json
{
  "image_url": "https://trmnl-screens.s3.amazonaws.com/...",
  "rendered_at": "2023-10-01T12:00:00Z",
  "playlist_item_id": 1,
  "plugin_setting_id": 1,
  "mashup_id": 1,
  "filename": "weather-1696161600"
}
```

<a id="scalar-schema-playlistitemparams"></a>

### PlaylistItemParams

**Type:** `object`, no additional properties

- **`visible`**: `boolean`

<a id="scalar-example-221"></a>

**Generated example:**

```json
{
  "visible": true
}
```

<a id="scalar-schema-plugin"></a>

### Plugin

**Type:** `object`, no additional properties

- **`categories`**: `(array of string) | null`
- **`category`**: `(array of string) | null`
- **`compatibility`**: `object`
- **`description`**: `string | null`
- **`form_fields`**: `array | null`

  getPlugin only — send these keynames to updatePluginSettingFields

  **Items:**
  - **`default`**: `any`
  - **`description`**: `string | null`
  - **`field_type`**: `string`
  - **`help_text`**: `string | null`
  - **`keyname`**: `string`
  - **`name`**: `string`
  - **`optional`**: `boolean | null`
  - **`options`**: `any`

    An array of choices, or a string naming a dynamic source
  - **`placeholder`**: `string | null`
- **`form_type`**: `string`, possible values: `"form_input", "oauth2"`
- **`id`**: `integer`
- **`image_url`**: `string | null`
- **`install_choices`**: `array of object`
- **`installable`**: `boolean`

  False for an oauth2 plugin whose authorize step needs a browser
- **`keyname`**: `string`

  The plugin\_id createPluginSetting expects, as a string
- **`kind`**: `string`, possible values: `"official", "recipe"`
- **`name`**: `string`
- **`oauth`**: `boolean`

  Whether connecting this plugin needs an OAuth authorize step
- **`plugin_type`**: `string`, possible values: `"native", "third_party"`
- **`refresh_every`**: `integer | null`

  Minutes between polls
- **`requirements`**: `array of string`
- **`source_revision`**: `string`

<a id="scalar-example-222"></a>

**Generated example:**

```json
{
  "kind": "official",
  "source_revision": "",
  "requirements": [
    ""
  ],
  "compatibility": {},
  "install_choices": [
    {}
  ],
  "categories": [
    ""
  ],
  "id": 1,
  "keyname": "weather",
  "name": "Weather",
  "description": "Current conditions and forecast",
  "category": [
    "news"
  ],
  "plugin_type": "native",
  "form_type": "form_input",
  "oauth": false,
  "image_url": "https://trmnl.com/images/plugins/weather.svg",
  "refresh_every": 30,
  "installable": true,
  "form_fields": [
    {
      "keyname": "username",
      "field_type": "string",
      "name": "User Name",
      "description": null,
      "help_text": null,
      "placeholder": null,
      "optional": null,
      "options": null,
      "default": null
    }
  ]
}
```

<a id="scalar-schema-pluginsetting"></a>

### PluginSetting

**Type:** `object`, no additional properties

- **`description`**: `string | null`
- **`health_notification_enabled`**: `boolean`
- **`icon_content_type`**: `string | null`
- **`icon_url`**: `string | null`
- **`id`**: `integer`
- **`name`**: `string`
- **`plugin_id`**: `integer`
- **`read_only?`**: `boolean`
- **`refresh_interval`**: `integer | null`

  Minutes between renders
- **`strategy`**: `string | null`
- **`sync`**: [PluginSync](#scalar-schema-pluginsync)

<a id="scalar-example-223"></a>

**Generated example:**

```json
{
  "sync": {
    "source_kinds": [
      ""
    ],
    "upload_allowed": true,
    "reason": null,
    "action_id": null
  },
  "id": 1,
  "name": "My Plugin Setting",
  "description": "Upcoming train departures",
  "plugin_id": 1,
  "refresh_interval": 60,
  "health_notification_enabled": false,
  "icon_url": "https://trmnl-public.s3.com/qwertyasdf",
  "icon_content_type": "image/png",
  "read_only?": false,
  "strategy": "webhook"
}
```

<a id="scalar-schema-pluginsettingarchive"></a>

### PluginSettingArchive

**Type:** `object`

- **`settings_yaml`**: `string`

  YAML settings file

<a id="scalar-example-224"></a>

**Generated example:**

```json
{
  "settings_yaml": ""
}
```

<a id="scalar-schema-pluginsettingparams"></a>

### PluginSettingParams

**Type:** `object`, no additional properties

- **`name` (required)**: `string`
- **`plugin_id` (required)**: `integer`

<a id="scalar-example-225"></a>

**Generated example:**

```json
{
  "name": "My Plugin Setting",
  "plugin_id": 1
}
```

<a id="scalar-schema-pluginsettingdataparams"></a>

### PluginSettingDataParams

**Type:** `object`, no additional properties

- **`merge_variables` (required)**: `any`

  The rejected value, echoed back as sent
- **`error`**: `string`

<a id="scalar-example-226"></a>

**Generated example:**

```json
{
  "merge_variables": null,
  "error": ""
}
```

<a id="scalar-schema-usertheme"></a>

### UserTheme

**Type:** `object`, no additional properties

- **`created_at`**: `string`, format: `date_time`
- **`id`**: `integer`
- **`name`**: `string`
- **`scss`**: `string`

  getUserTheme only: the framework SCSS file this theme exports
- **`settings`**: [UserThemeSettings](#scalar-schema-userthemesettings)

  Every key is optional; an omitted key keeps the framework default. A value outside the vocabulary (an unrecognized token, surface, hue, or advanced channel) answers 422; a key the recipe does not read is ignored, not rejected.
- **`updated_at`**: `string`, format: `date_time`

<a id="scalar-example-227"></a>

**Generated example:**

```json
{
  "id": 1,
  "name": "Sunny Days",
  "settings": {
    "font": null,
    "chip": "filled",
    "screen_scale": "",
    "surfaces": {
      "additionalProperty": ""
    },
    "remap": {
      "grays": "",
      "colors": {
        "additionalProperty": ""
      }
    },
    "layout": {
      "whitespace": "compact",
      "corners": "sharp",
      "title_bar_height": "slim",
      "progress": "slim"
    },
    "typography": {
      "additionalProperty": ""
    },
    "spacing": {
      "title_bar_padding": "flush",
      "item_padding": "none"
    },
    "title_bar": {
      "additionalProperty": ""
    },
    "item": {
      "fill": "",
      "border": "none",
      "ink": ""
    },
    "advanced": {
      "additionalProperty": "anything"
    }
  },
  "created_at": "2023-10-01T12:00:00Z",
  "updated_at": "2023-10-01T12:00:00Z",
  "scss": ""
}
```

<a id="scalar-schema-userthemesettings"></a>

### UserThemeSettings

**Type:** `object`, no additional properties

Every key is optional; an omitted key keeps the framework default. A value outside the vocabulary (an unrecognized token, surface, hue, or advanced channel) answers 422; a key the recipe does not read is ignored, not rejected.

- **`advanced`**: `object`

  Framework contract channels and slots restated on one token, e.g. { "channels": { "canvas": "black" } }. Also accepted as the same mapping written as YAML text.
- **`chip`**: `string`, possible values: `"filled", "outlined"`
- **`font`**: `string | null`

  A FontPack id such as "bitter" or "space-mono", or blank for the device font
- **`item`**: `object`
  - **`border`**: `string`, possible values: `"none", "outline", "corner-brackets"`
  - **`fill`**: `string`

    "none", "ramp", or a color token
  - **`ink`**: `string`

    "auto" or a color token
- **`layout`**: `object`
  - **`corners`**: `string`, possible values: `"sharp", "regular", "round"`
  - **`progress`**: `string`, possible values: `"slim", "regular", "thick"`
  - **`title_bar_height`**: `string`, possible values: `"slim", "regular", "tall"`
  - **`whitespace`**: `string`, possible values: `"compact", "regular", "airy", "spacious"`
- **`remap`**: `object`
  - **`colors`**: `object`

    target token per hue: red, orange, yellow, lime, green, cyan, blue, violet, purple, pink

    **Additional properties:**

    `string`
  - **`grays`**: `string`

    "keep", "invert", a hue side, "-linear", or any color token
- **`screen_scale`**: `string`

  "none", "invert", or a hue side like "red-dark"/"red-bright"
- **`spacing`**: `object`
  - **`item_padding`**: `string`, possible values: `"none", "small", "regular", "large"`
  - **`title_bar_padding`**: `string`, possible values: `"flush", "regular", "wide"`
- **`surfaces`**: `object`

  paper, text, muted\_text, strong\_fill, soft\_fills, borders, title\_bar, progress, dividers — each a color token ("black", "white", "gray-30", "red-40", ...) or "hidden" for dividers

  **Additional properties:**

  `string`
- **`title_bar`**: `object`

  ink, instance, stroke, instance\_stroke — a color token, or "auto"/"inherit"

  **Additional properties:**

  `string`
- **`typography`**: `object`

  weight ("light"/"regular"/"medium"/"bold"), plus \_case ("as-written"/"uppercase"/"lowercase") and \_tracking ("normal"/"wide"/"wider") for title, value, label, description, title\_bar, title\_bar\_instance, table\_head (table\_body has no \_case)

  **Additional properties:**

  `string`

<a id="scalar-example-228"></a>

**Generated example:**

```json
{
  "font": null,
  "chip": "filled",
  "screen_scale": "",
  "surfaces": {
    "additionalProperty": ""
  },
  "remap": {
    "grays": "",
    "colors": {
      "additionalProperty": ""
    }
  },
  "layout": {
    "whitespace": "compact",
    "corners": "sharp",
    "title_bar_height": "slim",
    "progress": "slim"
  },
  "typography": {
    "additionalProperty": ""
  },
  "spacing": {
    "title_bar_padding": "flush",
    "item_padding": "none"
  },
  "title_bar": {
    "additionalProperty": ""
  },
  "item": {
    "fill": "",
    "border": "none",
    "ink": ""
  },
  "advanced": {
    "additionalProperty": "anything"
  }
}
```

<a id="scalar-schema-mypluginparams"></a>

### MyPluginParams

**Type:** `object`, no additional properties

- **`category`**: `array`

  One or more of the categories listCategories answers

  **Items:**

  `string`, possible values: `"album", "analytics", "art", "calendar", "comics", "crm", "custom", "discovery", "ecommerce", "education", "email", "entertainment", "environment", "finance", "games", "humor", "images", "kpi", "life", "marketing", "morbid", "nature", "news", "personal", "productivity", "programming", "sales", "sports", "travel"`
- **`description`**: `string`, maxLength: `35`
- **`installation_success_webhook_url`**: `string | null`
- **`installation_url`**: `string`

  Where an installer is sent to connect; your OAuth authorize page
- **`knowledge_base_url`**: `string | null`
- **`name`**: `string`
- **`no_screen_padding`**: `boolean`
- **`plugin_management_url`**: `string | null`

  Where an installer manages the connection on your site
- **`plugin_markup_url`**: `string`

  Your endpoint that answers the markup for a render
- **`refresh_every`**: `integer`, possible values: `1440, 720, 480, 360, 240, 120, 60, 30, 15`

  Minutes between renders
- **`uninstallation_webhook_url`**: `string | null`

<a id="scalar-example-229"></a>

**Generated example:**

```json
{
  "name": "Metro Schedule",
  "description": "Local train and bus times",
  "category": [
    "album"
  ],
  "refresh_every": 1440,
  "no_screen_padding": true,
  "installation_url": "",
  "plugin_management_url": null,
  "plugin_markup_url": "",
  "installation_success_webhook_url": null,
  "uninstallation_webhook_url": null,
  "knowledge_base_url": null
}
```

<a id="scalar-schema-myplugin"></a>

### MyPlugin

**Type:** `object`, no additional properties

- **`category`**: `array of string`
- **`client_id`**: `string`

  The plugin client id; its secret is on the dashboard only
- **`created_at`**: `string`, format: `date_time`
- **`description`**: `string | null`
- **`id`**: `integer`
- **`installation_success_webhook_url`**: `string | null`
- **`installation_url`**: `string | null`
- **`keyname`**: `string`
- **`knowledge_base_url`**: `string | null`
- **`name`**: `string`
- **`no_screen_padding`**: `boolean`
- **`plugin_management_url`**: `string | null`
- **`plugin_markup_url`**: `string | null`
- **`plugin_type`**: `string`, possible values: `"third_party"`
- **`refresh_every`**: `integer`

  Minutes between renders
- **`status`**: `string`, possible values: `"development", "in_review", "published"`
- **`uninstallation_webhook_url`**: `string | null`
- **`updated_at`**: `string`, format: `date_time`

<a id="scalar-example-230"></a>

**Generated example:**

```json
{
  "id": 1,
  "name": "Metro Schedule",
  "keyname": "metro_schedule",
  "description": null,
  "plugin_type": "third_party",
  "status": "development",
  "category": [
    ""
  ],
  "refresh_every": 1,
  "no_screen_padding": true,
  "installation_url": null,
  "plugin_management_url": null,
  "plugin_markup_url": null,
  "installation_success_webhook_url": null,
  "uninstallation_webhook_url": null,
  "knowledge_base_url": null,
  "client_id": "",
  "created_at": "",
  "updated_at": ""
}
```

<a id="scalar-schema-user"></a>

### User

**Type:** `object`, no additional properties

- **`account_name`**: `string | null`
- **`api_key`**: `string`

  Only when the request authenticated with the account API key
- **`email`**: `string`
- **`first_name`**: `string`
- **`id`**: `integer`
- **`last_name`**: `string`
- **`locale`**: `string`
- **`low_battery_notification_email`**: `string | null`
- **`name`**: `string`
- **`time_zone`**: `string`
- **`time_zone_iana`**: `string`
- **`title_bar_enabled`**: `boolean`
- **`utc_offset`**: `integer`

<a id="scalar-example-231"></a>

**Generated example:**

```json
{
  "id": 42,
  "name": "Jim Bob",
  "email": "jimbob@gmail.net",
  "first_name": "Jim",
  "last_name": "Bob",
  "locale": "en",
  "time_zone": "Eastern Time (US & Canada)",
  "time_zone_iana": "America/New_York",
  "utc_offset": -14400,
  "title_bar_enabled": true,
  "account_name": null,
  "low_battery_notification_email": null,
  "api_key": "user_xxxxxx"
}
```

<a id="scalar-schema-timelinestep"></a>

### TimelineStep

**Type:** `object`

One simulated check-in: what the device fetches then, and how long it holds it

- **`at`**: `string`, format: `date_time`
- **`device_id`**: `integer`
- **`name`**: `string | null`

  The plugin setting or mashup shown
- **`playlist_item_id`**: `integer | null`

  Null while asleep or with nothing to show
- **`reason`**: `string | null`

  Why the wait is what it is: asleep, or the schedule or render that moved it
- **`refresh_rate_seconds`**: `integer`

  The wait before the next-render extension
- **`refresh_seconds`**: `integer`

  How long the device waits before its next check-in
- **`render_at`**: `string | null`, format: `date_time`

  When the shown content renders next
- **`render_reason`**: `string | null`

  Why no render is scheduled
- **`source_id`**: `integer | null`
- **`source_type`**: `string | null`, possible values: `"PluginSetting", "Mashup", "Plugin"`
- **`waits_past_refresh_rate`**: `boolean`

  The device is held past its refresh rate by the "waits for next render" setting

<a id="scalar-example-232"></a>

**Generated example:**

```json
{
  "at": "",
  "device_id": 1,
  "playlist_item_id": null,
  "source_type": "PluginSetting",
  "source_id": null,
  "name": null,
  "reason": null,
  "refresh_seconds": 1,
  "refresh_rate_seconds": 1,
  "render_at": null,
  "render_reason": null,
  "waits_past_refresh_rate": true
}
```

<a id="scalar-schema-timeline"></a>

### Timeline

**Type:** `object`

- **`available`**: `boolean`

  Telemetry is configured; recorded is empty when it is not
- **`check_in`**: `object | null`

  Whether the device checked in when it said it would; null for a plugin setting or mashup
  - **`expected_at`**: `string | null`, format: `date_time`
  - **`state`**: `string`, possible values: `"never_seen", "overdue", "asleep", "on_time"`
- **`date`**: `string`, format: `date`

  The day shown, in the owner’s time zone
- **`expected`**: array of [TimelineStep](#scalar-schema-timelinestep)

  The coming check-ins over the next 24 hours; across every device for a plugin setting or mashup
- **`query_failed`**: `boolean`

  Telemetry could not be read for this request; recorded is empty
- **`recorded`**: `array`

  The day’s check-ins and what they caused, oldest first, one entry per check-in

  **Items:**
  - **`at`**: `string`, format: `date_time`
  - **`leading`**: `object`

    The event that names the entry: serve, render, checkin or schedule; ts and refresh\_at\_after in the owner’s time zone, ids as integers
  - **`name`**: `string | null`

    The source shown on a device timeline; the device on a plugin setting or mashup timeline
  - **`outcome`**: `string | null`

    What the check-in decided
  - **`supporting`**: `array of object`

    The other events of the same check-in, in the same shape as leading
  - **`trace_entries`**: `array of object`

    The decisions the check-in recorded
- **`strip`**: array of [TimelineStep](#scalar-schema-timelinestep)

  A device’s whole rotation over the next hours, unfiltered by source; empty for a plugin setting or mashup
- **`today`**: `string`, format: `date`

  Today in the owner’s time zone; expected and strip are empty on any other day
- **`truncated`**: `boolean`

  The day had more events than one page holds (1000)
- **`zone`**: `string`

<a id="scalar-example-233"></a>

**Generated example:**

```json
{
  "date": "",
  "today": "",
  "zone": "America/New_York",
  "available": true,
  "query_failed": true,
  "truncated": true,
  "check_in": {
    "state": "never_seen",
    "expected_at": null
  },
  "recorded": [
    {
      "at": "",
      "name": null,
      "outcome": null,
      "leading": {},
      "supporting": [
        {}
      ],
      "trace_entries": [
        {}
      ]
    }
  ],
  "expected": [
    {
      "at": "",
      "device_id": 1,
      "playlist_item_id": null,
      "source_type": "PluginSetting",
      "source_id": null,
      "name": null,
      "reason": null,
      "refresh_seconds": 1,
      "refresh_rate_seconds": 1,
      "render_at": null,
      "render_reason": null,
      "waits_past_refresh_rate": true
    }
  ],
  "strip": [
    {
      "at": "",
      "device_id": 1,
      "playlist_item_id": null,
      "source_type": "PluginSetting",
      "source_id": null,
      "name": null,
      "reason": null,
      "refresh_seconds": 1,
      "refresh_rate_seconds": 1,
      "render_at": null,
      "render_reason": null,
      "waits_past_refresh_rate": true
    }
  ]
}
```

<a id="scalar-schema-app"></a>

### App

**Type:** `object`, no additional properties

- **`app_key`**: `string`
- **`installed`**: `boolean`
- **`name`**: `string`
- **`tagline`**: `string`

<a id="scalar-example-234"></a>

**Generated example:**

```json
{
  "app_key": "room_booking",
  "name": "Booking",
  "tagline": "Rooms and desks on your screens",
  "installed": false
}
```

<a id="scalar-schema-appinstallation"></a>

### AppInstallation

**Type:** `object`, no additional properties

- **`app_key`**: `string`
- **`created_at`**: `string`, format: `date_time`
- **`id`**: `string`, format: `uuid`
- **`name`**: `string`
- **`summary`**: `string | null`

<a id="scalar-example-235"></a>

**Generated example:**

```json
{
  "id": "5e2d0a3c-8f1b-4c8a-9c1e-2f6a7b8c9d0e",
  "app_key": "fleet",
  "name": "Fleet",
  "summary": "3 mirrors",
  "created_at": "2026-09-22T12:00:00Z"
}
```

<a id="scalar-schema-fleetmember"></a>

### FleetMember

**Type:** `object`, no additional properties

- **`check_in_state`**: `string | null`, possible values: `"overdue", "on_time", "asleep", "never_seen"`

  Whether the device checked in when it said it would; null for a device that was reset
- **`device_id`**: `integer`
- **`expected_check_in_at`**: `string | null`, format: `date_time`
- **`in_sync`**: `boolean`

  Pushed since the master playlist last changed
- **`last_pushed_at`**: `string | null`, format: `date_time`
- **`name`**: `string | null`
- **`role`**: `string`, possible values: `"master", "mirror"`

<a id="scalar-example-236"></a>

**Generated example:**

```json
{
  "device_id": 123,
  "name": "Lobby",
  "role": "master",
  "check_in_state": "overdue",
  "expected_check_in_at": null,
  "last_pushed_at": null,
  "in_sync": true
}
```

<a id="scalar-schema-fleet"></a>

### Fleet

**Type:** `object`, no additional properties

- **`alerts`**: `object`
  - **`email`**: `string | null`
  - **`enabled`**: `boolean`
  - **`threshold_minutes`**: `integer`
- **`id`**: `string`, format: `uuid`
- **`inherited_settings`**: `array`

  Master settings each push copies to the mirrors

  **Items:**

  `string`, possible values: `"refresh_interval", "orientation", "sleep", "palette"`
- **`last_pushed_at`**: `string | null`, format: `date_time`
- **`master`**: [FleetMember](#scalar-schema-fleetmember) | `null`
- **`mirror_count`**: `integer`
- **`name`**: `string`
- **`needs_push_count`**: `integer`

  Mirrors not pushed since the master playlist last changed
- **`overdue_count`**: `integer`

<a id="scalar-example-237"></a>

**Generated example:**

```json
{
  "id": "",
  "name": "Fleet",
  "master": {
    "device_id": 123,
    "name": "Lobby",
    "role": "master",
    "check_in_state": "overdue",
    "expected_check_in_at": null,
    "last_pushed_at": null,
    "in_sync": true
  },
  "mirror_count": 3,
  "needs_push_count": 1,
  "overdue_count": 0,
  "last_pushed_at": null,
  "inherited_settings": [
    "refresh_interval"
  ],
  "alerts": {
    "enabled": true,
    "threshold_minutes": 60,
    "email": null
  }
}
```

<a id="scalar-schema-roombookingcalendar"></a>

### RoomBookingCalendar

**Type:** `object`, no additional properties

- **`booking_mode`**: `string | null`, possible values: `null, "reservable", "on_demand", "unavailable"`

  Google and Microsoft calendars only
- **`bound_device_ids`**: `array of integer`

  Devices showing this calendar
- **`capacity`**: `integer | null`
- **`feed_backed`**: `boolean`

  Read-only feed: bookings are made in the source calendar, not here
- **`ics_url`**: `string | null`

  iCal calendars only
- **`id`**: `string`

  ":", the id every calendar operation takes
- **`kind`**: `string`, possible values: `"ical", "google", "microsoft"`
- **`last_sync_error`**: `string | null`
- **`last_synced_at`**: `string | null`, format: `date_time`
- **`name`**: `string`
- **`public_booking_token`**: `string | null`

  Public booking token, when Booking with TRMNL is open
- **`public_booking_url`**: `string | null`
- **`resource_kind`**: `string | null`

  Google and Microsoft calendars only
- **`show_booking_qr`**: `boolean`
- **`show_company_logo`**: `boolean`
- **`timezone`**: `string`
- **`url_display_url`**: `string | null`

  Browser display URL, or null when no URL display is configured

<a id="scalar-example-238"></a>

**Generated example:**

```json
{
  "id": "ical:12",
  "kind": "ical",
  "name": "Boardroom",
  "timezone": "Europe/Amsterdam",
  "capacity": null,
  "show_booking_qr": true,
  "show_company_logo": true,
  "feed_backed": true,
  "ics_url": null,
  "resource_kind": "room",
  "booking_mode": null,
  "last_synced_at": null,
  "last_sync_error": null,
  "bound_device_ids": [
    1
  ],
  "url_display_url": null,
  "public_booking_token": null,
  "public_booking_url": null
}
```

<a id="scalar-schema-roombooking"></a>

### RoomBooking

**Type:** `object`, no additional properties

- **`all_day`**: `boolean`
- **`booked_with_trmnl`**: `boolean`

  Made through TRMNL, so it can be canceled or ended here
- **`ends_at`**: `string`, format: `date_time`
- **`id`**: `integer`
- **`source`**: `string`, possible values: `"owner", "public_link", "external"`
- **`starts_at`**: `string`, format: `date_time`
- **`status`**: `string`, possible values: `"confirmed", "cancelled"`
- **`title`**: `string | null`

<a id="scalar-example-239"></a>

**Generated example:**

```json
{
  "id": 1,
  "title": null,
  "starts_at": "",
  "ends_at": "",
  "all_day": true,
  "source": "owner",
  "status": "confirmed",
  "booked_with_trmnl": true
}
```

<a id="scalar-schema-roombookingcollection"></a>

### RoomBookingCollection

**Type:** `object`, no additional properties

- **`bound_device_ids`**: `array of integer`

  Devices showing this collection
- **`collection_type`**: `string`, possible values: `"any", "rooms", "spaces", "desks", "desk_pools", "parking_spaces", "vehicles", "equipment", "people", "other"`
- **`display_layout`**: `string`, possible values: `"list", "grid"`
- **`id`**: `integer`
- **`name`**: `string`
- **`public_booking_token`**: `string | null`
- **`public_booking_url`**: `string | null`
- **`resource_ids`**: `array of string`

  The calendars in it
- **`show_booking_qr`**: `boolean`
- **`show_company_logo`**: `boolean`
- **`url_display_url`**: `string | null`

<a id="scalar-example-240"></a>

**Generated example:**

```json
{
  "id": 1,
  "name": "First Floor",
  "collection_type": "any",
  "display_layout": "list",
  "show_booking_qr": true,
  "show_company_logo": true,
  "resource_ids": [
    "ical:12",
    "google:3"
  ],
  "bound_device_ids": [
    1
  ],
  "url_display_url": null,
  "public_booking_token": null,
  "public_booking_url": null
}
```

<a id="scalar-schema-roombookingsummary"></a>

### RoomBookingSummary

**Type:** `object`, no additional properties

- **`billing`**: `object`
  - **`billing_url`**: `string`

    Where the owner subscribes or manages the subscription, in the browser
  - **`locked`**: `boolean`

    Changes answer 402 until the subscription is settled
  - **`subscription_required`**: `boolean`

    Rooms reach a screen but nothing is subscribed yet
  - **`subscription_status`**: `string | null`
- **`calendar_count`**: `integer`
- **`collection_count`**: `integer`
- **`configured`**: `boolean`

  An account is connected or a calendar exists
- **`connected_accounts`**: `object`
  - **`google_user`**: `integer`
  - **`google_workspace`**: `integer`
  - **`microsoft_user`**: `integer`
  - **`microsoft_workspace`**: `integer`
- **`device_ids`**: `array of integer`

  Devices showing a calendar or collection
- **`id`**: `string`, format: `uuid`
- **`name`**: `string`
- **`sync_error_count`**: `integer`

  Calendars whose last fetch failed
- **`timezone`**: `string | null`

<a id="scalar-example-241"></a>

**Generated example:**

```json
{
  "id": "",
  "name": "Booking",
  "timezone": null,
  "configured": true,
  "connected_accounts": {
    "google_user": 1,
    "google_workspace": 1,
    "microsoft_user": 1,
    "microsoft_workspace": 1
  },
  "calendar_count": 1,
  "collection_count": 1,
  "sync_error_count": 1,
  "device_ids": [
    1
  ],
  "billing": {
    "locked": true,
    "subscription_required": true,
    "subscription_status": "active",
    "billing_url": ""
  }
}
```

<a id="scalar-schema-roombookingsettings"></a>

### RoomBookingSettings

**Type:** `object`, no additional properties

- **`color_company_logo_attached`**: `boolean`
- **`company_logo_attached`**: `boolean`
- **`daily_public_booking_url_rotation`**: `boolean`
- **`dark_mode`**: `boolean`
- **`public_bookings`**: `boolean`

  Booking with TRMNL
- **`show_company_logo`**: `boolean`
- **`time_format`**: `string | null`, possible values: `null, "24_hour", "12_hour"`

  null detects it from the time zone
- **`timezone`**: `string | null`
- **`walk_up_booking_window_days`**: `integer`, possible values: `1, 2, 7, 30`
- **`walk_up_calendar`**: `boolean`

<a id="scalar-example-242"></a>

**Generated example:**

```json
{
  "timezone": "Europe/Amsterdam",
  "public_bookings": true,
  "dark_mode": true,
  "show_company_logo": true,
  "daily_public_booking_url_rotation": true,
  "time_format": null,
  "walk_up_calendar": true,
  "walk_up_booking_window_days": 1,
  "company_logo_attached": true,
  "color_company_logo_attached": true
}
```

<a id="scalar-schema-roombookingintegration"></a>

### RoomBookingIntegration

**Type:** `object`, no additional properties

- **`admin_email`**: `string | null`

  Workspaces only
- **`booking_organizer_email`**: `string | null`

  Microsoft workspaces only: the mailbox bookings are made from
- **`calendar_count`**: `integer`
- **`id`**: `integer`
- **`integration_type`**: `string`, possible values: `"google_user", "google_workspace", "microsoft_user", "microsoft_workspace"`
- **`label`**: `string | null`

  The account email, or the workspace domain
- **`last_attempted_at`**: `string | null`, format: `date_time`
- **`last_synced_at`**: `string | null`, format: `date_time`
- **`parking_resource_group_id`**: `string | null`

  Microsoft workspaces only
- **`provider`**: `string`, possible values: `"google", "microsoft"`
- **`selected_calendar_count`**: `integer`

  Calendars selected for sync, the ones listRoomBookingCalendars shows
- **`workspace`**: `boolean`

  An admin connection to a whole workspace, not one person's account

<a id="scalar-example-243"></a>

**Generated example:**

```json
{
  "id": 1,
  "integration_type": "google_user",
  "provider": "google",
  "workspace": true,
  "label": "owner@gmail.com",
  "admin_email": null,
  "booking_organizer_email": null,
  "parking_resource_group_id": null,
  "calendar_count": 1,
  "selected_calendar_count": 1,
  "last_synced_at": null,
  "last_attempted_at": null
}
```

<a id="scalar-schema-publicbookingevent"></a>

### PublicBookingEvent

**Type:** `object`, no additional properties

The shape the public booking page uses: times are local to the room, without a zone

- **`actionConfirm`**: `string`
- **`actionLabel`**: `string`

  Present when the visitor may cancel or end it
- **`actionMethod`**: `string`, possible values: `"delete", "patch"`
- **`actionPath`**: `string`
- **`endsAt`**: `string`
- **`id`**: `integer`
- **`startsAt`**: `string`
- **`title`**: `string`

<a id="scalar-example-244"></a>

**Generated example:**

```json
{
  "id": 1,
  "title": "",
  "startsAt": "2026-06-12T10:00",
  "endsAt": "2026-06-12T11:00",
  "actionLabel": "",
  "actionMethod": "delete",
  "actionPath": "/api/book/{token}/bookings/12",
  "actionConfirm": ""
}
```

<a id="scalar-schema-roombookingbilling"></a>

### RoomBookingBilling

**Type:** `object`, no additional properties

- **`amount_in_dollars`**: `object`

  Per calendar
  - **`month`**: `integer`
  - **`year`**: `integer`
- **`billable_resource_count`**: `integer`

  Calendars on a screen, directly or through a collection
- **`checkout_url`**: `string`

  The billing page, where the owner subscribes in the browser
- **`grace_until`**: `string | null`, format: `date_time`
- **`locked`**: `boolean`

  Changes answer 402 until the subscription is settled
- **`portal_url`**: `string | null`

  The billing page, where a subscribed owner opens the Stripe portal
- **`subscription`**: `object | null`
  - **`billed_quantity`**: `integer`
  - **`id`**: `string`
  - **`interval`**: `string`, possible values: `"month", "year"`
  - **`status`**: `string`
- **`subscription_required`**: `boolean`

  Calendars reach a screen but nothing is subscribed yet
- **`trial_days`**: `integer`

<a id="scalar-example-245"></a>

**Generated example:**

```json
{
  "locked": true,
  "subscription_required": true,
  "billable_resource_count": 1,
  "amount_in_dollars": {
    "month": 8,
    "year": 80
  },
  "trial_days": 14,
  "grace_until": null,
  "subscription": {
    "id": "",
    "status": "active",
    "interval": "month",
    "billed_quantity": 1
  },
  "checkout_url": "",
  "portal_url": null
}
```

<a id="scalar-schema-roombookingdevicescreen"></a>

### RoomBookingDeviceScreen

**Type:** `object`, no additional properties

A device showing a calendar or a collection, and the image it is showing

- **`device_id`**: `integer`
- **`device_name`**: `string`
- **`screen`**: [Screen](#scalar-schema-screen) | `null`

  null while that device has not rendered this calendar or collection yet

<a id="scalar-example-246"></a>

**Generated example:**

```json
{
  "device_id": 1,
  "device_name": "Boardroom display",
  "screen": {
    "image_url": "https://trmnl-screens.s3.amazonaws.com/...",
    "rendered_at": "2023-10-01T12:00:00Z",
    "playlist_item_id": 1,
    "plugin_setting_id": 1,
    "mashup_id": 1,
    "filename": "weather-1696161600"
  }
}
```

<a id="scalar-schema-roombookingcalendardetails"></a>

### RoomBookingCalendarDetails

**Type:** `object`, no additional properties

- **`booking_mode`**: `string | null`, possible values: `null, "reservable", "on_demand", "unavailable"`

  Google and Microsoft calendars only
- **`bound_device_ids`**: `array of integer`

  Devices showing this calendar
- **`capacity`**: `integer | null`
- **`current_booking`**: [RoomBooking](#scalar-schema-roombooking) | `null`
- **`feed_backed`**: `boolean`

  Read-only feed: bookings are made in the source calendar, not here
- **`ics_url`**: `string | null`

  iCal calendars only
- **`id`**: `string`

  ":", the id every calendar operation takes
- **`kind`**: `string`, possible values: `"ical", "google", "microsoft"`
- **`last_sync_error`**: `string | null`
- **`last_synced_at`**: `string | null`, format: `date_time`
- **`name`**: `string`
- **`public_booking_token`**: `string | null`

  Public booking token, when Booking with TRMNL is open
- **`public_booking_url`**: `string | null`
- **`resource_kind`**: `string | null`

  Google and Microsoft calendars only
- **`show_booking_qr`**: `boolean`
- **`show_company_logo`**: `boolean`
- **`timezone`**: `string`
- **`upcoming_bookings`**: array of [RoomBooking](#scalar-schema-roombooking)
- **`url_display_url`**: `string | null`

  Browser display URL, or null when no URL display is configured

<a id="scalar-example-247"></a>

**Generated example:**

```json
{
  "id": "ical:12",
  "kind": "ical",
  "name": "Boardroom",
  "timezone": "Europe/Amsterdam",
  "capacity": null,
  "show_booking_qr": true,
  "show_company_logo": true,
  "feed_backed": true,
  "ics_url": null,
  "resource_kind": "room",
  "booking_mode": null,
  "last_synced_at": null,
  "last_sync_error": null,
  "bound_device_ids": [
    1
  ],
  "url_display_url": null,
  "public_booking_token": null,
  "public_booking_url": null,
  "current_booking": {
    "id": 1,
    "title": null,
    "starts_at": "",
    "ends_at": "",
    "all_day": true,
    "source": "owner",
    "status": "confirmed",
    "booked_with_trmnl": true
  },
  "upcoming_bookings": [
    {
      "id": 1,
      "title": null,
      "starts_at": "",
      "ends_at": "",
      "all_day": true,
      "source": "owner",
      "status": "confirmed",
      "booked_with_trmnl": true
    }
  ]
}
```
