Sloppy Voice developer API
Build bots, plugins and administrative tools using device-authenticated HTTPS APIs. The JavaScript/TypeScript library supports Node.js 22.13+ and ESM bundlers. It signs commands and encrypts channel content using the same implementations as the official client.
Install
Install the versioned archive linked under Library download. The archive is an npm-format package named @sloppyvoice/api, published directly on this site. Public npm-registry publication is separate; use the HTTPS archive URL shown on the download page.
Connect an existing account
An account needs its existing approved device keys as well as its password. Generate keys only for a new account or the existing explicit device-link/recovery workflow. Keep private keys, passwords and login tokens outside source control and logs.
import { SloppyVoice } from '@sloppyvoice/api';
import { readFile } from 'node:fs/promises';
// Load your previously provisioned device keys from protected storage.
const keys = JSON.parse(await readFile(process.env.SLOPPY_KEYS_FILE, 'utf8'));
const client = new SloppyVoice({
baseUrl: 'https://chat.sloppyvoice.com/', keys,
});
await client.login(process.env.SLOPPY_USERNAME, process.env.SLOPPY_PASSWORD);
console.log(await client.servers());
The example expects an account and its approved device keys; it does not extract keys from an official-client profile. Signing in again with the same device replaces its previous session. Session expiry requires signing in again; the library does not store or renew credentials automatically.
Create a logical server
Accounts belong to the instance. Any enabled local account can create a logical server and becomes its first owner. Each logical server has separate owners, members, channels and settings. Instance ownership grants no implicit logical-server membership. Creation starts with no channels.
const server = await client.createServer('Build automation');
const { room } = await server.createChannel({
name: 'Build events', preset: 'text', access: 'private', encryption: 'e2ee',
});
await server.sendMessage(room.id, 'Build started');
Logical-server APIs provide resource administration and text/control exchange. This profile does not advertise a logical-server voice/media transport or official-client navigation support. Existing deployed-server voice and client APIs remain separate. A voice-only channel rejects text messages; use text or combined for bots.
Create an account and add it to the server
Instance account invitations require the existing instance owner's authority. Owning a logical server does not grant that authority. Use an already authenticated instance-owner client to issue the invitation. The new account registers with its own device keys, then a logical-server owner adds its account ID.
import { makeKeys } from '@sloppyvoice/api';
const invitation = await instanceOwner.inviteAccount('buildbot');
const botKeys = await makeKeys(); // Persist securely before registration.
const bot = new SloppyVoice({ baseUrl: client.api.base, keys: botKeys });
await bot.register({
username: 'buildbot', password: process.env.BOT_PASSWORD, token: invitation.token,
});
await server.setMember(bot.self.id, 'member');
const botServer = bot.server(server.id);
await botServer.joinChannel(room.id); // Admission is explicit; no voice starts.
If local public registration is enabled, use a username-bound /challenge result as the registration token instead of an invitation. Account policy remains instance-owned.
Provisioning and bootstrap
client.bootstrap({username, password, token}) initializes the first owner of an already deployed instance using its operator bootstrap token. Bootstrap closes after the first owner exists. This is separate from createServer, and neither method provisions DNS, a Worker, storage or a media relay.
Read history
import { decryptMessage } from '@sloppyvoice/api';
let cursor = 0;
const page = await server.history(room.id, cursor);
for (const message of page.messages) {
console.log(decryptMessage(message, keys));
}
cursor = page.cursor;
Keep a cursor per server/channel/account. Pages contain at most 50 eligible records before presentation filtering. An empty page can still advance the cursor because control messages are hidden by default. Continue until the cursor reaches the channel state's position; poll for subsequent changes within the existing account rate limit. Losing admission stops retrieval. Rejoining preserves old eligible intervals but excludes messages accepted while absent.