<!-- generated by bin/openapi-markdown from scripts/openapi/openapi.yaml sha256:c833d275b324621807a67c409829c21331110ac06a977780dd9754c685a0f8d0 -->
# 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>.

## Servers

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

## Operations

### Fetch the next screen

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

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Parameters

##### `Access-Token` required

- **In:** `header`

Device API Key (eg. abc-123)

`string`

##### `Battery-Voltage`

- **In:** `header`

Device battery voltage (eg. 3.7)

`number`

##### `Percent-Charged`

- **In:** `header`

Device percent charged (eg. 69.4)

`number`

##### `Battery-Count`

- **In:** `header`

Number of batteries in device (e.g. 1-2)

`number`

##### `Battery-Charging`

- **In:** `header`

Whether device is plugged into power (eg. true)

`number`

##### `Battery-Health`

- **In:** `header`

Quality of battery (eg. -1-100)

`number`

##### `Battery-Current`

- **In:** `header`

Battery current (eg. -1-N)

`number`

##### `Battery-Temp`

- **In:** `header`

Battery temperature in celsius (eg. -1-N)

`number`

##### `Battery-Capacity`

- **In:** `header`

Ratio of current / max capacity (eg. -1/-1 in fault case)

`number`

##### `USB-Connected`

- **In:** `header`

Whether device is plugged into power via USB ("true" or "false")

`string`

##### `WiFi-Band`

- **In:** `header`

WiFi band the device is connected on ("2.4" or "5")

`string`

##### `Panel-Rev`

- **In:** `header`

Identifier of the ePaper panel fitted to the device

`string`

##### `FW-Version`

- **In:** `header`

Device firmware version (eg. 0.0.1)

`string`

##### `FW-Commit`

- **In:** `header`

Device firmware commit hash, for the development channel (eg. a14914c)

`string`

##### `RSSI`

- **In:** `header`

Device RSSI (eg. -69)

`number`

##### `Height`

- **In:** `header`

Device screen height (eg. 480)

`string`

##### `Width`

- **In:** `header`

Device screen width (eg. 800)

`string`

##### `Special-Function`

- **In:** `header`

Device special function (eg. true)

`boolean`

##### `BASE64`

- **In:** `header`

Encode image function (eg. true)

`boolean`

##### `Sensors`

- **In:** `header`

Environmental sensor data

`string`

##### `Image-Cached`

- **In:** `header`

Whether the device served the previous image from cache (eg. true / false)

`string`

##### `Wake-Time`

- **In:** `header`

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

`integer`

#### Responses

##### Status: 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`

**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"
}
```

### Fetch the current screen

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

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Parameters

##### `Access-Token` required

- **In:** `header`

Device API Key (eg. abc-123)

`string`

#### Responses

##### Status: 200 Success

###### Content-Type: application/json

- **`filename`**

  `string | null`
- **`image_url`**

  `string | null`
- **`refresh_rate`**

  `integer`
- **`rendered_at`**

  `string | null`
- **`status`**

  `integer`

**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"
}
```

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

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

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Parameters

##### `Access-Token` required

- **In:** `header`

Device API Key (eg. abc-123)

`string`

#### Request Body

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

**Required:** `true`

##### Content-Type: application/json

- **`logs` (required)**

  `array`

  **Items:**

**Additional properties:**

never (false schema)

**Example:**

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

#### Responses

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

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Parameters

##### `ID` required

- **In:** `header`

Device MAC Address (eg. 41:B4:10:39:A1:24)

`string`

##### `Model` required

- **In:** `header`

DEVICE\_MODEL from firmware definitions

`string`

##### `Panel-Rev`

- **In:** `header`

Identifier of the ePaper panel fitted to the device

`string`

#### Responses

##### Status: 200 Success

###### Content-Type: application/json

- **`api_key`**

  `string | null`
- **`friendly_id`**

  `string | null`
- **`image_url`**

  `string | null`
- **`message`**

  `string`
- **`status`**

  `integer`

**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'"
}
```

### List all plugin categories

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

Returns a list of approved plugin categories.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Responses

##### Status: 200 Success

###### Content-Type: application/json

- **`data`**

  `array`

  **Items:**

  `string`

**Example:**

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

### 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).

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Responses

##### Status: 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`

**Example:**

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

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Responses

##### Status: 200 Success

###### Content-Type: application/json

- **`data`**

  `object`
  - **`ipv4`**

    `array`

    **Items:**

    `string`
  - **`ipv6`**

    `array`

    **Items:**

    `string`

**Example:**

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

### Render Liquid template

- **Method:** `POST`
- **Path:** `/api/markup`
- **Operation ID:** `renderMarkup`
- **Tags:** Markup

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### 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

- **`markup` (required)**

  `object`

  **One of:**

  `string`

  **Array of:**

  `string`
- **`variables` (required)**

  `object`

**Additional properties:**

never (false schema)

**Example:**

```json
{
  "markup": "Hello, {{ name }}!",
  "variables": {
    "name": "World"
  }
}
```

#### Responses

##### Status: 200 Success

###### Content-Type: application/json

- **`data`**

  `object`

  **One of:**

  `string`

  **Array of:**

  `string`

**Example:**

```json
{
  "data": ""
}
```

### List all device models

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

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Responses

##### Status: 200 Success

###### Content-Type: application/json

- **`data`**

  `array`

  **Items:**

  schema: `Model`
  - **`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`

      **Items:**

      **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` — Supported color palette IDs

    **Items:**

    `string`
  - **`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
  **Additional properties:**

  never (false schema)

**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"
          ]
        ]
      }
    }
  ]
}
```

### List all palettes

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

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Responses

##### Status: 200 Success

###### Content-Type: application/json

- **`data`**

  `array`

  **Items:**

  schema: `Palette`
  - **`id` (required)**

    `string` — Unique identifier
  - **`name` (required)**

    `string` — Human-readable name
  - **`colors`**

    `array | null` — Array of hex color codes (null for grayscale palettes)

    **Items:**

    `string`
  - **`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)
  **Additional properties:**

  never (false schema)

**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
    }
  ]
}
```

### List the bookings a walk-up 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 walk-up 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Parameters

##### `token` required

- **In:** `path`

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

`string`

##### `start_at` required

- **In:** `query`

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

`string`

##### `resource_type`

- **In:** `query`

Collection tokens only:

- `ical`
- `google`
- `microsoft`

`string`

##### `resource_id`

- **In:** `query`

Collection tokens only

`integer`

#### Responses

##### Status: 200 Success

###### Content-Type: application/json

- **`bookingLimit`**

  `integer` — How many bookings may overlap; more than one for a desk pool
- **`events`**

  `array`

  **Items:**

  schema: `PublicBookingEvent` — The shape the walk-up 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`
  **Additional properties:**

  never (false schema)
- **`fetched_at`**

  `string | null`, format: `date_time`

**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
}
```

##### Status: 400 start\_at is missing

##### Status: 410 Unknown token, walk-up booking off, or a room that cannot be booked: all look the same

### Book a room as a walk-up visitor

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

Public: no bearer token. The same rules as the walk-up 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Parameters

##### `token` required

- **In:** `path`

The room public booking token

`string`

##### `resource_type`

- **In:** `query`

Collection tokens only

`string`

##### `resource_id`

- **In:** `query`

Collection tokens only

`integer`

#### 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`

**Example:**

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

#### Responses

##### Status: 200 Booked

###### Content-Type: application/json

- **`data`**

  `object`, schema: `PublicBookingEvent` — The shape the walk-up page uses: times are local to the room, without a zone

  *Schema `PublicBookingEvent` is shown above.*

**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": ""
  }
}
```

##### Status: 409 The slot was just taken

##### Status: 410 Unknown token, walk-up booking off, or a room that cannot be booked: all look the same

##### Status: 422 Outside the booking window

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

### Cancel an upcoming walk-up 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Parameters

##### `token` required

- **In:** `path`

The room public booking token

`string`

##### `id` required

- **In:** `path`

Booking id

`integer`

#### Responses

##### Status: 204 Canceled

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

##### Status: 410 Unknown token, walk-up booking off, or a room that cannot be booked: all look the same

### End a walk-up 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Parameters

##### `token` required

- **In:** `path`

The room public booking token

`string`

##### `id` required

- **In:** `path`

Booking id

`integer`

#### Responses

##### Status: 200 Ended

###### Content-Type: application/json

- **`data`**

  `object`, schema: `PublicBookingEvent` — The shape the walk-up page uses: times are local to the room, without a zone

  *Schema `PublicBookingEvent` is shown above.*

**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": ""
  }
}
```

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

##### Status: 410 Unknown token, walk-up booking off, or a room that cannot be booked: all look the same

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Responses

##### Status: 200 Success

###### Content-Type: application/json

- **`data`**

  `object`
  - **`growth`**

    `array`

    **Items:**

    \[date, installs to date]

    **Array of:**
  - **`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`

**Example:**

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

##### Status: 401 Unauthorized

###### Content-Type: application/json

schema: `Error`

- **`error`**

  `string`

**Additional properties:**

never (false schema)

**Example:**

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

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Responses

##### Status: 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`

**Example:**

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

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "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

**Example:**

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

#### Responses

##### Status: 200 Hidden

###### Content-Type: application/json

- **`data`**

  `object`
  - **`hidden`**

    `boolean`

**Example:**

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

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

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

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

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `kind` required

- **In:** `query`

:

- `recipe`
- `third_party`

`string`

##### `subject_id` required

- **In:** `query`

`integer`

##### `owner` required

- **In:** `query`

`string`

##### `message` required

- **In:** `query`

`string`

#### Responses

##### Status: 200 Shown again

###### Content-Type: application/json

- **`data`**

  `object`
  - **`hidden`**

    `boolean`

**Example:**

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

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Responses

##### Status: 200 Success

###### Content-Type: application/json

- **`data`**

  `object`
  - **`reason_breakdown`**

    `array`

    **Items:**

    \[reason, count]

    **Array of:**
  - **`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`

**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
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Fleet installation id, from listAppInstallations

`string`

#### Responses

##### Status: 200 Success

###### Content-Type: application/json

- **`data`**

  `object`, schema: `Fleet`
  - **`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`**

    `object`

    **Any of:**

    schema: `FleetMember`
    - **`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"`
    **Additional properties:**

    never (false schema)

    `null`
  - **`mirror_count`**

    `integer`
  - **`name`**

    `string`
  - **`needs_push_count`**

    `integer` — Mirrors not pushed since the master playlist last changed
  - **`overdue_count`**

    `integer`
  **Additional properties:**

  never (false schema)

**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
    }
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Installation belongs to another user

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Fleet installation id

`string`

#### Responses

##### Status: 200 Success

###### Content-Type: application/json

- **`data`**

  `array`

  **Items:**

  *Schema `FleetMember` is shown above.*

**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
    }
  ]
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Installation belongs to another user

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Fleet installation id

`string`

#### Request Body

**Required:** `true`

##### Content-Type: application/json

- **`device_id` (required)**

  `integer`

**Example:**

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

#### Responses

##### Status: 200 Added

###### Content-Type: application/json

- **`data`**

  `object`, schema: `FleetMember`

  *Schema `FleetMember` is shown above.*

**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
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Device belongs to another user

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

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Fleet installation id

`string`

##### `device_id` required

- **In:** `path`

Device id

`integer`

#### Responses

##### Status: 204 Removed

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

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

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Fleet installation id

`string`

##### `device_id` required

- **In:** `path`

Device id of a mirror

`integer`

#### Responses

##### Status: 202 Queued

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

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

##### Status: 429 The mirror has had its hourly pushes

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Fleet installation id

`string`

#### Responses

##### Status: 202 Queued

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Installation belongs to another user

##### Status: 422 No master device

##### Status: 429 The fleet has had its hourly pushes

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Fleet installation id

`string`

#### Request Body

**Required:** `true`

##### Content-Type: application/json

- **`inherited_settings` (required)**

  `array`

  **Items:**

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

**Example:**

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

#### Responses

##### Status: 200 Updated

###### Content-Type: application/json

- **`data`**

  `object`, schema: `Fleet`

  *Schema `Fleet` is shown above.*

**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
    }
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Installation belongs to another user

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

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Fleet installation id

`string`

#### 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`

**Example:**

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

#### Responses

##### Status: 200 Updated

###### Content-Type: application/json

- **`data`**

  `object`, schema: `Fleet`

  *Schema `Fleet` is shown above.*

**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
    }
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Installation belongs to another user

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

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Fleet installation id

`string`

#### Request Body

**Required:** `true`

##### Content-Type: application/json

- **`device_id` (required)**

  `integer`

**Example:**

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

#### Responses

##### Status: 200 Set

###### Content-Type: application/json

- **`data`**

  `object`, schema: `Fleet`

  *Schema `Fleet` is shown above.*

**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
    }
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Device belongs to another user

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

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Fleet installation id

`string`

#### Responses

##### Status: 204 Removed

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Installation belongs to another user

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Responses

##### Status: 200 Success

###### Content-Type: application/json

- **`data`**

  `array`

  **Items:**

  schema: `App`
  - **`app_key`**

    `string`
  - **`installed`**

    `boolean`
  - **`name`**

    `string`
  - **`tagline`**

    `string`
  **Additional properties:**

  never (false schema)

