Skip to content

Wire protocol

Devices and companion apps hold one WebSocket to the backend. Every frame is a UTF-8 JSON object of at most 16384 bytes with a string discriminator t. Any message may carry an optional id (URL-safe, ≤64 chars); an error reply echoes it as ref. Unknown fields are ignored; unknown t values are rejected with bad_message.

Device connection: hello → (unpaired) pair.begin → pair.code … pair.done, then reconnect; (paired) auth.challenge → auth.proof → config. After that, call control and signaling.

App connection: app.hello (session token from HTTP login) → app.ready.

Endpoints and routing hints. Devices connect to /ws/device, adding ?device=<deviceId> once paired; apps connect to /ws/app?household=<householdId>. Hints only route the socket (to the household’s Durable Object on Cloudflare; the self-hosted server ignores them) — every connection is still authenticated by the handshake. Keep-alive {"t":"ping"} must be sent byte-for-byte as shown so hibernating servers can answer it without waking.

First message on every connection.

Field Type Required Notes
id string (len ≤64)
proto integer yes
deviceId string (len ≤64) Omitted by an unpaired device.
model "web-emulator" | "desktop" | "esp32s3" yes
fw string (len ≤32) yes Firmware / emulator version.
buttons integer (≥1, ≤16) yes Number of speed-dial buttons.
display "eink" | "seg14" | "oled" | "none" yes Status display fitted: eink strip (standard), seg14/oled I2C modules (cheaper option), none (Kids Lite: printed key labels, LEDs and voice).

Unpaired device asks for a pairing code to show on its display.

Field Type Required Notes
id string (len ≤64)
alg "ed25519" | "p256" Defaults to ed25519.
publicKey string yes Raw public key, base64url: Ed25519 32 bytes, or P-256 uncompressed SEC1 point 65 bytes.

Answer to auth.challenge.

Field Type Required Notes
id string (len ≤64)
sig string (len 86) yes Signature over the raw nonce bytes: Ed25519 (64 bytes) or P-256 ECDSA/SHA-256 as r‖s (64 bytes).

Handset lifted (up) or returned to the cradle (down).

Field Type Required Notes
id string (len ≤64)
state "up" | "down" yes

Speed-dial button pressed (0-based).

Field Type Required Notes
id string (len ≤64)
index integer (≥0, ≤15) yes

Periodic health report, forwarded to guardians.

Field Type Required Notes
id string (len ≤64)
battery { pct: integer (≥0, ≤100), charging: boolean }
rssi integer Wi-Fi signal strength in dBm.
uptimeS integer (≥0)
power { source: "default" | "1.5A" | "3A", reduced: boolean } USB power source; the Lounge phone needs a ≥1.5 A source for full features.

Accept an incoming call.

Field Type Required Notes
id string (len ≤64)
callId string (len ≤64) yes

Leave (or decline) a call.

Field Type Required Notes
id string (len ≤64)
callId string (len ≤64) yes

SDP offer/answer. Relayed to the peer (p2p) or to the SFU (cloudflare-realtime).

Field Type Required Notes
id string (len ≤64)
callId string (len ≤64) yes
type "offer" | "answer" yes
sdp string yes

Trickled ICE candidate.

Field Type Required Notes
id string (len ≤64)
callId string (len ≤64) yes
candidate string | null yes null signals end-of-candidates.
sdpMid string | null
sdpMLineIndex integer (≥0) | null

Keep-alive.

Field Type Required Notes
id string (len ≤64)

Paired device must sign nonce with its private key.

Field Type Required Notes
id string (len ≤64)
nonce string (len ≥22) yes

Code the device displays; a guardian types it into the companion app.

Field Type Required Notes
id string (len ≤64)
code string (^\d{6}$) yes
expiresAt integer (≥0) yes

Pairing succeeded; device persists deviceId and reconnects with it.

Field Type Required Notes
id string (len ≤64)
deviceId string (len ≤64) yes
householdId string (len ≤64) yes

Sent after authentication and whenever guardians change settings.

Field Type Required Notes
id string (len ≤64)
buttons { index: integer (≥0, ≤15), label: string (len ≤24) }[] yes Only mapped buttons are listed.
quiet boolean yes Quiet hours currently in effect.
quietUntil string (`^([01]\d 2[0-3]):[0-5]\d$`)
missed { from: string (len ≤24) }[] Unheard voicemails, newest first, for the status display.

Incoming call; device rings until answered, hung up, or ended.

Field Type Required Notes
id string (len ≤64)
callId string (len ≤64) yes
from { label: string (len ≤24) } yes

Call progress update.

Field Type Required Notes
id string (len ≤64)
callId string (len ≤64) yes
state "requesting" | "ringing" | "connecting" | "active" | "ended" yes Lifecycle of a call as seen by one participant.
reason "hangup" | "declined" | "busy" | "denied" | "voicemail" | "timeout" | "unreachable" | "unavailable" | "error" Present when state is ended.

