Server documentation · English
The status API: who is on the server right now?
Every Squorli server can publish its structure and presence as JSON: the server name with its icon, all categories and channels in the order the client shows them, and the members sitting in a voice channel right now with their channel, whether their microphone or sound is off and whether their camera or screen share is on. Members outside every voice channel are not listed; the member list stays inside the server. Use it for a widget on your website, a stream overlay or a bot. The interface is off by default and never returns messages, roles, permissions or keys.
Settings
Open Administration using the gear beside the server name and choose one of the three modes under Server > Status API. This needs the permission to manage the server.
| Mode | Effect |
|---|---|
| Off (default) | GET /api/status answers 404 with {"error":"status_api_off"}. Nothing leaves the server. |
| With the key only | On the first switch the server creates a key and shows it in Administration, with a copy button. Requests need it as the header Authorization: Bearer <key> or as the parameter ?key=<key>; without it or with a wrong key the server answers 401. Regenerate replaces the key at once; the old one stops working. |
| Public | Anyone may fetch the structure and who sits in the voice channels, without signing in and without a key. |
In the “Public” mode the names, handles and state of every member in a voice channel are visible to anyone who knows the address. A key inside a website widget can be read by anyone who opens the page source as well; it keeps casual requests out, not the page's visitors. Avatars are the pictures of the Directory account, which are public there anyway.
Request
The address is https://<your-domain>/api/status. The server allows requests from any origin (CORS), so a widget on another website can call it directly. Answers are cached for one second, so a widget need not ask more often than every few seconds.
curl -s https://chat.example.org/api/statuscurl -s -H 'Authorization: Bearer <key>' https://chat.example.org/api/status
# or, for a widget that cannot set headers:
curl -s 'https://chat.example.org/api/status?key=<key>'The response
The response is one JSON object. categories and channels are sorted as the client shows them: channels without a category first, then the categories by position, and inside a category the channels by position. Each channel names its category in categoryId (null = no category). members holds only the members sitting in a voice channel right now; voice is their voice channel with the reported state of the microphone (micMuted), the sound (deafened), the camera (cameraOn) and the screen share (screenOn); afk means without input for ten minutes. iconUrl and avatarUrl are absolute addresses or null.
{
"name": "Example Community",
"iconUrl": "https://chat.example.org/api/server-icon?v=1758648000000",
"time": "2026-09-23T18:00:00.000Z",
"categories": [
{ "id": "3b39ef51-…", "name": "Allgemein", "position": 0 }
],
"channels": [
{ "id": "8c1e…", "kind": "text", "name": "allgemein", "topic": "Willkommen", "categoryId": "3b39ef51-…", "position": 0 },
{ "id": "f04a…", "kind": "voice", "name": "Lobby", "topic": null, "categoryId": "3b39ef51-…", "position": 1 }
],
"members": [
{ "userId": "6f1c…", "displayName": "Lea", "handle": "lea", "avatarUrl": "https://directory.squorli.com/api/avatars/…",
"afk": false, "isOwner": true,
"voice": { "channelId": "f04a…", "micMuted": false, "deafened": false, "cameraOn": true, "screenOn": false } },
{ "userId": "a2d4…", "displayName": "Jules", "handle": null, "avatarUrl": null,
"afk": true, "isOwner": false,
"voice": { "channelId": "f04a…", "micMuted": true, "deafened": true, "cameraOn": false, "screenOn": false } }
]
}Example: a small website widget
The following example shows the server name, icon and the number of people in voice channels and, below them, every voice channel with the people sitting in it, marked when their microphone or sound is off and when their camera or screen share is on. It needs no library and refreshes every 15 seconds. Enter your domain and, only in the “With the key only” mode, the key. Names are inserted as text, never as HTML; keep it that way when you extend the widget.
<div id="squorli-status">Loading…</div>
<script src="squorli-widget.js"></script>// squorli-widget.js: who is on the server right now. Vanilla JavaScript, no library.
(function () {
var SERVER = "https://chat.example.org"; // your Squorli server
var KEY = ""; // the key from Administration > Server, only in mode "with key"
var EVERY_MS = 15000; // refresh interval; the server answers from a one-second cache
var box = document.getElementById("squorli-status");
// Names come from the server's members: always insert them as text, never as HTML.
function el(tag, className, text) {
var node = document.createElement(tag);
if (className) node.className = className;
if (text !== undefined) node.textContent = text;
return node;
}
function render(status) {
box.replaceChildren();
var head = el("div", "sq-head");
if (status.iconUrl) { var icon = el("img", "sq-icon"); icon.src = status.iconUrl; icon.alt = ""; head.appendChild(icon); }
// The server lists only the members sitting in a voice channel, so this is the number of people in voice.
head.appendChild(el("strong", null, status.name));
head.appendChild(el("span", "sq-count", status.members.length + " in voice"));
box.appendChild(head);
// Voice channels in the order the client shows them: uncategorized first, then the categories by position.
var order = {};
status.categories.forEach(function (c, i) { order[c.id] = i + 1; });
var voice = status.channels.filter(function (c) { return c.kind === "voice"; }).sort(function (a, b) {
return (order[a.categoryId] || 0) - (order[b.categoryId] || 0) || a.position - b.position;
});
voice.forEach(function (channel) {
var seated = status.members.filter(function (m) { return m.voice.channelId === channel.id; });
var block = el("div", "sq-channel");
block.appendChild(el("div", "sq-channel-name", channel.name + " (" + seated.length + ")"));
seated.forEach(function (m) {
var row = el("div", "sq-member" + (m.afk ? " sq-afk" : ""));
if (m.avatarUrl) { var avatar = el("img", "sq-avatar"); avatar.src = m.avatarUrl; avatar.alt = ""; row.appendChild(avatar); }
row.appendChild(el("span", null, m.displayName));
if (m.voice.micMuted) row.appendChild(el("span", "sq-badge", "mic off"));
if (m.voice.deafened) row.appendChild(el("span", "sq-badge", "sound off"));
if (m.voice.cameraOn) row.appendChild(el("span", "sq-badge", "camera"));
if (m.voice.screenOn) row.appendChild(el("span", "sq-badge", "screen"));
block.appendChild(row);
});
box.appendChild(block);
});
}
function refresh() {
var options = KEY ? { headers: { Authorization: "Bearer " + KEY } } : {};
fetch(SERVER + "/api/status", options)
.then(function (res) {
if (res.status === 404) throw new Error("The status API of this server is off.");
if (res.status === 401) throw new Error("The key is missing or wrong.");
if (!res.ok) throw new Error("Error " + res.status);
return res.json();
})
.then(render)
.catch(function (err) { box.replaceChildren(el("div", "sq-error", err.message)); });
}
refresh();
setInterval(refresh, EVERY_MS);
})();A little CSS is enough for a tidy display; adapt the colors to your site.
#squorli-status { font: 14px/1.5 system-ui, sans-serif; border: 1px solid #354762; border-radius: 12px; padding: 12px 16px; max-width: 320px; }
#squorli-status .sq-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
#squorli-status .sq-icon { width: 24px; height: 24px; border-radius: 6px; }
#squorli-status .sq-count { margin-left: auto; opacity: .7; }
#squorli-status .sq-channel-name { font-weight: 600; margin-top: 8px; }
#squorli-status .sq-member { display: flex; align-items: center; gap: 6px; padding-left: 12px; }
#squorli-status .sq-avatar { width: 18px; height: 18px; border-radius: 50%; }
#squorli-status .sq-afk { opacity: .55; }
#squorli-status .sq-badge { font-size: 11px; border: 1px solid currentColor; border-radius: 4px; padding: 0 4px; opacity: .7; }
#squorli-status .sq-error { opacity: .7; }Natural extensions: list the text channels with their topics, dim away members (afk), or build an overlay that shows only your own voice channel. The fields are in the response above; the source of the interface lives in the server repository.