# Basic WebSocket Server Message Format
WebSocket is a bidirectional transport: both parties can send any set of data in the JSON format at any time. The protocol implements a "request-response" mechanism, where each incoming message must be acknowledged by the recipient.
# Message Types
The protocol defines three types of messages corresponding to the MESSAGE_TYPE enumeration:
- RESERVED = 0 – reserved (not in use);
- REQUEST = 1 – request;
- RESPONSE = 2 – response.
Currently, only REQUEST and RESPONSE are used.
Any message, whether from the client or the server, contains at least two mandatory fields:
{
"type": 1,
"id": 1,
}
Field | Type | Req. | Description |
---|---|---|---|
type | uint32 | Yes | Message type: 1 for request (REQUEST), 2 for response (RESPONSE). |
id | uint32 | Yes | A unique message identifier, represented as an incrementing number. Used to link request-response pairs. For more details on how to use it, see below. |
# Server response
Within the operation of WebSocket, the server can initiate the sending of system messages without a prior request from the client. These messages notify about changes, system events, or actions of other users.
To maintain a stable connection, the client must acknowledge the receipt of each such message. The acknowledgment is sent as a separate response indicating the original event identifier.
{
"type": 2,
"id": 123456
}
If the server does not receive a response within 300 seconds, the connection is considered inactive and will be automatically closed, regardless of other signs of activity (e.g., ping/pong).
# Working with the id parameter
Since WebSocket does not have a request-response concept and the order of incoming messages can be arbitrary, the id
parameter is used to link outgoing commands with their responses. This parameter must be included in every sent request, and the same value will appear in the incoming message that serves as the response to the request. It can also be interpreted as a counter of sent requests for each party. A response with the same id
must be sent for each incoming request.
For example, the client sends a ping
message to the server:
{
"type": 1,
"id": 1,
"method": "ping",
"payload": {
"data": "Hello, World!"
}
}
The server responds to the client (note the id
parameter):
{
"type": 2,
"id": 1,
"payload": {
"some_field": true
}
}
Another example. The client sends a message to the server:
{
"type": 1,
"id": 23768,
"method": "getCurrentTime"
}
The server responds:
{
"type": 2,
"id": 23768,
"payload": {
"data": 1724936321584
}
}
# Timeout and long operations
If a response to the request is not received within 300 seconds (5 minutes), the server will assume that the connection has been lost or the client has become unresponsive, and will close the WebSocket session. For certain tasks, generating a response may take more than 300 seconds. In such cases, multiple request-response cycles will be generated.
As an example, let's consider the call handling logic. A person may not answer (reject) a call for more than 300 seconds. In such a case, we receive the call status as a separate message from the server after any length of time:
Client request:
{
"type": 1,
"id": 4444,
"method": "callToUser",
"payload": {
"callId": "john"
}
}
The server immediately responds with a call creation confirmation:
{
"type": 2,
"id": 1,
"payload": {
"userId": "john@as1.trueconfServer.loc#vcs"
}
}
After some time, when the user answers the call, the server sends a notification to the client:
{
"type": 1,
"id": 4445,
"method": "conferenceCreated",
"payload": {
"userId": "john@as1.trueconfServer.loc#vcs",
"isP2P": true,
"conferenceId": "477dh6731ac54e322@as1.trueconfServer.loc#vcs"
}
}
The client confirms that it has received the conference start event:
{
"type": 2,
"id": 4445
}
# Duplicate IDs in requests
If we send two requests with the same id
, the second request will be rejected, and an error will be returned in response. Similarly, if the id
value in a new request is less than an already used value, that request will also be rejected with an error.
Request:
{
"type": 1,
"id": 12,
"method": "ping"
}
Answer:
{
"type": 2,
"id": 12
}
Resending request with id
= 12:
{
"type": 1,
"id": 12,
"method": "someTest"
}
Error response with errorCode
= 2:
{
"type": 2,
"id": 12,
"payload": {
"errorCode": 2
}
}
# Asynchronous execution of requests
Since WebSocket libraries provide asynchronous APIs, the client or server can send responses to requests in any order and multiple at a time, without waiting for a response to a previous request. The key point is that the request must be sent within 5 minutes:
Request 1:
{
"type": 1,
"id": 278,
"method": "someLongTask"
}
Request 2:
{
"type": 1,
"id": 279,
"method": "createChat",
"payload": {
"chatTitle": "TEST"
}
}
Request 3:
{
"type": 1,
"id": 280,
"method": "createChat",
"payload": {
"chatTitle": "TEST 2"
}
}
Request 4:
{
"type": 1,
"id": 281,
"method": "someFastTask"
}
Response 1 to request 4:
{
"type": 2,
"id": 281
}
Response for request 2:
{
"type": 2,
"id": 279,
"payload": {
"chatId": "1390r4f03f"
}
}
Response 3 to request 3:
{
"type": 2,
"id": 280,
"payload": {
"chatId": "1f4f9f9f39j9f3"
}
}
Response 4 to request 1:
{
"type": 2,
"id": 278
}
# Standard error response
For most requests, in case of an error, a response will be sent in the following format:
{
"type": 2,
"id": 1,
"payload": {
"errorCode": 200
}
}
Category | Code | Name | Description |
---|---|---|---|
Network Errors | 100 | CONNECTION_ERROR | Connection error |
101 | CONNECTION_TIMEOUT | Connection timeout | |
102 | TLS_ERROR | TLS/SSL error | |
103 | UNSUPPORTED_PROTOCOL | Unsupported protocol | |
104 | ROUTE_NOT_FOUND | Route not found | |
Authorization Errors | 200 | NOT_AUTHORIZED | Not authorized |
201 | INVALID_CREDENTIALS | Invalid credentials | |
202 | USER_DISABLED | User disabled | |
203 | CREDENTIALS_EXPIRED | Credentials expired | |
Chat Errors | 300 | INTERNAL_ERROR | Internal server error |
301 | TIMEOUT | Operation timeout | |
302 | ACCESS_DENIED | Access denied | |
303 | NOT_ENOUGH_RIGHTS | Not enough rights | |
304 | CHAT_NOT_FOUND | Chat not found | |
305 | USER_IS_NOT_CHAT_PARTICIPANT | User is not a chat participant | |
306 | MESSAGE_NOT_FOUND | Message not found | |
307 | UNKNOWN_MESSAGE | Unknown message | |
308 | FILE_NOT_FOUND | File not found | |
309 | USER_ALREADY_IN_CHAT | User already in chat |
# Message Structure
Each message received as an event (server request), retrieved by message ID, or found in the chat message history corresponds to an Envelope object:
{
"method": "sendMessage",
"type": 1,
"id": 3,
"payload": {
"chatId": "1c1230635432aa7be051e4fda53a3d5a07c8c151",
"messageId": "dfb127d0-d174-4e11-8394-19482a98607d",
"timestamp": 1741881175593,
"author": {
"id": "user@video.example.com",
"type": 1
},
"isEdited": false,
"box": {
"id": 2,
"position": "0"
},
"type": 200,
"content": {
"text": "What's up?",
"parseMode": "text"
}
}
}
You can find the description of the parameters in this section.
# Message Storage "Box"
According to TrueConf's internal protocol, each message is placed in a separate storage - a Box, also known as an "envelope" (object EnvelopeBox):
{
"id" : 1,
"position" : ""
}
In most cases, there will be only one message in the box, and to properly sort the messages, it is sufficient to consider the id
parameter. The id
field is an automatically incrementing number, starting from zero within each chat. In the vast majority of cases, the first message in a chat will have a Box with id
= 0, the second with id
= 1, and so on. In this case, the position
field for each message will be an empty string.
However, in some cases, a single "box" may receive two or more messages. This is a very rare occurrence, possible, for example, when two or more users simultaneously send messages to the chat, and both of those users experience network connection issues.
If two or more messages end up in the same "box," the position
field is filled with the ordinal number of that message in the "box." The distinctive feature is that the position of the message in the box is not a number but a string containing the characters 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz{}
.
To sort messages by position
, it is sufficient to compare two strings based on their ASCII codes. This type of comparison is available by default in most programming languages.
Comparison of three messages that ended up in the same "box":
//Message 1:
{
"id" : 15,
"position" : "A"
}
//Message 2:
{
"id" : 15,
"position" : "B"
}
//Message 3:
{
"id" : 15,
"position" : "AAA"
}
In "standard" string comparison, the ASCII value of each character in the strings is compared sequentially. The string with the greater ASCII value is considered greater. If the ASCII values of all characters are equal, then the string with the greater number of characters is considered greater.
Compare the first and second message:
ASCII("A") = 41
ASCII("B") = 42
42 > 41
Thus, the second message has a sequence number greater than the first.
Compare the second and third messages:
ASCII("B") = 66
ASCII("AAA") = 65 65 65
66 < 65
As can be seen, the check does not proceed beyond the first character since the character B
has a higher ordinal number than the character A
. Therefore, the third message has a lower ordinal number than the second.
Compare the first and third messages:
ASCII("A") = 41
ASCII("AAA") = 41 41 41
41 = 41
The ordinal numbers of characters in both strings match, and in this case, the string with the greater number of characters is considered larger. Thus, the third message has a higher ordinal number than the first.
After sorting, the order of messages will be as follows:
//Message 1:
{
"id": 15,
"position": "A"
}
//Message 3:
{
"id" : 15,
"position" : "AAA"
}
//Message 2:
{
"id" : 15,
"position" : "B"
}