Screens API
Screens & Gadgets
Design wallboard layouts and pair physical displays
Screens are user-owned canvases of Gadgets, optionally shared as reusable Templates, and shown read-only on paired Devices. This page documents the full surface directly from the controllers and DTOs.
Authentication and Permissions
/api/screens,/api/screen-templates, and most of/api/devicesrequire a normal user JWT and thescreensplan feature (@RequireFeature('screens')).- Sharing a Screen as a Template (
POST /api/screen-templates/from-screen/:screenIdwith agroupId) additionally requires thescreen_sharingfeature (Pro+). /api/device-content/*routes authenticate via a Device bearer token (Authorization: Bearer dvc_...), never a user JWT. A Device token can only ever reach its own assigned Screen's rendered content — never arbitrary user data.POST /api/devices/pairing-codesis the one deliberately public, unauthenticated endpoint — the kiosk has no session yet when it requests a pairing code. It's rate-limited per IP.POST /api/device-content/gadgets/:gadgetId/quick-triggeradditionally requires the target automation rule to haveallowQuickTrigger: trueset by its owner, and is rate-limited per-rule-per-device (default: 1 fire per 5 minutes).
Endpoint Reference
| Method | Path | Purpose | Auth |
|---|---|---|---|
GET | /api/screens | List Screens owned by, or shared with, the caller. | JWT |
POST | /api/screens | Create a Screen. Enforces the plan's max_screens limit. | JWT |
GET | /api/screens/:id | Get a Screen with its gadgets. | JWT |
PATCH | /api/screens/:id | Update a Screen's settings. | JWT |
DELETE | /api/screens/:id | Delete a Screen. Assigned devices become unassigned, not deleted. | JWT |
POST | /api/screens/:id/gadgets | Add a Gadget to a Screen. | JWT |
PATCH | /api/screens/:id/gadgets/:gadgetId | Move, resize, or reconfigure a Gadget. | JWT |
DELETE | /api/screens/:id/gadgets/:gadgetId | Remove a Gadget. | JWT |
PUT | /api/screens/:id/gadgets/bulk-layout | Batch-save gadget positions/sizes after a drag session. | JWT |
POST | /api/screens/:id/wallpaper | Upload a wallpaper image (multipart, 200KB-20MB, jpeg/png/webp). | JWT |
DELETE | /api/screens/:id/wallpaper | Remove a Screen's wallpaper. | JWT |
GET | /api/screen-templates | List Templates: owned, system, and group-shared. | JWT |
GET | /api/screen-templates/:id | Get one Template. | JWT |
POST | /api/screen-templates/from-screen/:screenId | Save an existing Screen's layout as a new Template. | JWT (+screen_sharing if shared) |
POST | /api/screen-templates/:id/clone | Clone a system/shared Template into a personal, editable Template. | JWT |
POST | /api/screen-templates/:id/apply | Apply a Template's layout onto an existing or new Screen. | JWT |
PATCH | /api/screen-templates/:id | Rename/redescribe an owned Template. | JWT |
DELETE | /api/screen-templates/:id | Delete an owned Template. | JWT |
POST | /api/devices/pairing-codes | Generate a pairing code (called by the kiosk itself). | Public, IP rate-limited |
GET | /api/devices | List Devices owned by the caller. | JWT |
POST | /api/devices/pair | Redeem a pairing code from an authenticated session. | JWT |
GET | /api/devices/:id | Get a Device. | JWT |
PATCH | /api/devices/:id | Rename a Device or reassign its Screen. | JWT |
POST | /api/devices/:id/revoke | Revoke a Device's credential immediately (no cache delay). | JWT |
GET | /api/device-content/pairing-status | Kiosk long-polls this to detect it has just been paired. | Pairing code + session secret |
GET | /api/device-content/screen | The read-only content payload the kiosk renders. | Device bearer token |
POST | /api/device-content/heartbeat | Lightweight liveness ping. | Device bearer token |
POST | /api/device-content/gadgets/:gadgetId/quick-trigger | Fire an automation rule from a Quick Trigger gadget. | Device bearer token |
Request Shapes
CreateScreenDto
name (string, ≤120), description?, resolutionPreset?, width/height (200-8000), hideNavChrome? (boolean), displayDensity? (comfortable | minimal_large), wallpaperTheme? ({ colorValue, gradient? }), ownerScopeType? (personal | group | organisation), groupId?, organisationId?.
CreateGadgetDto
gadgetType (see Gadget Catalog for the full type list), x/y/width/height (int), zIndex?, gadgetConfig? (type-specific — validated by that Gadget's own provider, not a fixed schema).
BulkGadgetLayoutDto
gadgets: Array<{ id, x, y, width, height, zIndex? }> — up to 60 items.
RedeemPairingCodeDto
code (exactly 8 chars), deviceName (≤120), screenId?.
Example Calls
# Create a Screen
curl -X POST https://api.primecal.eu/api/screens \
-H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \
-d '{"name":"Kitchen Wallboard","width":1920,"height":1080}'
# Add a Clock & Date gadget
curl -X POST https://api.primecal.eu/api/screens/42/gadgets \
-H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \
-d '{"gadgetType":"clock_date","x":40,"y":40,"width":360,"height":160}'
# Kiosk: fetch the current Screen content payload
curl https://api.primecal.eu/api/device-content/screen \
-H "Authorization: Bearer $DEVICE_TOKEN"
Response and Behavior Notes
GET /api/device-content/screenreturns{ state: 'ok', screen, gadgets, pollIntervalSeconds },{ state: 'unassigned' }, or{ state: 'unavailable', reason }— never a 404/403 for these expected states, so an unattended kiosk can render a calm message instead of an error screen.- Each gadget's
datashape is specific to itsgadgetType— read thegadgetTypeof each gadget before interpreting itsdata. Screen.wallpaperBlobUrlandScreen.wallpaperThemeare mutually exclusive — setting one clears the other.- Template
gadgetConfigfields that reference user-specific data (e.g.filterLabelIds,ruleIds) are cleared on clone/apply across users — a Template never carries live or user-specific data.
Best Practices
- Always call
PUT .../gadgets/bulk-layoutafter a drag session instead of onePATCHper gadget. - Re-probe wallpaper uploads server-side rather than trusting
Content-Type— this API already does, but if you're proxying uploads, don't strip that validation. - Treat a Device bearer token as a long-lived secret with no expiry by default — revoke it (
POST /api/devices/:id/revoke) rather than assuming it will lapse on its own.