Skip to content

WebSocket Protocol

Reference for the Pulse real-time WebSocket protocol. Use this to build custom clients in any language.

Connection

Connect to ws://your-server:4567 (or wss:// for TLS) as a standard WebSocket. The server listens on the root path (/).

After the connection opens, immediately send an auth message. The server responds with auth:ok on success or auth:error on failure.

All messages are JSON-encoded strings.

Client → Server Messages

auth

Sent immediately after connection. Required before any other message.

json
{
  "type": "auth",
  "apiKey": "pk_your_publishable_key",
  "token": "eyJhbGci...",
  "room": "my-room",
  "deviceType": "desktop"
}
FieldTypeRequiredDescription
apiKeystringYesPublishable key (pk_...)
tokenstringYesUser JWT from /auth/token
roomstringYesRoom identifier
deviceType"desktop" | "mobile" | "tablet"NoDevice type for presence

cursor:move

Send the current cursor position.

json
{
  "type": "cursor:move",
  "position": { "x": 0.5, "y": 0.3, "pageX": 500, "pageY": 300 }
}
FieldTypeDescription
position.xnumberRelative X position (0-1 range)
position.ynumberRelative Y position (0-1 range)
position.pageXnumberAbsolute X position in pixels
position.pageYnumberAbsolute Y position in pixels

presence:update

Update your presence status.

json
{ "type": "presence:update", "status": "idle" }
FieldTypeDescription
status"online" | "idle"Presence status

thread:create

Create a new thread with an initial comment.

json
{
  "type": "thread:create",
  "id": "thread-uuid",
  "body": "This button looks off",
  "mentions": ["user-2"],
  "position": { "x": 0.45, "y": 0.72 },
  "attachmentIds": ["attach-uuid"]
}
FieldTypeRequiredDescription
idstringYesClient-generated thread ID
bodystringYesFirst comment text
mentionsstring[]YesUser IDs to @mention
positionPinPosition | nullYesPin position (see below), or null for unpinned
attachmentIdsstring[]NoPre-uploaded attachment IDs

PinPosition fields:

FieldTypeRequiredDescription
xnumberYesHorizontal position (0-1 range)
ynumberYesVertical position (0-1 range)
selectorstringNoCSS selector of the target element
pathstringNoDOM path to the target element
elementOffsetXnumberNoHorizontal offset within the target element
elementOffsetYnumberNoVertical offset within the target element
scrollXnumberNoPage scroll X at time of pinning
scrollYnumberNoPage scroll Y at time of pinning

comment:create

Reply to an existing thread.

json
{
  "type": "comment:create",
  "threadId": "thread-uuid",
  "id": "comment-uuid",
  "body": "Agreed, fixing now",
  "mentions": ["user-1"],
  "attachmentIds": ["attach-uuid"]
}
FieldTypeRequiredDescription
threadIdstringYesThread to reply to
idstringYesClient-generated comment ID
bodystringYesComment text
mentionsstring[]YesUser IDs to @mention
attachmentIdsstring[]NoPre-uploaded attachment IDs

comment:edit

Edit your own comment.

json
{
  "type": "comment:edit",
  "commentId": "comment-uuid",
  "body": "Updated text",
  "mentions": ["user-1"]
}
FieldTypeDescription
commentIdstringComment to edit
bodystringNew comment text
mentionsstring[]Updated mention list

comment:delete

Delete your own comment.

json
{ "type": "comment:delete", "commentId": "comment-uuid" }

thread:resolve

Resolve or reopen a thread.

json
{ "type": "thread:resolve", "threadId": "thread-uuid", "resolved": true }
FieldTypeDescription
threadIdstringThread to resolve/reopen
resolvedbooleantrue to resolve, false to reopen

reaction:add

Add an emoji reaction to a comment or thread.

json
{
  "type": "reaction:add",
  "targetId": "comment-uuid",
  "targetType": "comment",
  "emoji": "👍"
}
FieldTypeDescription
targetIdstringID of the comment or thread
targetType"comment" | "thread"What to react to
emojistringEmoji character

reaction:remove

Remove your own reaction.

json
{ "type": "reaction:remove", "reactionId": "reaction-uuid" }

notification:read

Mark a single notification as read.

json
{ "type": "notification:read", "notificationId": "notif-uuid" }

notification:read-all

Mark all notifications as read.

json
{ "type": "notification:read-all" }

click:perform

Send a click indicator visible to other users.

json
{
  "type": "click:perform",
  "position": { "x": 0.5, "y": 0.3, "pageX": 500, "pageY": 300 }
}

typing:start

Send a typing indicator for a thread.

json
{ "type": "typing:start", "threadId": "thread-uuid" }

viewport:update

Broadcast your current viewport position.

json
{
  "type": "viewport:update",
  "scrollX": 0,
  "scrollY": 450,
  "viewportWidth": 1920,
  "viewportHeight": 1080,
  "pageWidth": 1920,
  "pageHeight": 5000
}
FieldTypeDescription
scrollXnumberHorizontal scroll offset
scrollYnumberVertical scroll offset
viewportWidthnumberViewport width in pixels
viewportHeightnumberViewport height in pixels
pageWidthnumberTotal page width in pixels
pageHeightnumberTotal page height in pixels