**Example:**

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

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Responses

##### Status: 200 Success

###### Content-Type: application/json

- **`data`**

  `array`

  **Items:**

  schema: `AppInstallation`
  - **`app_key`**

    `string`
  - **`created_at`**

    `string`, format: `date_time`
  - **`id`**

    `string`, format: `uuid`
  - **`name`**

    `string`
  - **`summary`**

    `string | null`
  **Additional properties:**

  never (false schema)

**Example:**

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

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "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

**Example:**

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

#### Responses

##### Status: 200 Installed, or already was

###### Content-Type: application/json

- **`data`**

  `object`, schema: `AppInstallation`

  *Schema `AppInstallation` is shown above.*

**Example:**

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

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 No such app

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `id` required

- **In:** `path`

Installation id

`string`

#### Responses

##### Status: 204 Uninstalled

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Not an installation id

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

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Room booking installation id

`string`

#### Responses

##### Status: 200 Success

###### Content-Type: application/json

- **`data`**

  `array`

  **Items:**

  schema: `RoomBookingIntegration`
  - **`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
  **Additional properties:**

  never (false schema)

**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
    }
  ]
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Installation belongs to another user

### 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).

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Room booking installation id

`string`

##### `integration_type` required

- **In:** `path`

`string`

#### Responses

##### Status: 200 Success

###### Content-Type: application/json

- **`data`**

  `object`
  - **`integrations_url`**

    `string`
  - **`method`**

    `string`
  - **`url`**

    `string`

**Example:**

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

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Not an integration type

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Room booking installation id

`string`

##### `integration_type` required

- **In:** `path`

`string`

##### `id` required

- **In:** `path`

Integration id

`integer`

#### Responses

##### Status: 200 Synchronized

###### Content-Type: application/json

- **`data`**

  `object`, schema: `RoomBookingIntegration`

  *Schema `RoomBookingIntegration` is shown above.*

**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
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Integration of another type

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

##### Status: 429 The account has had its hourly syncs

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Room booking installation id

`string`

##### `integration_type` required

- **In:** `path`

`string`

##### `id` required

- **In:** `path`

Integration id

`integer`

#### Request Body

**Required:** `true`

##### Content-Type: application/json

- **`booking_organizer_email`**

  `string | null`
- **`calendar_ids`**

  `array`

  **Items:**

  `integer`
- **`parking_resource_group_id`**

  `string | null`

**Example:**

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

#### Responses

##### Status: 200 Updated

###### Content-Type: application/json

- **`data`**

  `object`, schema: `RoomBookingIntegration`

  *Schema `RoomBookingIntegration` is shown above.*

**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
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Installation belongs to another user

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

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Room booking installation id

`string`

##### `integration_type` required

- **In:** `path`

`string`

##### `id` required

- **In:** `path`

Integration id

`integer`

#### Responses

##### Status: 204 Disconnected

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Integration belongs to another installation

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Room booking installation id, from listAppInstallations

`string`

#### Responses

##### Status: 200 Success

###### Content-Type: application/json

- **`data`**

  `object`, schema: `RoomBookingSummary`
  - **`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` — Devices showing a calendar or collection

    **Items:**

    `integer`
  - **`id`**

    `string`, format: `uuid`
  - **`name`**

    `string`
  - **`sync_error_count`**

    `integer` — Calendars whose last fetch failed
  - **`timezone`**

    `string | null`
  **Additional properties:**

  never (false schema)

**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": ""
    }
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Installation belongs to another user

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Room booking installation id

`string`

#### Responses

##### Status: 200 The billing state

###### Content-Type: application/json

- **`data`**

  `object`, schema: `RoomBookingBilling`
  - **`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`
  **Additional properties:**

  never (false schema)

**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
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Installation belongs to another user

### 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

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Room booking installation id

`string`

#### Responses

##### Status: 200 Success

###### Content-Type: application/json

- **`data`**

  `object`, schema: `RoomBookingSettings`
  - **`color_company_logo_attached`**

    `boolean`
  - **`company_logo_attached`**

    `boolean`
  - **`daily_public_booking_url_rotation`**

    `boolean`
  - **`dark_mode`**

    `boolean`
  - **`public_bookings`**

    `boolean` — Walk-up QR booking
  - **`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`
  **Additional properties:**

  never (false schema)

**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
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Installation belongs to another user

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Room booking installation id

`string`

#### Request Body

**Required:** `true`

##### Content-Type: application/json

- **`daily_public_booking_url_rotation`**

  `boolean`
- **`dark_mode`**

  `boolean`
- **`public_bookings`**

  `boolean` — Walk-up QR booking
- **`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 walk-up page

**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

##### Status: 200 Updated

###### Content-Type: application/json

- **`data`**

  `object`, schema: `RoomBookingSettings`

  *Schema `RoomBookingSettings` is shown above.*

**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
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Installation belongs to another user

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

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Room booking installation id

`string`

#### Request Body

**Required:** `true`

##### Content-Type: application/json

- **`image_base64` (required)**

  `string` — The image bytes, base64 encoded

**Example:**

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

#### Responses

##### Status: 200 Uploaded

###### Content-Type: application/json

- **`data`**

  `object`, schema: `RoomBookingSettings`

  *Schema `RoomBookingSettings` is shown above.*

**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
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Installation belongs to another user

##### Status: 422 Not base64

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Room booking installation id

`string`

#### Responses

##### Status: 204 Removed

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Installation belongs to another user

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Room booking installation id

`string`

#### Request Body

**Required:** `true`

##### Content-Type: application/json

- **`image_base64` (required)**

  `string`

**Example:**

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

#### Responses

##### Status: 200 Uploaded

###### Content-Type: application/json

- **`data`**

  `object`, schema: `RoomBookingSettings`

  *Schema `RoomBookingSettings` is shown above.*

**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
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Installation belongs to another user

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

### 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

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Room booking installation id

`string`

#### Responses

##### Status: 204 Removed

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Installation belongs to another user

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Room booking installation id, from listAppInstallations

`string`

#### Responses

##### Status: 200 Success

###### Content-Type: application/json

- **`data`**

  `array`

  **Items:**

  schema: `RoomBookingCalendar`
  - **`booking_mode`**

    `string | null`, possible values: `null, "reservable", "on_demand", "unavailable"` — Google and Microsoft calendars only
  - **`bound_device_ids`**

    `array` — Devices showing this calendar

    **Items:**

    `integer`
  - **`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` — "\<kind>:\<number>", 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` — Walk-up QR booking token, when walk-up booking is open
  - **`public_booking_url`**

    `string | null`
  - **`resource_kind`**

    `string | null` — Google and Microsoft calendars only
  - **`show_booking_qr`**

    `boolean`
  - **`timezone`**

    `string`
  **Additional properties:**

  never (false schema)

**Example:**

```json
{
  "data": [
    {
      "id": "ical:12",
      "kind": "ical",
      "name": "Boardroom",
      "timezone": "Europe/Amsterdam",
      "capacity": null,
      "show_booking_qr": 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
      ],
      "public_booking_token": null,
      "public_booking_url": null
    }
  ]
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Installation belongs to another user

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Room booking installation id, from listAppInstallations

`string`

#### 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`
- **`timezone`**

  `string | null` — IANA name; the installation time zone when left out

**Example:**

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

#### Responses

##### Status: 200 Created

###### Content-Type: application/json

- **`data`**

  `object`, schema: `RoomBookingCalendar`

  *Schema `RoomBookingCalendar` is shown above.*

**Example:**

```json
{
  "data": {
    "id": "ical:12",
    "kind": "ical",
    "name": "Boardroom",
    "timezone": "Europe/Amsterdam",
    "capacity": null,
    "show_booking_qr": 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
    ],
    "public_booking_token": null,
    "public_booking_url": null
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

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

##### Status: 404 Installation belongs to another user

##### Status: 422 Refused

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Room booking installation id

`string`

##### `id` required

- **In:** `path`

Calendar id, as listRoomBookingCalendars answers it

`string`

#### Responses

##### Status: 200 Success

###### Content-Type: application/json

- **`data`**

  `object`, schema: `RoomBookingCalendarDetails`
  - **`booking_mode`**

    `string | null`, possible values: `null, "reservable", "on_demand", "unavailable"` — Google and Microsoft calendars only
  - **`bound_device_ids`**

    `array` — Devices showing this calendar

    **Items:**

    `integer`
  - **`capacity`**

    `integer | null`
  - **`current_booking`**

    `object`

    **Any of:**

    schema: `RoomBooking`
    - **`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`
    **Additional properties:**

    never (false schema)

    `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` — "\<kind>:\<number>", 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` — Walk-up QR booking token, when walk-up booking is open
  - **`public_booking_url`**

    `string | null`
  - **`resource_kind`**

    `string | null` — Google and Microsoft calendars only
  - **`show_booking_qr`**

    `boolean`
  - **`timezone`**

    `string`
  - **`upcoming_bookings`**

    `array`

    **Items:**

    *Schema `RoomBooking` is shown above.*
  **Additional properties:**

  never (false schema)

**Example:**

```json
{
  "data": {
    "id": "ical:12",
    "kind": "ical",
    "name": "Boardroom",
    "timezone": "Europe/Amsterdam",
    "capacity": null,
    "show_booking_qr": 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
    ],
    "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
      }
    ]
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Not a calendar id

### 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 and show\_booking\_qr. A Google or Microsoft calendar keeps its provider name and takes override\_name and show\_booking\_qr.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Room booking installation id

`string`

##### `id` required

- **In:** `path`

Calendar id, as listRoomBookingCalendars answers it

`string`

#### 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`
- **`timezone`**

  `string | null`

**Example:**

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

#### Responses

##### Status: 200 Updated

###### Content-Type: application/json

- **`data`**

  `object`, schema: `RoomBookingCalendar`

  *Schema `RoomBookingCalendar` is shown above.*

**Example:**

