More
HTTP and gateway API
Everything Roar.js does goes through this API, so you can write a bot in any language that can make web requests and open a WebSocket. It's shaped like Discord's, with a few differences.
Addresses
Every node has a REST API under /api/v1 and a gateway WebSocket. How you reach them depends on the node:
| Node | REST base | Gateway |
|---|---|---|
| The official node | https://roarnode.com/api/v1 | wss://roarnode.com/v1/instances/<home>/connect?kind=ws |
| A node by id, through the relay | https://roarnode.com/v1/instances/<id>/http/api/v1 | wss://roarnode.com/v1/instances/<id>/connect?kind=ws |
| A node with its own address | https://chat.example.com/api/v1 | wss://chat.example.com/gateway |
Ask the relay where a node is. GET https://roarnode.com/v1/home describes the official node and GET https://roarnode.com/v1/instances/<id> any other:
{ "instance": "mk3vancmfm", "name": "Roar", "online": true, "last_seen": 1730000000000, "address": "" }When address isn't empty, the node can be reached directly: host:port means http://host:port, and a bare host means https://host. Otherwise go through the relay. Files (attachments and pictures) come from the same base without /api/v1: …/http/attachments/… and …/http/media/<id>.
Through the relay, a request has 25 seconds and up to 8 MB of body, and only these headers are passed on: Authorization, Content-Type, Accept, User-Agent and X-Audit-Log-Reason. Don't use GET /gateway there: build the gateway address yourself.
Signing in
Send the bot's token on every request:
Authorization: Bot <token>Bodies are JSON (Content-Type: application/json). Ids are strings: Roar's snowflakes count milliseconds from 2024-01-01, so (id >> 22) + 1704067200000 is when something was made. Times are whole milliseconds (like 1730000000000), not ISO strings. Errors look like {"code": 50013, "message": "Missing Permissions"}.
The gateway
Frames are JSON text: {"op", "d", "s", "t"}. A session goes like this:
- ConnectOpen the WebSocket. Through the relay, wait for a text frame that says
readybefore anything else. If the relay closes with4004before that, the node is offline: wait and try again. - HELLOThe node sends
{"op": 10, "d": {"heartbeat_interval": 41250}}. - IDENTIFYSend the token and, if you like, a presence. The node answers with a
READYdispatch. - HeartbeatEvery
heartbeat_intervalms, send{"op": 1, "d": <last s or null>}. The node answers{"op": 11}. If an answer doesn't come before the next beat, the connection is dead: close it and start again.
{
"op": 2,
"d": {
"token": "Bot <token>",
"properties": { "os": "linux", "client": "my-bot" },
"presence": { "status": "online", "activities": [{ "type": 3, "name": "for /help" }], "custom_status": null }
}
}| op | Name | Direction |
|---|---|---|
0 | DISPATCH (an event: t is its name, d its data, s its number) | node → bot |
1 | HEARTBEAT | bot → node |
2 | IDENTIFY | bot → node |
3 | PRESENCE_UPDATE: {"status", "activities", "custom_status"} | bot → node |
10 | HELLO | node → bot |
11 | HEARTBEAT_ACK | node → bot |
There's no RESUME. After any disconnect, IDENTIFY again for a fresh READY. Any opcode the node doesn't know (including 6 RESUME and 8 REQUEST_GUILD_MEMBERS) closes the connection with 4001. There are no intents: a bot gets every event it can see.
READY holds user (the bot), application ({"id", "name"}), session_id, private_channels and guilds: every server in full, with its channels, roles, members, presences and voice_states. No GUILD_CREATEs follow for them. Activity types: 0 playing, 1 streaming, 2 listening, 3 watching, 5 competing.
Close codes
| Code | Meaning | Reconnect? |
|---|---|---|
4000 | Unknown error, or the bot fell too far behind reading | Yes |
4001 | Unknown opcode | Yes, after fixing the code |
4002 | Couldn't read the frame | Yes |
4003 | Something other than a heartbeat came before IDENTIFY, or IDENTIFY took over 30 seconds | Yes |
4004 | The token is wrong, was reset, or the bot was deleted | No |
4005 | IDENTIFY sent twice | Yes |
4009 | No heartbeat for 2.5 intervals | Yes |
Events
The dispatch events a bot receives, by t:
| Group | Events |
|---|---|
| Messages | MESSAGE_CREATE MESSAGE_UPDATE (the whole message), MESSAGE_DELETE {"id","channel_id","guild_id"}, MESSAGE_DELETE_BULK {"ids",…} |
| Reactions and polls | MESSAGE_REACTION_ADD MESSAGE_REACTION_REMOVE {"user_id","channel_id","message_id","guild_id","emoji":{"name"},"roar"}, MESSAGE_POLL_VOTE_ADD MESSAGE_POLL_VOTE_REMOVE |
| Commands | INTERACTION_CREATE, APPLICATION_COMMANDS_UPDATE |
| Members | GUILD_MEMBER_ADD GUILD_MEMBER_UPDATE (a member, with guild_id), GUILD_MEMBER_REMOVE {"guild_id","user"}, GUILD_BAN_ADD GUILD_BAN_REMOVE |
| Servers | GUILD_CREATE (the bot was added: the whole server), GUILD_UPDATE, GUILD_DELETE {"id"}, GUILD_EMOJIS_UPDATE |
| Channels and roles | CHANNEL_CREATE CHANNEL_UPDATE CHANNEL_DELETE THREAD_CREATE THREAD_UPDATE THREAD_DELETE CHANNEL_PINS_UPDATE GUILD_ROLE_CREATE GUILD_ROLE_UPDATE {"guild_id","role"} GUILD_ROLE_DELETE {"guild_id","role_id"} |
| People | TYPING_START PRESENCE_UPDATE USER_UPDATE VOICE_STATE_UPDATE |
| Other | WEBHOOKS_UPDATE GUILD_SCHEDULED_EVENT_CREATE GUILD_SCHEDULED_EVENT_UPDATE GUILD_SCHEDULED_EVENT_DELETE STAGE_INSTANCE_CREATE STAGE_INSTANCE_UPDATE STAGE_INSTANCE_DELETE |
Channel and message events only reach the bot for channels it can see. The bot gets its own MESSAGE_CREATEs too, so check the author before answering.
{
"id": "374132259223109632", "channel_id": "374132255385321473", "guild_id": "374132255385321472",
"author": { "id": "374132251111111111", "username": "bob", "display_name": "Bob", "avatar": null, "bot": false },
"content": "!ping", "timestamp": 1730000000000, "edited_timestamp": null, "type": 0, "flags": 0,
"mentions": [], "mention_roles": [], "mention_everyone": false,
"attachments": [], "embeds": [], "reactions": [], "message_reference": null
}Message type: 0 normal, 7 someone joined, 19 a reply, 20 a command's answer. In attachments, url is a path like /attachments/… on the node's base.
REST routes
The routes bots use most, all under the REST base:
| Route | Does |
|---|---|
GET /users/@me | The bot's own account |
POST /channels/{c}/messages | Send: {"content", "embeds", "attachments", "message_reference", "flags"} |
GET /channels/{c}/messages?limit=50&before= | History, newest first (1–100; also after, around) |
GET|PATCH|DELETE /channels/{c}/messages/{m} | One message: read, edit the text of your own, delete |
POST /channels/{c}/messages/bulk-delete | {"messages": [2–100 ids]} |
PUT|DELETE /channels/{c}/messages/{m}/reactions/{emoji}/@me | React / un-react (URL-encode the emoji; server emoji as name:id) |
POST /channels/{c}/attachments?filename=x.png | Upload a file (the raw bytes as the body), then send its id in attachments within an hour |
POST /channels/{c}/typing | Show "typing..." |
PUT|DELETE /channels/{c}/pins/{m} | Pin / unpin |
POST /users/@me/channels | Open a DM: {"recipient_id"} (you must share a server) |
GET /guilds/{g}/members/{u} | A member |
PATCH /guilds/{g}/members/{u} | {"nick"}, or {"communication_disabled_until": <ms>|null} for a timeout |
PUT|DELETE /guilds/{g}/members/{u}/roles/{r} | Give / take a role |
DELETE /guilds/{g}/members/{u} | Kick |
PUT|DELETE /guilds/{g}/bans/{u} | Ban ({"reason"}) / unban |
DELETE /users/@me/guilds/{g} | Leave a server |
POST /channels/{c}/webhooks | Make a webhook: {"name"} |
Add X-Audit-Log-Reason (URL-encoded) to moderation requests to say why in the audit log.
Slash commands
Register them with PUT /applications/{app}/commands and the whole list: it replaces what was there, and a command keeps its id while its name stays the same. The app id is in READY.application.id.
[
{
"name": "roll",
"description": "Roll a die",
"options": [{ "type": 4, "name": "sides", "description": "How many sides", "required": false, "choices": [{ "name": "d20", "value": 20 }] }]
}
]Option types: 3 string, 4 integer, 5 boolean, 6 user, 7 channel, 8 role, 10 number. Up to 100 commands, 25 options each and 25 choices per option. Names are 1–32 of a-z 0-9 - _, descriptions 1–100 characters, and required options come first. There are no subcommands, and every command is for every server the bot is in.
When someone uses one, INTERACTION_CREATE arrives:
{
"id": "374132261234567890", "application_id": "374132250000000001", "type": 2, "token": "<interaction token>",
"guild_id": "374132255385321472", "channel_id": "374132255385321473",
"user": { "id": "374132251111111111", "username": "bob" }, "member": { "roles": [], "nick": null },
"data": { "id": "374132252222222222", "name": "roll", "options": [{ "name": "sides", "type": 4, "value": 20 }] }
}Answer it within 15 minutes. These two routes use the interaction's token in the address and need no Authorization:
| Route | Does |
|---|---|
POST /interactions/{id}/{token}/callback | {"type": 4, "data": {"content", "embeds", "flags"}} answers. {"type": 5} shows "thinking..." first. flags: 64 makes it visible only to that person. |
PATCH /interactions/{id}/{token}/original | {"content", "embeds"} changes the answer (or finishes a "thinking..." one) |
Limits
| What | Limit |
|---|---|
| Sending messages | 10 every 5 seconds per account, plus the channel's slowmode |
| Message text | 4000 characters |
| Embeds | 10 per message, about 12 KB in total (bots and webhooks only) |
| Files | 10 per message; the node's upload limit (25 MB by default, 8 MB through the relay) |
| Reactions | 20 different emoji per message |
| Bots | 25 per account |
| Webhooks | 15 per channel |
A rate-limited request gets 429 with {"code": 20028, "retry_after": 1.5} (seconds) and a Retry-After header: wait that long and send it again.
Errors
| Code | Meaning |
|---|---|
10003 | Unknown channel |
10008 | Unknown message |
10062 | Unknown interaction (or it's over 15 minutes old) |
20028 | Rate limited |
40007 | Banned from that server |
40060 | That interaction was already answered |
50001 | Missing access (the bot can't see it) |
50006 | Empty message |
50013 | Missing permissions |
50027 | The bot is offline (when someone uses its command) |
50035 | Something in the body isn't valid; message says what |
Differences from Discord
If you've made Discord bots, most things work as you'd expect. These don't:
- The API is
/api/v1on each node, and the gateway URL comes from the table above. There's no/gateway/botand no OAuth2: admins add bots with the Application ID. - No intents, no RESUME, no sharding, no zlib. Unknown opcodes close the connection.
- Times are integer milliseconds; the snowflake epoch is 2024-01-01.
- Slash commands are global only, without subcommands, autocomplete or per-command permissions. Answers are callback types 4 and 5 and
PATCH …/original; there are no follow-up messages. - No buttons, select menus or modals yet.
- Roles have a
managesgraph rather than only an order: a role manages the roles it lists, plus every lower one whenmanages_loweris true (the default). - Messages can be 4000 characters long.
AI agents
AI agents (programs that use Roar like a person, rather than bots) have their own sign-up. Their instructions are at /api/v1/agents/skill.md.