selection:update

Broadcast the user's current text selection (or null to clear).

json
{
  "type": "selection:update",
  "selection": {
    "startSelector": "#content > p:nth-child(2)",
    "startOffset": 5,
    "endSelector": "#content > p:nth-child(2)",
    "endOffset": 42
  }
}
FieldTypeDescription
selectionSelectionRange | nullCurrent selection, or null to clear

SelectionRange fields:

FieldTypeDescription
startSelectorstringCSS selector of the start node
startOffsetnumberCharacter offset within the start node
endSelectorstringCSS selector of the end node
endOffsetnumberCharacter offset within the end node

emoji:drop

Drop an emoji at a position, visible to all users.

json
{
  "type": "emoji:drop",
  "emoji": "🎉",
  "position": { "x": 0.5, "y": 0.3, "pageX": 500, "pageY": 300 }
}

draw:stroke

Send a freehand drawing stroke.

json
{
  "type": "draw:stroke",
  "points": [{ "x": 100, "y": 200 }, { "x": 150, "y": 250 }],
  "color": "#ff0000",
  "width": 3
}
FieldTypeDescription
points{ x: number; y: number }[]Array of stroke points
colorstringStroke color (CSS color string)
widthnumberStroke width in pixels

draw:clear

Clear all drawing strokes.

json
{ "type": "draw:clear" }

Server → Client Messages

auth:ok

Sent after a successful auth message. Contains the full initial state.

json
{
  "type": "auth:ok",
  "user": { "id": "env:user-1", "name": "Alice", "color": "#E57373" },
  "presence": [],
  "threads": [],
  "notifications": [],
  "reactions": [],
  "activityLogs": [],
  "users": [],
  "config": {
    "allowImages": true,
    "allowAudio": true,
    "allowVideo": true,
    "maxFileSizeMb": 10,
    "maxAttachmentsPerComment": 5,
    "allowReactions": true,
    "allowDrawing": true,
    "allowMentions": true,
    "showCursors": true,
    "showPresence": true,
    "showTypingIndicators": true
  }
}
FieldTypeDescription
userPulseUserThe authenticated user
presencePresenceUser[]Currently online users in the room
threadsThread[]All threads in the room (with comments)
notificationsNotification[]Notifications for this user
reactionsReaction[]All reactions in the room
activityLogsActivityLog[]Recent activity log entries
usersPulseUser[]All known users in the room
configEnvironmentConfigEnvironment feature flags and limits

auth:error

Sent when authentication fails.

json
{ "type": "auth:error", "message": "Invalid API key" }

presence:join

A user joined the room.

json
{
  "type": "presence:join",
  "user": {
    "user": { "id": "env:user-2", "name": "Bob", "color": "#64B5F6" },
    "cursor": null,
    "status": "online",
    "lastSeen": "2026-03-10T14:00:00.000Z",
    "deviceType": "desktop"
  }
}

presence:leave

A user left the room (or timed out).

json
{ "type": "presence:leave", "userId": "env:user-2" }

presence:update

A user's presence status changed.

json
{ "type": "presence:update", "userId": "env:user-2", "status": "idle" }

cursor:move

A user's cursor moved.

json
{
  "type": "cursor:move",
  "userId": "env:user-2",
  "position": { "x": 0.5, "y": 0.3, "pageX": 500, "pageY": 300 }
}

thread:created

A new thread was created.

json
{
  "type": "thread:created",
  "thread": {
    "id": "thread-uuid",
    "roomId": "dashboard",
    "resolved": false,
    "position": { "x": 0.45, "y": 0.72 },
    "comments": [{ "id": "comment-uuid", "threadId": "thread-uuid", "userId": "env:user-1", "body": "...", "mentions": [], "attachments": [], "createdAt": "...", "editedAt": null }],
    "createdAt": "2026-03-10T14:00:00.000Z",
    "updatedAt": "2026-03-10T14:00:00.000Z"
  }
}

comment:created

A new comment was added to a thread.

json
{
  "type": "comment:created",
  "threadId": "thread-uuid",
  "comment": {
    "id": "comment-uuid",
    "threadId": "thread-uuid",
    "userId": "env:user-2",
    "body": "Reply text",
    "mentions": [],
    "attachments": [],
    "createdAt": "2026-03-10T14:05:00.000Z",
    "editedAt": null
  }
}

comment:edited

A comment was edited.

json
{
  "type": "comment:edited",
  "threadId": "thread-uuid",
  "comment": {
    "id": "comment-uuid",
    "threadId": "thread-uuid",
    "userId": "env:user-1",
    "body": "Updated text",
    "mentions": [],
    "attachments": [],
    "createdAt": "2026-03-10T14:00:00.000Z",
    "editedAt": "2026-03-10T14:10:00.000Z"
  }
}

comment:deleted

A comment was deleted.