```json
{
  "data": {
    "id": "ical:12",
    "kind": "ical",
    "name": "Boardroom",
    "timezone": "Europe/Amsterdam",
    "capacity": null,
    "show_booking_qr": 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
    ],
    "public_booking_token": null,
    "public_booking_url": null
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Installation belongs to another user

##### Status: 422 Refused

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Room booking installation id

`string`

##### `id` required

- **In:** `path`

Calendar id, as listRoomBookingCalendars answers it

`string`

#### Responses

##### Status: 204 Removed

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Installation belongs to another user

##### Status: 422 A provider calendar

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Room booking installation id

`string`

##### `id` required

- **In:** `path`

Calendar id

`string`

#### Responses

##### Status: 200 Refetched

###### Content-Type: application/json

- **`data`**

  `object`, schema: `RoomBookingCalendar`

  *Schema `RoomBookingCalendar` is shown above.*

**Example:**

```json
{
  "data": {
    "id": "ical:12",
    "kind": "ical",
    "name": "Boardroom",
    "timezone": "Europe/Amsterdam",
    "capacity": null,
    "show_booking_qr": 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
    ],
    "public_booking_token": null,
    "public_booking_url": null
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Installation belongs to another user

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

##### Status: 429 The calendar has had its hourly refetches

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Room booking installation id

`string`

##### `id` required

- **In:** `path`

Calendar id

`string`

##### `device_id` required

- **In:** `path`

Device id

`integer`

#### Responses

##### Status: 200 Showing

###### Content-Type: application/json

- **`billing_url`**

  `string | null`
- **`data`**

  `object`, schema: `RoomBookingCalendar`

  *Schema `RoomBookingCalendar` is shown above.*
- **`subscription_required`**

  `boolean`

**Example:**

```json
{
  "data": {
    "id": "ical:12",
    "kind": "ical",
    "name": "Boardroom",
    "timezone": "Europe/Amsterdam",
    "capacity": null,
    "show_booking_qr": 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
    ],
    "public_booking_token": null,
    "public_booking_url": null
  },
  "subscription_required": true,
  "billing_url": null
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

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

##### Status: 404 Device belongs to another user

##### Status: 422 The account holds as many plugin settings as it may

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Room booking installation id

`string`

##### `id` required

- **In:** `path`

Calendar id

`string`

##### `device_id` required

- **In:** `path`

Device id

`integer`

#### Responses

##### Status: 204 Removed

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Installation belongs to another user

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Room booking installation id

`string`

##### `id` required

- **In:** `path`

Calendar id

`string`

#### Responses

##### Status: 200 The devices showing it

###### Content-Type: application/json

- **`data`**

  `array`

  **Items:**

  schema: `RoomBookingDeviceScreen` — A device showing a calendar or a collection, and the image it is showing
  - **`device_id`**

    `integer`
  - **`device_name`**

    `string`
  - **`screen`**

    `object` — null while that device has not rendered this calendar or collection yet

    **Any of:**

    schema: `Screen`
    - **`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`
    **Additional properties:**

    never (false schema)

    `null`
  **Additional properties:**

  never (false schema)

**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"
      }
    }
  ]
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

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

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Room booking installation id

`string`

##### `id` required

- **In:** `path`

Calendar id

`string`

##### `from`

- **In:** `query`

Window start; now when left out

`string`

##### `to`

- **In:** `query`

Window end; seven days after from when left out

`string`

##### `status`

- **In:** `query`

confirmed when left out:

- `confirmed`
- `cancelled`

`string`

#### Responses

##### Status: 200 Success

###### Content-Type: application/json

- **`data`**

  `array`

  **Items:**

  *Schema `RoomBooking` is shown above.*

**Example:**

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

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Installation belongs to another user

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

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Room booking installation id

`string`

##### `id` required

- **In:** `path`

Calendar id

`string`

#### 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`

**Example:**

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

#### Responses

##### Status: 200 Booked

###### Content-Type: application/json

- **`data`**

  `object`, schema: `RoomBooking`

  *Schema `RoomBooking` is shown above.*

**Example:**

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

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

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

##### Status: 404 Installation belongs to another user

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

##### Status: 422 Ends before it starts

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Room booking installation id

`string`

##### `id` required

- **In:** `path`

Calendar id

`string`

##### `booking_id` required

- **In:** `path`

Booking id

`integer`

#### Responses

##### Status: 204 Canceled

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

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

##### Status: 404 A booking already in progress

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Room booking installation id

`string`

##### `id` required

- **In:** `path`

Calendar id

`string`

##### `booking_id` required

- **In:** `path`

Booking id

`integer`

#### Responses

##### Status: 200 Ended

###### Content-Type: application/json

- **`data`**

  `object`, schema: `RoomBooking`

  *Schema `RoomBooking` is shown above.*

**Example:**

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

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

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

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

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Room booking installation id

`string`

#### Responses

##### Status: 200 Success

###### Content-Type: application/json

- **`data`**

  `array`

  **Items:**

  schema: `RoomBookingCollection`
  - **`bound_device_ids`**

    `array` — Devices showing this collection

    **Items:**

    `integer`
  - **`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` — The calendars in it

    **Items:**

    `string`
  - **`show_booking_qr`**

    `boolean`
  **Additional properties:**

  never (false schema)

**Example:**

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

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Installation belongs to another user

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Room booking installation id

`string`

#### Request Body

**Required:** `true`

##### Content-Type: application/json

- **`name` (required)**

  `string`
- **`resource_ids` (required)**

  `array` — Calendar ids, as listRoomBookingCalendars answers them

  **Items:**

  `string`
- **`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`

**Example:**

```json
{
  "name": "",
  "collection_type": "any",
  "display_layout": "list",
  "show_booking_qr": true,
  "resource_ids": [
    ""
  ]
}
```

#### Responses

##### Status: 200 Created

###### Content-Type: application/json

- **`data`**

  `object`, schema: `RoomBookingCollection`

  *Schema `RoomBookingCollection` is shown above.*

**Example:**

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

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

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

##### Status: 404 Installation belongs to another user

##### Status: 422 A layout the collection does not have

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Room booking installation id

`string`

##### `id` required

- **In:** `path`

Collection id

`integer`

#### 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`

  **Items:**

  `string`
- **`show_booking_qr`**

  `boolean`

**Example:**

```json
{
  "name": "",
  "collection_type": "any",
  "display_layout": "list",
  "show_booking_qr": true,
  "resource_ids": [
    ""
  ]
}
```

#### Responses

##### Status: 200 Updated

###### Content-Type: application/json

- **`data`**

  `object`, schema: `RoomBookingCollection`

  *Schema `RoomBookingCollection` is shown above.*

**Example:**

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

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

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

##### Status: 404 Collection belongs to another installation

##### Status: 422 Emptying the calendars is refused, and the current ones stay

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Room booking installation id

`string`

##### `id` required

- **In:** `path`

Collection id

`integer`

#### Responses

##### Status: 204 Removed

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Installation belongs to another user

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Room booking installation id

`string`

##### `id` required

- **In:** `path`

Collection id

`integer`

##### `device_id` required

- **In:** `path`

Device id

`integer`

#### Responses

##### Status: 200 Showing

###### Content-Type: application/json

- **`billing_url`**

  `string | null`
- **`data`**

  `object`, schema: `RoomBookingCollection`

  *Schema `RoomBookingCollection` is shown above.*
- **`subscription_required`**

  `boolean`

**Example:**

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

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

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

##### Status: 404 Device belongs to another user

### 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

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Room booking installation id

`string`

##### `id` required

- **In:** `path`

Collection id

`integer`

##### `device_id` required

- **In:** `path`

Device id

`integer`

#### Responses

##### Status: 204 Removed

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Installation belongs to another user

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `installation_id` required

- **In:** `path`

Room booking installation id

`string`

##### `id` required

- **In:** `path`

Collection id

`integer`

#### Responses

##### Status: 200 The devices showing it

###### Content-Type: application/json

- **`data`**

  `array`

  **Items:**

  *Schema `RoomBookingDeviceScreen` is shown above.*

**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"
      }
    }
  ]
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Collection belongs to another installation

### Prepare an installation

- **Method:** `POST`
- **Path:** `/api/plugin_installations`
- **Operation ID:** `prepareInstallation`
- **Tags:** Installations

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `Idempotency-Key` required

- **In:** `header`

`string`, format: `uuid`

##### `X-TRMNL-Configuration-Capabilities`

- **In:** `header`

`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

**Example:**

```json
{
  "kind": "official",
  "source_id": 1,
  "source_revision": "",
  "choice_id": "",
  "device_id": 1
}
```

#### Responses

##### Status: 200 Retained installation replay

###### Content-Type: application/json

- **`data`**

  `object`, schema: `PluginInstallation`
  - **`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"`

**Example:**

```json
{
  "data": {
    "id": "",
    "state": "setup_required",
    "source": {
      "kind": "",
      "id": 1,
      "revision": "",
      "choice_id": "",
      "device_id": 1
    },
    "plugin_setting_id": null,
    "expires_at": ""
  }
}
```

##### Status: 201 Created installation

###### Content-Type: application/json

- **`data`**

  `object`, schema: `PluginInstallation`

  *Schema `PluginInstallation` is shown above.*

**Example:**

```json
{
  "data": {
    "id": "",
    "state": "setup_required",
    "source": {
      "kind": "",
      "id": 1,
      "revision": "",
      "choice_id": "",
      "device_id": 1
    },
    "plugin_setting_id": null,
    "expires_at": ""
  }
}
```

##### Status: 409 Idempotency key conflict

###### Content-Type: application/json

schema: `ConfigurationError`

- **`error` (required)**

  `object`
  - **`code` (required)**

    `string`
  - **`field_errors` (required)**

    `array`

    **Items:**
    - **`code` (required)**

      `string`
    - **`message` (required)**

      `string`
    - **`path` (required)**

      `string`
  - **`message` (required)**

    `string`

**Example:**

```json
{
  "error": {
    "code": "",
    "message": "",
    "field_errors": [
      {
        "path": "",
        "code": "",
        "message": ""
      }
    ]
  }
}
```

### Read a retained installation

- **Method:** `GET`
- **Path:** `/api/plugin_installations/{id}`
- **Operation ID:** `getInstallation`
- **Tags:** Installations

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `id` required

- **In:** `path`

`string`, format: `uuid`

#### Responses

##### Status: 200 Installation

###### Content-Type: application/json

- **`data`**

  `object`, schema: `PluginInstallation`

  *Schema `PluginInstallation` is shown above.*

**Example:**

```json
{
  "data": {
    "id": "",
    "state": "setup_required",
    "source": {
      "kind": "",
      "id": 1,
      "revision": "",
      "choice_id": "",
      "device_id": 1
    },
    "plugin_setting_id": null,
    "expires_at": ""
  }
}
```

### Cancel a pending installation

- **Method:** `DELETE`
- **Path:** `/api/plugin_installations/{id}`
- **Operation ID:** `cancelInstallation`
- **Tags:** Installations

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `id` required

- **In:** `path`

`string`, format: `uuid`

#### Responses

##### Status: 204 Cancelled

### Complete an installation once

- **Method:** `POST`
- **Path:** `/api/plugin_installations/{plugin_installation_id}/completion`
- **Operation ID:** `completeInstallation`
- **Tags:** Installations

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `plugin_installation_id` required

- **In:** `path`

`string`, format: `uuid`

#### Responses

##### Status: 200 Completed installation

###### Content-Type: application/json

- **`data`**

  `object`, schema: `PluginInstallation`

  *Schema `PluginInstallation` is shown above.*

**Example:**

```json
{
  "data": {
    "id": "",
    "state": "setup_required",
    "source": {
      "kind": "",
      "id": 1,
      "revision": "",
      "choice_id": "",
      "device_id": 1
    },
    "plugin_setting_id": null,
    "expires_at": ""
  }
}
```

##### Status: 422 Required setup is incomplete

### Read pending configuration

- **Method:** `GET`
- **Path:** `/api/plugin_installations/{plugin_installation_id}/configuration`
- **Operation ID:** `getInstallationConfiguration`
- **Tags:** Installations

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `plugin_installation_id` required

- **In:** `path`

`string`, format: `uuid`

#### Responses

##### Status: 200 Pending configuration

###### Content-Type: application/json

- **`data`**

  `object`

  **One of:**

  schema: `PluginConfiguration`
  - **`actions` (required)**

    `array`

    **Items:**

    `object`
  - **`connections` (required)**

    `array`

    **Items:**

    `object`
  - **`readiness` (required)**

    `object`
    - **`blocking_paths` (required)**

      `array`

      **Items:**

      `string`
    - **`ready` (required)**

      `boolean`
    - **`reason`**

      `string | null`
  - **`required_capabilities` (required)**

    `array`

    **Items:**

    `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`

      **Items:**

      schema: `ConfigurationField`
      - **`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`**

        `object`

        **One of:**

        **Array of:**
        - **`label` (required)**

          `string`
        - **`value` (required)**

          `object`
        * **`depends_on` (required)**

          `array`

          **Items:**

          `string`
        * **`paginated` (required)**

          `boolean`
        * **`resolver_id` (required)**

          `string`
        * **`searchable` (required)**

          `boolean`
      - **`help`**

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

      `string`
    - **`title` (required)**

      `string`
  - **`sync` (required)**

    `object`, schema: `PluginSync`
    - **`source_kinds` (required)**

      `array`

      **Items:**

      `string`
    - **`upload_allowed` (required)**

      `boolean`
    - **`action_id`**

      `string | null`
    - **`reason`**

      `string | null`
  - **`values` (required)**

    `object` — Only declared nonsecret values. Secret values are never returned.
  *Schema `PluginInstallation` is shown above.*

**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
    }
  }
}
```

### Save pending configuration

- **Method:** `PATCH`
- **Path:** `/api/plugin_installations/{plugin_installation_id}/configuration`
- **Operation ID:** `updateInstallationConfiguration`
- **Tags:** Installations

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `plugin_installation_id` required

- **In:** `path`

`string`, format: `uuid`

#### Request Body

**Required:** `true`

##### Content-Type: application/json

schema: `ConfigurationWrite`

- **`changes` (required)**

  `array`

  **Items:**

  schema: `ConfigurationChange`
  - **`op` (required)**

    `string`, possible values: `"set", "unset", "clear_secret"`
  - **`path` (required)**

    `string`, pattern: `^/values/`
  - **`value`**

    `object`
  **Additional properties:**

  never (false schema)
  - Max items: `200`
- **`revision` (required)**

  `string`

**Example:**

```json
{
  "revision": "",
  "changes": [
    {
      "op": "set",
      "path": "",
      "value": null
    }
  ]
}
```

#### Responses

##### Status: 200 Updated pending configuration

###### Content-Type: application/json

- **`data`**

  `object`, schema: `PluginConfiguration`

  *Schema `PluginConfiguration` is shown above.*

**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
    }
  }
}
```

### Evaluate pending configuration without saving

- **Method:** `POST`
- **Path:** `/api/plugin_installations/{plugin_installation_id}/configuration/evaluation`
- **Operation ID:** `evaluateInstallationConfiguration`
- **Tags:** Installations

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `plugin_installation_id` required

- **In:** `path`

`string`, format: `uuid`

#### Request Body

**Required:** `true`

##### Content-Type: application/json

- **`draft` (required)**

  `array`

  **Items:**

  *Schema `ConfigurationChange` is shown above.*
  - Max items: `200`
- **`revision` (required)**

  `string`

**Example:**

```json
{
  "revision": "",
  "draft": [
    {
      "op": "set",
      "path": "",
      "value": null
    }
  ]
}
```

#### Responses

##### Status: 200 Evaluated configuration

###### Content-Type: application/json

- **`data`**

  `object`, schema: `PluginConfiguration`

  *Schema `PluginConfiguration` is shown above.*

**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
    }
  }
}
```

### Resolve pending configuration choices

- **Method:** `POST`
- **Path:** `/api/plugin_installations/{plugin_installation_id}/configuration/choices/{resolver_id}`
- **Operation ID:** `getInstallationConfigurationChoices`
- **Tags:** Installations

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `plugin_installation_id` required

- **In:** `path`

`string`, format: `uuid`

##### `resolver_id` required

- **In:** `path`

`string`

#### Request Body

**Required:** `true`

##### Content-Type: application/json

- **`draft` (required)**

  `array`

  **Items:**

  *Schema `ConfigurationChange` is shown above.*
  - Max items: `200`
- **`revision` (required)**

  `string`
- **`cursor`**

  `string`
- **`query`**

  `string`, maxLength: `200`

**Example:**

```json
{
  "revision": "",
  "draft": [
    {
      "op": "set",
      "path": "",
      "value": null
    }
  ],
  "query": "",
  "cursor": ""
}
```

#### Responses

##### Status: 404 Unknown resolver

### Preview installation removal

- **Method:** `GET`
- **Path:** `/api/plugin_settings/{plugin_setting_id}/removal_preview`
- **Operation ID:** `getPluginSettingRemovalPreview`
- **Tags:** Plugin Settings

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `plugin_setting_id` required

- **In:** `path`

`integer`

#### Responses

##### Status: 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`

