Build on Sunflare
Writing bots
A Sunflare bot is an account of its own: it joins servers, reads and sends messages, takes DMs, and holds roles, all through the same API the app itself uses. It signs in with a token instead of a password, and wears a BOT tag wherever its name shows up.
Create a bot#
Open User Settings → Developer Portal → Create Bot. Or, to create one and add it to a server you manage in one step, use Server Settings → Bots → Create Bot.
Either way you get a token back exactly once. Copy it right away; it can't be shown again, only regenerated (which stops the old one working):
<botId>.<secret> The Developer Portal is also where you give the bot a name, an avatar, and an "about" line for its profile card, find its ID, regenerate its token, or delete it. Deleting a bot removes it from every server it's in. Deleting your own account deletes your bots too.
Add a bot to a server#
Every bot has an invite link (Developer Portal → Invite Link):
https://sunflare.me/oauth2/authorize?client_id=<botId> Anyone who opens it while logged in can add the bot to a server they manage, without ever seeing its token. A server manager can also paste the link or the bot's ID into Server Settings → Bots → Add Existing Bot. A bot banned from a server can't be added back until it's unbanned.
What a bot can do in a server comes from its roles, the same as a person. Give it a role under Server Settings → Roles. A bot can never own a server.
Quick start with the SDK#
bots/sunflare-bot.js in the repo is a small Node.js client with no dependencies. It needs Node 18 or newer; on Node 22+ messages arrive the moment they're sent, over the realtime socket (on older Node, npm install ws for the same, or it polls instead).
const SunflareBot = require('./sunflare-bot');
const bot = new SunflareBot(process.env.SUNFLARE_BOT_TOKEN);
bot.on('ready', () => console.log(`Logged in as ${bot.user.username}`));
// "!ping" anywhere the bot can read
bot.command('ping', msg => msg.reply('Pong!'));
// "!roll 20"
bot.command('roll', (msg, args) => {
const sides = Number(args[0]) || 6;
msg.reply(`You rolled ${1 + Math.floor(Math.random() * sides)}`);
}, { description: 'Roll a die' });
bot.on('message', msg => {
if (!msg.isMe && msg.mentionsMe) msg.react('👋');
});
bot.login(); The bot watches every text channel and DM it can read, and picks up new ones (a server it was just added to, a person DMing it for the first time) right away. Only messages sent after it's ready count as new; it never replays history on startup.
Options#
| Option | Default | |
|---|---|---|
apiUrl | https://api.sflr.cloud/v2 | The API root. baseUrl: 'http://localhost:3000' points at a local server instead. |
prefix | ! | What bot.command() names start with. |
respondToBots | false | Whether other bots can run your commands. Off by default so two bots can't set each other off. |
realtime | true | Set false to always poll. |
Events#
| Event | When |
|---|---|
ready | Logged in and listening. Gets { realtime }: whether messages arrive live or by polling. |
message | A new message in any channel or DM the bot can read, including its own (check msg.isMe). |
messageUpdate | A message was edited. |
messageDelete | A message was deleted: { id, channelId, serverId } or { id, dmId }. |
voiceStateUpdate | Someone joined or left a voice channel in a server: { serverId }. |
dmCall | A call started or ended in one of the bot's DMs: { dmId }. |
error | A request failed. Listen for it, or Node treats it as a crash. |
Messages#
A message has id, text, author (username), authorId, displayName, avatarUrl, isBot, isMe, mentionsMe, channelId and serverId (or dmId), attachments, replyTo, reactions, createdAt and editedAt. And a few shortcuts that act on the same conversation:
| Method | |
|---|---|
msg.reply(text) | Sends a reply to this message. |
msg.send(text) | Sends a plain message to the same channel or DM. |
msg.react(emoji) / msg.unreact(emoji) | Adds or removes the bot's reaction. |
msg.edit(text) / msg.delete() | For the bot's own messages (deleting others' needs Manage Messages). |
Everything else#
sendMessage(channelId, text) and sendDmMessage(dmId, text) (either takes { text, replyTo } too), sendToUser(username, text), openDm(username), editMessage / deleteMessage and their DM twins, react / unreact, sendTyping(channelId), getServers(), getChannels(serverId), getMembers(serverId), getUser(username), getVoicePresence(serverId), getDms(), getMessages(channelId), getDmMessages(dmId), and destroy() to shut down. A failed call throws a SunflareAPIError with the HTTP status; one that hits the rate limit waits and retries on its own.
bots/example-bot.js is a runnable starting point. bots/music-bot/ joins voice channels and plays audio from YouTube (!play, !skip, !queue, !np, !help); its README covers setup.
Without the SDK#
Any language works. Every request carries the token, with the literal word Bot (not Bearer):
curl -X POST https://api.sflr.cloud/v2/channels/CHANNEL_ID/messages \
-H "Authorization: Bot YOUR_BOT_ID.YOUR_SECRET" \
-H "Content-Type: application/json" \
-d '{"text":"Hello from curl"}' Route (under https://api.sflr.cloud/v2) | What it does |
|---|---|
GET /profile | Who the token belongs to |
GET /servers | Servers the bot is in |
GET /servers/:id/channels | A server's channels |
GET /channels/:id/messages | The newest 200 messages, oldest first |
POST /channels/:id/messages | Send: { text, replyToId? } |
PATCH / DELETE /channels/:id/messages/:msgId | Edit or delete a message |
POST /channels/:id/messages/:msgId/reactions | Toggle a reaction: { emoji } |
GET /dms, POST /dms { username }, /dms/:id/messages | DMs, same shape as channels |
The same routes also answer under https://sunflare.me/api.
The realtime socket#
Open wss://api.sflr.cloud/v2/realtime with the same Authorization: Bot ... header on the handshake, then send the list of what to watch:
{ "t": "sub", "serverIds": ["..."], "channelIds": ["..."] } The server checks each id against the bot's access and answers { "t": "subbed", "servers": 3, "channels": 12 }. Send the full list again whenever it changes; ids already checked cost nothing. Events only say what changed, never the data, so you re-read it with the normal routes above:
| Event | Meaning |
|---|---|
{ "t": "ch", "id": channelId } | Messages, reactions, or pins changed in a channel |
{ "t": "dm", "id": dmId } | The same for a DM. Arrives for every DM the bot is in, including brand-new ones, without subscribing |
{ "t": "sv", "id": serverId } | Something happened in a server. For a server you don't know yet: the bot was just added to it |
{ "t": "vp", "id": serverId } | Voice channel membership changed |
Send { "t": "hb" } every 25 seconds: it keeps the bot showing as online, and the server echoes it back, which is how you can tell a dead connection from a quiet one.
Good to know#
- A token is as powerful as a password. If one leaks, regenerate it from the Developer Portal; the old one stops working at once.
- Bots can't sign in through the login page; the token is the only way in.
- Bots share the general API limit: 1200 requests a minute per IP. The realtime socket keeps a bot far below it, since it only reads a channel when something changed there.
- A banned bot account is turned away (
403) before any route runs.