Herald

WebSocket Protocol

Herald clients communicate over WebSocket using JSON frames. Every frame follows the same envelope. Authentication happens during the upgrade via query parameters.

Frame envelope

{
  "type": "event.publish",
  "ref": "unique-client-ref",
  "payload": { ... }
}
  • type — message type (required)
  • ref — client-generated reference for request/response correlation (optional)
  • payload — message-specific data

Authentication

Connect with HMAC-SHA256 signed tokens as query parameters:

wss://herald.skeptik.io/ws?key=API_KEY&token=SIGNED_TOKEN&user_id=USER_ID&streams=STREAM_1,STREAM_2

Your backend generates the token by signing a payload containing the user ID, stream IDs, and expiry timestamp with your API secret. On success, Herald responds with auth_ok. On failure, auth_error and the connection closes.

Client → Server messages

TypeDescription
subscribeSubscribe to a stream
unsubscribeUnsubscribe from a stream
event.publishPublish an event to a stream
event.triggerTrigger an ephemeral event (not persisted, sender excluded)
events.fetchFetch historical events by sequence range
cursor.updateUpdate read cursor position
presence.setSet manual presence override (away, dnd, offline)
typing.startBroadcast typing indicator
typing.stopClear typing indicator
reaction.addAdd a reaction to an event
reaction.removeRemove a reaction from an event
pingApplication-level keepalive

Server → Client messages

TypeDescription
auth_okAuthentication succeeded
auth_errorAuthentication failed (connection closes)
subscribedSubscription confirmed with stream metadata
event.newNew event in a subscribed stream
event.ackServer acknowledged a published event
events.batchResponse to events.fetch
presence.changedA user's presence changed
cursor.movedA user's read cursor updated
member.joinedA user joined a stream
member.leftA user left a stream
typingTyping indicator
reaction.changedReaction added or removed
stream.subscriber_countUpdated subscriber count
watchlist.onlineA watched user came online
watchlist.offlineA watched user went offline
system.token_expiringToken expires in 60 seconds
errorError with code and message

Publishing

// Client sends:
{
  "type": "event.publish",
  "ref": "msg-1",
  "payload": {
    "stream_id": "STREAM_ID",
    "type": "message",
    "body": {"text": "Hello"},
    "meta": {}
  }
}

// Server responds:
{
  "type": "event.ack",
  "ref": "msg-1",
  "payload": {
    "event_id": "evt_abc123",
    "seq": 42
  }
}

Ephemeral events

Ephemeral events fan out to subscribers but are not stored. The sender is excluded from delivery. Use these for lightweight signals like cursor position broadcasts or status indicators.

{
  "type": "event.trigger",
  "payload": {
    "stream_id": "STREAM_ID",
    "type": "cursor_move",
    "body": {"x": 120, "y": 340}
  }
}

Reconnect protocol

  1. Connection drops
  2. Client starts exponential backoff (jittered)
  3. Client reconnects with its highest received sequence per stream
  4. Herald replays missed events in order
  5. Client deduplicates by event ID

The SDKs handle reconnect and deduplication automatically.

At-least-once delivery

Opt-in protocol for guaranteed delivery. The client acknowledges each event with event.ack. On reconnect, unacknowledged events are re-delivered.

// Server delivers:
{"type": "event.new", "payload": {"event_id": "evt_abc", "seq": 42, ...}}

// Client acknowledges:
{"type": "event.ack", "payload": {"event_id": "evt_abc", "stream_id": "STREAM_ID"}}

Error codes

CodeMeaning
auth_failedInvalid or expired token
not_foundStream or resource does not exist
forbiddenNot authorized for this action
invalid_payloadMalformed frame payload
rate_limitedToo many requests
stream_fullStream member limit reached
blockedUser is blocked in this stream
internalServer error