**Example:**

```json
{
  "data": {
    "revision": "",
    "placements": [
      {
        "id": 1,
        "device_id": 1,
        "device_name": "",
        "impact": "removed"
      }
    ]
  }
}
```

### Read supported Companion contract versions

- **Method:** `GET`
- **Path:** `/api/capabilities`
- **Operation ID:** `getCompanionCapabilities`
- **Tags:** Companion

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Responses

##### Status: 200 Exact supported versions; missing features are unavailable

###### Content-Type: application/json

- **`data`**

  `object`

  **Additional properties:**

  `integer`

**Example:**

```json
{
  "data": {
    "additionalProperty": 1
  }
}
```

### Unlink an owned physical device

- **Method:** `DELETE`
- **Path:** `/api/devices/{device_id}/association`
- **Operation ID:** `deleteDeviceAssociation`
- **Tags:** Devices

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `device_id` required

- **In:** `path`

`integer`

#### Responses

##### Status: 204 Device unlinked; the device record remains

### Read preset layouts and compatible plugin settings

- **Method:** `GET`
- **Path:** `/api/devices/{device_id}/mashups/options`
- **Operation ID:** `getMashupOptions`
- **Tags:** Mashups

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `device_id` required

- **In:** `path`

`integer`

##### `mashup_id`

- **In:** `query`

`integer`

#### Responses

##### Status: 200 Options for this device

###### Content-Type: application/json

- **`data`**

  `object`, schema: `MashupOptions`
  - **`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`

      **Items:**

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

      `integer`
    - **`name` (required)**

      `string`
    - **`plugin_name` (required)**

      `string`

**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": [
          ""
        ]
      }
    ]
  }
}
```

### Read the current rendered playlist image

- **Method:** `GET`
- **Path:** `/api/playlists/items/{item_id}/preview`
- **Operation ID:** `getPlaylistItemPreview`
- **Tags:** Playlists

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `item_id` required

- **In:** `path`

`integer`

#### Responses

##### Status: 200 Private image; no storage redirect or device check-in

### 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

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `plugin_setting_id` required

- **In:** `path`

`integer`

##### `connection_id` required

- **In:** `path`

`string`

#### Request Body

**Required:** `true`

##### Content-Type: application/json

- **`action_id` (required)**

  `string`
- **`callback_id` (required)**

  `string`, possible values: `"companion"`
- **`revision` (required)**

  `string`

**Example:**

```json
{
  "action_id": "",
  "revision": "",
  "callback_id": "companion"
}
```

#### Responses

##### Status: 201 Created; launch\_url is returned only on creation

###### Content-Type: application/json

- **`data`**

  `object`, schema: `PluginConnectionAttempt`
  - **`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)**

      `object`

      **One of:**

      `integer`

      `string`, format: `uuid`
    - **`kind` (required)**

      `string`, possible values: `"instance", "installation"`
  - **`launch_url`**

    `string`, format: `uri`
  **Additional properties:**

  never (false schema)

**Example:**

```json
{
  "data": {
    "id": "",
    "state": "pending",
    "expires_at": "",
    "connection_id": "",
    "launch_url": "",
    "target": {
      "kind": "instance",
      "id": 1
    }
  }
}
```

### Disconnect only this target provider grant

- **Method:** `DELETE`
- **Path:** `/api/plugin_settings/{plugin_setting_id}/connections/{connection_id}`
- **Operation ID:** `disconnectPluginSettingsConnection`
- **Tags:** Provider authorization

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `plugin_setting_id` required

- **In:** `path`

`integer`

##### `connection_id` required

- **In:** `path`

`string`

#### Request Body

**Required:** `true`

##### Content-Type: application/json

- **`revision` (required)**

  `string`

**Example:**

```json
{
  "revision": ""
}
```

#### Responses

##### Status: 200 Configuration after disconnect

###### Content-Type: application/json

- **`data`**

  `object`, schema: `PluginConfiguration`

  *Schema `PluginConfiguration` is shown above.*

**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
    }
  }
}
```

### 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

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `plugin_installation_id` required

- **In:** `path`

`string`

##### `connection_id` required

- **In:** `path`

`string`

#### Request Body

**Required:** `true`

##### Content-Type: application/json

- **`action_id` (required)**

  `string`
- **`callback_id` (required)**

  `string`, possible values: `"companion"`
- **`revision` (required)**

  `string`

**Example:**

```json
{
  "action_id": "",
  "revision": "",
  "callback_id": "companion"
}
```

#### Responses

##### Status: 201 Created; launch\_url is returned only on creation

###### Content-Type: application/json

- **`data`**

  `object`, schema: `PluginConnectionAttempt`

  *Schema `PluginConnectionAttempt` is shown above.*

**Example:**

```json
{
  "data": {
    "id": "",
    "state": "pending",
    "expires_at": "",
    "connection_id": "",
    "launch_url": "",
    "target": {
      "kind": "instance",
      "id": 1
    }
  }
}
```

### Disconnect only this target provider grant

- **Method:** `DELETE`
- **Path:** `/api/plugin_installations/{plugin_installation_id}/connections/{connection_id}`
- **Operation ID:** `disconnectPluginInstallationsConnection`
- **Tags:** Provider authorization

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `plugin_installation_id` required

- **In:** `path`

`string`

##### `connection_id` required

- **In:** `path`

`string`

#### Request Body

**Required:** `true`

##### Content-Type: application/json

- **`revision` (required)**

  `string`

**Example:**

```json
{
  "revision": ""
}
```

#### Responses

##### Status: 200 Configuration after disconnect

###### Content-Type: application/json

- **`data`**

  `object`, schema: `PluginConfiguration`

  *Schema `PluginConfiguration` is shown above.*

**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
    }
  }
}
```

### Read authoritative owned attempt state

- **Method:** `GET`
- **Path:** `/api/plugin_connection_attempts/{id}`
- **Operation ID:** `getConnectionAttempt`
- **Tags:** Provider authorization

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `id` required

- **In:** `path`

`string`, format: `uuid`

#### Responses

##### Status: 200 Attempt state without launch ticket or provider data

###### Content-Type: application/json

- **`data`**

  `object`, schema: `PluginConnectionAttempt`

  *Schema `PluginConnectionAttempt` is shown above.*

**Example:**

```json
{
  "data": {
    "id": "",
    "state": "pending",
    "expires_at": "",
    "connection_id": "",
    "launch_url": "",
    "target": {
      "kind": "instance",
      "id": 1
    }
  }
}
```

### Cancel an owned pending or exchanging attempt

- **Method:** `DELETE`
- **Path:** `/api/plugin_connection_attempts/{id}`
- **Operation ID:** `cancelConnectionAttempt`
- **Tags:** Provider authorization

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `id` required

- **In:** `path`

`string`, format: `uuid`

#### Responses

##### Status: 200 Authoritative state; an already connected attempt remains connected

###### Content-Type: application/json

- **`data`**

  `object`, schema: `PluginConnectionAttempt`

  *Schema `PluginConnectionAttempt` is shown above.*

**Example:**

```json
{
  "data": {
    "id": "",
    "state": "pending",
    "expires_at": "",
    "connection_id": "",
    "launch_url": "",
    "target": {
      "kind": "instance",
      "id": 1
    }
  }
}
```

### 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

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `id` required

- **In:** `path`

`string`, format: `uuid`

#### Request Body

**Required:** `true`

##### Content-Type: application/json

- **`confirmation` (required)**

  `string`

**Example:**

```json
{
  "confirmation": ""
}
```

#### Responses

##### Status: 200 Connected; the callback confirmation works once and only for the user who started the attempt

###### Content-Type: application/json

- **`data`**

  `object`, schema: `PluginConnectionAttempt`

  *Schema `PluginConnectionAttempt` is shown above.*

**Example:**

```json
{
  "data": {
    "id": "",
    "state": "pending",
    "expires_at": "",
    "connection_id": "",
    "launch_url": "",
    "target": {
      "kind": "instance",
      "id": 1
    }
  }
}
```

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "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

**Example:**

```json
{
  "friendly_id": ""
}
```

#### Responses

##### Status: 200 Claimed

###### Content-Type: application/json

- **`data`**

  `object`, schema: `Device`
  - **`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`
  **Additional properties:**

  never (false schema)

**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,
    "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"
      ]
    }
  }
}
```

##### Status: 400 friendly\_id is missing

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 422 A concurrent claim already took the device

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 429 Too many failed claims from this account or address

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

### List my devices

- **Method:** `GET`
- **Path:** `/api/devices`
- **Operation ID:** `listDevices`
- **Tags:** Devices

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Responses

##### Status: 200 Success

###### Content-Type: application/json

- **`data`**

  `array`

  **Items:**

  *Schema `Device` is shown above.*

**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,
      "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"
        ]
      }
    }
  ]
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `device_id` required

- **In:** `path`

Device id

`integer`

#### Responses

##### Status: 200 The gaps in the schedule

###### Content-Type: application/json

- **`data`**

  `object`
  - **`gaps`**

    `array`

    **Items:**
    - **`covered`**

      `array` — \[start\_minute, end\_minute] windows the playlist shows

      **Items:**

      **Array of:**

      `integer`
    - **`uncovered`**

      `array` — \[start\_minute, end\_minute] windows nothing is scheduled

      **Items:**

      **Array of:**

      `integer`
    - **`week_days`**

      `array` — 0 is Sunday through 6 is Saturday

      **Items:**

      `integer`
  - **`period_count`**

    `integer` — How many uncovered windows across the week

**Example:**

```json
{
  "data": {
    "gaps": [
      {
        "week_days": [
          1
        ],
        "covered": [
          [
            1
          ]
        ],
        "uncovered": [
          [
            1
          ]
        ]
      }
    ],
    "period_count": 1
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Device belongs to another user

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `device_id` required

- **In:** `path`

Device id

`integer`

#### Responses

##### Status: 200 Backoff cleared

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Device belongs to another user

### 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).

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `device_id` required

- **In:** `path`

Device id

`integer`

##### `hours`

- **In:** `query`

How far ahead to simulate, 1 to 48. Defaults to 24

`integer`

#### Responses

##### Status: 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

**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
      }
    ]
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Device belongs to another user

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `device_id` required

- **In:** `path`

Device id

`integer`

#### Responses

##### Status: 202 Identification requested

###### Content-Type: application/json

- **`data` (required)**

  `object`
  - **`success` (required)**

    `boolean`

**Example:**

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

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Device not found in your account

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 422 Device could not be updated

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `device_id` required

- **In:** `path`

Device id

`integer`

#### Request Body

**Required:** `true`

##### Content-Type: application/json

- **`master_friendly_id` (required)**

  `string` — friendly\_id of the sharable device to mirror

**Example:**

```json
{
  "master_friendly_id": ""
}
```

#### Responses

##### Status: 200 Mirroring the master

##### Status: 400 master\_friendly\_id is missing

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Device belongs to another user

##### Status: 422 The friendly ID is not one this device can mirror

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `device_id` required

- **In:** `path`

Device id

`integer`

#### Responses

##### Status: 204 Mirroring removed

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Device belongs to another user

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `device_id` required

- **In:** `path`

Device id

`integer`

#### Responses

##### Status: 200 Resynced

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Device belongs to another user

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `device_id` required

- **In:** `path`

Source device id

`integer`

#### Request Body

**Required:** `true`

##### Content-Type: application/json

- **`target_device_id` (required)**

  `integer` — Destination device id

**Example:**

```json
{
  "target_device_id": 1
}
```

#### Responses

##### Status: 200 Playlist copied and refreshes queued

###### Content-Type: application/json

- **`data` (required)**

  `object`
  - **`success` (required)**

    `boolean`

**Example:**

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

##### Status: 400 Destination device id is missing or is not a scalar

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Source or destination device not found in your account

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 422 Source and destination are the same, or an item is invalid

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

### List the playlist for one device

- **Method:** `GET`
- **Path:** `/api/devices/{device_id}/playlist_items`
- **Operation ID:** `listDevicePlaylist`
- **Tags:** Playlists

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `device_id` required

- **In:** `path`

Device id

`integer`

#### Responses

##### Status: 200 Reads as empty for a device belonging to someone else

###### Content-Type: application/json

- **`data`**

  `array`

  **Items:**

  schema: `PlaylistItem`
  - **`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`**

    `object`, schema: `PluginSetting`
    - **`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`**

      `object`, schema: `PluginSync`

      *Schema `PluginSync` is shown above.*
    **Additional properties:**

    never (false schema)
  - **`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`
  **Additional properties:**

  never (false schema)

**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"
    }
  ]
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

