CLEMA protocol

Handshake

After establishing a WebSocket connection, the client must send connection.hello to declare supported message types (capabilities). This must be the first message before any other communication.

Hello Request

Client:

{
  "type": "connection.hello",
  "data": {
    "types": [
      "auth.code", "auth.phone", "auth.qr", "auth.token", "auth.refresh", "auth.logout", "qr.get", "qr.accept",
      "session.list", "session.revoke", "phone.get", "phone.code", "phone.set", "push.register", "push.unregister", "activity.set",
      "transport.list", "transport.setup", "transport.password", "transport.enable", "transport.disable", "transport.reset",
      "message.send", "message.forward", "message.edit", "message.delete", "message.react", "message.play",
      "message.list", "message.search", "message.clear", "read.set", "delivery.set",
      "pin.set", "pin.remove", "pin.list", "notification.set", "draft.update", "reactions.see",
      "block.add", "block.remove", "block.list", "privacy.get", "privacy.update",
      "ban.add", "ban.remove", "ban.list", "restriction.set", "sync.state", "sync.pull",
      "conversation.create", "conversation.list", "conversation.search", "conversation.get", "conversation.update", "conversation.delete",
      "member.list", "member.add", "member.update", "member.remove",
      "invite.create", "invite.list", "invite.get", "invite.accept", "invite.revoke", "invite.update",
      "contact.list", "contact.add", "contact.rename", "contact.delete",
      "phonebook.update", "phonebook.get", "phonebook.clear",
      "presence.get", "presence.set", "profile.get", "profile.update", "user.search", "typing.set",
      "file.create", "file.get", "file.complete", "file.abort",
      "transfer.create", "transfer.accept", "transfer.decline", "transfer.cancel", "button.press", "button.answer",
      "pack.list", "pack.get", "pack.install", "pack.remove", "pack.order",
      "session.revoked", "transport.updated", "phone.updated", "qr.accepted",
      "message.created", "message.edited", "message.deleted", "message.reacted",
      "message.cleared", "message.played", "read.updated", "delivery.updated", "pin.added", "pin.removed", "notification.updated", "draft.updated",
      "reactions.updated",
      "block.added", "block.removed", "privacy.updated",
      "ban.added", "ban.removed", "restriction.updated",
      "conversation.created", "conversation.updated", "conversation.deleted",
      "member.added", "member.updated", "member.removed",
      "invite.created", "invite.accepted", "invite.revoked", "invite.updated",
      "contact.added", "contact.renamed", "contact.deleted", "contact.requested",
      "presence.online", "presence.offline", "presence.updated", "presence.self", "profile.updated", "file.updated", "typing.changed",
      "transfer.created", "transfer.accepted", "transfer.declined", "transfer.cancelled", "button.pressed",
      "pack.installed", "pack.removed", "pack.ordered"
    ]
  }
}
Field Type Description
types array of strings Message types the client supports
active boolean Optional, default true. false opens the connection passive, as a background sync does

Hello Response

Success:

{
  "type": "connection.hello",
  "ok": true,
  "data": {
    "types": [
      "auth.code", "auth.phone", "auth.qr", "auth.token", "auth.refresh", "auth.logout", "qr.get", "qr.accept",
      "session.list", "session.revoke", "phone.get", "phone.code", "phone.set", "push.register", "push.unregister", "activity.set",
      "transport.list", "transport.setup", "transport.password", "transport.enable", "transport.disable", "transport.reset",
      "message.send", "message.forward", "message.edit", "message.delete", "message.react", "message.play",
      "message.list", "message.search", "message.clear", "read.set", "delivery.set",
      "pin.set", "pin.remove", "pin.list", "notification.set", "draft.update", "reactions.see",
      "block.add", "block.remove", "block.list", "privacy.get", "privacy.update",
      "ban.add", "ban.remove", "ban.list", "restriction.set", "sync.state", "sync.pull",
      "conversation.create", "conversation.list", "conversation.search", "conversation.get", "conversation.update", "conversation.delete",
      "member.list", "member.add", "member.update", "member.remove",
      "invite.create", "invite.list", "invite.get", "invite.accept", "invite.revoke", "invite.update",
      "contact.list", "contact.add", "contact.rename", "contact.delete",
      "phonebook.update", "phonebook.get", "phonebook.clear",
      "presence.get", "presence.set", "profile.get", "profile.update", "user.search", "typing.set",
      "file.create", "file.get", "file.complete", "file.abort",
      "transfer.create", "transfer.accept", "transfer.decline", "transfer.cancel", "button.press", "button.answer",
      "pack.list", "pack.get", "pack.install", "pack.remove", "pack.order",
      "session.revoked", "transport.updated", "phone.updated", "qr.accepted",
      "message.created", "message.edited", "message.deleted", "message.reacted",
      "message.cleared", "message.played", "read.updated", "delivery.updated", "pin.added", "pin.removed", "notification.updated", "draft.updated",
      "reactions.updated",
      "block.added", "block.removed", "privacy.updated",
      "ban.added", "ban.removed", "restriction.updated",
      "conversation.created", "conversation.updated", "conversation.deleted",
      "member.added", "member.updated", "member.removed",
      "invite.created", "invite.accepted", "invite.revoked", "invite.updated",
      "contact.added", "contact.renamed", "contact.deleted", "contact.requested",
      "presence.online", "presence.offline", "presence.updated", "presence.self", "profile.updated", "file.updated", "typing.changed",
      "transfer.created", "transfer.accepted", "transfer.declined", "transfer.cancelled", "button.pressed",
      "pack.installed", "pack.removed", "pack.ordered"
    ]
  }
}

Server responds with the list of types it supports. The intersection of client and server types determines what can be used in the session. A signed-in connection's session remembers the intersection its client said last, for when nothing of it is connected: a device a push wakes is woken only for what its client can answer, such as a direct transfer's offer.

A server whose service has links of its own also answers link_base, such as "link_base":"https://mandarin.im": where every link it hands out starts — invitations, bots and QR sign-in codes. A client opens such links itself instead of leaving them to a browser, and an app of the service claims them on its platform (Android App Links, iOS Universal Links). Without link_base there are no such links.

Failure:

{
  "type": "connection.hello",
  "ok": false,
  "error": {
    "code": "no_common_types",
    "message": "No common message types supported"
  }
}

Capability Negotiation

Protocol Evolution

When a message type needs breaking changes, a new type is introduced:

// Existing client
{"types": ["message.send", ...]}

// New client with updated message format
{"types": ["message.send", "message.send.v2", ...]}

Server uses the most advanced type both parties support. Old clients continue to work with original types.

An event of a stream is kept once, in no particular form, and given its form as it is delivered — live or in a pull: the newest version of its type the client declared, the original one when it declared none. A stream therefore holds one event for every version, and a client that knows only the original form reads every place of it.