API

Endpoints & protocol

The hub's HTTP and WebSocket surfaces, authentication, streaming, and timings.

The hub has a LAN HTTPS surface for paired displays and data producers, and a localhost HTTP admin surface for MCP, the CLI, and local tools. The default ports are 8443 and 8444, respectively.

Widget JavaScript cannot call these endpoints directly. It uses the window.PREEN SDK, and the native app makes authenticated requests on its behalf. The SDK has no arbitrary URL proxy.

LAN surface

Authentication varies by operation. Session means a paired device’s bearer token; write means the hub’s producer/admin token. Pairing and WebSocket control use the handshakes described below.

PathMethodAuthPurpose
/widget/:idGETSessionWidget HTML
/widget/:id/iconGETSessionPNG icon bytes; 404 for a non-PNG icon
/api/data/:idGETSessionLatest JSON feed envelope
/api/data/:idPOSTWriteReplace the JSON feed
/api/data/:id/streamGETSessionSSE feed stream
/api/store/:id/:collectionGET / POST / DELETESession or writeList / append / clear datastore records
/api/store/:id/:collection/:recordIdPUT / DELETESession or writeReplace / delete one record
/activeGETSessionThis device’s active widget and version
/widgetsGETSessionWidget list and this device’s active id
/controlWebSocketChallenge-responseControl messages, including switches and reloads
/pairPOSTPairing token in bodyExchange a one-time token for a session
/api/action/:widgetId/:actionIdPOSTSession + consentInvoke a registered action
/api/pulsePOSTSessionAnimate the sending device’s bird
/api/active/:idPOSTSessionSelect a widget for this device
/api/foregroundPOSTSessionReport the displayed widget, or an empty object for none
/api/widget/:id/versionsGETSessionVersion history
/api/widget/:id/revert/:versionPOSTSessionSelect an earlier version
/api/widget/:id/deletePOSTSessionDelete a widget
/api/widgets/orderPOSTSessionSet the shared widget order
/api/unpairPOSTSessionRevoke this device’s pairing
/api/renamePOSTSessionRename this device
/api/bird-colorPOSTSessionSet this device’s bird color
/api/relayGETSessionRead relay configuration and address; not an uplink health check
/api/relay/passPOSTSessionDeliver a Preen Plus relay pass

The native SDK supplies the current widget id for db and trigger calls; widget code cannot select another id through those methods. Tokens themselves are not scoped to a single widget. A paired native client can access the hub’s widget library and datastores, while the write token can feed any widget and access any datastore. Keep both tokens out of widget HTML.

Streaming and WebSocket control

GET /api/data/:id/stream returns text/event-stream. Each data: event contains an envelope; the native app delivers only its data field to onData:

data: {"widgetId":"wgt_abc123","data":{"tempC":21.4},"ts":1788640000}

The hub sends the current snapshot if one exists, streams subsequent updates, and sends a comment heartbeat about every 15 seconds. There is no event-id replay history. The phone reconnects interrupted streams and also runs a roughly 10-second backup GET poll, so duplicate snapshot delivery is possible.

/control is a separate WebSocket. The hub sends a challenge nonce; the client responds with its device id and an HMAC made using the session token. After acceptance, the hub sends control messages such as active-widget selection, HTML reloads, and appearance changes. The raw token is not a WebSocket query parameter or authentication frame.

With Preen Plus, the native app carries requests, streamed data, and control messages through an end-to-end encrypted relay. The relay path uses sealed requests and a sealed control WebSocket, rather than exposing the LAN routes as ordinary public endpoints. The widget SDK behaves the same on both paths.

Admin surface (127.0.0.1, write token)

Every route below requires the write token. The listener is bound to loopback; MCP and the CLI are clients of this surface, and other local tools may use it.

PathMethodPurpose
/admin/publishPOSTPublish a widget; inline actions remain pending
/admin/updatePOSTUpdate widget HTML and optional metadata
/admin/set-activePOSTSelect the active widget
/admin/listGETList widgets
/admin/deletePOSTDelete a widget
/admin/data/:idGET / POSTRead / replace the JSON feed
/admin/html/:idGETRead widget HTML
/admin/data-endpoint/:idGETGet the LAN producer URL and write token
/admin/store/:id/:collectionGET / POST / DELETEList / append / clear datastore records
/admin/store/:id/:collection/:recordIdPUT / DELETEReplace / delete one record
/admin/pair-statusGETPairing and connection status
/admin/set-iconPOSTSet or replace an icon
/admin/versions/:idGETVersion history
/admin/revertPOSTSelect an earlier version
/admin/register-actionPOSTRegister an action, pending approval
/admin/registry-linkPOSTRecord the registry entry a widget publishes to

There is no admin endpoint to approve consent. Registering capabilities does not arm them. Approval happens in the Mac app, including through its optional Auto-consent controls setting. See Interactive actions.

Mac Notch pairing, settings, alerts, and rules use /admin/surfaces and /admin/surfaces/:id, under the same write authentication. Each Notch is bound to one display; see the Mac Notch endpoint reference.

Discovery and TLS

service type:   _preen._tcp
domain:         local.

The LAN listener uses a self-signed HTTPS certificate. The native app pins its fingerprint when pairing. External producers need to trust the certificate; see TLS setup.

The iOS client chooses the route. The pairing QR supplies a local host and port, a certificate fingerprint, a one-time pairing token, and an optional relay base URL. An empty host supports relay-only pairing. Bonjour rediscovery uses the resolved address and advertised port; a candidate must authenticate with the saved certificate pin and session token before it replaces the saved endpoint.

With Local Network enabled, reconnect normally tries the saved address, an optional Wake-on-LAN retry, and Bonjour before relay. A recently working relay route or a pairing without a saved local address leads with relay. During a relay connection, the client probes for a verified local route before switching back. Disabled routes are skipped. See the connection guide for user controls.

PairResponse.relayBase supplies the relay address to save; when absent, the client retains the QR’s address for compatibility. Successful LAN connections refresh this through GET /api/relay. Its enabled and baseURL fields describe the Mac’s configuration, independently of uplink liveness. enabled: false clears the saved relay address. An advertised address alone does not establish Plus entitlement or relay admission.

POST /pair returns HTTP 401 with {"reason":"expired"} or {"reason":"invalid"} when the token is refused. The shared client preserves these reasons on LAN and relay. A relay handshake refusal may have no pairing reason body; clients must not assume it means the code expired. A pairing refusal ends the attempt and is separate from an established-session rejection.

Headers

Authorization: Bearer <token>     # authenticated HTTP routes
X-Preen-Schema: <version>         # wire schema metadata

Pairing supplies its one-time token in the request body. WebSocket control uses the challenge-response exchange above. Neither puts a token in the URL.

Timings

BehaviorCurrent value
Live data deliverySSE as updates arrive
Native backup data pollImmediately, then roughly every 10 s
Stored refreshMs default1500 ms; not used to schedule the current iOS feed loop
SSE heartbeat15 s
Control heartbeat20 s
Pairing token TTL120 s
Minimum action invocation spacing0.5 s per widget