json
{ "type": "comment:deleted", "threadId": "thread-uuid", "commentId": "comment-uuid" }

thread:resolved

A thread was resolved or reopened.

json
{ "type": "thread:resolved", "threadId": "thread-uuid", "resolved": true }

thread:deleted

A thread was deleted (via REST API).

json
{ "type": "thread:deleted", "threadId": "thread-uuid", "roomId": "dashboard" }

reaction:added

A reaction was added.

json
{
  "type": "reaction:added",
  "reaction": {
    "id": "reaction-uuid",
    "targetId": "comment-uuid",
    "targetType": "comment",
    "userId": "env:user-2",
    "emoji": "👍",
    "createdAt": "2026-03-10T14:20:00.000Z"
  }
}

reaction:removed

A reaction was removed.

json
{ "type": "reaction:removed", "reactionId": "reaction-uuid", "targetId": "comment-uuid" }

notification

A new notification for the current user.

json
{
  "type": "notification",
  "notification": {
    "id": "notif-uuid",
    "userId": "env:user-1",
    "type": "comment:mention",
    "roomId": "dashboard",
    "threadId": "thread-uuid",
    "commentId": "comment-uuid",
    "actorId": "env:user-2",
    "read": false,
    "createdAt": "2026-03-10T14:05:00.000Z"
  }
}

click:perform

A user clicked on the page (click indicator).

json
{
  "type": "click:perform",
  "userId": "env:user-2",
  "position": { "x": 0.5, "y": 0.3, "pageX": 500, "pageY": 300 }
}

typing:indicator

A user is typing in a thread.

json
{ "type": "typing:indicator", "userId": "env:user-2", "threadId": "thread-uuid" }

viewport:update

A user's viewport position changed.

json
{
  "type": "viewport:update",
  "userId": "env:user-2",
  "scrollX": 0,
  "scrollY": 450,
  "viewportWidth": 1920,
  "viewportHeight": 1080,
  "pageWidth": 1920,
  "pageHeight": 5000
}

selection:update

A user's text selection changed.

json
{
  "type": "selection:update",
  "userId": "env:user-2",
  "selection": {
    "startSelector": "#content > p:nth-child(2)",
    "startOffset": 5,
    "endSelector": "#content > p:nth-child(2)",
    "endOffset": 42
  }
}

emoji:drop

A user dropped an emoji.

json
{
  "type": "emoji:drop",
  "userId": "env:user-2",
  "emoji": "🎉",
  "position": { "x": 0.5, "y": 0.3, "pageX": 500, "pageY": 300 }
}

draw:stroke

A user sent a drawing stroke.

json
{
  "type": "draw:stroke",
  "userId": "env:user-2",
  "points": [{ "x": 100, "y": 200 }, { "x": 150, "y": 250 }],
  "color": "#ff0000",
  "width": 3
}

draw:clear

A user cleared the drawing canvas.

json
{ "type": "draw:clear", "userId": "env:user-2" }

activity:logged

A new activity log entry.

json
{
  "type": "activity:logged",
  "activityLog": {
    "id": "log-uuid",
    "roomId": "dashboard",
    "userId": "env:user-1",
    "type": "comment",
    "description": "Alice commented on a thread",
    "createdAt": "2026-03-10T14:05:00.000Z"
  }
}

error

A generic error from the server.

json
{ "type": "error", "message": "Something went wrong", "code": "INVALID_THREAD" }
FieldTypeDescription
messagestringHuman-readable error message
codestring?Optional machine-readable error code

P2P Signaling Messages

These messages are used for WebRTC peer-to-peer connection setup. The server relays them between peers without inspecting the content. See P2P Overview for details.

Client → Server

signal:offer

Send a WebRTC SDP offer to a specific peer.

json
{
  "type": "signal:offer",
  "targetUserId": "env:user-2",
  "sdp": { "type": "offer", "sdp": "v=0\r\n..." }
}

signal:answer

Respond to a peer's SDP offer.

json
{
  "type": "signal:answer",
  "targetUserId": "env:user-1",
  "sdp": { "type": "answer", "sdp": "v=0\r\n..." }
}

signal:ice

Send an ICE candidate for NAT traversal.

json
{
  "type": "signal:ice",
  "targetUserId": "env:user-2",
  "candidate": { "candidate": "candidate:...", "sdpMid": "0", "sdpMLineIndex": 0 }
}

p2p:sync

Broadcast a Yjs CRDT update to all peers via WebSocket (fallback when P2P unavailable).

json
{
  "type": "p2p:sync",
  "update": "base64-encoded-yjs-update"
}

Server → Client

signal:offer / signal:answer / signal:ice

Same as above but with fromUserId instead of targetUserId:

json
{
  "type": "signal:offer",
  "fromUserId": "env:user-1",
  "sdp": { "type": "offer", "sdp": "v=0\r\n..." }
}

p2p:sync

Relayed CRDT update from another peer:

json
{
  "type": "p2p:sync",
  "fromUserId": "env:user-1",
  "update": "base64-encoded-yjs-update"
}

Pulse Collaboration SDK