Live Demo →

API Reference

All endpoints served by trmnl-lua. Examples use https://trmnl.mntechstudios.com as the base URL — replace with your own host.


Authentication

Three auth schemes are used depending on the caller:


Device API

These endpoints implement the TRMNL device protocol. The device polls /api/setup once on first boot, then polls /api/display on the interval returned in each response.

GET /api/setup Register device, get API key and first image
Request headers
HeaderRequiredDescription
IDyesDevice MAC address — colon or dash format (e.g. AA:BB:CC:DD:EE:FF or AA-BB-CC-DD-EE-FF)
Response — 200
{
  "status": 200,
  "api_key": "a3f9...",        // 32-char hex, use as Access-Token
  "friendly_id": "TRM-A1B2C3",
  "image_url": "https://.../images/clock.bmp",
  "message": "Welcome to trmnl-lua"
}

Re-registering the same MAC returns the same device record. No render is triggered at setup — the first GET /api/display poll triggers the initial render.

GET /api/display Get current image URL and refresh interval
Request headers
HeaderRequiredDescription
IDyesDevice MAC address
Access-TokenyesAPI key from /api/setup
Response — 200
{
  "status": 200,
  "image_url": "https://.../images/clock.bmp",
  "image_url_timeout": 60,     // same value as refresh_rate
  "filename": "clock.bmp",
  "refresh_rate": 60,          // seconds until next poll
  "update_firmware": false,
  "reset_firmware": false,
  "special_function": "none"
}

If the screen is stale (elapsed time ≥ refresh_rate), a fresh render is triggered before responding.

POST /api/log Device telemetry (written to server stdout)

Body is ignored. Returns 204 No Content.


Plugin API

Manage plugins stored in Backblaze B2. Plugins are Lua files fetched at render time — changes take effect immediately without redeploying.

GET /api/plugins List all plugins
Auth

None.

Response — 200
[
  { "name": "calendar" },
  { "name": "clock" },
  { "name": "sysinfo" }
]
PUT /api/plugins/:name Create or update a plugin
Auth

Admin. Authorization: Bearer <ADMIN_KEY>

Request

Body is raw Lua source. Content-Type is optional.

Response — 200
{ "ok": true, "plugin": "myplugin" }
Errors
StatusReason
400Invalid plugin name or empty body
401Missing or wrong ADMIN_KEY
503ADMIN_KEY not configured on server

Plugin names must match ^[a-zA-Z0-9_-]+$. The file is written to plugins/<name>.lua in B2.

DELETE /api/plugins/:name Delete a plugin from B2
Auth

Admin. Authorization: Bearer <ADMIN_KEY>

Response — 200
{ "ok": true, "plugin": "myplugin" }

Demo API

Used by the demo UI. Public, no auth required.

GET /demo/plugins List plugin names (for sidebar)
Response — 200
["calendar", "clock", "sysinfo"]
GET /demo/plugin-source Get plugin Lua source
Query params
ParamRequiredDescription
pluginyesPlugin name
Response — 200
{ "plugin": "clock", "source": "local M = {}\n..." }
GET /demo/render Get cached render URL (no re-render)
Query params
ParamRequiredDescription
pluginnoPlugin name (default: clock)
Response — 200
{
  "image_url": "https://.../images/demo-clock.bmp",
  "plugin": "clock",
  "width": 800,
  "height": 480
}
POST /demo/render Trigger a fresh render and return new URL
Query params
ParamRequiredDescription
pluginnoPlugin name (default: clock)
Response — 200
{
  "image_url": "https://.../images/demo-clock.bmp",
  "plugin": "clock",
  "width": 800,
  "height": 480
}
Error — 500
{ "error": "love render failed: myplugin" }

Devices API

Manage registered devices. List is public; mutations require admin auth.

GET /api/devices List all registered devices with telemetry
Auth

None.

Response — 200
[
  {
    "mac": "AA:BB:CC:DD:EE:FF",
    "friendly_id": "TRM-A1B2C3",
    "screen_id": "clock",
    "plugin": "clock",
    "playlist_id": null,
    "current_entry_index": null,
    "last_seen": 1720000000,
    "battery_voltage": 3.85,
    "rssi": -62,
    "firmware_version": "4.1.0"
  }
]
PATCH /api/devices/:mac Assign a plugin or playlist to a device
Auth

