API Compatibility
Chatto’s public API is experimental until 1.0. Chatto prefers additive changes, but a minor release can change RPCs, messages, error codes, authentication, or the realtime protocol in ways that require client changes.
Compatibility policy
Section titled “Compatibility policy”The public API has these tiers:
| Tier | Package | Contents |
|---|---|---|
| Discovery | chatto.discovery.v1 | Unauthenticated bootstrap: server profile, version, and login options. |
| Auth | chatto.auth.v1 | Sign-in flows that use a short-lived capability token. |
| Integration | chatto.api.v1 | Authenticated operations for clients and bots. |
| Administrative | chatto.admin.v1 | Server settings, roles, permissions, member administration, and audit reads. |
| Realtime | chatto.realtime.v1 at /api/realtime | Snapshots, events, and resume over a WebSocket. |
All tiers are pre-1.0. The v1 suffix is the current wire name, not a
stability promise. The bundled web client uses the same packages. The operator
socket (chatto.operator.v1) is for local automation and is not part of the
public network API.
The realtime protocol has its own version in
RealtimeSubscribe.protocol_version. The package name does not select a
protocol version. When clients must change their realtime behavior, Chatto adds
a new protocol version.
Between minor releases, expect these changes:
- Additions. New fields, enum values, methods, and services. Older clients ignore them.
- Renames. The binary wire format stays compatible, but generated code and ProtoJSON names change.
- Removals. A removed method returns
Unimplemented. Chatto reserves the numbers and names of removed fields. - Behavior changes. Error codes, pagination, defaults, and permission checks can change.
The release notes describe each intentional breaking change and the required migration. They are not a complete compatibility report.
Check the server version
Section titled “Check the server version”Call ServerDiscoveryService.GetServer before sign-in. profile.version
contains the server software version:
curl -s https://chat.example.com/api/connect/chatto.discovery.v1.ServerDiscoveryService/GetServer \ -H 'Content-Type: application/json' \ -H 'Connect-Protocol-Version: 1' \ -d '{}'{ "profile": { "name": "Example Chat", "version": "0.5.0" }}The response also contains login and setup_required.
- Keep an explicit minimum server version in your client, and compare it with
profile.version. Discovery has no feature flags and no list of supported methods. - Generate your client from the
proto/directory of the release tag that you target, for examplev0.5.0. A client from newer source can fail on an older server, and an older client can fail on a newer server. - Treat
Unimplementedfrom a method as “this server does not have this method”. - Request realtime protocol version
4. A server that does not support the requested version closes withREALTIME_CLOSE_CODE_UNSUPPORTED_PROTOCOL.
The 0.5 bundled web client does not connect to remote 0.4 servers. Select Server information in the server menu to see the server version. Its Compatibility section shows known feature limitations and the required server versions. Follow its upgrade recommendation if one appears.
Before upgrading
Section titled “Before upgrading”- Read the release notes of each release between your current version and the target version. For 0.4 to 0.5, also read Migrate from 0.4 to 0.5.
- Regenerate your client from the protobufs of the target release.
- Test sign-in, token renewal, the RPCs that you use, error handling, pagination, and realtime reconnects against the exact target version.
- Deploy the updated integration together with the server upgrade.
Migrate from 0.4 to 0.5
Section titled “Migrate from 0.4 to 0.5”This migration starts from 0.4.26, the last stable 0.4 release. It describes the changes needed for 0.5, without intermediate beta migration steps.
Chatto 0.5 has breaking changes in the public API and in the realtime protocol. Regenerate every client from the 0.5 protobufs. Regeneration shows most changes as compile errors. The behavior changes in the table do not cause compile errors.
Operators stop all 0.4 processes before they start 0.5, so a client never talks to a mix of 0.4 and 0.5 servers. See the 0.5.0 release notes for the operator steps.
| Area | What changed | What to do |
|---|---|---|
| Direct messages | StartDM rejects new conversations with more than two distinct participants, including the caller, with INVALID_ARGUMENT. Existing group DMs remain accessible. | Use a channel room for new group conversations. User pages use /chat/<server>/user/<userId>; old one-to-one DM URLs redirect there. |
| Realtime | 0.5 accepts only protocol 4. The protocol 1 frames, such as hello, subscribe_events, ping, and error, are removed. | Send one RealtimeSubscribe with protocol version 4. Handle snapshot, event, caught_up, heartbeat, and close. See Realtime. |
| Human sessions | Access tokens are short-lived. A rotating refresh credential renews them. 0.5 rejects bearer tokens from 0.4. | Sign in again once. Store refresh_token and renew at /oauth/token before expires_in ends. CreateExternalIdentityAccountResponse also returns these fields. A browser client on a different origin must use OAuth. See Chatto API. |
| Privileged mode | Administrative and moderation permissions are inactive until the user activates them. | Call ViewerService.ActivatePrivilegedMode before administrative or moderation calls. Activation lasts 15 minutes and does not extend. See Privileged mode. |
| Message access | Channel messages and realtime message delivery require message.read. An upgraded 0.4 server grants it to everyone one time. Bots need a direct grant. | Make sure that your account has message.read. Without it, message reads fail with a permission error. |
| Notifications | NotificationService uses notification occurrences. ListNotifications, GetNotification, BatchGetNotifications, DismissNotification, DismissAllNotifications, HasNotifications, ListRoomNotifications, and ListRoomNotificationCounts are removed. | Use ListNotificationOccurrences, GetNotificationOccurrence, BatchGetNotificationOccurrences, MarkNotificationRead, and the Delete…NotificationOccurrence… methods. Read unread_count and room_unread_counts from the list response. |
| Notification settings | NotificationPreferencesService and its notification levels are removed. GetViewer no longer returns notification preferences. | Use NotificationPolicyService with an explicit NotificationPolicyScope: server, room group, or room. See Notification policy. |
| Room members | RoomService.ListMembers returns user_ids, not member rows. | Load profiles with UserService.BatchGetUsers, at most 100 IDs for each call. Advance the offset by the number of returned IDs. |
| Admin members | AdminUserService.ListMembers returns user_ids and page, without member rows or roles. | Load rows and role summaries with AdminUserService.BatchGetMembers, at most 100 IDs for each call. |
| Role members | AdminRoleService.GetRole no longer returns users. | Page through AdminRoleService.ListMembers (default 20, maximum 100). It requires role.assign. |
| Room directory | RoomDirectoryService.ListRooms returns pages (default 50, maximum 100) and accepts archive_filter, which defaults to active rooms. It also returns accessible empty DMs. | Read pages until page.hasMore is false. Set ROOM_ARCHIVE_FILTER_ARCHIVED or ROOM_ARCHIVE_FILTER_ALL to get archived rooms. Hide a DM when has_message_history is false. |
| Permission reads | AdminPermissionService decision lists now return pages of scopes. | See Read permission decisions. |
| Followed threads | In ListFollowedThreadsRequest, unread_only moved from field 4 to field 5. Field 4 is now include_direct_message_threads. The JSON names do not change. | Regenerate binary clients. An old binary client that sends unread_only gets DM threads instead of the unread filter. |
| Thread state | ThreadViewerState.has_unread is now has_unread_replies. FollowThread and UnfollowThread no longer return following. MarkThreadAsReadResponse.previous_read_at is now previous_last_read_at, and the response adds last_read_at. | Update the names, including the JSON names hasUnreadReplies and previousLastReadAt. Read state.following. |
| Room moderation | BanMember, UnbanMember, ListBans, and RoomBan are removed. | Use RemoveUser, LiftSuspension, ListSuspensions, and RoomSuspension. RemoveUser requires a reason. Omit the suspension to let the user rejoin, or set suspension_expires_at or suspend_indefinitely. Universal rooms require a suspension. Existing bans show as suspensions. |
| Avatars | UploadAvatar and DeleteAvatar moved from MyAccountService to UserService. | Call UserService and send the target user_id, also for your own avatar. |
| Profiles | MyAccountService.UpdateProfile is now UserService.UpdateUserProfile. AdminUserService.UpdateUser is removed. | Call UserService.UpdateUserProfile and send the target user_id, also for your own profile. To change another person, you need user.manage-accounts. To change a bot, you must own it or have bot.manage or user.manage-accounts. The response contains only user. UpdateUser also returned member. To get the updated AdminMember, call AdminUserService.BatchGetMembers. |
| Account deletion | AdminUserService.DeleteUser no longer accepts current_password. | Stop sending it. The call requires user.delete-any, or user.delete-self for your own account. |
| Sidebar order | Sidebar item inputs no longer use kind and id. AdminRoomLayoutItemKind is removed. | Send exactly one of roomId or sidebarLinkId, for example {"roomId":"room-123"}. This applies to item, items, and before. Omit before to move an item to the end. |
| Empty responses | Delete, leave, cancel, clear, disconnect, typing, and push subscription commands return empty messages, for example DeleteMessageResponse and LeaveRoomResponse. | Treat a successful call as success. A failed call returns a Connect error. Fields that report a real result, such as LeaveCallResponse.left, remain. |
| Type names | SubscribePush* is now Subscribe* and UnsubscribePush* is now Unsubscribe*. ListRoomMembers*, GetRoomMember*, and BatchGetRoomMembers* drop Room. | Update type references. The RPC paths do not change. |
| Generated files | UserService moved from member_directory.proto to user_service.proto. | Update imports that use the generated file name. |
| Error codes | A username change during the 30-day cooldown returns FAILED_PRECONDITION. An unknown permission in SetRolePermission or SetUserPermission returns INVALID_ARGUMENT. Both returned INTERNAL before. | Handle these as request errors. Do not retry them unchanged. |
| Updates | All resource Update* RPCs accept update_mask. | See Resource updates in 0.5. |
| Upload limits | ServerRuntimeConfig.max_video_upload_size is deprecated and reports the same value as max_upload_size. | Use max_upload_size to validate every attachment, including GIFs and videos. Check video upload enablement separately. |
Renamed methods
Section titled “Renamed methods”The old methods and their request and response types are removed.
| 0.4 method | 0.5 method |
|---|---|
MyAccountService.UpdateCustomStatus | MyAccountService.SetCustomStatus |
MyAccountService.UpdatePassword | MyAccountService.ChangePassword |
MyAccountService.UpdatePresence | MyAccountService.SetPresence |
AdminUserService.UpdateUserPassword | AdminUserService.ChangeUserPassword |
RoomService.UpdateTypingIndicator | RoomService.RefreshTypingIndicator |
VoiceCallService.GetCallToken | VoiceCallService.CreateCallToken |
SetCustomStatus replaces the complete status. Send both emoji and text. Omit
expires_at to remove an expiry. Use DeleteCustomStatus to clear the status.
SetPresence is a legacy report. Use SetPresencePreference to save the
user’s choice, and call RefreshPresence every 30 seconds while the client is
active. Liveness expires after 60 seconds without a refresh.
Resource updates in 0.5
Section titled “Resource updates in 0.5”Each resource Update* RPC in chatto.api.v1 and chatto.admin.v1 accepts
google.protobuf.FieldMask update_mask. In Connect JSON, the mask is a
comma-separated string of camelCase paths. This
MyAccountService.UpdateSettings request clears the timezone, disables
sharing, and keeps the time format:
{ "updateMask": "timezone,shareTimezone", "timezone": null, "shareTimezone": false}- Fields outside the mask do not change, even if the request contains values.
- A selected field without a value resets to its default. Validation still applies: you can clear a bio or a timezone, but not a login, display name, or room name.
- A selected list replaces the old list. An empty list clears it.
- Without a mask, populated fields select themselves. An explicit default in
an optional field counts as populated. An empty list or JSON
nulldoes not select itself, so use a mask to clear a value. - For notification policies, paths name fields in
overrides. A selected override without a value restores inheritance. - An empty mask or an unknown path returns
INVALID_ARGUMENT. You cannot select IDs or revision tokens. *selects all editable fields, including fields that later releases add. Use explicit paths.
Generated clients use snake_case paths, such as share_timezone.
Read permission decisions
Section titled “Read permission decisions”ListRolePermissionDecisions and ListUserPermissionDecisions return pages of
scopes: 20 by default, and 100 at most. In 0.4.26, these methods returned all
decisions in one response. Add pagination to read the complete result.
{ "roleName": "moderator", "page": { "limit": 20, "offset": 0 } }- For decision lists, advance
offsetby the number of entries inscopes, not the number of decisions. - Repeat with the same filters while
page.hasMoreis true. Pages are live reads, so remove duplicates by scope and permission. - To read one scope, add
"scope": { "kind": "PERMISSION_SCOPE_KIND_ROOM", "id": "<room-id>" }. - The Direct messages scope appears only when you set
includeDirectMessageScopetotrue.