### Add a plugin instance to a device playlist

- **Method:** `POST`
- **Path:** `/api/devices/{device_id}/playlist_items`
- **Operation ID:** `addDevicePlaylistItem`
- **Tags:** Playlists

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `device_id` required

- **In:** `path`

Device id

`integer`

#### Request Body

**Required:** `true`

##### Content-Type: application/json

- **`plugin_setting_id` (required)**

  `integer` — Plugin setting id

**Example:**

```json
{
  "plugin_setting_id": 1
}
```

#### Responses

##### Status: 200 Added to the playlist

###### Content-Type: application/json

- **`data`**

  `object`, schema: `PlaylistItem`

  *Schema `PlaylistItem` is shown above.*

**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"
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Device belongs to another user

### Reorder a device playlist

- **Method:** `PUT`
- **Path:** `/api/devices/{device_id}/playlist_items/order`
- **Operation ID:** `reorderDevicePlaylist`
- **Tags:** Playlists

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `device_id` required

- **In:** `path`

Device id

`integer`

#### Request Body

**Required:** `true`

##### Content-Type: application/json

- **`playlist_item_ids` (required)**

  `array`

  **Items:**

  `integer`

**Example:**

```json
{
  "playlist_item_ids": [
    1
  ]
}
```

#### Responses

##### Status: 200 Reordered

###### Content-Type: application/json

- **`data`**

  `object`
  - **`success`**

    `boolean`

**Example:**

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

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Device belongs to another user

##### Status: 422 Ids do not name every item on the device

### Get the data of a device

- **Method:** `GET`
- **Path:** `/api/devices/{id}`
- **Operation ID:** `getDevice`
- **Tags:** Devices

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `id` required

- **In:** `path`

Device ID

`integer`

#### Responses

##### Status: 200 Success

###### Content-Type: application/json

- **`data`**

  `object`, schema: `Device`

  *Schema `Device` is shown above.*

**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,
    "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"
      ]
    }
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Not found

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `id` required

- **In:** `path`

Device ID

`integer`

#### Request Body

##### Content-Type: application/json

- **`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`

**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": "",
  "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

##### Status: 200 Updated

###### Content-Type: application/json

- **`data`**

  `object`, schema: `Device`

  *Schema `Device` is shown above.*

**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,
    "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"
      ]
    }
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 422 Unprocessable Entity

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 429 An appearance change renders every playlist item again, past the hourly render allowance

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `device_id` required

- **In:** `path`

Device id

`integer`

#### Responses

##### Status: 200 Cleared

###### Content-Type: application/json

- **`data`**

  `object`
  - **`success`**

    `boolean`

**Example:**

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

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Device belongs to another user

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `device_id` required

- **In:** `path`

Device id

`integer`

##### `level`

- **In:** `query`

:

- `debug`
- `info`
- `warn`
- `error`

`string`

##### `source`

- **In:** `query`

`string`

##### `limit`

- **In:** `query`

1 to 100, default 20

`integer`

##### `before_ts`

- **In:** `query`

`string`

##### `before_event_id`

- **In:** `query`

`string`

#### Responses

##### Status: 200 Scopes the query to the device within the account

###### Content-Type: application/json

- **`data`**

  `object`
  - **`logs`**

    `array`

    **Items:**

    `object`
  - **`more`**

    `boolean`
  - **`oldest_cursor`**

    `object | null`
    - **`event_id`**

      `string`
    - **`ts`**

      `string`

**Example:**

```json
{
  "data": {
    "logs": [
      {}
    ],
    "more": true,
    "oldest_cursor": {
      "ts": "",
      "event_id": ""
    }
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Device belongs to another user

##### Status: 422 A level the logs do not have

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `device_id` required

- **In:** `path`

Device id

`integer`

#### 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`

**Example:**

```json
{
  "layout": "1x1",
  "grid_config": [
    {
      "pos": "",
      "col": 1,
      "row": 1,
      "cs": 1,
      "rs": 1
    }
  ],
  "contents": {
    "additionalProperty": 1
  }
}
```

#### Responses

##### Status: 200 Added to the playlist

###### Content-Type: application/json

- **`data`**

  `object`, schema: `PlaylistItem`

  *Schema `PlaylistItem` is shown above.*

**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"
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Device belongs to another user

##### Status: 422 A layout that does not exist

### Read a mashup and its sections

- **Method:** `GET`
- **Path:** `/api/mashups/{id}`
- **Operation ID:** `getMashup`
- **Tags:** Mashups

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `id` required

- **In:** `path`

Mashup id

`integer`

#### Responses

##### Status: 200 Success

###### Content-Type: application/json

- **`data`**

  `object`, schema: `Mashup`
  - **`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` — The sections the layout has

    **Items:**

    `string`
  - **`updated_at`**

    `string`, format: `date_time`
  - **`version`**

    `string`
  **Additional properties:**

  never (false schema)

**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"
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Mashup belongs to another user

### Change the sections of a mashup

- **Method:** `PATCH`
- **Path:** `/api/mashups/{id}`
- **Operation ID:** `updateMashup`
- **Tags:** Mashups

Positions left out keep their plugin setting.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `id` required

- **In:** `path`

Mashup id

`integer`

#### Request Body

**Required:** `true`

##### Content-Type: application/json

- **`contents`**

  `object` — Plugin setting id by position

  **Additional properties:**

  `integer`
- **`health_notification_enabled`**

  `boolean`

**Example:**

```json
{
  "contents": {
    "additionalProperty": 1
  },
  "health_notification_enabled": true
}
```

#### Responses

##### Status: 200 Updated

###### Content-Type: application/json

- **`data`**

  `object`, schema: `Mashup`

  *Schema `Mashup` is shown above.*

**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"
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Mashup belongs to another user

##### Status: 422 A position the layout does not have

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `id` required

- **In:** `path`

Mashup id

`integer`

#### Responses

##### Status: 200 Healthy again

###### Content-Type: application/json

- **`data`**

  `object`
  - **`success`**

    `boolean`

**Example:**

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

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Mashup belongs to another user

### Get my user data

- **Method:** `GET`
- **Path:** `/api/me`
- **Operation ID:** `getMe`
- **Tags:** Users

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Responses

##### Status: 200 Success

###### Content-Type: application/json

- **`data`**

  `object`, schema: `User`
  - **`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`
  **Additional properties:**

  never (false schema)

**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"
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "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

**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

##### Status: 200 Takes a Rails zone name as well

###### Content-Type: application/json

- **`data`**

  `object`, schema: `User`

  *Schema `User` is shown above.*

**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"
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 422 A locale the app does not have

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Responses

##### Status: 200 Success

###### Content-Type: application/json

- **`data`**

  `array`

  **Items:**

  schema: `MyPlugin`
  - **`category`**

    `array`

    **Items:**

    `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`
  **Additional properties:**

  never (false schema)

**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": ""
    }
  ]
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Request Body

**Required:** `true`

##### Content-Type: application/json

schema: `MyPluginParams`

- **`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`

**Additional properties:**

never (false schema)

**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

##### Status: 200 Created

###### Content-Type: application/json

- **`data`**

  `object`, schema: `MyPlugin`

  *Schema `MyPlugin` is shown above.*

**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": ""
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 403 No developer edition device on the account

##### Status: 422 A refresh rate the plugin API does not offer

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `id` required

- **In:** `path`

Plugin id

`integer`

#### Request Body

**Required:** `true`

##### Content-Type: application/json

*Schema `MyPluginParams` is shown above.*

**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

##### Status: 200 Updated

###### Content-Type: application/json

- **`data`**

  `object`, schema: `MyPlugin`

  *Schema `MyPlugin` is shown above.*

**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": ""
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Plugin belongs to another user

##### Status: 422 A required field is blanked

### Start Google Photos selection

- **Method:** `POST`
- **Path:** `/api/plugin_settings/{plugin_setting_id}/configuration/photo_selection`
- **Operation ID:** `startPhotoSelection`
- **Tags:** Plugin Configuration

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `plugin_setting_id` required

- **In:** `path`

`integer`

#### Request Body

**Required:** `true`

##### Content-Type: application/json

- **`revision` (required)**

  `string`

**Example:**

```json
{
  "revision": ""
}
```

#### Responses

##### Status: 200 Picker URI and account-bound ticket

###### Content-Type: application/json

- **`data`**

  `object`
  - **`picker_uri` (required)**

    `string`
  - **`revision` (required)**

    `string`
  - **`ticket` (required)**

    `string`

**Example:**

```json
{
  "data": {
    "picker_uri": "",
    "ticket": "",
    "revision": ""
  }
}
```

### Confirm Google Photos selection

- **Method:** `PATCH`
- **Path:** `/api/plugin_settings/{plugin_setting_id}/configuration/photo_selection`
- **Operation ID:** `completePhotoSelection`
- **Tags:** Plugin Configuration

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `plugin_setting_id` required

- **In:** `path`

`integer`

#### Request Body

**Required:** `true`

##### Content-Type: application/json

- **`revision` (required)**

  `string`
- **`ticket` (required)**

  `string`

**Example:**

```json
{
  "revision": "",
  "ticket": ""
}
```

#### Responses

##### Status: 200 Updated configuration

###### Content-Type: application/json

- **`data`**

  `object`, schema: `PluginConfiguration`

  *Schema `PluginConfiguration` is shown above.*

