Skip to content

WebSocket Push Extension

Capability URI: https://specs.serverlessinbox.com/websocket

Status: Draft


RFC 8620 §7 defines an EventSource-based push mechanism. This extension defines an alternative using a persistent WebSocket connection, which suits environments where Server-Sent Events (SSE) are unavailable or undesirable (e.g. certain CDN/proxy configurations, mobile clients).

The protocol uses a simple JSON envelope with a oneof-style discriminator: each message is a JSON object with exactly one of the known keys (subscribe, subscribed, stateChange, error).


A server that supports this extension includes the capability URI in its session capabilities object. The capability value contains the WebSocket endpoint URL and the server’s subscription limit.

{
"capabilities": {
"https://specs.serverlessinbox.com/websocket": {
"url": "wss://ws.serverlessinbox.com",
"maxSubscriptions": 10
}
}
}
PropertyTypeDescription
urlstringThe WebSocket endpoint to connect to.
maxSubscriptionsintegerMaximum number of JMAP data types that can be listed in a single subscribe message.

  1. Client opens a WebSocket connection to the url from the capability object.
  2. Client sends one or more subscribe messages, one per account.
  3. Server responds with subscribed for each successful subscription.
  4. Server sends stateChange messages whenever object state changes on a subscribed account.
  5. Either party may close the connection at any time.

A single connection can hold subscriptions for multiple accounts.


All messages are JSON text frames. Each frame contains exactly one top-level key that identifies the message type.

Request a subscription to state changes for an account.

{
"subscribe": {
"id": "sub-1",
"accountId": "u1",
"types": ["Email", "Mailbox"]
}
}
FieldTypeRequiredDescription
idstringyesClient-generated correlation id. Returned in the subscribed confirmation.
accountIdstringyesJMAP account id to subscribe to.
typesstring[]noJMAP data types to receive changes for (e.g. ["Email", "Mailbox"]). Empty array or omitted means all types.

Confirms that a subscribe request was accepted.

{
"subscribed": {
"id": "sub-1"
}
}
FieldTypeDescription
idstringThe id from the corresponding subscribe message.

Notifies the client that one or more JMAP data types have a new state on an account. The client should follow up with the appropriate */changes calls.

{
"stateChange": {
"accountId": "u1",
"changes": {
"Email": "state-abc123",
"Mailbox": "state-def456"
}
}
}
FieldTypeDescription
accountIdstringThe account where changes occurred.
changes{ [dataType: string]: string }Map of JMAP data type name to its new state string.

The changes map contains only the types that actually changed. Clients must not assume that a type is unchanged if it is absent from a given stateChange message.

Sent when a client message cannot be processed.

{
"error": {
"id": "sub-1",
"code": "forbidden",
"description": "Account not accessible."
}
}
FieldTypeDescription
idstringCorrelation id from the triggering message, if applicable. Empty string if not applicable.
codestringStable error code (see Error codes below).
descriptionstringHuman-readable description. Not intended for programmatic use.

CodeDescription
forbiddenThe client does not have permission to subscribe to the requested account.
invalidArgumentsThe subscribe message is missing required fields or contains invalid values.
accountNotFoundThe requested account does not exist.
tooManySubscriptionsThe types array in the subscribe message exceeds the server’s maxSubscriptions limit.
serverFailAn unexpected server error occurred. The client may retry.

stateChange messages carry new state strings, not object diffs. Upon receiving one, the client must call the appropriate JMAP */changes method using the new state to fetch the actual ids that changed, then fetch the changed objects with */get.

The typical flow for an Email state change:

stateChange → Email/changes (sinceState: previousState, newState from message)
→ Email/get (ids: created + updated from changes response)