Use the API directly
The same /ping bot with plain fetch and a WebSocket, for any language.
A baff.gg bot is a normal account that your own code controls, over HTTPS and one WebSocket. You don't need a library. If your language can make an HTTP request and open a WebSocket, it can run a bot: JavaScript, Python, Go, Rust, anything.
This route is optional. Writing JavaScript or TypeScript? baff.js does everything on this page for you, and Build your first bot uses it. Come back here to see what it does underneath, or if you want no dependencies.
The three things every bot does
- Authenticate. Send
Authorization: Bot <token>with every request, and on the WebSocket handshake. See Authentication. - Keep the gateway open. Connect a WebSocket to
wss://baff.gg/api/gateway. It sends you JSON events like{ "t": "MESSAGE_CREATE", "d": { ... } }. While it's open, your bot shows as online. If it drops, reconnect with a growing delay. See Gateway. - Answer interactions. When someone uses a slash command, you get an
INTERACTION_CREATEevent with anidand atoken. Within 3 seconds,POSTyour answer tohttps://baff.gg/api/interactions/{id}/{token}/callback. That URL needs no bot token. See Interactions.
Besides those, a bot registers its slash commands with PUT https://baff.gg/api/applications/{appId}/commands and sends messages with POST https://baff.gg/api/channels/{channelId}/messages.
Before you start
You need an application, its bot token and the bot added to a server. Steps 1 to 3 of Build your first bot cover that; they're the same with or without a library. Note the Application ID too.
The walkthrough below needs Node.js 22 or newer and no packages. A Python version follows it.
Write the bot
Make a .env file next to the script:
BOT_TOKEN=bfx_paste_your_token_here
APP_ID=paste_your_application_id_hereSave this as bot.mjs:
// Answers /ping with "Pong!".
// Run with: node --env-file=.env bot.mjs
const API = 'https://baff.gg/api';
const GATEWAY = 'wss://baff.gg/api/gateway';
const TOKEN = process.env.BOT_TOKEN;
const APP_ID = process.env.APP_ID;
const auth = { Authorization: `Bot ${TOKEN}`, 'Content-Type': 'application/json' };
// Tell baff.gg which slash commands this bot has (replaces the whole list).
await fetch(`${API}/applications/${APP_ID}/commands`, {
method: 'PUT',
headers: auth,
body: JSON.stringify([{ name: 'ping', description: 'Checks the bot is awake' }])
});
function connect() {
const ws = new WebSocket(GATEWAY, { headers: { Authorization: `Bot ${TOKEN}` } });
ws.onmessage = async (event) => {
const { t, d } = JSON.parse(event.data);
if (t === 'READY') console.log(`Logged in as ${d.user.username}`);
if (t === 'INTERACTION_CREATE' && d.data.name === 'ping') {
// Answer within 3 seconds.
await fetch(`${API}/interactions/${d.id}/${d.token}/callback`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'message', content: 'Pong!' })
});
}
};
// Connections drop now and then (deploys, network blips). Come back.
ws.onclose = () => setTimeout(connect, 5000);
}
connect();This sketch waits a flat 5 seconds before reconnecting. The Gateway page recommends 1 s, 2 s, 4 s and so on up to 30 s, and explains that events missed while offline aren't replayed.
The WebSocket with a headers option is Node.js's built-in one (Node 22 and newer). Browsers can't set headers on a WebSocket, so a bot can't run in a web page.
Run it
node --env-file=.env bot.mjsThis works the same in Windows PowerShell.
Try it
In your server, type / in the message box. Your bot's commands are listed under its name. Pick /ping and press Enter. The reply shows up with "your name used /ping" above it.
While the bot runs, its status dot is green in the member list.
The same bot in Python
This uses two packages, httpx and websockets (version 14 or newer): pip install httpx websockets.
# Answers /ping with "Pong!".
# Run with: BOT_TOKEN=... APP_ID=... python bot.py
import asyncio
import json
import os
import httpx
import websockets
API = "https://baff.gg/api"
GATEWAY = "wss://baff.gg/api/gateway"
TOKEN = os.environ["BOT_TOKEN"]
APP_ID = os.environ["APP_ID"]
AUTH = {"Authorization": f"Bot {TOKEN}"}
async def main():
async with httpx.AsyncClient() as http:
# Replaces the whole list of slash commands.
await http.put(
f"{API}/applications/{APP_ID}/commands",
headers=AUTH,
json=[{"name": "ping", "description": "Checks the bot is awake"}],
)
# Iterating over connect() reconnects with a growing delay when the socket drops.
async for ws in websockets.connect(GATEWAY, additional_headers=AUTH):
try:
async for raw in ws:
event = json.loads(raw)
t, d = event["t"], event["d"]
if t == "READY":
print("Logged in as", d["user"]["username"])
elif t == "INTERACTION_CREATE" and d["data"]["name"] == "ping":
# Answer within 3 seconds. No bot token needed here.
await http.post(
f"{API}/interactions/{d['id']}/{d['token']}/callback",
json={"type": "message", "content": "Pong!"},
)
except websockets.ConnectionClosed:
continue
asyncio.run(main())Go, Rust, Ruby and the rest work the same way: one HTTP client, one WebSocket client, and the Authorization header on both.
What a library adds
Once a bot grows past a few commands, baff.js saves work: builders that check limits before a request goes out, rate limit handling, reconnects that refill its caches, collectors, and TypeScript types for every payload.
Where next
- Authentication: tokens, and what bots can't do.
- Gateway: every event your bot can receive.
- Commands: options (
/say text: hello times: 3), types and per-server commands. - Interactions: private replies, "thinking..." for slow work, follow-ups, and receiving commands on your own HTTPS endpoint instead of the gateway.
- Messages: posting on your own, not just in answer to a command.
- baff.js: the same bot written with a library.