Skip to content
Chatto v0.5.0

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.

The public API has these tiers:

TierPackageContents
Discoverychatto.discovery.v1Unauthenticated bootstrap: server profile, version, and login options.
Authchatto.auth.v1Sign-in flows that use a short-lived capability token.
Integrationchatto.api.v1Authenticated operations for clients and bots.
Administrativechatto.admin.v1Server settings, roles, permissions, member administration, and audit reads.
Realtimechatto.realtime.v1 at /api/realtimeSnapshots, 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.

Call ServerDiscoveryService.GetServer before sign-in. profile.version contains the server software version:

Terminal window
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 example v0.5.0. A client from newer source can fail on an older server, and an older client can fail on a newer server.
  • Treat Unimplemented from 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 with REALTIME_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.

  1. 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.
  2. Regenerate your client from the protobufs of the target release.
  3. Test sign-in, token renewal, the RPCs that you use, error handling, pagination, and realtime reconnects against the exact target version.
  4. Deploy the updated integration together with the server upgrade.

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.

AreaWhat changedWhat to do
Direct messagesStartDM 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.
Realtime0.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 sessionsAccess 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 modeAdministrative 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 accessChannel 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.
NotificationsNotificationService 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 settingsNotificationPreferencesService 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 membersRoomService.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 membersAdminUserService.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 membersAdminRoleService.GetRole no longer returns users.Page through AdminRoleService.ListMembers (default 20, maximum 100). It requires role.assign.
Room directoryRoomDirectoryService.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 readsAdminPermissionService decision lists now return pages of scopes.See Read permission decisions.
Followed threadsIn 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 stateThreadViewerState.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 moderationBanMember, 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.
AvatarsUploadAvatar and DeleteAvatar moved from MyAccountService to UserService.Call UserService and send the target user_id, also for your own avatar.
ProfilesMyAccountService.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 deletionAdminUserService.DeleteUser no longer accepts current_password.Stop sending it. The call requires user.delete-any, or user.delete-self for your own account.
Sidebar orderSidebar 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 responsesDelete, 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 namesSubscribePush* is now Subscribe* and UnsubscribePush* is now Unsubscribe*. ListRoomMembers*, GetRoomMember*, and BatchGetRoomMembers* drop Room.Update type references. The RPC paths do not change.
Generated filesUserService moved from member_directory.proto to user_service.proto.Update imports that use the generated file name.
Error codesA 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.
UpdatesAll resource Update* RPCs accept update_mask.See Resource updates in 0.5.
Upload limitsServerRuntimeConfig.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.

The old methods and their request and response types are removed.

0.4 method0.5 method
MyAccountService.UpdateCustomStatusMyAccountService.SetCustomStatus
MyAccountService.UpdatePasswordMyAccountService.ChangePassword
MyAccountService.UpdatePresenceMyAccountService.SetPresence
AdminUserService.UpdateUserPasswordAdminUserService.ChangeUserPassword
RoomService.UpdateTypingIndicatorRoomService.RefreshTypingIndicator
VoiceCallService.GetCallTokenVoiceCallService.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.

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 null does 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.

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 offset by the number of entries in scopes, not the number of decisions.
  • Repeat with the same filters while page.hasMore is 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 includeDirectMessageScope to true.