**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
    }
  }
}
```

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `device_id` required

- **In:** `path`

Device id

`integer`

#### Request Body

**Required:** `true`

##### Content-Type: application/json

- **`action_type` (required)**

  `string`, possible values: `"hide", "show"`
- **`playlist_item_ids` (required)**

  `array`

  **Items:**

  `integer`

**Example:**

```json
{
  "action_type": "hide",
  "playlist_item_ids": [
    1
  ]
}
```

#### Responses

##### Status: 200 Applied, answering the items in the submitted order

###### Content-Type: application/json

- **`data`**

  `array`

  **Items:**

  *Schema `PlaylistItem` is shown above.*

**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"
    }
  ]
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Device belongs to another user

##### Status: 422 No named id belongs to the device

### Read when a playlist item is allowed to display

- **Method:** `GET`
- **Path:** `/api/playlists/items/{item_id}/schedule`
- **Operation ID:** `getPlaylistItemSchedule`
- **Tags:** Playlists

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `item_id` required

- **In:** `path`

Playlist item id

`integer`

#### Responses

##### Status: 200 Reports an item with no windows as always active

###### Content-Type: application/json

- **`data`**

  `object`
  - **`always_active`**

    `boolean` — True when no window is set, so the item always displays
  - **`week_schedules`**

    `array`

    **Items:**
    - **`end_time`**

      `string`
    - **`start_time`**

      `string`
    - **`week_days`**

      `array` — 0 is Sunday through 6 is Saturday

      **Items:**

      `integer`

**Example:**

```json
{
  "data": {
    "week_schedules": [
      {
        "week_days": [
          1
        ],
        "start_time": "09:00",
        "end_time": "17:00"
      }
    ],
    "always_active": false
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Playlist item belonging to another user

### Replace when a playlist item is allowed to display

- **Method:** `PUT`
- **Path:** `/api/playlists/items/{item_id}/schedule`
- **Operation ID:** `replacePlaylistItemSchedule`
- **Tags:** Playlists

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `item_id` required

- **In:** `path`

Playlist item id

`integer`

#### 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` — 0 is Sunday through 6 is Saturday

    **Items:**

    `integer`

**Example:**

```json
{
  "week_schedules": [
    {
      "week_days": [
        1
      ],
      "start_time": "",
      "end_time": ""
    }
  ]
}
```

#### Responses

##### Status: 200 An empty array clears the schedule, so the item always displays

###### Content-Type: application/json

- **`data`**

  `object`
  - **`always_active`**

    `boolean` — True when no window is set, so the item always displays
  - **`week_schedules`**

    `array`

    **Items:**
    - **`end_time`**

      `string`
    - **`start_time`**

      `string`
    - **`week_days`**

      `array` — 0 is Sunday through 6 is Saturday

      **Items:**

      `integer`

**Example:**

```json
{
  "data": {
    "week_schedules": [
      {
        "week_days": [
          1
        ],
        "start_time": "09:00",
        "end_time": "17:00"
      }
    ],
    "always_active": false
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Playlist item belonging to another user

##### Status: 422 A time that is not HH:MM is refused

### List my playlist items

- **Method:** `GET`
- **Path:** `/api/playlists/items`
- **Operation ID:** `listPlaylistItems`
- **Tags:** Playlists

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Responses

##### Status: 200 Success

###### Content-Type: application/json

- **`data`**

  `array`

  **Items:**

  *Schema `PlaylistItem` is shown above.*

**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"
    }
  ]
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `id` required

- **In:** `path`

ID of the playlist item

`integer`

#### 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`

**Example:**

```json
{
  "visible": true,
  "palette_id": "bw",
  "font_family": "classic",
  "text_scale": "small",
  "theme": "dark"
}
```

#### Responses

##### Status: 200 item that is not an object is read as no change

###### Content-Type: application/json

- **`data`**

  `object`, schema: `PlaylistItem`

  *Schema `PlaylistItem` is shown above.*

**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"
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 422 An appearance value the device cannot show

### Remove a playlist item

- **Method:** `DELETE`
- **Path:** `/api/playlists/items/{id}`
- **Operation ID:** `deletePlaylistItem`
- **Tags:** Playlists

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `id` required

- **In:** `path`

ID of the playlist item

`integer`

#### Responses

##### Status: 200 Removed

###### Content-Type: application/json

- **`data`**

  `object`
  - **`success`**

    `boolean`

**Example:**

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

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Playlist item belonging to another user

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `id` required

- **In:** `path`

Playlist item id

`integer`

#### Responses

##### Status: 200 The new playlist item

###### Content-Type: application/json

- **`data`**

  `object`, schema: `PlaylistItem`

  *Schema `PlaylistItem` is shown above.*

**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"
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Playlist item belongs to another user

##### Status: 422 A placeholder has nothing to copy until it is configured

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `id` required

- **In:** `path`

Plugin setting ID

`integer`

#### Responses

##### Status: 200 Success

##### Status: 404 Not Found

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 422 Unprocessable Entity

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

### Upload a plugin setting archive

- **Method:** `POST`
- **Path:** `/api/plugin_settings/{id}/archive`
- **Operation ID:** `uploadPluginSettingArchive`
- **Tags:** Plugin Settings

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `id` required

- **In:** `path`

Plugin setting ID

`integer`

#### Request Body

Plugin setting archive file

**Required:** `true`

##### Content-Type: multipart/form-data

`null`

**Example:**

```json
null
```

#### Responses

##### Status: 200 Success

###### Content-Type: application/json

- **`data`**

  `object`, schema: `PluginSettingArchive`
  - **`settings_yaml`**

    `string` — YAML settings file

**Example:**

```json
{
  "data": {
    "settings_yaml": ""
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 422 Unprocessable Entity

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

### Evaluate a typed draft without saving

- **Method:** `POST`
- **Path:** `/api/plugin_settings/{plugin_setting_id}/configuration/evaluation`
- **Operation ID:** `evaluatePluginConfiguration`
- **Tags:** Plugin Configuration

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `plugin_setting_id` required

- **In:** `path`

`integer`

#### Request Body

**Required:** `true`

##### Content-Type: application/json

- **`draft` (required)**

  `array`

  **Items:**

  *Schema `ConfigurationChange` is shown above.*
  - Max items: `200`
- **`revision` (required)**

  `string`

**Example:**

```json
{
  "revision": "",
  "draft": [
    {
      "op": "set",
      "path": "",
      "value": null
    }
  ]
}
```

#### Responses

##### Status: 200 Evaluated configuration and safe field errors

###### Content-Type: application/json

- **`data`**

  `object`, schema: `PluginConfiguration`

  *Schema `PluginConfiguration` is shown above.*

**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
    }
  }
}
```

##### Status: 409 Configuration changed

### Read owner-bound remote choices

- **Method:** `POST`
- **Path:** `/api/plugin_settings/{plugin_setting_id}/configuration/choices/{resolver_id}`
- **Operation ID:** `getPluginConfigurationChoices`
- **Tags:** Plugin Configuration

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `plugin_setting_id` required

- **In:** `path`

`integer`

##### `resolver_id` required

- **In:** `path`

`string`

#### Request Body

**Required:** `true`

##### Content-Type: application/json

- **`draft` (required)**

  `array`

  **Items:**

  *Schema `ConfigurationChange` is shown above.*
  - Max items: `200`
- **`revision` (required)**

  `string`
- **`cursor`**

  `string | null`
- **`query`**

  `string | null`, maxLength: `200`

**Example:**

```json
{
  "revision": "",
  "draft": [
    {
      "op": "set",
      "path": "",
      "value": null
    }
  ],
  "query": null,
  "cursor": null
}
```

#### Responses

##### Status: 404 Unknown resolver for owned target

### Read safe typed configuration

- **Method:** `GET`
- **Path:** `/api/plugin_settings/{plugin_setting_id}/configuration`
- **Operation ID:** `getPluginConfiguration`
- **Tags:** Plugin Configuration

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `plugin_setting_id` required

- **In:** `path`

`integer`

#### Responses

##### Status: 200 Typed configuration with write-only secrets omitted

###### Content-Type: application/json

- **`data` (required)**

  `object`, schema: `PluginConfiguration`

  *Schema `PluginConfiguration` is shown above.*

**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
    }
  }
}
```

##### Status: 401 Account bearer token required

###### Content-Type: application/json

*Schema `ConfigurationError` is shown above.*

**Example:**

```json
{
  "error": {
    "code": "",
    "message": "",
    "field_errors": [
      {
        "path": "",
        "code": "",
        "message": ""
      }
    ]
  }
}
```

##### Status: 404 Absent or unowned instance

### Apply atomic typed changes

- **Method:** `PATCH`
- **Path:** `/api/plugin_settings/{plugin_setting_id}/configuration`
- **Operation ID:** `updatePluginConfiguration`
- **Tags:** Plugin Configuration

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `plugin_setting_id` required

- **In:** `path`

`integer`

#### Request Body

**Required:** `true`

##### Content-Type: application/json

*Schema `ConfigurationWrite` is shown above.*

**Example:**

```json
{
  "revision": "",
  "changes": [
    {
      "op": "set",
      "path": "",
      "value": null
    }
  ]
}
```

#### Responses

##### Status: 200 Configuration saved

##### Status: 409 Stale revision

##### Status: 422 Invalid typed change; no fields saved

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "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

**Example:**

```json
{
  "custom_fields": ""
}
```

#### Responses

##### Status: 200 Valid

###### Content-Type: application/json

- **`data`**

  `object`
  - **`errors`**

    `array`

    **Items:**

    `string`
  - **`valid`**

    `boolean`

**Example:**

```json
{
  "data": {
    "valid": true,
    "errors": [
      ""
    ]
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 422 custom\_fields that is not a string

### 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

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `id` required

- **In:** `path`

Plugin setting ID or UUID

`string`

#### Responses

##### Status: 200 Success

###### Content-Type: application/json

- **`data` (required)**

  `object` — The merge variables the instance currently renders from

**Example:**

```json
{
  "data": {}
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Not found

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 422 A private or global plugin has no data endpoint

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

### Update data for a webhook plugin setting

- **Method:** `POST`
- **Path:** `/api/plugin_settings/{id}/data`
- **Operation ID:** `updatePluginSettingData`
- **Tags:** Plugin Settings

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `id` required

- **In:** `path`

Plugin setting ID or UUID

`string`

#### Request Body

The value of `merge_variables` must be a JSON object

**Required:** `true`

##### Content-Type: application/json

- **`merge_variables` (required)**

  `object`

**Additional properties:**

never (false schema)

**Example:**

```json
{
  "merge_variables": null
}
```

#### Responses

##### Status: 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

**Example:**

```json
{
  "error": null,
  "merge_variables": null,
  "processing": "serverless"
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Not found

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 422 Data cannot be modified

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `id` required

- **In:** `path`

Plugin setting id or uuid

`string`

#### Responses

##### Status: 202 Generation queued

###### Content-Type: application/json

- **`data`**

  `object`
  - **`status`**

    `string`, possible values: `"generating"`

**Example:**

```json
{
  "data": {
    "status": "generating"
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Plugin setting belongs to another user

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `id` required

- **In:** `path`

Plugin setting id or uuid

`string`

#### Responses

##### Status: 204 Featured image removed

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Plugin setting belongs to another user

##### Status: 422 No featured image to remove

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `plugin_setting_id` required

- **In:** `path`

Plugin setting id

`integer`

#### Responses

##### Status: 200 Reads another author's published recipe without its secrets

###### Content-Type: application/json

- **`data`**

  `object`
  - **`files`**

    `object`

    **Additional properties:**

    `string`

**Example:**

```json
{
  "data": {
    "files": {
      "additionalProperty": ""
    }
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Not the caller's and not a published recipe

##### Status: 422 A plugin type that has no files

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `plugin_setting_id` required

- **In:** `path`

Plugin setting id

`integer`

#### Request Body

**Required:** `true`

##### Content-Type: application/json

- **`files` (required)**

  `object`

  **Additional properties:**

  `string`

**Example:**

```json
{
  "files": {
    "settings.yml": "---\nstrategy: static\nname: Mine\n",
    "full.liquid": "<div>{{ title }}</div>"
  }
}
```

#### Responses

##### Status: 200 Persists a settings-only import, with no markup file to save through

###### Content-Type: application/json

- **`data`**

  `object`, schema: `PluginSettingArchive`

  *Schema `PluginSettingArchive` is shown above.*

**Example:**

```json
{
  "data": {
    "settings_yaml": ""
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Plugin setting belongs to another user

##### Status: 422 settings.yml missing or malformed

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Parameters

##### `id` required

- **In:** `path`

Plugin setting id or uuid

`string`

#### Request Body

**Required:** `true`

##### Content-Type: application/json

- **`image_base64` (required)**

  `string` — The image bytes, base64 encoded

**Example:**

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

##### Content-Type: image/png

- **`image_base64` (required)**

  `string` — The image bytes, base64 encoded

**Example:**

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

##### Content-Type: image/jpeg

- **`image_base64` (required)**

  `string` — The image bytes, base64 encoded

**Example:**

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

##### Content-Type: image/webp

- **`image_base64` (required)**

  `string` — The image bytes, base64 encoded

**Example:**

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

#### Responses

##### Status: 200 Full-color image on a color device

###### Content-Type: application/json

- **`data`**

  `object`
  - **`message`**

    `string`

**Example:**

```json
{
  "data": {
    "message": ""
  }
}
```

##### Status: 401 Numeric id without an API key

##### Status: 404 Not found

##### Status: 422 Image too large

##### Status: 429 Rate limited

### Read plugin instance health and recent logs

- **Method:** `GET`
- **Path:** `/api/plugin_settings/{plugin_setting_id}/logs`
- **Operation ID:** `getPluginSettingLogs`
- **Tags:** Plugin Settings - Logs

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `plugin_setting_id` required

- **In:** `path`

Plugin setting id or uuid

`string`

##### `level`

- **In:** `query`

Only return logs at this level:

- `debug`
- `info`
- `warn`
- `error`

`string`

##### `limit`

- **In:** `query`

Maximum logs to return, 1 to 50. Defaults to 20

`integer`

#### Responses

##### Status: 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: "\<level>, \<timestamp>, \<message>" }
      - **`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

**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": ""
          }
        ]
      }
    ]
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 UUID belonging to another user

##### Status: 422 A level the logs do not have

### Read markup for a size

- **Method:** `GET`
- **Path:** `/api/plugin_settings/{plugin_setting_id}/markup/{size}`
- **Operation ID:** `readMarkup`
- **Tags:** Plugin Settings - Markup

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `plugin_setting_id` required

- **In:** `path`

Plugin setting id or uuid

`string`

##### `size` required

- **In:** `path`

Markup size

`string`, possible values: `"markup_full", "markup_half_horizontal", "markup_half_vertical", "markup_quadrant", "markup_shared", "tidbyt_canvas", "transform_js"`

#### Responses

##### Status: 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`

**Example:**

```json
{
  "data": {
    "size": "markup_full",
    "markup": null,
    "shared_markup": ""
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 UUID belonging to another user

##### Status: 422 Invalid size

### Write markup for a size

- **Method:** `PUT`
- **Path:** `/api/plugin_settings/{plugin_setting_id}/markup/{size}`
- **Operation ID:** `writeMarkup`
- **Tags:** Plugin Settings - Markup

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `plugin_setting_id` required

- **In:** `path`

Plugin setting id or uuid

`string`

##### `size` required

- **In:** `path`

Markup size

`string`, possible values: `"markup_full", "markup_half_horizontal", "markup_half_vertical", "markup_quadrant", "markup_shared", "tidbyt_canvas", "transform_js"`

#### Request Body

**Required:** `true`

##### Content-Type: application/json

- **`content` (required)**

  `string`

**Example:**

```json
{
  "content": ""
}
```

#### Responses

##### Status: 200 Markup written successfully

###### Content-Type: application/json

- **`data`**

  `object`
  - **`size`**

    `string`
  - **`success`**

    `boolean`

**Example:**

```json
{
  "data": {
    "success": true,
    "size": "markup_full"
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 422 Invalid size

### 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

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `plugin_setting_id` required

- **In:** `path`

Plugin setting id or uuid

`string`

#### Responses

##### Status: 200 Infers the type of each variable held as static data

###### Content-Type: application/json

- **`data`**

  `object`
  - **`global_variables`**

    `object` — trmnl.\* variables, with sensitive settings masked
  - **`schema`**

    `object` — Type inferred for each merge variable
  - **`sensor_readings`**

    `object` — Present only when the owner has sensor readings
  - **`strategy`**

    `string | null`
  - **`transform_raw_input`**

    `object` — Pre-transform input, present only when a transform is attached
  - **`transform_raw_input_schema`**

    `object` — Type inferred for each pre-transform variable
  - **`variables`**

    `object` — The merge variables the instance renders from

**Example:**

```json
{
  "data": {
    "strategy": "polling",
    "variables": null,
    "schema": null,
    "global_variables": null,
    "transform_raw_input": null,
    "transform_raw_input_schema": null,
    "sensor_readings": null
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 UUID belonging to another user

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `id` required

- **In:** `path`

Plugin setting id or uuid

`string`

#### 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

**Example:**

```json
{
  "name": "",
  "description": null,
  "refresh_interval": 1440,
  "health_notification_enabled": true
}
```

#### Responses

##### Status: 200 Updated

###### Content-Type: application/json

- **`data`**

  `object`, schema: `PluginSetting`

  *Schema `PluginSetting` is shown above.*

**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"
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Plugin setting belongs to another user

##### Status: 422 Pushes nothing to GitHub when the save is refused

### Delete a plugin setting

- **Method:** `DELETE`
- **Path:** `/api/plugin_settings/{id}`
- **Operation ID:** `deletePluginSetting`
- **Tags:** Plugin Settings

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `id` required

- **In:** `path`

ID of the plugin setting to delete

`integer`

#### Responses

##### Status: 204 Deleted

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Not found

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `id` required

- **In:** `path`

Plugin setting id or uuid

`string`

#### Responses

##### Status: 200 The copy

###### Content-Type: application/json

- **`data`**

  `object`, schema: `PluginSetting`

  *Schema `PluginSetting` is shown above.*

**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"
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Plugin setting belongs to another user

##### Status: 422 The copy does not pass validation

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `id` required

- **In:** `path`

Plugin setting id or uuid

`string`

#### Responses

##### Status: 204 State cleared

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Plugin setting belongs to another user

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `id` required

- **In:** `path`

Plugin setting id or uuid

`string`

#### Responses

##### Status: 204 Credential forgotten and setting removed

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Plugin setting belongs to another user

##### Status: 422 A published recipe cannot be removed

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `id` required

- **In:** `path`

Plugin setting id or uuid

`string`

#### Responses

##### Status: 200 Debug logs enabled

###### Content-Type: application/json

- **`data`**

  `object`
  - **`debug_logs_until`**

    `string`, format: `date-time`

**Example:**

```json
{
  "data": {
    "debug_logs_until": ""
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Plugin setting belongs to another user

##### Status: 422 Only a private plugin records debug logs

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `id` required

- **In:** `path`

Plugin setting id or uuid

`string`

##### `include_forks`

- **In:** `query`

true also resets the installs of the recipe this setting publishes

`boolean`

#### Responses

##### Status: 204 Health reset

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Plugin setting belongs to another user

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `id` required

- **In:** `path`

Plugin setting id or uuid

`string`

#### Responses

##### Status: 204 Transform removed

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Plugin setting belongs to another user

##### Status: 422 The setting refuses to save

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `plugin_setting_id` required

- **In:** `path`

Plugin setting id or uuid

`string`

#### Responses

##### Status: 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"`

**Example:**

```json
{
  "data": {
    "job_id": "3f1e...",
    "status": "pending",
    "render_in_progress": true
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 UUID belonging to another user

##### Status: 422 Instance has no polling URL

##### Status: 429 The account has used its hourly render allowance, shared with the dashboard and MCP

### Read the result of a refresh

- **Method:** `GET`
- **Path:** `/api/plugin_settings/{plugin_setting_id}/refreshes/{id}`
- **Operation ID:** `getRefresh`
- **Tags:** Plugin Settings - Refresh

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `plugin_setting_id` required

- **In:** `path`

Plugin setting id or uuid

`string`

##### `id` required

- **In:** `path`

The job\_id returned when the refresh was started

`string`

#### Responses

##### Status: 200 Returns the freshly fetched variables

###### Content-Type: application/json

- **`data`**

  `object`
  - **`job_id`**

    `string`
  - **`logs`**

    `array`

    **Items:**

    A log entry from the fetch
  - **`polling_url`**

    `string | null`
  - **`schema`**

    `object` — Type inferred for each merge variable
  - **`status`**

    `string`, possible values: `"complete"`
  - **`success`**

    `boolean`
  - **`variables`**

    `object` — The merge variables the fetch returned
  - **`warning`**

    `string` — Present when the data is stale after a transform failure

**Example:**

```json
{
  "data": {
    "job_id": "",
    "status": "complete",
    "success": true,
    "variables": null,
    "schema": null,
    "polling_url": null,
    "warning": "",
    "logs": []
  }
}
```

##### Status: 202 Refresh has not finished yet

###### Content-Type: application/json

- **`data`**

  `object`
  - **`job_id`**

    `string`
  - **`status`**

    `string`, possible values: `"pending"`

**Example:**

```json
{
  "data": {
    "job_id": "3f1e...",
    "status": "pending"
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 UUID belonging to another user

##### Status: 410 Result aged out, or the job id was never issued

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `plugin_setting_id` required

- **In:** `path`

Plugin setting id or uuid

`string`

#### 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"`

**Example:**

```json
{
  "size": "markup_full",
  "device_model": "og"
}
```

#### Responses

##### Status: 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"`

**Example:**

```json
{
  "data": {
    "job_id": "",
    "status": "pending"
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 UUID belonging to another user

##### Status: 422 Unknown device model

##### Status: 429 The account has used its hourly render allowance, shared with the dashboard and MCP

### 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

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `plugin_setting_id` required

- **In:** `path`

Plugin setting id or uuid

`string`

##### `id` required

- **In:** `path`

The job\_id returned when the render was started

`string`

#### Responses

##### Status: 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"`

**Example:**

```json
{
  "data": {
    "job_id": "",
    "status": "complete",
    "preview_base64": ""
  }
}
```

##### Status: 202 Render has not finished yet

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 UUID belonging to another user

##### Status: 410 Result aged out, or the job id was never issued

##### Status: 422 The render failed

### Update plugin settings fields

- **Method:** `PATCH`
- **Path:** `/api/plugin_settings/{plugin_setting_id}/settings`
- **Operation ID:** `updatePluginSettingFields`
- **Tags:** Plugin Settings - Settings

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `plugin_setting_id` required

- **In:** `path`

Plugin setting id or uuid

`string`

#### Request Body

**Required:** `true`

##### Content-Type: application/json

- **`fields` (required)**

  `object`

**Example:**

```json
{
  "fields": {}
}
```

#### Responses

##### Status: 200 Settings updated successfully

###### Content-Type: application/json

- **`data`**

  `object`
  - **`success`**

    `boolean`
  - **`warnings`**

    `array` — Fields that were written but are hidden under the current settings

    **Items:**

    `string`

**Example:**

```json
{
  "data": {
    "success": true,
    "warnings": [
      ""
    ]
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Unknown UUID

##### Status: 422 Non-string field values

### List my plugin settings

- **Method:** `GET`
- **Path:** `/api/plugin_settings`
- **Operation ID:** `listPluginSettings`
- **Tags:** Plugin Settings

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `plugin_id`

- **In:** `query`

ID of a plugin or "calendars" to filter calendar plugins

`string`

#### Responses

##### Status: 200 Returns all calendar plugin settings except Google Calendar

###### Content-Type: application/json

- **`data`**

  `array`

  **Items:**

  *Schema `PluginSetting` is shown above.*

**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"
    }
  ]
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Request Body

**Required:** `true`

##### Content-Type: application/json

schema: `PluginSettingParams`

- **`name` (required)**

  `string`
- **`plugin_id` (required)**

  `integer`

**Additional properties:**

never (false schema)

**Example:**

```json
{
  "name": "My Plugin Setting",
  "plugin_id": 1
}
```

#### Responses

##### Status: 200 Success

###### Content-Type: application/json

- **`data`**

  `object`, schema: `PluginSetting`

  *Schema `PluginSetting` is shown above.*

**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"
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 422 Unprocessable Entity

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

### Get plugin setting details

- **Method:** `GET`
- **Path:** `/api/plugin_settings/{id}/details`
- **Operation ID:** `getPluginSettingDetails`
- **Tags:** Plugin Settings

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `id` required

- **In:** `path`

Plugin setting id or uuid

`string`

#### Responses

##### Status: 200 Returns plugin details with sizes

###### Content-Type: application/json

- **`data`**

  `object`
  - **`available_fields`**

    `object` — Fields writable under the current settings
  - **`custom_fields`**

    `array`

    **Items:**

    A user-defined form field
  - **`error_message`**

    `string` — Present only while the instance is failing
  - **`form_fields`**

    `array`

    **Items:**

    A form field definition
  - **`framework`**

    `object` — Design system version and asset URLs the markup renders against
  - **`name`**

    `string`
  - **`plugin_name`**

    `string`
  - **`serverless_language`**

    `string | null`
  - **`settings`**

    `object` — 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`

**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
    }
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 UUID belonging to another user

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `category`

- **In:** `query`

One of Plugin::CATEGORIES

`string`

##### `query`

- **In:** `query`

Matches the plugin name

`string`

##### `page_size`

- **In:** `query`

Selects a cursor page with items and next\_cursor instead of the default array

`integer`, minimum: `1`, maximum: `100`

##### `cursor`

- **In:** `query`

Opaque cursor bound to this account and filters

`string`

#### Responses

##### Status: 200 Success

###### Content-Type: application/json

- **`data`**

  `array`

  **Items:**

  schema: `Plugin`
  - **`categories`**

    `array | null`

    **Items:**

    `string`
  - **`category`**

    `array | null`

    **Items:**

    `string`
  - **`compatibility`**

    `object`
  - **`description`**

    `string | null`
  - **`form_fields`**

    `array | null` — getPlugin only — send these keynames to updatePluginSettingFields

    **Items:**
    - **`default`**

      `object`
    - **`description`**

      `string | null`
    - **`field_type`**

      `string`
    - **`help_text`**

      `string | null`
    - **`keyname`**

      `string`
    - **`name`**

      `string`
    - **`optional`**

      `boolean | null`
    - **`options`**

      `object` — 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`

    **Items:**

    `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`

    **Items:**

    `string`
  - **`source_revision`**

    `string`
  **Additional properties:**

  never (false schema)

**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
        }
      ]
    }
  ]
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `id` required

- **In:** `path`

Plugin id or keyname

`string`

#### Responses

##### Status: 200 Success

###### Content-Type: application/json

- **`data`**

  `object`, schema: `Plugin`

  *Schema `Plugin` is shown above.*

**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
      }
    ]
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 No plugin matches the id or keyname

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `query`

- **In:** `query`

Keywords, or #category

`string`

##### `sort_by`

- **In:** `query`

:

- `newest`
- `oldest`
- `popularity`
- `install`
- `fork`

`string`

##### `limit`

- **In:** `query`

1 to 25, default 10

`integer`

##### `page_size`

- **In:** `query`

Selects a cursor page with items and next\_cursor instead of the default array

`integer`, minimum: `1`, maximum: `100`

##### `cursor`

- **In:** `query`

Opaque cursor bound to this account and filters

`string`

#### Responses

##### Status: 200 Lists the newest recipes when no query is given

###### Content-Type: application/json

- **`data`**

  `array`

  **Items:**

  schema: `Recipe`
  - **`ai_keywords`**

    `array | null`

    **Items:**

    `string`
  - **`ai_tags`**

    `array | null`

    **Items:**

    `string`
  - **`author`**

    `string | null`
  - **`categories`**

    `array | null`

    **Items:**

    `string`
  - **`compatibility`**

    `object`
  - **`custom_fields`**

    `array` — Fields a fork needs filled

    **Items:**

    `object`
  - **`description`**

    `string | null`
  - **`id`**

    `integer`
  - **`install_choices`**

    `array`

    **Items:**

    `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`

    **Items:**

    `string`
  - **`screenshot_url`**

    `string | null`
  - **`share_oauth_config`**

    `boolean` — getRecipe only
  - **`source_revision`**

    `string`
  - **`stats`**

    `object`
    - **`forks`**

      `integer`
    - **`installs`**

      `integer`
  - **`strategy`**

    `string | null`
  **Additional properties:**

  never (false schema)

**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
    }
  ]
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `id` required

- **In:** `path`

Recipe id

`integer`

#### Responses

##### Status: 200 A recipe with custom fields must be forked so the installer can fill them

###### Content-Type: application/json

- **`data`**

  `object`, schema: `Recipe`

  *Schema `Recipe` is shown above.*

**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
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Not a published recipe

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `id` required

- **In:** `path`

Recipe id

`integer`

##### `sizes`

- **In:** `query`

Comma-separated markup sizes to read, default all: markup\_full, markup\_half\_horizontal, markup\_half\_vertical, markup\_quadrant, shared\_markup

`string`

#### Responses

##### Status: 200 Success

###### Content-Type: application/json

- **`data`**

  `object`
  - **`custom_fields`**

    `array`

    **Items:**

    `object`
  - **`id`**

    `integer`
  - **`markups`**

    `object` — Markup by size

    **Additional properties:**

    `string`
  - **`name`**

    `string`
  - **`strategy`**

    `string | null`

**Example:**

```json
{
  "data": {
    "id": 1,
    "name": "",
    "strategy": null,
    "custom_fields": [
      {}
    ],
    "markups": {
      "additionalProperty": ""
    }
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Not a published recipe

##### Status: 422 A size that does not exist

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `id` required

- **In:** `path`

Recipe id

`integer`

#### 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

**Example:**

```json
{
  "device_id": 1,
  "inherit_oauth": true
}
```

#### Responses

##### Status: 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`**

    `object`, schema: `PluginSetting`

    *Schema `PluginSetting` is shown above.*

**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"
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Not a published recipe

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `device_id` required

- **In:** `path`

Device id

`integer`

##### `date`

- **In:** `query`

ISO 8601 date in the owner’s time zone, default today; an unreadable date reads as today

`string`

##### `source`

- **In:** `query`

PluginSetting: or Mashup:, one of the account’s own

`string`

##### `hours`

- **In:** `query`

How far ahead the strip looks, default 24:

- `3`
- `12`
- `24`

`integer`

#### Responses

##### Status: 200 Success

###### Content-Type: application/json

- **`data`**

  `object`, schema: `Timeline`
  - **`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` — The coming check-ins over the next 24 hours; across every device for a plugin setting or mashup

    **Items:**

    schema: `TimelineStep` — 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
  - **`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` — The other events of the same check-in, in the same shape as leading

      **Items:**

      `object`
    - **`trace_entries`**

      `array` — The decisions the check-in recorded

      **Items:**

      `object`
  - **`strip`**

    `array` — A device’s whole rotation over the next hours, unfiltered by source; empty for a plugin setting or mashup

    **Items:**

    *Schema `TimelineStep` is shown above.*
  - **`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`

**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
      }
    ]
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Source belongs to another user

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `plugin_setting_id` required

- **In:** `path`

Plugin setting id

`integer`

##### `date`

- **In:** `query`

ISO 8601 date in the owner’s time zone, default today

`string`

#### Responses

##### Status: 200 Success

###### Content-Type: application/json

- **`data`**

  `object`, schema: `Timeline`

  *Schema `Timeline` is shown above.*

**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
      }
    ]
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Plugin setting belongs to another user

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `mashup_id` required

- **In:** `path`

Mashup id

`integer`

##### `date`

- **In:** `query`

ISO 8601 date in the owner’s time zone, default today

`string`

#### Responses

##### Status: 200 Success

###### Content-Type: application/json

- **`data`**

  `object`, schema: `Timeline`

  *Schema `Timeline` is shown above.*

**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
      }
    ]
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Mashup belongs to another user

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Responses

##### Status: 200 Success

###### Content-Type: application/json

- **`data`**

  `array`

  **Items:**

  schema: `UserTheme`
  - **`created_at`**

    `string`, format: `date_time`
  - **`id`**

    `integer`
  - **`name`**

    `string`
  - **`scss`**

    `string` — getUserTheme only: the framework SCSS file this theme exports
  - **`settings`**

    `object`, 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.
    - **`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.

      **Additional properties:**

      any (true schema)
    - **`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, "\<hue>-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 \<role>\_case ("as-written"/"uppercase"/"lowercase") and \<role>\_tracking ("normal"/"wide"/"wider") for title, value, label, description, title\_bar, title\_bar\_instance, table\_head (table\_body has no \_case)

      **Additional properties:**

      `string`
    **Additional properties:**

    never (false schema)
  - **`updated_at`**

    `string`, format: `date_time`
  **Additional properties:**

  never (false schema)

**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": ""
    }
  ]
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 The theme builder is not enabled for the account

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Request Body

