API Documentation
Everything on a Deckboard — messages, live channels, images, playlists, schedules — can be pushed over a plain REST API. No SDK required; if you can send an HTTP request, you can flip a board. Free while in beta.
Overview
The API is served from:
https://api.deckboard.party # primary
https://deckboard-api.johntdavenport.workers.dev # fallback
http://localhost:8787 # local dev (wrangler dev)
- Requests and responses are JSON. Send
Content-Type: application/json. - All requests are authenticated with an
Authorization: Bearerheader (see Authentication), except signup, login, forgot/reset and health. - Errors always have the shape
{"error":{"code","message"}}with a matching HTTP status. - Boards render semantically: you push what a board should show, and every display re-flows it to its own grid, density and orientation. The same payload looks right on a phone, a portrait lobby screen and an ultrawide.
/engine/api-client.js is a small ES module that
wraps all of this — base-URL fallback, bearer headers, error normalization and
wsUrl() for WebSocket connections. It is what Control and the display page use.
Quickstart
- Create an account and make a board in Control — the first-run flow walks you through it.
- Create an API key: account menu → API keys → New key.
Keys look like
dck_5f4d…and the full key is shown once — store it somewhere safe. - Grab your board id from board settings in Control (or list your boards with
GET /api/boards). - Push your first message:
curl -X POST https://api.deckboard.party/api/boards/BOARD_ID/show \
-H "Authorization: Bearer dck_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"type":"message","text":"HELLO {red}WORLD{/}","align":"center","valign":"middle"}'
Response:
{"ok": true, "seq": 42}
Every connected display flips to the new content within a heartbeat, and the push lands in
your board's history with an api source badge (one-click replay from Control).
curl.exe from PowerShell and swap the single-quoted JSON body
for a double-quoted string with escaped inner quotes, or put the JSON in a file and pass
-d @payload.json.
Authentication
Three credentials exist, each with a distinct scope:
| Credential | Looks like | Scope | Where it lives |
|---|---|---|---|
| Session token | opaque, 32 bytes | Everything your account can do | Issued by login/signup; 30-day sliding expiry. The web app keeps it in
localStorage.dk_session. |
| API key | dck_ + 32 hex |
Pushing to your boards (/show, /state) |
Created in Control, shown once, revocable with
DELETE /api/keys/:id. |
| Display token | 24 hex | Read-only: current state + WebSocket subscribe | Minted per board; embedded in the display URL; rotate to revoke (details). |
Pass whichever credential you hold as a bearer header:
Authorization: Bearer dck_5f4dcc3b5aa765d61d8327deb882cf99
Payload types
POST /api/boards/:id/show accepts one payload object. Six types cover
everything a board can do. Max payload size is 128 KB (512 KB for
image).
message — text with formatting
curl -X POST https://api.deckboard.party/api/boards/BOARD_ID/show \
-H "Authorization: Bearer dck_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "message",
"text": "ORDER {green}#42{/} READY\nPICK UP AT THE BAR",
"align": "center",
"valign": "middle"
}'
Text word-wraps to the display's grid and honors \n. align is
left | center | right, valign is top | middle | bottom.
Color codes, chips, bold and live variables are described under
Formatting & variables.
channel — live content
curl -X POST https://api.deckboard.party/api/boards/BOARD_ID/show \
-H "Authorization: Bearer dck_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "channel",
"channel": "countdown",
"config": { "target": "2026-12-31T00:00", "label": "NEW YEAR" }
}'
Available channels and their common config:
| Channel | Config example | Behavior |
|---|---|---|
clock | {} | Big centered time, optional date line; ticks every minute. |
date | {} | Day name and date. |
countdown | {"target":"2026-12-31T00:00","label":"NEW YEAR"} | DAYS / HRS / MIN / SEC, layout adapts to the grid; shows the label when done. |
timer | {"seconds":300} | Count up or down. |
weather | {"lat":30.27,"lon":-97.74,"place":"AUSTIN"} | Current conditions + 3-day, refreshes every 10 min (Open-Meteo). |
news | {"src":"bbc"} | Rotating headlines (bbc, reuters, ap), one at a time. |
crypto | {} | BTC / ETH / SOL prices (configurable list), refreshes every 60 s. |
poker | {} | Replays 50 famous poker hands street by street with live equities. |
welcome | {"names":["MS. VU","MR. CHEN"],"template":"WELCOME {name}"} | Rotating guest welcome (hotel use-case). |
Channels tick on the display itself — pushing a channel once keeps it live indefinitely. The easiest way to discover per-channel config is the Channels tab in Control (its cards emit exactly these payloads).
image — photo or mosaic
curl -X POST https://api.deckboard.party/api/boards/BOARD_ID/show \
-H "Authorization: Bearer dck_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "image",
"src": "data:image/jpeg;base64,/9j/4AAQSkZJRg…",
"mode": "mosaic"
}'
"mode":"photo"— the image is sliced across the flaps; each tile flips in its crop (picture-flap)."mode":"mosaic"— the image is quantized to the eight color chips (Vestaboard-style pixel art). High-contrast, simple images read best.srcis a data URI, ≤ 400 KB encoded (≤ 512 KB total payload). Control resizes uploads client-side for you.
playlist — rotate several payloads
curl -X POST https://api.deckboard.party/api/boards/BOARD_ID/show \
-H "Authorization: Bearer dck_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "playlist",
"shuffle": false,
"items": [
{ "payload": { "type": "message", "text": "TODAY'\''S SPECIALS\nFLAT WHITE 4.50\nCOLD BREW 5.00" }, "seconds": 20 },
{ "payload": { "type": "channel", "channel": "clock", "config": {} }, "seconds": 10 },
{ "payload": { "type": "channel", "channel": "weather",
"config": { "lat": 30.27, "lon": -97.74, "place": "AUSTIN" } }, "seconds": 15 }
]
}'
Each item is any payload from this page plus a dwell time in seconds. Rotation is driven server-side (Durable Object alarms), so it survives display reloads and keeps every connected display in sync.
schedule — dayparting
curl -X POST https://api.deckboard.party/api/boards/BOARD_ID/show \
-H "Authorization: Bearer dck_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "schedule",
"rules": [
{ "cron": "0 8 * * *", "payload": { "type": "message", "text": "BREAKFAST\nSERVED UNTIL 11" } },
{ "cron": "0 17 * * *", "payload": { "type": "message", "text": "{red}HAPPY HOUR{/}\n5 TO 7" } }
],
"fallback": { "payload": { "type": "channel", "channel": "clock", "config": {} } }
}'
Standard 5-field cron (minute hour day-of-month month day-of-week). When a
rule fires, its payload takes the board; fallback shows when nothing has fired
yet. Evaluated server-side by alarms — nothing depends on a browser staying open.
clear
curl -X POST https://api.deckboard.party/api/boards/BOARD_ID/show \
-H "Authorization: Bearer dck_YOUR_API_KEY" \
-d '{"type":"clear"}'
Flips every tile back to blank.
Formatting & variables
Message text supports inline codes, parsed by the board's composer:
| Code | Effect |
|---|---|
{red}TEXT{/} | Colored text. Colors: red orange yellow green blue violet white black. |
{chip:red} | A solid color-chip tile (for bars, pixel art, bullet markers). |
{b}TEXT{/b} | Bold glyphs. |
\n | Line break (word-wrap handles the rest). |
Live variables are resolved on the display and keep updating in place:
| Variable | Renders as |
|---|---|
{time} / {time24} | Current time, 12h / 24h. |
{date} / {day} | Current date / day name. |
{weather} / {temp} | Condition / temperature for the board's configured place. |
{countdown:2026-12-31T00:00} | Live countdown to an ISO timestamp. |
{price:BTC} | Live crypto price. |
{feed:items[0].title} | A field from your custom JSON feed (dot/bracket paths). |
{ "type": "message", "text": "{day} {date}\nNOW {time} · {temp} OUTSIDE" }
The character set is uppercase: A–Z 0–9 punctuation
. , : ; ! ? @ # $ % & ( ) + - = / ' " ° and the eight color chips.
Lowercase input is uppercased; unknown characters become spaces.
Endpoint reference
Auth & account
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/auth/signup | — | {email, password} (min 8 chars) → {token, user} |
| POST | /api/auth/login | — | {email, password} → {token, user} |
| POST | /api/auth/logout | session | Invalidates the current session. |
| POST | /api/auth/forgot | — | {email} → {ok:true} — always, regardless of account existence. |
| POST | /api/auth/reset | — | {token, password} → {ok:true}. Single-use, 1 h expiry; revokes all sessions. |
| GET | /api/me | session | → {user:{id,email,created}, prefs} |
| PUT | /api/me/prefs | session | Merge-update preferences: {density?, theme?, soundEnabled?, soundVolume?, …} |
Boards
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/boards | session | → [{id, name, settings, displayToken, createdAt, lastSeenAt}] |
| POST | /api/boards | session | {name} → board. Settings default from your prefs. |
| GET | /api/boards/:id | session | → board + current payload. |
| PUT | /api/boards/:id | session | {name?, settings?}; settings (density, theme, soundEnabled, soundVolume, flipSpeed) broadcast live to displays. |
| DELETE | /api/boards/:id | session | Delete the board. |
| POST | /api/boards/:id/rotate-token | session | → new display token. Revokes the old one everywhere. |
Push, state & history
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/boards/:id/show | session or API key | Push a payload → {ok, seq}. Logged to history. This is the public push API. |
| GET | /api/boards/:id/state | session, API key or display token | → {payload, settings} — poll fallback for displays. |
| GET | /api/boards/:id/history?limit=50 | session | → [{seq, payload, source, createdAt}] — source is control | api | replay | schedule. Last 200 kept. |
| POST | /api/boards/:id/history/:seq/replay | session | Re-push that payload. |
API keys
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/keys | session | → [{id, prefix, name, createdAt, lastUsedAt}] (prefix = first 8 chars). |
| POST | /api/keys | session | {name} → {key} — the full key, shown exactly once. |
| DELETE | /api/keys/:id | session | Revoke a key immediately. |
Proxies & health
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/proxy/news?src=bbc|reuters|ap | — | → {headlines:[{title, ago}]}, cached 5 min. |
| GET | /api/proxy/feed?url=<https-url> | — | JSON passthrough for custom feeds. HTTPS only, public hosts only, 256 KB cap, 10 s timeout, cached 60 s. |
| GET | /api/health | — | → {ok:true} |
POST /api/boards/:id/show accepts any API key belonging to the board's owner —
one key can drive all of your boards.
Errors & rate limits
Every error is JSON with an HTTP status that means what it says:
HTTP/1.1 429 Too Many Requests
{ "error": { "code": "rate_limited", "message": "Too many requests. Try again shortly." } }
| Limit | Value |
|---|---|
| Auth endpoints (signup, login, forgot, reset) | 10 requests / 5 min / IP |
Message push (/show) | 120 / min / board |
| Payload size | 128 KB (image payloads: 512 KB) |
| History retained | last 200 pushes per board |
| Custom feed proxy response | 256 KB, 10 s timeout |
On 429, back off and retry after a few seconds. 120 pushes/min is far above
what flap physics can display anyway — for rapidly changing data, prefer a
variable or a custom feed so the display pulls on its own tick.
WebSocket protocol
Displays hold a WebSocket to the board's room and receive pushes in real time. Connect with
the board's display token in the t query parameter (the one place a
token is allowed in a URL — browser WebSockets cannot send headers, and the token is
read-only):
wss://api.deckboard.party/api/boards/<boardId>/ws?t=<displayToken>
wsUrl(boardId, params) from /engine/api-client.js over
hand-building this URL — it also handles the fallback host and local dev.
All frames are JSON. The server sends three message types:
| Message | When |
|---|---|
{"type":"hello", "seq", "payload", "settings"} | On connect — current content and board settings, so a display can render immediately. |
{"type":"show", "seq", "payload"} | Someone pushed new content (Control, API, replay or schedule). |
{"type":"settings", "settings"} | Board settings changed (density, theme, sound, flip speed). |
seqincreases monotonically per board — ignore anything ≤ the last seq you rendered (guards against replays after reconnect).- Display connections are read-only; the server ignores client frames. Writes only happen through the REST API with a session or API key.
- Ping/pong keepalive is automatic — you don't need to send heartbeats.
- Reconnect with exponential backoff (1 s → 30 s with jitter). If the socket
won't come back after a few attempts, poll
GET /api/boards/:id/stateevery 20 s until it does — that's exactly what the built-in display page does.
Display URLs & tokens
A board's display URL is what you open on the TV:
https://deckboard.party/board?b=<boardId>&t=<displayToken>
- The display token grants read-only access: current state and the WebSocket subscription. It cannot push, cannot read history, cannot touch your account.
- It's minted when the board is created and shown in Control. If a URL leaks (it will —
it's on a TV), rotate it: board settings → rotate display token, or
POST /api/boards/:id/rotate-token. Old URLs die instantly. - Displays apply the board's server-side settings, but you can override per screen with URL parameters — handy when one board feeds several very different screens:
| Parameter | Values | Effect |
|---|---|---|
&density= | ultimate | high | medium | low | Tile density preset for this screen only. |
&theme= | classic | amber | terminal | paper | navy | crimson | Board theme for this screen only. |
&sound= | on | off | Flap sound. |
&volume= | 0.0 – 1.0 | Flap volume. |
&fit= | COLSxROWS e.g. 22x6 | Fixed grid override for exotic installs (auto-tiling is the default and usually better). |
On the display itself, a keyboard (or TV remote that maps to one) can drive:
F fullscreen S sound +/- volume D cycle density T cycle theme ? help
No account and just exploring? https://deckboard.party/board#demo runs a local
demo loop — no server, no token.
Keeping the screen awake
A split-flap board is meant to run all day. The display page does what a web page can: it requests a Screen Wake Lock as soon as it's visible (and re-acquires it whenever you tab back), and a single click or tap sends it fullscreen. On a laptop or a Chromecast/Fire TV-class browser, that's usually all it takes.
The rest is up to the device, and TVs ship aggressive defaults. A checklist that covers the common cases:
- TV sleep timers — disable the idle/eco/auto-off timer in the TV's power settings. This is the #1 cause of "my board turned off overnight".
- Screensavers — on Android TV / Google TV, set the screensaver to "off" or the daydream timeout to "never" while the browser is foregrounded.
- Fire TV — install a browser (e.g. Silk), open the board URL, go fullscreen; raise Display & Sounds → screensaver timeout.
- LG / Samsung smart TVs — the built-in browsers work; add the board URL as a bookmark/home shortcut and check the "no signal / idle standby" settings.
- HDMI-CEC — a connected device switching off can drag the TV to standby with it; disable CEC power-off sync if your board dies when other gear sleeps.
- Old TV browser? — a $30 streaming stick with a modern browser beats fighting a 2015 web engine. Any evergreen browser is enough; there is no app to sideload.
- Kiosk hardware — on a Raspberry Pi or NUC, launch Chromium with
--kiosk <board-url>and disable OS display sleep. This is the most reliable 24/7 setup.
Questions or a stuck board? The whole system is described in this page — and the Control app emits every payload documented here, so it doubles as a live payload reference.