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 auth —
IDheader (MAC address) +Access-Tokenheader (API key returned by/api/setup) - Admin auth —
Authorization: Bearer <ADMIN_KEY>header, whereADMIN_KEYis set in the server's environment - Public — no auth required
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.
| Header | Required | Description |
|---|---|---|
| ID | yes | Device MAC address — colon or dash format (e.g. AA:BB:CC:DD:EE:FF or AA-BB-CC-DD-EE-FF) |
{
"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.
| Header | Required | Description |
|---|---|---|
| ID | yes | Device MAC address |
| Access-Token | yes | API key from /api/setup |
{
"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.
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.
None.
[
{ "name": "calendar" },
{ "name": "clock" },
{ "name": "sysinfo" }
]
Admin. Authorization: Bearer <ADMIN_KEY>
Body is raw Lua source. Content-Type is optional.
{ "ok": true, "plugin": "myplugin" }
| Status | Reason |
|---|---|
| 400 | Invalid plugin name or empty body |
| 401 | Missing or wrong ADMIN_KEY |
| 503 | ADMIN_KEY not configured on server |
Plugin names must match ^[a-zA-Z0-9_-]+$. The file is written to plugins/<name>.lua in B2.
Admin. Authorization: Bearer <ADMIN_KEY>
{ "ok": true, "plugin": "myplugin" }
Demo API
Used by the demo UI. Public, no auth required.
["calendar", "clock", "sysinfo"]
| Param | Required | Description |
|---|---|---|
| plugin | yes | Plugin name |
{ "plugin": "clock", "source": "local M = {}\n..." }
| Param | Required | Description |
|---|---|---|
| plugin | no | Plugin name (default: clock) |
{
"image_url": "https://.../images/demo-clock.bmp",
"plugin": "clock",
"width": 800,
"height": 480
}
| Param | Required | Description |
|---|---|---|
| plugin | no | Plugin name (default: clock) |
{
"image_url": "https://.../images/demo-clock.bmp",
"plugin": "clock",
"width": 800,
"height": 480
}
{ "error": "love render failed: myplugin" }
Devices API
Manage registered devices. List is public; mutations require admin auth.
None.
[
{
"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"
}
]
Admin.
{ "plugin": "clock" }
{ "playlist_id": "my-playlist" }
{ "ok": true, "screen_id": "clock" }
// or
{ "ok": true, "playlist_id": "my-playlist" }
Admin.
{ "ok": true }
Playlists API
Playlists cycle through a list of plugins on a schedule. List is public; mutations require admin auth.
None.
[
{
"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" }
]
}
]
Admin.
{
"id": "my-playlist",
"entries": [
{ "plugin": "clock", "duration_seconds": 60 }
]
}
| Field | Required | Description |
|---|---|---|
| plugin | yes | Plugin name to render |
| duration_seconds | yes | How long to show this entry (positive integer) |
| active_from / active_until | no | UTC time window as HH:MM — entry is skipped outside this window |
| weekdays | no | Array of ISO weekday integers (1=Mon … 7=Sun) — entry is skipped on other days |
{ "ok": true, "id": "my-playlist" }
Admin.
{ "entries": [ { "plugin": "clock", "duration_seconds": 30 } ] }
{ "ok": true, "id": "my-playlist" }
Admin.
Returns 409 Conflict if any device is currently assigned to this playlist.
{ "ok": true, "id": "my-playlist" }
Utility
{ "ok": true }
None.
{
"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
}
}
None.
Returns counters, gauges, and a render-duration histogram. Intended for Prometheus/Grafana scraping.
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
| Field | Required | Description |
|---|---|---|
| M.draw(W, H) | yes | Called once per render. Draw to the full W×H canvas. |
| M.refresh_seconds | no | Suggested render interval for devices. Default: 60. |
| M.name | no | Human-readable display name. |
| M.description | no | Short 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.