**Required:** `true`

##### Content-Type: application/json

- **`name` (required)**

  `string` — Unique per account
- **`settings`**

  `object`, 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.

  *Schema `UserThemeSettings` is shown above.*

**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

##### Status: 200 Created

###### Content-Type: application/json

- **`data`**

  `object`, schema: `UserTheme`

  *Schema `UserTheme` is shown above.*

**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": ""
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 422 Rejects advanced YAML that does not parse

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `id` required

- **In:** `path`

Theme id

`integer`

#### Responses

##### Status: 200 Success

###### Content-Type: application/json

- **`data`**

  `object`, schema: `UserTheme`

  *Schema `UserTheme` is shown above.*

**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": ""
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Theme belongs to another user

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `id` required

- **In:** `path`

Theme id

`integer`

#### Request Body

**Required:** `true`

##### Content-Type: application/json

- **`name`**

  `string`
- **`settings`**

  `object`, 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.

  *Schema `UserThemeSettings` is shown above.*

**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

##### Status: 200 Updated

###### Content-Type: application/json

- **`data`**

  `object`, schema: `UserTheme`

  *Schema `UserTheme` is shown above.*

**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": ""
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Theme belongs to another user

##### Status: 422 Rejects settings outside the recipe vocabulary

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "API key or OAuth access token"
  }
  ```

#### Parameters

##### `id` required

- **In:** `path`

Theme id

`integer`

#### Responses

##### Status: 204 Deleted

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 Theme belongs to another user

### 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.

#### Effective servers

- `https://{defaultHost}`
  - defaultHost: `trmnl.com`

