DECKBOARD Docs

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: Bearer header (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.
Building in the browser? /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

  1. Create an account and make a board in Control — the first-run flow walks you through it.
  2. Create an API key: account menu → API keysNew key. Keys look like dck_5f4d… and the full key is shown once — store it somewhere safe.
  3. Grab your board id from board settings in Control (or list your boards with GET /api/boards).
  4. 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).

On Windows, use 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:

CredentialLooks likeScopeWhere 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
Never put session tokens or API keys in query strings. The single exception in the whole system is the display token in display/WebSocket URLs — it is read-only by design, because browser WebSockets cannot send headers.

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:

ChannelConfig exampleBehavior
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.
  • src is 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:

CodeEffect
{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.
\nLine break (word-wrap handles the rest).

Live variables are resolved on the display and keep updating in place:

VariableRenders 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

MethodPathAuthDescription
POST/api/auth/signup{email, password} (min 8 chars) → {token, user}
POST/api/auth/login{email, password}{token, user}
POST/api/auth/logoutsessionInvalidates 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/mesession{user:{id,email,created}, prefs}
PUT/api/me/prefssessionMerge-update preferences: {density?, theme?, soundEnabled?, soundVolume?, …}

Boards

MethodPathAuthDescription
GET/api/boardssession[{id, name, settings, displayToken, createdAt, lastSeenAt}]
POST/api/boardssession{name} → board. Settings default from your prefs.
GET/api/boards/:idsession→ board + current payload.
PUT/api/boards/:idsession{name?, settings?}; settings (density, theme, soundEnabled, soundVolume, flipSpeed) broadcast live to displays.
DELETE/api/boards/:idsessionDelete the board.
POST/api/boards/:id/rotate-tokensession→ new display token. Revokes the old one everywhere.

Push, state & history

MethodPathAuthDescription
POST/api/boards/:id/showsession or API keyPush a payload{ok, seq}. Logged to history. This is the public push API.
GET/api/boards/:id/statesession, API key or display token{payload, settings} — poll fallback for displays.
GET/api/boards/:id/history?limit=50session[{seq, payload, source, createdAt}] — source is control | api | replay | schedule. Last 200 kept.
POST/api/boards/:id/history/:seq/replaysessionRe-push that payload.

API keys

MethodPathAuthDescription
GET/api/keyssession[{id, prefix, name, createdAt, lastUsedAt}] (prefix = first 8 chars).
POST/api/keyssession{name}{key} — the full key, shown exactly once.
DELETE/api/keys/:idsessionRevoke a key immediately.

Proxies & health

MethodPathAuthDescription
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." } }
LimitValue
Auth endpoints (signup, login, forgot, reset)10 requests / 5 min / IP
Message push (/show)120 / min / board
Payload size128 KB (image payloads: 512 KB)
History retainedlast 200 pushes per board
Custom feed proxy response256 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>
Prefer 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:

MessageWhen
{"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).
  • seq increases 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/state every 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:
ParameterValuesEffect
&density=ultimate | high | medium | lowTile density preset for this screen only.
&theme=classic | amber | terminal | paper | navy | crimsonBoard theme for this screen only.
&sound=on | offFlap sound.
&volume=0.0 – 1.0Flap volume.
&fit=COLSxROWS e.g. 22x6Fixed 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.
Displays are resilient by design: if the network blips, the board keeps its last frame and shows a tiny status dot while it reconnects — it never goes blank or shows an error page to your customers.

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.