Gryt
Build on Gryt

Server API

Every REST endpoint and Socket.IO event the server answers

Two surfaces. REST handles uploads, downloads and a few things that want plain HTTP caching. Everything interactive runs over Socket.IO: chat, presence, voice signalling, moderation, settings.

There are no authentication endpoints here. Identity comes from an account issuer or from a keypair on the device, and joining is a signed challenge over the socket. See Identity.

REST

Default port 5000. Endpoints marked Bearer want Authorization: Bearer <accessToken>.

Public

MethodPathReturns
GET/health{ status, service, serverName, timestamp }
GET/infoThe join preview. See below
GET/iconThe server icon. 404 when none is set
GET/metricsPrometheus, text/plain

What /info returns

{
  "serverId": "...",
  "name": "...",
  "description": "...",
  "members": "12",
  "lanOpen": false,
  "identityTiers": ["account"],
  "joinPolicy": "invite"
}

identityTiers and joinPolicy are deliberately unauthenticated. The point is that a client can say "you don't need an account for this one" before anybody tries, and neither field tells a caller anything they could not learn by attempting the join and reading the refusal.

Two things depend on who is asking. A server whose discoverable setting is off answers 404 to anybody who isn't already a member, so it disappears rather than advertising itself. And version is only included for members, because a precise build number lets anyone on the network scan for hosts running a version with a known vulnerability, and this endpoint has to stay open for the add-server flow.

No JWKS endpoint

The server doesn't publish /.well-known/jwks.json. It fetches one, from the identity issuer it's configured to trust, and caches it. Nothing here serves keys.

Messages

MethodPathAuthNotes
GET/api/messages/:conversationIdBearer?limit= up to 200, ?before= an ISO date

Sending is chat:send over the socket. There's no POST here.

Uploads

MethodPathAuthNotes
POST/api/uploadsBearermultipart file
POST/api/uploads/avatarBearermultipart
DELETE/api/uploads/avatarBearer
GET/api/uploads/files/:fileId?thumb=1 for the thumbnail, ?download=1 for an attachment header

Server

MethodPathAuthNotes
POST/api/server/iconBearerOwner only, multipart file
DELETE/api/server/iconBearerOwner only

GRYT_SERVER_ICON_MAX_MB caps the upload, 25 MB by default. PNG, JPEG, WebP, GIF, AVIF and SVG are accepted; SVG is sanitised before it's stored.

Members

MethodPathAuthReturns
GET/api/membersBearer{ items: [{ serverUserId, nickname, lastSeen }] }

Emojis

MethodPathAuthNotes
GET/api/emojis[{ name, file_id }]
GET/api/emojis/allIncluding staged
GET/api/emojis/img/:nameImage bytes, cached
POST/api/emojisBearerAdmin or owner. file + name, or files + a names JSON array
PATCH/api/emojis/:nameBearerRename. Body { name }
DELETE/api/emojis/:nameBearer
GET/api/emojis/queueBearerThe processing queue
POST/api/emojis/stageBearerStage emojis before committing them

Importing emojis

From BetterTTV:

MethodPathAuth
GET/api/emojis/bttv/user/:userId
GET/api/emojis/bttv/emote/:emoteId
GET/api/emojis/bttv/file/:emoteId
POST/api/emojis/bttv/importBearer, admin or owner, 200 per request

From emoji.gg:

MethodPathAuth
GET/api/emojis/emojigg/user/:username
GET/api/emojis/emojigg/pack/:slug
GET/api/emojis/emojigg/emoji/:slug
GET/api/emojis/emojigg/file

The file routes fetch the image server-side and return the bytes, so the browser isn't the thing talking to BetterTTV or emoji.gg.

MethodPathAuthNotes
GET/api/link-preview?url=BearerOpen Graph metadata
GET/api/oembed?url=BearerX, SoundCloud, Spotify, TikTok
GET/api/media/metadata?url=BearerDimensions and MIME type

Webhooks

MethodPathAuthNotes
POST/api/webhooks/:webhookId/:tokenToken in the pathPost as the webhook
GET/api/webhooksBearerList
POST/api/webhooksBearerCreate
GET/api/webhooks/:webhookIdBearerRead one
PATCH/api/webhooks/:webhookIdBearerUpdate
DELETE/api/webhooks/:webhookIdBearerDelete

The management API

A separate listener on GRYT_ADMIN_PORT (5099), started only when GRYT_ADMIN_TOKEN is set, published to loopback only. It exists so somebody administering the machine can change database-backed settings without being the server's owner.

MethodPathReturns
GET/management/settingsjoinPolicy, discoverable, lanOpen, profanityMode
PATCH/management/settingsThe same, updated. Send only the keys you're changing
GET/management/health{ ok: true }

Authorization: Bearer <GRYT_ADMIN_TOKEN> on all three. Requires server 1.5.0. A PATCH goes through the same apply path as a change made from a client, so it withdraws mDNS advertising, drops caches and writes an audit entry rather than only updating a row.

Socket.IO

Joining

The handshake is three messages, and it's a signature exchange rather than a password.

EventDirectionPayload
server:infoclient → server
server:joinclient → server{ nickname?, inviteCode? }
server:challengeserver → clientA nonce to sign
server:verifyclient → server{ certificate, assertion }
server:joinedserver → clientSuccess, with tokens
server:setup_requiredserver → clientNothing has claimed ownership yet

The server also proves itself, over server:identify, as a connection-level exchange rather than part of the join. A client reconnecting with a saved token never joins at all, so a proof carried on join would leave the common path handing a bearer token to a server nobody authenticated.