#### Authentication

- **bearer\_auth**
  ```
  {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "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

**Example:**

```json
{
  "token": "",
  "name": ""
}
```

#### Responses

##### Status: 200 Created

###### Content-Type: application/json

- **`data`**

  `object`, schema: `UserTheme`

  *Schema `UserTheme` is shown above.*

**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": ""
  }
}
```

##### Status: 401 Unauthorized

###### Content-Type: application/json

*Schema `Error` is shown above.*

**Example:**

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

##### Status: 404 The theme builder is not enabled for the account

##### Status: 422 name is missing

## Schemas

### PluginConnectionAttempt

- **Type:** `object`

*Schema `PluginConnectionAttempt` is shown above.*

**Example:**

```json
{
  "id": "",
  "state": "pending",
  "expires_at": "",
  "connection_id": "",
  "launch_url": "",
  "target": {
    "kind": "instance",
    "id": 1
  }
}
```

### CatalogEntry

- **Type:** `object`

* **`categories` (required)**

  `array`

  **Items:**

  `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`

    **Items:**

    `string`
  - **`help`**

    `string`
* **`requirements`**

  `array`

  **Items:**

  `string`
* **`source_revision`**

  `string`

**Example:**

```json
{
  "kind": "official",
  "id": 1,
  "name": "",
  "description": null,
  "categories": [
    ""
  ],
  "source_revision": "",
  "requirements": [
    ""
  ],
  "compatibility": {},
  "install_choices": [
    {
      "id": "",
      "label": "",
      "help": "",
      "requirements": [
        ""
      ]
    }
  ]
}
```

### PluginInstallation

- **Type:** `object`

*Schema `PluginInstallation` is shown above.*

**Example:**

```json
{
  "id": "",
  "state": "setup_required",
  "source": {
    "kind": "",
    "id": 1,
    "revision": "",
    "choice_id": "",
    "device_id": 1
  },
  "plugin_setting_id": null,
  "expires_at": ""
}
```

### ConfigurationField

- **Type:** `object`

*Schema `ConfigurationField` is shown above.*

**Example:**

```json
{
  "path": "",
  "type": "",
  "label": "",
  "help": "",
  "required": true,
  "read_only": true,
  "nullable": true,
  "constraints": {},
  "choices": [
    {
      "label": "",
      "value": null
    }
  ]
}
```

### PluginSync

- **Type:** `object`

*Schema `PluginSync` is shown above.*

**Example:**

```json
{
  "source_kinds": [
    ""
  ],
  "upload_allowed": true,
  "reason": null,
  "action_id": null
}
```

### PluginConfiguration

- **Type:** `object`

*Schema `PluginConfiguration` is shown above.*

**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
  }
}
```

### ConfigurationChange

- **Type:** `object`

*Schema `ConfigurationChange` is shown above.*

**Example:**

```json
{
  "op": "set",
  "path": "",
  "value": null
}
```

### ConfigurationWrite

- **Type:** `object`

*Schema `ConfigurationWrite` is shown above.*

**Example:**

```json
{
  "revision": "",
  "changes": [
    {
      "op": "set",
      "path": "",
      "value": null
    }
  ]
}
```

### ConfigurationError

- **Type:** `object`

*Schema `ConfigurationError` is shown above.*

**Example:**

```json
{
  "error": {
    "code": "",
    "message": "",
    "field_errors": [
      {
        "path": "",
        "code": "",
        "message": ""
      }
    ]
  }
}
```

### MashupOptions

- **Type:** `object`

*Schema `MashupOptions` is shown above.*

**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": [
        ""
      ]
    }
  ]
}
```

### Error

- **Type:** `object`

*Schema `Error` is shown above.*

**Example:**

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

### Device

- **Type:** `object`

*Schema `Device` is shown above.*

**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,
  "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"
    ]
  }
}
```

### Model

- **Type:** `object`

*Schema `Model` is shown above.*

**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"
      ]
    ]
  }
}
```

### Palette

- **Type:** `object`

*Schema `Palette` is shown above.*

**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
}
```

### PlaylistItem

- **Type:** `object`

*Schema `PlaylistItem` is shown above.*

**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"
}
```

### Mashup

- **Type:** `object`

*Schema `Mashup` is shown above.*

**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"
}
```

### Recipe

- **Type:** `object`

*Schema `Recipe` is shown above.*

**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
}
```

### Screen

- **Type:** `object`

*Schema `Screen` is shown above.*

**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"
}
```

### PlaylistItemParams

- **Type:** `object`

* **`visible`**

  `boolean`

**Additional properties:**

never (false schema)

**Example:**

```json
{
  "visible": true
}
```

### Plugin

- **Type:** `object`

*Schema `Plugin` is shown above.*

**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
    }
  ]
}
```

### PluginSetting

- **Type:** `object`

*Schema `PluginSetting` is shown above.*

**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"
}
```

### PluginSettingArchive

- **Type:** `object`

*Schema `PluginSettingArchive` is shown above.*

**Example:**

```json
{
  "settings_yaml": ""
}
```

### PluginSettingParams

- **Type:** `object`

*Schema `PluginSettingParams` is shown above.*

**Example:**

```json
{
  "name": "My Plugin Setting",
  "plugin_id": 1
}
```

### PluginSettingDataParams

- **Type:** `object`

* **`merge_variables` (required)**

  `object` — The rejected value, echoed back as sent
* **`error`**

  `string`

**Additional properties:**

never (false schema)

**Example:**

```json
{
  "merge_variables": null,
  "error": ""
}
```

### UserTheme

- **Type:** `object`

*Schema `UserTheme` is shown above.*

**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": ""
}
```

### UserThemeSettings

- **Type:** `object`

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.

*Schema `UserThemeSettings` is shown above.*

**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"
  }
}
```

### MyPluginParams

- **Type:** `object`

*Schema `MyPluginParams` is shown above.*

**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
}
```

### MyPlugin

- **Type:** `object`

*Schema `MyPlugin` is shown above.*

**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": ""
}
```

### User

- **Type:** `object`

*Schema `User` is shown above.*

**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"
}
```

### TimelineStep

- **Type:** `object`

One simulated check-in: what the device fetches then, and how long it holds it

*Schema `TimelineStep` is shown above.*

**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
}
```

### Timeline

- **Type:** `object`

*Schema `Timeline` is shown above.*

**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
    }
  ]
}
```

### App

- **Type:** `object`

*Schema `App` is shown above.*

**Example:**

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

### AppInstallation

- **Type:** `object`

*Schema `AppInstallation` is shown above.*

**Example:**

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

### FleetMember

- **Type:** `object`

*Schema `FleetMember` is shown above.*

**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
}
```

### Fleet

- **Type:** `object`

*Schema `Fleet` is shown above.*

**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
  }
}
```

### RoomBookingCalendar

- **Type:** `object`

*Schema `RoomBookingCalendar` is shown above.*

**Example:**

```json
{
  "id": "ical:12",
  "kind": "ical",
  "name": "Boardroom",
  "timezone": "Europe/Amsterdam",
  "capacity": null,
  "show_booking_qr": 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
  ],
  "public_booking_token": null,
  "public_booking_url": null
}
```

### RoomBooking

- **Type:** `object`

*Schema `RoomBooking` is shown above.*

**Example:**

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

### RoomBookingCollection

- **Type:** `object`

*Schema `RoomBookingCollection` is shown above.*

**Example:**

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

### RoomBookingSummary

- **Type:** `object`

*Schema `RoomBookingSummary` is shown above.*

**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": ""
  }
}
```

### RoomBookingSettings

- **Type:** `object`

*Schema `RoomBookingSettings` is shown above.*

**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
}
```

### RoomBookingIntegration

- **Type:** `object`

*Schema `RoomBookingIntegration` is shown above.*

**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
}
```

### PublicBookingEvent

- **Type:** `object`

The shape the walk-up page uses: times are local to the room, without a zone

*Schema `PublicBookingEvent` is shown above.*

**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": ""
}
```

### RoomBookingBilling

- **Type:** `object`

*Schema `RoomBookingBilling` is shown above.*

**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
}
```

### RoomBookingDeviceScreen

- **Type:** `object`

A device showing a calendar or a collection, and the image it is showing

*Schema `RoomBookingDeviceScreen` is shown above.*

**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"
  }
}
```

### RoomBookingCalendarDetails

- **Type:** `object`

*Schema `RoomBookingCalendarDetails` is shown above.*

**Example:**

```json
{
  "id": "ical:12",
  "kind": "ical",
  "name": "Boardroom",
  "timezone": "Europe/Amsterdam",
  "capacity": null,
  "show_booking_qr": 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
  ],
  "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
    }
  ]
}
```
