quickstart
create an installer-only /hello command that replies privately. these examples use the public REST API and gateway directly.
1. register and configure
sign in with a verified sometimes account and create a bot in the developer dashboard. set a name, description, icon, support URL and privacy-policy URL. keep it private while testing and add test users by their stable account IDs. choose either account-wide or one-chat installation. configure commands and bot.messages.ephemeral as required; leave all other permissions disabled. this example needs no message-reading permission.
choose your runtime
TypeScript is selected by default. the examples below use the same REST and gateway protocol in every language. install the listed dependencies and save each block under its indicated filename. Java commands use a colon between classpath entries on macOS/Linux; use a semicolon on Windows. publication and installation must be completed before the bot can receive commands.
scroll the table horizontally to see all columns.
| language | requirements | files and execution |
|---|---|---|
| TypeScript | Node.js 24+; no package dependencies | concatenate the three blocks into bot.ts; run node bot.ts |
| JavaScript | Node.js 24+; no package dependencies | concatenate the three blocks into bot.mjs; run node bot.mjs |
| Python | Python 3.11+; python -m pip install websockets==16.0 | save client.py, register_command.py and bot.py; run python register_command.py once, then python bot.py |
| Java | Java 17+; Maven dependency com.google.code.gson:gson:2.14.0 | save BotClient.java, RegisterCommand.java and Bot.java. obtain the jar with mvn dependency:copy -Dartifact=com.google.code.gson:gson:2.14.0 -DoutputDirectory=.; compile with javac -cp gson-2.14.0.jar BotClient.java RegisterCommand.java Bot.java; run java -cp .:gson-2.14.0.jar RegisterCommand once, then java -cp .:gson-2.14.0.jar Bot |
2. keep the bot token on your server
create a token in the dashboard’s credentials tab and copy it before dismissing the one-time reveal. store it in the SOMETIMES_BOT_TOKEN environment variable on your server. never put it in frontend JavaScript, an install URL or a repository. choose a language in the code bar; your choice follows you between examples. no bot SDK is required.
// bot.ts — Node.js 24+
const base = 'https://sometimeschat.com/api/bots/v1';
const token = process.env.SOMETIMES_BOT_TOKEN;
if (!token) throw new Error('set SOMETIMES_BOT_TOKEN');
type RequestBody = Record<string, unknown> & { installation?: string; key?: string };
async function request<T = unknown>(
path: string, { installation, key, ...body }: RequestBody = {},
): Promise<T | null> {
const r = await fetch(base + path, {
method: 'POST',
headers: {
Authorization: `Bot ${token}`,
'Content-Type': 'application/json',
...(installation ? { 'X-Sometimes-Installation': installation } : {}),
...(key ? { 'Idempotency-Key': key } : {}),
},
body: JSON.stringify(body),
});
if (!r.ok) throw new Error(`${r.status}: ${await r.text()}`);
return r.status === 204 ? null : r.json();
}3. register a command
register /hello using the application token. command registration edits a draft command list; publish the configuration in the dashboard to make that command available to consenting installations. command names are scoped to the bot, so two bots can both provide /hello.
await request('/applications/@me/commands', {
key: 'register-hello-v1', name: 'hello',
description: 'say hello privately', options: [],
});4. publish and install
publish the draft configuration without an admin review. the dashboard supplies https://sometimeschat.com/bots/APPLICATION_ID/install. open that link as a permitted user, choose a scope, inspect the permissions and authorize. permissions are loaded from the published dashboard configuration; adding permissions to the URL never changes the request. the owner must authorize their own installation too.
5. receive a command
obtain a short-lived gateway ticket, connect to the returned URL and identify in the first frame. acknowledge heartbeats. after receiving interaction.created, acknowledge the interaction promptly and create its private response. these single-connection examples exit on disconnection so a process supervisor can restart it; use the gateway chapter’s durable cursor, resumption and deduplication contract for production.
type Ticket = { url: string; ticket: string };
type GatewayFrame = {
op: string; nonce: string; type: string; id: string; cursor: string;
installation_id: string; data: { id: string };
};
const ticket = await request<Ticket>('/gateway/tickets');
if (!ticket) throw new Error('missing gateway ticket');
const socket = new WebSocket(ticket.url);
let work = Promise.resolve();
const fail = (error: unknown) => {
console.error(error);
socket.close(1000, 'restart after error');
};
socket.addEventListener('open', () => socket.send(JSON.stringify({
op: 'identify', ticket: ticket.ticket, subscriptions: ['interaction.created'],
})));
socket.addEventListener('message', ({ data }) => {
try {
const event = JSON.parse(String(data)) as GatewayFrame;
if (event.op === 'heartbeat') {
socket.send(JSON.stringify({ op: 'heartbeat_ack', nonce: event.nonce }));
return;
}
if (event.op !== 'event') return;
const interaction = event.type === 'interaction.created';
const id = interaction ? encodeURIComponent(event.data.id) : '';
const installation = event.installation_id;
// Acknowledge commands immediately, but commit gateway cursors in order.
const acknowledged = interaction
? request(`/interactions/${id}/ack`, {
installation, key: `${event.id}:ack`, ephemeral: true,
}).then(() => null, error => error)
: Promise.resolve(null);
work = work.then(async () => {
const error = await acknowledged;
if (error) throw error;
if (interaction) await request(`/interactions/${id}/responses`, {
installation, key: `${event.id}:reply`, content: 'hello!', ephemeral: true,
});
socket.send(JSON.stringify({ op: 'ack', cursor: event.cursor }));
});
void work.catch(fail);
} catch (error) { fail(error); }
});
socket.addEventListener('close', () => process.exit(1));6. verify the boundaries
invoke /hello from an authorized chat. only you should see the reply, with its faint background and “(only visible to you)” label. attempt to read chat history with the same installation: it must return permission_denied. uninstall the bot and confirm both new actions and replayed content stop. private invite removal and a group-owner exclusion must also deny access. these are launch acceptance requirements, not tests performed by this documentation.