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#

OptionDefault
apiUrlhttps://api.sflr.cloud/v2The API root. baseUrl: 'http://localhost:3000' points at a local server instead.
prefix!What bot.command() names start with.
respondToBotsfalseWhether other bots can run your commands. Off by default so two bots can't set each other off.
realtimetrueSet false to always poll.

Events#

EventWhen
readyLogged in and listening. Gets { realtime }: whether messages arrive live or by polling.
messageA new message in any channel or DM the bot can read, including its own (check msg.isMe).
messageUpdateA message was edited.
messageDeleteA message was deleted: { id, channelId, serverId } or { id, dmId }.
voiceStateUpdateSomeone joined or left a voice channel in a server: { serverId }.
dmCallA call started or ended in one of the bot's DMs: { dmId }.
errorA 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 /profileWho the token belongs to
GET /serversServers the bot is in
GET /servers/:id/channelsA server's channels
GET /channels/:id/messagesThe newest 200 messages, oldest first
POST /channels/:id/messagesSend: { text, replyToId? }
PATCH / DELETE /channels/:id/messages/:msgIdEdit or delete a message
POST /channels/:id/messages/:msgId/reactionsToggle a reaction: { emoji }
GET /dms, POST /dms { username }, /dms/:id/messagesDMs, 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:

EventMeaning
{ "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#