Refusals come back on server:error with a reason: invite_required, invalid_invite, invite_rate_limited, identity_tier_refused, identity_verification_failed, challenge_expired, nonce_mismatch, account_already_member, approval_pending, auth_required, auth_disabled, auth_misconfigured, join_refused, join_failed, invalid_nickname.

Sessions and tokens

EventDirection
session:restoreclient → server
token:refreshclient → server
token:refreshed, token:invalid, token:revoked, token:errorserver → client

Chat

EventDirectionNotes
chat:sendclient → server
chat:fetchclient → server
chat:edit, chat:delete, chat:reactclient → server
chat:reportclient → server
chat:typing, chat:stop_typingboth
chat:new, chat:edited, chat:deleted, chat:reactionserver → client
chat:historyserver → clientAnswer to chat:fetch
chat:purge_userserver → clientEverything from one member is gone
chat:errorserver → client

Direct messages

A conversation between two people, or a group. dm:open is idempotent — asking twice for the same pair returns the conversation that already exists.

EventDirectionNotes
dm:openclient → server{ accessToken, targetServerUserId }
dm:listbothRequest takes { accessToken }; the answer comes back under the same name
dm:setHiddenclient → server{ accessToken, conversationId, hidden } — hides it from your list without leaving
dm:group:createclient → server{ accessToken, memberIds, name?, iconFileId? }
dm:group:addclient → server{ accessToken, conversationId, targetServerUserId }
dm:group:updateclient → server{ accessToken, conversationId, name?, iconFileId? } — null clears
dm:group:leaveclient → server{ accessToken, conversationId }
dm:key:publishclient → server{ binding? } — the key binding others need to write to you
dm:openedserver → clientThe conversation, whether it was new or already there
dm:hidden, dm:leftserver → client
dm:errorserver → client

Calls

Ringing only. The call itself is voice, and goes through the events above.

EventDirectionNotes
call:ringclient → server{ accessToken, conversationId }
call:decline, call:cancelclient → server{ accessToken, conversationId }
call:incomingserver → clientTo everybody else in the conversation
call:ringingserver → clientTo the caller, confirming it went out
call:withdrawnserver → clientDeclined, cancelled, or answered elsewhere
call:errorserver → client

Threads

A thread hangs off one message. thread:create names the message it grows from.

EventDirectionNotes
thread:createclient → server{ conversationId, rootMessageId, accessToken, title? }
thread:fetchclient → server{ conversationId, threadId, limit?, before? }
thread:status:setclient → server{ conversationId, threadId, status, accessToken }
thread:tags:setclient → server{ conversationId, threadId, tagIds, accessToken }
thread:created, thread:updated, thread:deletedserver → client
thread:historyserver → clientAnswer to thread:fetch
thread:errorserver → client

Forums

A forum channel is threads with a topic list in front of them, so the events here sit on top of the thread events above.

EventDirectionNotes
forum:topicsclient → server{ conversationId }
forum:topic:createclient → server{ conversationId, title, text?, accessToken, tagIds? }
forum:topics:listserver → clientAnswer to forum:topics
forum:topic:createdserver → clientArrives with a thread:created for the same topic
forum:errorserver → client

Voice

EventDirectionNotes
voice:room:requestclient → serverAsks for access to a voice channel
voice:room:grantedserver → client{ room_id, join_token, sfu_url }
voice:channel:joinedboth
voice:state:updateclient → serverMute, deafen
voice:stream:set, voice:camera:state, voice:screen:state, voice:framing:setclient → server
voice:peer:connected, voice:peer:disconnectedclient → server
voice:peer:joined, voice:peer:leftserver → client
voice:latency:reportclient → server
voice:latency:updateserver → clientEverybody's round-trip times
voice:disconnect:userclient → serverModeration
voice:kicked, voice:device:disconnect, voice:room:leaveserver → client
voice:state:restoredserver → clientAfter a reconnect
voice:error, voice:room:errorserver → client

Members, profile and presence

EventDirection
members:fetchclient → server
members:list, members:errorserver → client
profile:updateclient → server
profile:updated, profile:errorserver → client
avatar:updatedclient → server
presence:heartbeatclient → server
presence:staleserver → client

Moderation

EventDirection
server:kick, server:ban, server:unbanclient → server
server:mute, server:deafenclient → server
server:bans:listclient → server
server:kicked, server:muted, server:deafenedserver → client
server:kick:success, server:ban:success, server:unban:success, server:mute:success, server:deafen:successserver → client
server:bansserver → client
reports:list, reports:resolveclient → server
reports:list, reports:resolved, report:submitted, report:already_reportedserver → client

Administration

EventDirectionNotes
server:settings:get, server:settings:updateclient → serverThe database-backed settings
server:settingsserver → client
server:channels:list, :upsert, :delete, :reorderclient → server
server:channelsserver → client
server:sidebar:list, :item:upsert, :item:delete, :reorderclient → server
server:sidebarserver → client
server:roles:list, server:roles:setclient → server
server:roles, server:role:updatedserver → client
server:invites:create, :list, :revokeclient → server
server:invites, server:invite:created, server:invite:revokedserver → client
server:member:inviteboth
server:joinRequests:list, server:joinRequests:decideclient → server
server:joinRequests, server:joinRequest:decidedserver → client
server:audit:listclient → server
server:auditserver → client
server:user:replaceclient → server
server:user:replace:successserver → client
server:leaveclient → server
server:leftserver → client
server:detailsboth
server:clientsserver → client
server:emojiQueue:getclient → server
server:emojiQueue:state, server:emojis:updatedserver → client
server:version:checkclient → server
server:version:statusserver → client

Diagnostics

EventDirection
diagnostics:pingclient → server
diagnostics:pongserver → client

On this page