Admin.

Request body — assign plugin
{ "plugin": "clock" }
Request body — assign playlist
{ "playlist_id": "my-playlist" }
Response — 200
{ "ok": true, "screen_id": "clock" }
// or
{ "ok": true, "playlist_id": "my-playlist" }
DELETE /api/devices/:mac Delete a device record
Auth

Admin.

Response — 200
{ "ok": true }

Playlists API

Playlists cycle through a list of plugins on a schedule. List is public; mutations require admin auth.

GET /api/playlists List all playlists
Auth

None.

Response — 200
[
  {
    "id": "my-playlist",
    "entries": [
      { "plugin": "clock", "duration_seconds": 60 },
      { "plugin": "calendar", "duration_seconds": 120,
        "weekdays": [1,2,3,4,5],
        "active_from": "08:00", "active_until": "18:00" }
    ]
  }
]
POST /api/playlists Create a new playlist
Auth

Admin.

Request body
{
  "id": "my-playlist",
  "entries": [
    { "plugin": "clock", "duration_seconds": 60 }
  ]
}
Entry fields
FieldRequiredDescription
pluginyesPlugin name to render
duration_secondsyesHow long to show this entry (positive integer)
active_from / active_untilnoUTC time window as HH:MM — entry is skipped outside this window
weekdaysnoArray of ISO weekday integers (1=Mon … 7=Sun) — entry is skipped on other days
Response — 200
{ "ok": true, "id": "my-playlist" }
PUT /api/playlists/:id Replace entries on an existing playlist
Auth

Admin.

Request body
{ "entries": [ { "plugin": "clock", "duration_seconds": 30 } ] }
Response — 200
{ "ok": true, "id": "my-playlist" }
DELETE /api/playlists/:id Delete a playlist
Auth

Admin.

Returns 409 Conflict if any device is currently assigned to this playlist.

Response — 200
{ "ok": true, "id": "my-playlist" }

Utility

GET /health Liveness check
{ "ok": true }
GET /api/info Server version, uptime, counts, and B2 health
Auth

None.

Response — 200
{
  "version": "0.3.0",
  "uptime": 3600,
  "devices": 2,
  "screens": 3,
  "playlists": 1,
  "b2": {
    "ok": true,
    "last_write_at": 1720000000,
    "last_error": null,
    "last_error_at": null
  }
}
GET /metrics Prometheus metrics (text/plain exposition format)
Auth

None.

Returns counters, gauges, and a render-duration histogram. Intended for Prometheus/Grafana scraping.

GET /images/:filename BMP proxy — fetches from B2 and streams to client

Filename must end in .bmp. Returns the raw BMP with Content-Type: image/bmp. Used by devices to download rendered images; the device only sees this server, not B2 directly.


Plugin Format

A plugin is a Lua module that returns a table with a draw(W, H) function. It runs inside LÖVE2D with access to the full love.graphics API.

local M = {}

M.name            = "My Plugin"          -- human-readable name
M.description     = "What it shows"      -- short description
M.refresh_seconds = 60                   -- how often devices re-render

function M.draw(W, H)
  -- W, H are the screen dimensions in pixels (e.g. 800, 480)
  love.graphics.setBackgroundColor(1, 1, 1)  -- white background
  love.graphics.setColor(0, 0, 0)             -- black ink

  local font = love.graphics.newFont(64)
  love.graphics.setFont(font)
  love.graphics.printf("Hello!", 0, H / 2 - 32, W, "center")
end

return M
FieldRequiredDescription
M.draw(W, H)yesCalled once per render. Draw to the full W×H canvas.
M.refresh_secondsnoSuggested render interval for devices. Default: 60.
M.namenoHuman-readable display name.
M.descriptionnoShort description shown in UI.

Plugins are uploaded via PUT /api/plugins/:name and stored in B2. They are fetched fresh on every render — no server restart needed.