Protocol model
OriginChats clients talk to a server over one WebSocket, using JSON text frames. This page describes the rules that apply to every frame. Read it once before you use the command reference.
Frames
Section titled “Frames”Every frame, in both directions, is a JSON object with a cmd field:
type Frame = { cmd: string; listener?: unknown; [field: string]: unknown;};There is no envelope around it. Requests, responses, and events all use the same shape:
- A request is a frame your client sends, such as
messages_get. - A response is a frame the server sends back to the connection that made the request.
- An event is a frame the server pushes without a request, such as another user’s message. See server events.
The server also has HTTP routes on the same host and port for uploads, images, and webhooks. They use their own authentication.
Connection order
Section titled “Connection order”- The client opens a WebSocket to the server’s root path, for example
wss://chat.example.com/. - The server sends
handshakeright away. - The client signs in with
auth,login, orregister. - The server answers
auth_success, thenready. - The client can now send any command its permissions allow.
Every 30 seconds the server also sends {"cmd": "ping"} to every open connection. No reply is needed.
The handshake
Section titled “The handshake”The handshake tells you who the server is, how to sign in, and what it supports. It is the first frame on every connection.
{ "cmd": "handshake", "val": { "version": "1", "server_time": 1788177600.253, "signing_url": "https://chat.example.com", "server": { "name": "Example Chat", "icon": "https://chat.example.com/server-assets/7f1c2d3e-4b5a-6978-8a9b-0c1d2e3f4a5b.png", "banner": "", "url": "https://chat.example.com", "owner": { "name": "alice" } }, "limits": { "post_content": 2000, "search_results": 30, "max_reactions_per_message": 20, "max_members": 0 }, "uploads": { "emoji_allowed_file_types": ["gif", "jpg", "jpeg", "png", "webp", "bmp", "avif", "tiff"], "sticker_allowed_file_types": ["gif", "jpg", "jpeg", "png", "webp", "bmp", "avif", "tiff"], "uploads_per_minute": 10 }, "attachments": { "enabled": true, "max_size": 104857600, "allowed_types": ["image/*", "video/*", "audio/*", "application/pdf"], "max_attachments_per_user": -1, "permanent_tiers": ["pro", "max"] }, "validator_key": "originChats-https://chat.example.com-4c1f0a9b2e7d45f8a3b6c9d0e1f2a3b4", "auth_mode": "cracked", "capabilities": ["ping", "capabilities", "channels_get", "message_new", "auth", "login", "register"], "permissions": [ "manage_server", "manage_channels", "manage_users", "manage_roles", "manage_emojis", "manage_config", "manage_access", "create_invite", "review_applications", "review_reports", "view_audit_log" ], "access_control": { "mode": "open", "has_password": false } }}The capabilities array above is shortened. A real server lists about 130 entries.
Handshake fields
Section titled “Handshake fields”| Field | Type | Description |
|---|---|---|
version |
string | Protocol version. "1" for the current server. |
server_time |
number | The server’s clock, in Unix seconds with a fraction. Use it to start a server clock. |
signing_url |
string | Server identity used inside signed payloads. Copy it exactly. |
server |
object | The server’s public identity. See below. |
limits |
object | Message and membership limits. See below. |
uploads |
object | Emoji and sticker upload rules. See below. |
attachments |
object | Attachment upload policy. See below. |
validator_key |
string | A new key for this connection, used to get a Rotur validator for auth and for HTTP uploads. Its form is originChats-<server.url>-<32 random characters>. Check the URL before you use it. |
auth_mode |
string | Which accounts the server accepts: rotur, cracked, or cracked-only. See authentication. |
capabilities |
string[] | What this server supports. See capabilities. |
permissions |
string[] | The administrative permission names the server knows about, always the eleven shown above. This is not the list of permissions you hold. |
access_control |
object | How new members get in. See below. |
server
Section titled “server”| Field | Type | Description |
|---|---|---|
name |
string | Display name. Don’t use it to identify the server. |
url |
string | The configured public URL. It matches the URL in validator_key. |
icon |
string | Icon image URL, or "" when there is none. |
banner |
string | Banner image URL, or "" when there is none. |
owner.name |
string | The owner’s username. Clients use it to allow Web Push from this owner. |
limits
Section titled “limits”| Field | Type | Description |
|---|---|---|
post_content |
number | Most characters allowed in a message’s content. Default 2000. |
search_results |
number | Most results one messages_search returns. Default 30. |
max_reactions_per_message |
number | Most different emoji one message can have as reactions. Default 20. |
max_members |
number | Member limit. 0 means no limit. |
uploads
Section titled “uploads”| Field | Type | Description |
|---|---|---|
emoji_allowed_file_types |
string[] | File extensions accepted by emoji_add. |
sticker_allowed_file_types |
string[] | File extensions accepted by sticker_add. |
uploads_per_minute |
number | Always 10. The real attachment rate limit is the operator’s attachments.uploads_per_minute setting, which can differ. |
attachments
Section titled “attachments”| Field | Type | Description |
|---|---|---|
enabled |
boolean | Whether attachment uploads work. |
max_size |
number | Largest upload in bytes. |
allowed_types |
string[] | Accepted MIME types, such as image/*. An empty list accepts any MIME type. |
max_attachments_per_user |
number | Unexpired uploads one account can hold. -1 means no limit, 0 blocks uploads. |
permanent_tiers |
string[] | Reserved. No upload is permanent on this server. |
access_control
Section titled “access_control”| Field | Type | Description |
|---|---|---|
mode |
string | open, whitelist, password, application, or invite. |
has_password |
boolean | Whether a server password is set, even when the mode isn’t password. |
Access control explains each mode and what a new member must send.
Capabilities
Section titled “Capabilities”capabilities lists what the server supports. Check it before you use an optional feature, and hide features the server doesn’t list.
Most entries are command names. If a command is in the list, the server accepts it. The rest are features: event formats, HTTP routes, or rules that change how commands behave.
Feature capabilities
Section titled “Feature capabilities”| Capability | Advertised | Meaning |
|---|---|---|
auth |
Unless auth_mode is cracked-only |
Rotur sign-in with auth works. |
login |
When auth_mode is cracked or cracked-only |
login works for server accounts. |
register |
With login, when the operator allows registration |
register can create server accounts. |
local_attachment_auth_v1 |
With login |
Server accounts can upload attachments with ready.local_upload_token. |
server_account_avatars_v1 |
With login |
Server accounts can upload their own avatar with POST /avatar/{username}. |
attachment_upload |
Always | POST /attachments/upload exists. It is an HTTP route, not a WebSocket command. |
message_signatures_v1 |
Always | Messages, edits, and embeds can carry signatures. |
slash_signatures_v1 |
Always | Rotur accounts must sign slash_call, and result messages carry the signed interaction. |
user_metadata_v1 |
Always | User objects carry clients, devices, and bot, and the server sends user_clients events. |
unreads_update |
Always | After unreads_ack, the server sends an unreads_update event to every connection of your account, so they stay in sync. It is an event, not a command. |
server_side_embeds |
When link embeds are on | The server adds link previews to messages. Connections that report this capability receive the message_edit events that add them. |
message_react_add, message_react_remove |
Always | Old names for reaction_add and reaction_remove. See legacy compatibility. |
Command capabilities
Section titled “Command capabilities”Each of these names a command the server accepts.
| Area | Capabilities |
|---|---|
| Connection and session | ping, capabilities, typing |
| Server administration | config_get, config_update, server_update, server_stats, server_maintenance |
| Webhooks | webhook_create, webhook_get, webhook_list, webhook_update, webhook_delete, webhook_regenerate |
| Channels | channels_get, channel_get, channel_create, channel_update, channel_move, channel_delete |
| Messages | message_new, message_get, messages_batch, message_replies, messages_get, messages_around, messages_search, message_edit, message_delete, message_pin, message_unpin, messages_pinned |
| Pings | pings_get |
| Polls | poll_vote, poll_end, poll_get |
| Unreads | unreads_get, unreads_ack |
| Threads | threads_get, thread_create, thread_get, thread_delete, thread_update, thread_join, thread_leave, thread_pin, thread_unpin |
| Users and presence | users_online, users_list, users_banned, user_ban, user_unban, user_kick, user_timeout, user_update, user_leave, status_set, status_get |
| Roles | roles_list, role_create, role_update, role_delete, role_reorder, user_roles_get, user_roles_set, self_role_add, self_role_remove, self_roles_list, self_roles_reorder |
| Reactions | reaction_add, reaction_remove |
| Emoji | emoji_add, emoji_delete, emoji_list, emoji_get, emoji_update |
| Stickers | sticker_add, sticker_delete, sticker_list, sticker_get, sticker_update |
| Attachments | attachment_get, attachment_list, attachment_delete |
| Slash commands | slash_register, slash_list, slash_call, slash_response |
| Voice | voice_join, voice_leave, voice_mute, voice_unmute, voice_state |
| Reports | report_create, report_list, report_resolve |
| Audit log | modlog_get, modlog_summary |
| Access control | access_mode_set, access_mode_get, whitelist_add, whitelist_remove, whitelist_list, server_password_set, application_submit, application_list, application_review, application_delete, invite_create, invite_list, invite_delete |
| Plugins | plugin_list, plugin_get, plugin_install, plugin_uninstall, plugin_enable, plugin_disable, plugin_reload, plugin_file_list, plugin_file_get, plugin_file_put |
Authentication
Section titled “Authentication”Until a connection signs in, the server accepts only auth, login, register, and application_submit. Any other command gets an error with val: "Authentication required".
auth_mode |
Accepted commands |
|---|---|
rotur |
auth |
cracked |
auth, login, and register if advertised |
cracked-only |
login, and register if advertised |
A signed-in connection must also belong to a current member. If the account left or was removed, commands fail with You are not currently a member of this server.
Sending a sign-in command on a connection that is already signed in switches it to the new account. Connection lifecycle lists the frames that follow.
Correlation with listener
Section titled “Correlation with listener”listener is a value you choose. The server copies it onto every direct response to that request, so you can match them up:
{ "cmd": "message_get", "channel": "general", "id": "b2d4f6a8-1c3e-4a5b-8d7f-0e9c1b2a3d4e", "listener": "request-42" }{ "cmd": "message_get", "channel": "general", "message": { "id": "b2d4f6a8-1c3e-4a5b-8d7f-0e9c1b2a3d4e", "content": "Hello!" }, "listener": "request-42"}Rules to rely on:
- Use a unique string. Other JSON values work, but strings are easiest to track.
- A request can produce several frames, one frame, or none. Some responses use a different
cmdfrom the request. Each command page says what to expect. - Events never carry a
listener. When a response is also broadcast, the copy other connections get has thelistenerremoved. - Always time out requests you wait for.
Errors
Section titled “Errors”A failed command returns one frame with cmd: "error":
{ "cmd": "error", "val": "Channel not found", "src": "messages_get", "listener": "request-42" }val is text you can show. src names the command that failed.
Failures fall into these groups:
- Bad JSON. A frame that isn’t a valid JSON object gets
{"cmd": "error", "val": "Invalid message format"}, with nosrcorlistener. - Not signed in, or not a member.
Authentication requiredorYou are not currently a member of this server. - Missing permission.
Access denied: 'manage_channels' permission required, orAccess denied: 'view_audit_log' or 'manage_users' permission requiredwhen either of two permissions works. - Invalid fields. When a request doesn’t match a command’s schema,
valhas the form<path>: <message>. The path names the field, using dots for nested objects and[n]for array items. When the problem is the whole request, the path isvalue. For example:channel: Requiredlimit: Expected integername: Expected string, received numbervalue: Expected a value for at least one of [id name](fromrole_updatewith neither field)
- Command-specific failures. Each command page lists its own
valstrings. - Sign-in failures use
auth_errorinstead oferror. See common frames. - Timeouts and slow mode use
rate_limit. See timeouts.
An unknown command from a signed-in connection gets no response at all.
Command permissions
Section titled “Command permissions”Some commands need an administrative permission before the server even looks at the request:
| Permission | Commands |
|---|---|
manage_channels |
channel_create, channel_update, channel_move, channel_delete |
manage_emojis |
emoji_add, emoji_update, emoji_delete, sticker_add, sticker_update, sticker_delete |
manage_server |
server_update, server_stats, server_maintenance, attachment_list, every webhook_* command, every plugin_* command |
manage_users |
users_banned, user_ban, user_unban, user_kick, user_timeout, user_update |
manage_roles |
role_create, role_update, role_delete, role_reorder, user_roles_set |
manage_access |
access_mode_set, server_password_set, whitelist_add, whitelist_remove, whitelist_list, invite_list, invite_delete |
create_invite |
invite_create |
manage_access or review_applications |
application_list, application_review |
review_reports |
report_list, report_resolve |
manage_config |
config_get, config_update |
view_audit_log or manage_users |
modlog_get, modlog_summary |
The server owner has every permission. So does anyone with a role that has the administrator permission.
Other commands have no permission gate here, but they still check channel permissions, authorship, and similar rules before they change anything. Roles and permissions explains channel permissions.
Timeouts
Section titled “Timeouts”A moderator can put an account in timeout. The account stays a signed-in member and can keep reading, but most actions are refused until the timeout ends.
A refused action returns rate_limit instead of running:
{ "cmd": "rate_limit", "src": "reaction_add", "reason": "You are in timeout for 12 more seconds", "length": 12000, "listener": "request-42"}length is the time left in milliseconds.
These commands are blocked during a timeout:
- Messages:
message_new,message_edit,message_delete,message_pin,message_unpin,typing - Reactions and polls:
reaction_add,reaction_remove,message_react_add,message_react_remove,poll_vote,poll_end - Threads:
thread_create,thread_update,thread_delete,thread_join,thread_pin,thread_unpin - Slash commands:
slash_register,slash_call,slash_response - Voice:
voice_join,voice_unmute - Profile and roles:
status_set,self_role_add,self_role_remove,self_roles_reorder - Moderation:
user_update,user_ban,user_unban,user_kick,user_timeout,user_roles_set,report_create,report_resolve - Administration: every
role_*,channel_*,emoji_*, andsticker_*write, everywebhook_*write,plugin_install,plugin_uninstall,plugin_enable,plugin_disable,plugin_reload,plugin_file_put,server_update,server_maintenance,config_update, andattachment_delete - Access control:
access_mode_set,whitelist_add,whitelist_remove,server_password_set,application_review,application_delete,invite_create,invite_delete
Everything else still works, including reads, searches, unreads_ack, thread_leave, voice_leave, voice_mute, and user_leave. HTTP uploads of attachments, avatars, and server images return HTTP 429 during a timeout. Downloads still work.
The server also pushes rate_limit without src or listener to every connection of the account:
- When the timeout starts, with the full
length. - When a moderator removes it, with
reason: "Unmuted: your timeout has been removed"andlength: 0. - When it expires, with
reason: "Your timeout has expired"andlength: 0.
Clear the timeout for that server when length is 0. Slow mode also uses rate_limit, but adds cooldown. See common frames.
Broadcasts
Section titled “Broadcasts”A change can produce both a direct response and events for other connections. The connection that made the change gets the response with its listener. Other connections get the event without it, but only if they are allowed to see the change. For example, a message event goes only to connections that can view its channel.
Some events are sent to every signed-in connection, including the one that caused them. Others skip the sender. Server events says which is which.
Client capability report
Section titled “Client capability report”After ready, a client can tell the server which optional features it understands:
{ "cmd": "capabilities", "val": ["channel_get", "server_side_embeds"] }The server doesn’t respond. It uses the report in two places:
channel_get. When a channel changes, a connection that reportedchannel_getgets a singlechannel_getevent for that channel. Other connections get a fullchannels_getlist.server_side_embeds. Only connections that reported it receive themessage_editevents that add link previews.
Report only features your client actually handles. capabilities has the details.
Compatibility rules
Section titled “Compatibility rules”- Ignore fields you don’t recognise on known frames.
- Ignore or log unknown events. Don’t close the socket.
- Don’t send optional commands the handshake doesn’t list.
- Keep identifiers exactly as given, including case, unless a schema says to normalise them.
- Use
versionfor protocol-wide changes, andcapabilitiesfor features added one at a time. - After a reconnect, rebuild everything tied to the connection. Capabilities and validator keys can change.
Legacy compatibility
Section titled “Legacy compatibility”The old Python server in the originchats repository is legacy. These docs describe the current OSL server. Support older servers only if you choose to.
| Legacy form | Current form | Notes |
|---|---|---|
message_react_add, message_react_remove |
reaction_add, reaction_remove |
The OSL server still accepts the old names. Its events then use the old name too, so handle both. |
originChats://<server>/emojis/<id> |
originChats:<emoji>//<server>/<id> |
The OSL server still accepts the old emoji reference in reactions. |
poll_create, poll_results |
message_new with a poll embed, poll_get |
The OSL server doesn’t accept the old commands. |
ping answered with pong |
ping answered with timing fields |
Keep an old heartbeat handler separate from clock code. |
MessagePack frames (?format=msgpack) |
JSON text frames | The OSL server only speaks JSON. |
A global field on frames |
No field | Older servers sent it. Ignore it if present. |
| Guessing bots from names | bot: true with user_metadata_v1 |
Bot status on older servers is unknown. |
cracked, cracked-only, and the USR:local_ prefix are current names for server accounts, not legacy ones. desktop and laptop are accepted as device names and become computer. New clients should send computer.
Older servers may leave out auth_mode. Fall back to Rotur-only sign-in only when you know you are talking to such a server.
