ServerService
Each call is POST {base}/sblurp.v1.ServerService/<Call> with a Connect, gRPC or gRPC-Web body. See Protocol and transport for the basics. Field types in links are on the Types page. Tools can read every call and type from schema.json.
Server info (public, no auth)
GetServerInfo
Request GetServerInfoRequest:
No fields.
Response GetServerInfoResponse:
| Field | Type | Notes |
|---|---|---|
name |
string |
|
description |
string |
Markdown, shown in the sidebar |
icon_url |
string |
|
protocol_version |
int32 |
currently 1 |
trusted_issuers |
repeated string |
auth issuers this server accepts, e.g. "https://sblurp.chat" |
base_url |
string |
canonical URL; also the JWT audience |
member_count |
int32 |
|
accent_color |
string |
optional "#rrggbb" for the rail icon |
invite_only |
bool |
Join needs a valid invite_code |
join_questions |
repeated string |
Questions a plugin asks people who join; show each with a text field and send the answers in JoinRequest.answers, in the same order. |
banner |
Attachment | optional image above the name in the sidebar |
retention_days |
int32 |
Message retention. 0 = keep forever. Otherwise the server deletes messages (with their thread replies, reactions and files) older than this many days, and ListMessages stops returning them. Clients hide loaded messages older than now minus this many days; no per-message events. |
needs_setup |
bool |
A brand-new community whose owner has not finished or skipped the setup wizard yet. Clients show the owner the wizard; others ignore it. |
Joining
Authentication: every call except GetServerInfo carries
Authorization: Bearer <token>
The token is either
(a) a JWT from a trusted issuer with aud = this server's base_url, or
(b) a bot token ("sbb_..."), created by an admin.
A human token for a person who has not joined yet is accepted only by Join and GetServerInfo; other calls return PERMISSION_DENIED.
Join
Request JoinRequest:
| Field | Type | Notes |
|---|---|---|
display_name |
string |
Profile fields the user agreed to share with this community. |
avatar_url |
string |
|
bio |
string |
|
invite_code |
string |
checked only when the server is invite_only; case-insensitive |
answers |
repeated string |
to GetServerInfoResponse.join_questions, in order |
Response JoinResponse:
| Field | Type | Notes |
|---|---|---|
member |
Member | |
created |
bool |
false if already a member |
GetMe
Request GetMeRequest:
No fields.
Response GetMeResponse:
| Field | Type | Notes |
|---|---|---|
member |
Member | |
server_version |
string |
The next three are for admins and the owner only; empty for others. "1.2.3", "1.3.0-beta.1", "nightly-20261016-abc1234" or "dev" |
server_track |
string |
"stable", "beta" or "nightly" |
update_available |
string |
a newer version on server_track, or "" |
UpdateProfile
Request UpdateProfileRequest:
| Field | Type | Notes |
|---|---|---|
display_name |
optional string |
|
avatar_url |
optional string |
|
bio |
optional string |
|
status_text |
optional string |
|
status_emoji |
optional string |
Response UpdateProfileResponse:
| Field | Type | Notes |
|---|---|---|
member |
Member |
Sync
GetState
Everything a client needs to render the sidebar after connecting.
Request GetStateRequest:
No fields.
Response GetStateResponse:
| Field | Type | Notes |
|---|---|---|
me |
Member | |
channels |
repeated Channel | channels I am in (my sidebar), incl. DMs |
members |
repeated Member | |
read_states |
repeated ReadState | |
seq |
int64 |
server seq at snapshot time; Subscribe from here |
roles |
repeated Role | sorted by position |
channel_mode |
ChannelMode | |
blocked_member_ids |
repeated string |
members I blocked, see BlockMember |
muted_member_ids |
repeated string |
members I muted, see MuteMember |
Subscribe
Server stream. Browsers support server streams over Connect; there is deliberately no bidirectional stream. Clients send with unary calls.
Server stream of Event messages.
Request SubscribeRequest:
| Field | Type | Notes |
|---|---|---|
after_seq |
int64 |
Last seq the client has applied. The server first replays every event after it (that the caller may see), then streams live events. If the gap is too large the server sends Event.resync and the client must call GetState again. |
Membership and moderation
Rank: owner > admin > moderator > member. Nobody acts on a member of equal or higher rank.
SetMemberRole
Admin or owner only. The new role must be MEMBER, MODERATOR or ADMIN and lower than the caller's, and the target must rank lower than the caller.
Request SetMemberRoleRequest:
| Field | Type | Notes |
|---|---|---|
member_id |
string |
|
role |
MemberRole |
Response SetMemberRoleResponse:
| Field | Type | Notes |
|---|---|---|
member |
Member |
LeaveCommunity
Leave this community. The owner cannot leave. You may Join again later. Others get member_left.
Request LeaveCommunityRequest:
No fields.
Response LeaveCommunityResponse:
No fields.
KickMember
Moderator or higher, and a higher rank than the target. The target leaves the community and its private channels but may Join again. Not for bots (use BotService.DeleteBot).
Request KickMemberRequest:
| Field | Type | Notes |
|---|---|---|
member_id |
string |
Response KickMemberResponse:
No fields.
BanMember
Like KickMember, and the person's login (iss, sub) cannot Join again.
Request BanMemberRequest:
| Field | Type | Notes |
|---|---|---|
member_id |
string |
Response BanMemberResponse:
No fields.
ListBans
Moderator or higher. Newest first.
Request ListBansRequest:
No fields.
Response ListBansResponse:
| Field | Type | Notes |
|---|---|---|
bans |
repeated Ban |
UnbanMember
Moderator or higher. The login may Join again (with an invite if the community is invite-only) and comes back with the same member id.
Request UnbanMemberRequest:
| Field | Type | Notes |
|---|---|---|
member_id |
string |
Response UnbanMemberResponse:
No fields.
Invites and settings (admin or owner)
An open community lets anyone with a valid login Join. An invite-only one needs JoinRequest.invite_code, except for the first person (the owner) and people who are already members. A banned login cannot join either way.
Roles (admins only, except that everyone sees them in GetState).
CreateInvite
Request CreateInviteRequest:
| Field | Type | Notes |
|---|---|---|
expires_in_seconds |
int64 |
0 = never; max one year |
max_uses |
int32 |
0 = no limit |
Response CreateInviteResponse:
| Field | Type | Notes |
|---|---|---|
invite |
Invite |
ListInvites
Every invite that was not revoked, newest first, including expired and used-up ones (compare expires_at_ms and uses to show their state).
Request ListInvitesRequest:
No fields.
Response ListInvitesResponse:
| Field | Type | Notes |
|---|---|---|
invites |
repeated Invite |
RevokeInvite
Request RevokeInviteRequest:
| Field | Type | Notes |
|---|---|---|
code |
string |
Response RevokeInviteResponse:
No fields.
UpdateServerSettings
Request UpdateServerSettingsRequest:
| Field | Type | Notes |
|---|---|---|
invite_only |
optional bool |
|
channel_mode |
ChannelMode | UNSPECIFIED = leave as is |
description |
optional string |
Markdown; "" falls back to the server's -description |
banner_id |
optional string |
an uploaded image's id (POST /upload); "" removes it |
icon_id |
optional string |
rail icon, an uploaded image's id; "" falls back to -icon-url |
retention_days |
optional int32 |
0 = keep forever, else 1 to 3650 |
setup_done |
bool |
true clears GetServerInfoResponse.needs_setup |
Response UpdateServerSettingsResponse:
| Field | Type | Notes |
|---|---|---|
invite_only |
bool |
|
channel_mode |
ChannelMode | |
description |
string |
|
banner |
Attachment | |
icon_url |
string |
|
retention_days |
int32 |
CreateRole
Request CreateRoleRequest:
| Field | Type | Notes |
|---|---|---|
name |
string |
|
color |
string |
"#rrggbb" or empty |
Response CreateRoleResponse:
| Field | Type | Notes |
|---|---|---|
role |
Role |
UpdateRole
Request UpdateRoleRequest:
| Field | Type | Notes |
|---|---|---|
role_id |
string |
|
name |
optional string |
|
color |
optional string |
|
position |
optional int32 |
Response UpdateRoleResponse:
| Field | Type | Notes |
|---|---|---|
role |
Role |
DeleteRole
Fails with FAILED_PRECONDITION while the role still limits a channel, so deleting a role never opens a channel to everyone.
Request DeleteRoleRequest:
| Field | Type | Notes |
|---|---|---|
role_id |
string |
Response DeleteRoleResponse:
No fields.
SetMemberRoles
Replaces the member's roles. Works for bots too.
Request SetMemberRolesRequest:
| Field | Type | Notes |
|---|---|---|
member_id |
string |
|
role_ids |
repeated string |
Response SetMemberRolesResponse:
| Field | Type | Notes |
|---|---|---|
member |
Member |
Custom emoji
Messages and reactions use a custom emoji as ":name:". Unknown names are plain text. The list and the uploads are plain HTTP, see server/README.md:
GET {base}/emoji ListEmojiResponse as protojson, with an ETag.
Send If-None-Match; 304 means nothing changed.
POST {base}/emoji multipart "file" parts (images or .zip files);
moderator or higher. Reply: UploadEmojiResponse.
GET {base}/emoji.zip every emoji as name.ext in one zip.
GET {base}/emoji/{id} the image, public, cached forever.
After a change every member gets Event.emoji_changed.
DeleteEmoji
Moderator or higher.
Request DeleteEmojiRequest:
| Field | Type | Notes |
|---|---|---|
name |
string |
Response DeleteEmojiResponse:
No fields.
Blocking
BlockMember
Any member, for themselves only. Blocking hides nothing on the server except DMs: the two cannot open a DM with each other or send in a shared DM. Clients collapse a blocked member's messages. The blocker's devices get Event.member_blocked or member_unblocked. Blocking twice is fine.
Request BlockMemberRequest:
| Field | Type | Notes |
|---|---|---|
member_id |
string |
Response BlockMemberResponse:
No fields.
UnblockMember
Request UnblockMemberRequest:
| Field | Type | Notes |
|---|---|---|
member_id |
string |
Response UnblockMemberResponse:
No fields.
MuteMember
Any member, for themselves only. Muting is quieter than blocking and does not stop DMs: a muted member's messages do not count toward my unread or mention counts, and clients collapse them, skip their notifications and hide their typing. Blocking does the same. Only the muter's devices see the mute list (GetState.muted_member_ids, Event.member_muted or member_unmuted). Muting twice is fine.
Request MuteMemberRequest:
| Field | Type | Notes |
|---|---|---|
member_id |
string |
Response MuteMemberResponse:
No fields.
UnmuteMember
Request UnmuteMemberRequest:
| Field | Type | Notes |
|---|---|---|
member_id |
string |
Response UnmuteMemberResponse:
No fields.
Reports (1119)
ReportMessage
Any member, about a message they can see that someone else wrote, with an optional note (up to 1,000 characters). The server keeps a copy of the text, so the report outlives an edit or a delete. A report of a DM message shares that one message with moderators and nothing else from the DM. Reporting the same message again replaces the note. People may send 5 reports, then 1 every 10 minutes (RESOURCE_EXHAUSTED). Nobody is told who reported, and the reporter cannot list reports.
Request ReportMessageRequest:
| Field | Type | Notes |
|---|---|---|
message_id |
string |
|
note |
string |
Response ReportMessageResponse:
No fields.
ListReports
Moderator or higher. Open reports, newest first, about members the caller outranks (the owner sees reports about everyone, the owner included).
Request ListReportsRequest:
No fields.
Response ListReportsResponse:
| Field | Type | Notes |
|---|---|---|
reports |
repeated Report |
ResolveReport
Moderator or higher, for a report ListReports shows them. Resolving deletes every open report of that message, copies included.
Request ResolveReportRequest:
| Field | Type | Notes |
|---|---|---|
report_id |
string |
Response ResolveReportResponse:
No fields.
View this page as Markdown · Glossary · API schema.json · openapi.json