ICE servers for this call; precedes any rtc.sdp.

Field Type Required Notes
id string (len ≤64)
callId string (len ≤64) yes
iceServers { urls: string | string[], username?: string, credential?: string }[] yes

SDP offer/answer. Relayed to the peer (p2p) or to the SFU (cloudflare-realtime).

Field Type Required Notes
id string (len ≤64)
callId string (len ≤64) yes
type "offer" | "answer" yes
sdp string yes

Trickled ICE candidate.

Field Type Required Notes
id string (len ≤64)
callId string (len ≤64) yes
candidate string | null yes null signals end-of-candidates.
sdpMid string | null
sdpMLineIndex integer (≥0) | null

Request failed. Connection stays open unless code is unauthorized.

Field Type Required Notes
id string (len ≤64)
code "bad_message" | "unsupported_version" | "unauthorized" | "not_found" | "rate_limited" | "internal" yes
message string (len ≤256) yes
ref string (len ≤64) id of the message that caused the error.

Keep-alive reply.

Field Type Required Notes
id string (len ≤64)

First message from a companion app.

Field Type Required Notes
id string (len ≤64)
proto integer yes
token string (len ≥16, len ≤512) yes

Companion app calls a device.

Field Type Required Notes
id string (len ≤64)
deviceId string (len ≤64) yes

Companion app calls another member of the same server, app to app. Refused unless they’re online and available.

Field Type Required Notes
id string (len ≤64)
userId string (len ≤64) yes

Whether this person is taking app-to-app calls (persisted).

Field Type Required Notes
id string (len ≤64)
available boolean yes

Accept an incoming call.

Field Type Required Notes
id string (len ≤64)
callId string (len ≤64) yes

Leave (or decline) a call.

Field Type Required Notes
id string (len ≤64)
callId string (len ≤64) yes

SDP offer/answer. Relayed to the peer (p2p) or to the SFU (cloudflare-realtime).

Field Type Required Notes
id string (len ≤64)
callId string (len ≤64) yes
type "offer" | "answer" yes
sdp string yes

Trickled ICE candidate.

Field Type Required Notes
id string (len ≤64)
callId string (len ≤64) yes
candidate string | null yes null signals end-of-candidates.
sdpMid string | null
sdpMLineIndex integer (≥0) | null

Keep-alive.

Field Type Required Notes
id string (len ≤64)

Companion app authenticated.

Field Type Required Notes
id string (len ≤64)
userId string (len ≤64) yes

Presence of another member of the server; sent on connect and on every change.

Field Type Required Notes
id string (len ≤64)
userId string (len ≤64) yes
online boolean yes Has at least one open companion session.
available boolean yes Taking app-to-app calls.

Presence and health of a device in the guardian’s household.

Field Type Required Notes
id string (len ≤64)
deviceId string (len ≤64) yes
online boolean yes
battery { pct: integer (≥0, ≤100), charging: boolean }
rssi integer Wi-Fi signal strength in dBm.
power { source: "default" | "1.5A" | "3A", reduced: boolean } USB power source; the Lounge phone needs a ≥1.5 A source for full features.
lastSeen integer (≥0) yes

A voicemail was left for a phone in the guardian’s household.

Field Type Required Notes
id string (len ≤64) yes
deviceId string (len ≤64) yes
from string (len ≤24) yes

Incoming call; device rings until answered, hung up, or ended.

Field Type Required Notes
id string (len ≤64)
callId string (len ≤64) yes
from { label: string (len ≤24) } yes

Call progress update.

Field Type Required Notes
id string (len ≤64)
callId string (len ≤64) yes
state "requesting" | "ringing" | "connecting" | "active" | "ended" yes Lifecycle of a call as seen by one participant.
reason "hangup" | "declined" | "busy" | "denied" | "voicemail" | "timeout" | "unreachable" | "unavailable" | "error" Present when state is ended.

ICE servers for this call; precedes any rtc.sdp.

Field Type Required Notes
id string (len ≤64)
callId string (len ≤64) yes
iceServers { urls: string | string[], username?: string, credential?: string }[] yes

SDP offer/answer. Relayed to the peer (p2p) or to the SFU (cloudflare-realtime).

Field Type Required Notes
id string (len ≤64)
callId string (len ≤64) yes
type "offer" | "answer" yes
sdp string yes

Trickled ICE candidate.

Field Type Required Notes
id string (len ≤64)
callId string (len ≤64) yes
candidate string | null yes null signals end-of-candidates.
sdpMid string | null
sdpMLineIndex integer (≥0) | null

Request failed. Connection stays open unless code is unauthorized.

Field Type Required Notes
id string (len ≤64)
code "bad_message" | "unsupported_version" | "unauthorized" | "not_found" | "rate_limited" | "internal" yes
message string (len ≤256) yes
ref string (len ≤64) id of the message that caused the error.

Keep-alive reply.

Field Type Required Notes
id string (len ≤64)