Handwrite uses a binary WebSocket protocol for live document co-editing. This is not Socket.IO, a speech socket, or an account-wide event feed. Personal-account integrations can join only when the document/caller qualifies through a team or class with live editing and effective policy permits it. Ordinary standalone personal accounts do not automatically qualify.Call POST https://handwrites.io/api/v1/documents/{id}/live with a user-owned credential carrying documents:read and access to the document. The server checks access, paid-plan eligibility and liveEditing policy. The principal must be a person; organisation and isolated app test credentials cannot join.
The response example is illustrative. Use the returned URL and expiry. Convert its https: scheme to wss: for a WebSocket client and append token through URL query encoding. Never substitute your personal REST API key. A room token is tied to a document and admits a connection within ten minutes; it is not a REST credential. Fetch a fresh admission on reconnect. Avoid logging full socket URLs.
Admission can fail with 402 plan_required, 403 policy_denied/forbidden, 404 not_found, or 503 live_unavailable. The upgrade rejects invalid or expired room tokens with HTTP 401. Paths that are not supported WebSocket upgrades return 404; room loading can return 503. Handle HTTP failure before assuming a socket exists.
Each WebSocket binary message carries one protocol frame. Encoding uses lib0/encoding: unsigned variable-length integers, length-prefixed byte arrays (varUint8Array), and length-prefixed UTF-8 strings (varString). The first value is the message type. There is no JSON envelope and no subprotocol negotiation requirement.
Type
Name
Direction and body
0
SYNC
Both ways: varUint step, varUint8Array payload. Step 0 = Yjs state vector, 1 = state update in reply, 2 = incremental update.
1
AWARENESS
Both ways: varUint8Array containing a y-protocols awareness update.
Client → server, empty body: request current awareness states.
4
PING
Client → server, empty body.
5
PONG
Server → client, empty body, in response to PING.
6
TEXT
Client sends empty body to subscribe to full text; server responds with varString full text and sends subsequent text changes.
7
SPLICE
Client → server: varUint index, varUint deleteCount, varString insert; UTF-16 positions in the current text.
Unsigned varints use seven data bits per byte with a continuation high bit. A byte array starts with its byte length; a string starts with the UTF-8 byte length, followed by those bytes. For example, PING is hex 04, PONG is 05, QUERY is 03, TEXT subscription is 06, full text Hi is 06 02 48 69, and inserting A at index 0 is 07 00 00 01 41.The shared Yjs text is doc.getText("content"). A Yjs client should process the server's welcome, initial sync step 1 (wire step value 0) and awareness. Send its own state vector to receive missing server state; an editor can answer the server's state vector with its missing updates. Apply received changes with a distinct remote origin to prevent echo loops, and send local editor changes as SYNC update frames. Viewers must not send step 2/update frames, even empty updates, because those are treated as writes.Awareness payloads contain varUint count followed by varUint clientId, varUint clock, varString JSON state per entry (null removes presence). User display names/colours/roles are server-controlled from admission. There are at most four awareness client IDs per connection. Remove local awareness when leaving and clear stale peer state when reconnecting.The type numbers 0–3 resemble y-websocket, but Handwrite's status and extensions are custom. Do not assume an unmodified y-websocket provider is fully compatible. Use a protocol adapter with the frame definitions above. For concurrent editors, Yjs updates preserve collaborative intent; positional SPLICE has no base revision or operation acknowledgment and must not be treated as an offline conflict-resolution protocol.
On join, the server sends STATUS welcome with the current role. editor can send updates and splices; viewer can sync/read and publish permitted presence. Update the UI on STATUS role. A viewer's first write is dropped and returns STATUS readonly; a second closes the connection. STATUS denied means stop editing and refresh access, not reconnect forever.
Close code
Meaning
Recovery
1009
Text frame or binary message larger than 2 MiB
Correct framing or split updates; do not retry the same oversized frame.
4400
Malformed protocol frame or Yjs update
Fix the encoder; retain local work for diagnosis/recovery.
4003
Repeated write from a viewer
Switch to read-only; do not replay writes.
4403
Access revoked
Stop and recheck document access.
The decoder returns no message for unknown/malformed types, and the room closes with 4400. Text frames are rejected even when their text is valid JSON. PING/PONG are application frames, distinct from native WebSocket control frames; no fixed client heartbeat interval is mandated by this contract.After a transient disconnect, use bounded backoff with jitter, acquire a new admission, and resync using Yjs state vectors. Do not blindly replay positional splices. Do not infer persistence acknowledgment from socket send success or PONG: the protocol has no per-edit durable-commit acknowledgment. Preserve unsynced local edits and show connection state.While a live room is active, REST text replacement can return 409 live_session_active. Use the socket for text changes, and re-fetch metadata/revision after leaving before using conditional REST edits. The implementation periodically snapshots dirty state (approximately 30 seconds), flushes on closing, and periodically rechecks access; these timings are implementation behavior, not a persistence SLA.
This contract was checked against the server/client codec and room implementation on 2026-10-26. Protocol fixture and room tests passed. This documentation work did not establish an authenticated live room or verify deployment/provider configuration. Consumers should run a two-client test with an authorized test document: editor changes, viewer sync, role downgrade, disconnect/rejoin, Unicode offsets and revoked access.