Only two Highrise JavaScript libraries exist: our highrise.bot and highrise.sdk.dev by Sphinix
Sphinix’s SDK is the first JS library for the platform and it’s a great lightweight SDK for developers who want low-level control. highrise.bot was built with a goal: Developer Experience. also provides a complete framework. Every action has a clean API, responses are fully structured and ready to use with bulit-in methods. You spend your time building features.
All it take is 3 Lines to spawn a bot in your room:
const { Highrise } = require("highrise.bot")
const bot = new Highrise()
bot.login("BOT_TOKEN", "ROOM_ID");
What you need
Three things before we get started.
Node.js 18 or higher
This is the runtime that executes your bot code. Head to
nodejs.org and download the LTS
version. LTS stands for Long-Term Support which means it is the most
stable and thoroughly tested version available. If you already have
Node.js installed, open your terminal and run node --version to check.
Anything 18 or above is fine.
A bot token
This is the key that proves to Highrise that your bot is allowed to connect. Go to the Highrise developer portal, create a new bot, give it a name you will recognize, generate a new token from the 3 dots next to it, and copy the token it generates. Keep it somewhere safe because you will need it in a moment.
Your room ID
This tells the bot which room to live in. Open the Rooms tab in the developer portal, find the room you want, click the three dots next to it, and copy the room ID.
Got all three? Head to Installation and let’s get your bot running.
Installation
Let’s get highrise.bot installed and your project ready. This should take less than five minutes.
Requirements
- Node.js 18 or higher
- npm (comes with Node.js automatically)
Not sure if you have Node.js? Open your terminal and run:
node --version
If you see a version number like v20.11.0 you are good to go. If you
get an error, head over to nodejs.org
and download the LTS version.
Setting up your project
Before installing anything, you need a folder for your bot to live in. Open your terminal and run these commands one by one:
mkdir my-bot
cd my-bot
npm init -y
Here is what each one does:
mkdir my-bot creates a new folder called my-bot. You can name it
whatever you want.
cd my-bot moves you into that folder.
npm init -y sets up a package.json file which keeps track of your
project and its dependencies.
Installing highrise.bot
Now install the SDK:
npm install highrise.bot
Using the starter template
highrise.bot comes with a built-in command that sets up everything for you automatically. Run this inside your project folder:
npx highrise-init
After running it you will see something like this:
[highrise.bot] Initializing project...
+ created index.js
+ created .env
+ updated package.json scripts
--- Setup Complete ---
1. Run: npm install dotenv
2. Add your credentials to the .env file
3. Start your bot with: npm start
Two files will be created for you:
index.js is your bot file. It already has comments explaining every
part of it so you know exactly what is going on.
.env is where you will put your bot token and room ID. We will cover
this on the next page.
Follow the steps it prints and you will be ready to go.
Manual setup
If you prefer to set things up yourself, create an index.js file in
your project folder and paste this in:
const { Highrise } = require('highrise.bot');
require('dotenv').config();
const bot = new Highrise();
bot.once('Ready', async (metadata) => {
console.log(`Online in ${metadata.room.room_name}`);
});
bot.login(process.env.BOT_TOKEN, process.env.ROOM_ID);
You will also need dotenv to read your credentials from the .env file:
npm install dotenv
Do not worry too much about what all of this means right now. We will go through every line in the Quick Start page.
Next up, let’s get your bot token and room ID configured so your bot can actually connect.
Configuration
Your bot needs two things to connect to Highrise: a bot token and a room ID. This page will show you where to get them and how to set them up properly.
The .env file
If you used npx highrise-init, a .env file was already created for
you in your project folder. Open it and you will see something like this:
BOT_TOKEN=your_bot_token_here
ROOM_ID=your_room_id_here
Replace the placeholder values with your actual credentials. We will get those in a moment.
If you set things up manually, create a new file called .env in your
project folder and paste the lines above into it.
Getting your bot token
Your bot token is what tells Highrise that your bot is allowed to connect. Think of it like a password for your bot.
- Go to the Highrise developer portal
- Sign in with your Highrise account
- Click Create API Key
- Give it a name so you can recognize it later
- Copy the token it generates
Now paste it into your .env file:
BOT_TOKEN=paste_your_token_here
Important
Your token is sensitive. Anyone who has it can control your bot and act as it in any room. Never share it, never post it in a Discord server, and never commit it to GitHub.
Getting your room ID
Your room ID tells the bot which room to connect to.
- Go to the Rooms tab in the developer portal
- Click the 3 dots beside the room you want your bot to connect to and click Copy ID
Paste it into your .env file:
ROOM_ID=paste_your_room_id_here
Your .env file should now look something like this:
BOT_TOKEN=64_character_string_here
ROOM_ID=24_character_string_here
Keeping your credentials safe
There is one more thing you need to do before moving on. You never want
your .env file to end up on GitHub because it contains sensitive
information. Create a file called .gitignore in your project folder
and add this to it:
node_modules
.env
This tells Git to ignore both your credentials and your installed packages when you push your code. If you are not using Git yet, you can skip this step for now but it is a good habit to get into early.
Verifying everything works
Let’s make sure your credentials are set up correctly. Run your bot:
npm start
If everything is good, you will see something like this in your terminal:
[Highrise] │ 12:00:00 │ [INFO] │ Connected to Highrise Bot Server
[Highrise] │ 12:00:01 │ [INFO] │ Now online in ****
If you see an error instead, here are the most common causes:
Extra spaces around the = in your .env file. It should be
BOT_TOKEN=abc not BOT_TOKEN = abc, Also do not use quotes (" ") or spaces. Just put the raw string after the equals sign.
The token or room ID was copied incorrectly. Try copying it again.
Your bot token was not created properly. Go back to the Highrise developer portal and check.
Once your bot is online, let’s write some actual code!
Quick Start
Your bot is installed and your credentials are set up. Now let’s write something that actually does something. By the end of this page you will have a working bot running in your room.
Open your bot file
If you used npx highrise-init, open the index.js that was created
for you. It already has code in it with comments explaining everything.
If you set things up manually, open your index.js file.
Either way, replace everything in it with this:
const { Highrise, Logger } = require('highrise.bot');
require('dotenv').config();
const log = new Logger("MyBot");
const bot = new Highrise();
bot.once('Ready', async (metadata) => {
log.info('Bot', `Online in ${metadata.room.room_name}`);
await bot.message.send('Hello everyone!');
});
bot.on('Chat', async (user, message) => {
if (message.command() === '!ping') {
await bot.message.send('Pong! 🏓');
return;
}
});
bot.on('UserJoined', async (user, position) => {
await bot.message.send(`Welcome, ${user.username}!`);
});
bot.login(process.env.BOT_TOKEN, process.env.ROOM_ID);
Run it
npm start
Go to your Highrise room. Your bot should be online. Try typing !ping
in the chat and watch it respond. When someone joins the room, the bot
will welcome them by name.
What just happened
Let’s go through it piece by piece so you understand what every part does.
Importing the SDK
const { Highrise, Logger } = require('highrise.bot');
require('dotenv').config();
The first line imports two things from the SDK: Highrise which is the
bot itself, and Logger which gives you colored, timestamped output in
your terminal. The second line loads your .env file so process.env
can read your credentials.
Creating the bot and logger
const log = new Logger("MyBot");
const bot = new Highrise();
new Logger("MyBot") creates a logger that will show
[MyBot] at the start of every line in your terminal. Change
"MyBot" to whatever you want.
new Highrise() creates your bot. Everything you do goes through this
one object. Sending messages, listening to events, managing users, all
of it lives on bot.
The Ready event
bot.once('Ready', async (metadata) => {
log.info('Bot', `Online in ${metadata.room.room_name}`);
await bot.message.send('Hello everyone!');
});
This fires when your bot successfully connects to the room. We use
once instead of on here because your bot reconnects automatically
whenever the connection drops, and you probably do not want it saying
“Hello everyone!” every single time that happens. With once it only
runs the very first time.
The metadata object tells you things about the bot and the room.
metadata.room.room_name is the display name of the room. We will
cover everything inside metadata in the Ready event page.
The Chat event
bot.on('Chat', async (user, message) => {
if (message.command() === '!ping') {
await bot.message.send('Pong! 🏓');
return;
}
});
This fires every time someone sends a message in the room. You get the
user who sent it and the message they sent.
message.command() returns the first word of the message. So if someone
types !ping hello world, message.command() returns "!ping".
The return after sending the reply is important. It tells the code to
stop running after this command matches. Without it the code keeps
checking every other condition even though a command already matched.
The UserJoined event
bot.on('UserJoined', async (user, position) => {
await bot.message.send(`Welcome, ${user.username}!`);
});
This fires every time someone enters the room. user.username is their
display name.
Connecting
bot.login(process.env.BOT_TOKEN, process.env.ROOM_ID);
This is always the last line. It reads your credentials from the .env
file and starts the connection. Everything above it registers what the
bot should do when things happen. This line is what actually turns it on.
Something went wrong?
If your bot connected but is not responding to !ping, make sure you
are typing it exactly with the exclamation mark and all lowercase.
If your bot is not connecting at all, go back to the Configuration page and double check your credentials.
What is next
Now that your bot is running, head to the Fundamentals section to understand how everything fits together. Once you understand the foundation, building any feature becomes straightforward.
The Bot Object
When you write const bot = new Highrise(), you are creating the heart
of your entire bot. Everything goes through this one object. This page
explains what it is, what lives on it, and how it all fits together.
What bot actually is
bot is an instance of the Highrise class which is built on top of
Node.js’s EventEmitter. That means two things: it can listen to events
with bot.on() and bot.once(), and it can emit events internally when
things happen on the WebSocket connection.
When you call bot.login(token, roomId), the bot creates a WebSocket
connection to Highrise, sets up all of its internal handlers, and starts
listening. You never have to touch the WebSocket directly.
What lives on bot
After you call bot.login(), these properties are available on bot:
bot.message // send room messages and whispers
bot.whisper // send private whispers to users
bot.channel // send hidden channel messages
bot.direct // direct messages, conversations
bot.room // room users, moderation, voice, privilege
bot.player // actions, emotes, tips, teleport, outfit
bot.inventory // wallet, outfit, items, boosts
bot.utils // sleep, uptime, splitMessages and more
Each one is a domain. bot.room knows everything about the room.
bot.player knows everything about interacting with players.
bot.inventory knows everything about items and currency in bot. You will
never need to go looking for something in the wrong place.
The three getters
The bot also has three read-only properties you can access at any time:
bot.metadata
This gives you information about the bot and the room it connected to.
It is only available after the Ready event fires.
bot.metadata.bot_id // the bot's own user ID
bot.metadata.room.room_name // the display name of the room
bot.metadata.room.owner_id // the user ID of the room owner
You will use bot.metadata often. Checking bot.metadata.bot_id lets
you know the bot’s own identity. Checking bot.metadata.room.owner_id
lets you give special permissions to the room owner automatically.
bot.credential
This gives you the token and room ID that were used to log in.
bot.credential.token // the bot token
bot.credential.roomId // the room ID
You rarely need this directly. The SDK uses it internally when reconnecting after a dropped connection.
bot.connectTime
This is the timestamp in milliseconds of when the bot last successfully connected. It resets every time the bot reconnects.
bot.connectTime // e.g. 1710000000000
You can use this to calculate uptime yourself, but bot.utils.uptime()
already does that for you and returns a clean string like 2h 30m 15s.
Listening to events
You attach behavior to your bot by listening to events:
// runs every time
bot.on('Chat', async (user, message) => { });
// runs only once
bot.once('Ready', async (metadata) => { });
// remove a listener
bot.off('Chat', someFunction);
The difference between on and once is important. on listens
forever, every time the event fires, your function runs. once listens
one time only and then removes itself automatically. You will almost
always use on except for the Ready event where once is the correct
choice because Ready fires again on every reconnect.
The login method
bot.login(token, roomId)
This is always the last line in your file. It starts the WebSocket connection and triggers everything else. If you call it before registering your event listeners, the bot might connect before your code is ready to handle anything.
The reason bot.login() is always at the bottom is simple: JavaScript
reads your file from top to bottom. All your bot.on() and bot.once()
calls need to be registered before the connection starts, otherwise the
first few events might fire before your handlers exist to catch them.
A clean bot file structure
Here is the order that every bot file should follow:
// 1. imports
const { Highrise, Logger } = require('highrise.bot');
require('dotenv').config();
// 2. setup
const log = new Logger("MyBot");
const bot = new Highrise();
// 3. event listeners (as many as you need)
bot.once('Ready', async (metadata) => { });
bot.on('Chat', async (user, message) => { });
bot.on('UserJoined', async (user, position) => { });
bot.on('Tip', async (sender, receiver, currency) => { });
// 4. login, always last
bot.login(process.env.BOT_TOKEN, process.env.ROOM_ID);
Follow this structure every time and your bot will work correctly. Move
bot.login() above your event listeners and you might miss the first
few events. Keep it at the bottom and everything works as expected.
What the bot does automatically
Before we move on, it is worth knowing what the bot handles for you so you do not have to think about it:
The connection reconnects automatically when it drops. You do not need to write any reconnect logic.
Keepalive messages are sent to Highrise every 15 seconds automatically. You never need to worry about the connection going stale.
Your bot’s own messages are filtered out of the Chat event. If your bot sends “Hello everyone!” in the room, that will not trigger your Chat handler.
Fatal errors like an invalid token or a room that does not exist stop the reconnect loop automatically. The bot will not keep retrying forever on something that will never succeed.
All of this happens behind the scenes so you can focus on building what your bot actually does.
Events
Events are how Highrise talks to your bot. Every time something happens in the room, a message, a join, a tip, a movement, Highrise sends your bot a notification. That notification is an event. Your job is to listen for the events you care about and tell the bot what to do when they fire.
How events work
When something happens in your room, the Highrise server sends a message to your bot over the WebSocket connection. The SDK receives that message, figures out what type of event it is, wraps the data in a clean object, and calls your listener function with that data.
You never touch the raw WebSocket message. By the time the event reaches your code, it has already been parsed, validated, and turned into something readable.
Listening to events
You listen to events using bot.on():
bot.on('Chat', async (user, message) => {
console.log(`${user.username} said: ${message.content}`);
});
The first argument is the event name. The second is a function that runs when it fires. The parameters inside that function are the data the event gives you to work with. Different events give you different data.
on vs once
You have two ways to listen to an event.
bot.on() listens forever. Every time the event fires, your function
runs. Use this for events you want to respond to continuously.
bot.on('Chat', async (user, message) => {
// runs every single time someone sends a message
});
bot.once() listens one time only. After the event fires once, the
listener removes itself automatically.
bot.once('Ready', async (metadata) => {
// runs once when the bot first connects, then never again
});
You will use bot.on() for almost everything. The main place you use
bot.once() is the Ready event, because your bot reconnects
automatically when the connection drops and you usually do not want your
setup code running every time that happens.
Multiple listeners on the same event
You can have as many listeners as you want on the same event. They all run when it fires.
bot.on('Chat', async (user, message) => {
// handles commands
if (message.command() === '!ping') {
await bot.message.send('Pong!');
}
});
bot.on('Chat', async (user, message) => {
// logs every message separately
log.debug('Chat', `${user.username}: ${message.content}`);
});
Both of these will run every time someone sends a message. This is useful when you want to keep different pieces of logic separate from each other. Just be aware that they run in the order they were registered.
async and await
Every event handler has async in front of it:
bot.on('Chat', async (user, message) => {
await bot.message.send('Hello!');
});
This is because sending messages, fetching room data, and most things your bot does take a small amount of time. They are asynchronous, meaning they do not finish instantly.
The async keyword lets you use await inside the function. The
await keyword tells JavaScript to wait for that operation to finish
before moving on to the next line.
If you forget await, your code keeps running before the operation
finishes and you might get unexpected behavior. As a rule of thumb, any
time you call something on bot that fetches or sends data, put await
in front of it.
The full list of events
Here is every event your bot can listen to:
| Event | When it fires |
|---|---|
Ready | Bot connects to the room |
Chat | Someone sends a room message |
Whisper | Someone sends a whisper to the bot |
UserJoined | Someone enters the room |
UserLeft | Someone leaves the room |
Movement | Someone moves or sits |
Tip | Someone tips in the room |
Moderation | A moderation action happens |
Voice | Voice chat status changes |
Direct | A direct message is received |
Channel | A hidden channel message arrives |
Each one is covered in detail in the Events section. Head there when you are ready to start using them.
Requests and Responses
Every time your bot does something active, sending a message, teleporting a user, fetching who is in the room, it sends a request to Highrise and waits for a response. This page explains how that works and what you get back.
Sending a request
Requests are simple. You call a method on bot and wait for it:
await bot.message.send('Hello!');
That single line sends a message to the room. Under the hood it packages the request, sends it over the WebSocket connection, waits for Highrise to confirm it was received, and gives you back the result.
The await is important. Without it your code moves on before the
request finishes and you will not have the response.
What comes back
Every request returns a response object. No matter what you asked for, that object always has two things on it:
const result = await bot.message.send('Hello!');
result.ok // true if it worked, false if it did not
result.error // null if it worked, a string explaining what went wrong if it did not
And this method:
result.hasError() // returns true if there is an error
This means you never have to worry about your bot crashing from a failed request. If something goes wrong, the error is captured and handed to you cleanly instead of blowing up your code.
Checking if it worked
The simplest pattern is checking ok before doing anything with the
result:
const result = await bot.message.send('Hello!');
if (!result.ok) {
log.warn('Chat', `Message failed: ${result.error}`);
return;
}
// message sent successfully, continue
You do not have to check every single response. For things like a welcome message, you might not care if it occasionally fails. But for anything important, purchases, moderation actions, fetching data you need, it is worth checking.
Responses with data
Some requests give you back more than just ok and error. When you
ask Highrise for information, the response includes the data you asked
for along with methods to work with it:
const wallet = await bot.inventory.wallet.get();
wallet.ok // true if it worked
wallet.error // null if it worked
wallet.gold // how much gold the bot has
wallet.boostToken // boost tokens
wallet.voiceToken // voice tokens
const room = await bot.room.users.get();
room.ok // true if it worked
room.error // null if it worked
room.users // array of users and their positions
room.count() // how many users are in the room
room.has('Unfairly') // is this user in the room
room.find('Unfairly') // get the full user object
room.position('Unfairly') // where are they standing
Notice that room has both properties like room.users and methods
like room.count() and room.has(). The response object is not just a
container for data. It knows how to answer the most common questions
about that data so you do not have to write the logic yourself.
Also notice that room.has('Unfairly') and room.position('Unfairly') accept
either a username or a user ID. You pass in whatever you have and the
response figures out the rest.
Shorthand methods
For common lookups you do not need to call .get() first and then call
a method on the result. You can call the method directly:
// these two do exactly the same thing
const room = await bot.room.users.get();
const count = room.count();
// shorthand
const count = await bot.room.users.count();
// same thing for checking presence
const room = await bot.room.users.get();
const inRoom = room.has('Unfairly');
// shorthand
const inRoom = await bot.room.users.has('Unfairly');
Use the shorthand when you only need one piece of information. Use
.get() when you need multiple pieces so you are not making the same
network request several times.
Timeouts
If Highrise does not respond to a request within 10 seconds, the request times out automatically:
const result = await bot.message.send('Hello!');
// if it timed out
result.ok // false
result.error // "Request timed out after 10 seconds"
You do not need to set up any timers yourself. The SDK handles this for you.
Acknowledgment responses
Some requests do not give you any meaningful data back. Kicking a user, sending an emote, teleporting someone. For these you get back a simple acknowledgment that tells you whether it worked:
const result = await bot.room.moderation.kick(userId);
result.ok // true if the kick went through
result.error // what went wrong if it did not
Every request follows the same pattern. Once you understand it once you understand it everywhere.
The Message Object
The message object appears in the Chat, Whisper, and Direct
events. It is more than just a container for text. It has built-in
methods that make working with commands and user input feel natural.
Understanding it well will make everything else in your bot easier.
The content property
message.content is the full, trimmed text of what the user sent:
bot.on('Chat', async (user, message) => {
console.log(message.content);
// user typed "!ping hello world"
// message.content is "!ping hello world"
});
The content is always trimmed of leading and trailing whitespace. You
never have to call .trim() yourself.
message.command()
message.command() returns the first word of the message. This is how
you detect commands:
bot.on('Chat', async (user, message) => {
console.log(message.command());
// "!ping hello world" → "!ping"
// "hello everyone" → "hello"
// "!help" → "!help"
// "" → null
});
If the message is empty, message.command() returns null. It will
never crash.
message.args()
message.args() returns everything after the first word. You can call
it with no arguments to get the full array, or pass a number to get a
specific argument at that position:
bot.on('Chat', async (user, message) => {
// user typed: "!kick @Unfairly spamming"
message.args() // ["@Unfairly", "spamming"]
message.args(0) // "@Unfairly" — first argument
message.args(1) // "spamming" — second argument
message.args(5) // null — does not exist, never crashes
});
If there are no arguments, message.args() returns an empty array []
and message.args(0) returns null. You can always safely access any
index without worrying about it crashing.
message.mentions()
message.mentions() returns any words in the message that start with
@. The @ symbol is stripped from the returned value so you get the
username directly. Just like args(), you can call it without an
argument to get all mentions, or pass a number to get a specific one:
bot.on('Chat', async (user, message) => {
// user typed: "!tip @Unfairly nice one"
message.mentions() // ["Unfairly"]
message.mentions(0) // "Unfairly"
// user typed: "!game @Unfairly vs @WSA0"
message.mentions() // ["Unfairly", "WSA0"]
message.mentions(0) // "Unfairly"
message.mentions(1) // "WSA0"
// user typed: "hello everyone"
message.mentions() // []
message.mentions(0) // null
});
Combining args and mentions
A common pattern is accepting input as either a plain username or an @mention and letting the user use whichever they prefer:
bot.on('Chat', async (user, message) => {
if (message.command() === '!find') {
// accept "!find Unfairly" or "!find @Unfairly"
const target = message.mentions(0) || message.args(0);
if (!target) {
await bot.message.send('Usage: !find <username> or !find @username');
return;
}
const pos = await bot.room.users.position(target);
if (!pos) {
await bot.message.send(`${target} is not in the room.`);
return;
}
await bot.message.send(
`${target} is at ${pos.x}, ${pos.y}, ${pos.z}`
);
}
});
message.mentions(0) || message.args(0) checks for a mention first and
falls back to a plain username if there is no mention. Both !find Unfairly
and !find @Unfairly work exactly the same way. Small touches like this
make your bot feel much more natural to use.
Practical example
Here is a realistic Chat handler showing how to use all three methods together effectively:
bot.on('Chat', async (user, message) => {
const cmd = message.command();
// store command once at the top, use it throughout
// this is cleaner than calling message.command() in every if statement
if (cmd === '!ping') {
await bot.message.send('Pong! 🏓');
return;
}
// command with a plain argument
if (cmd === '!say') {
const text = message.args().join(' ');
if (!text) {
await bot.message.send('Usage: !say <text>');
return;
}
await bot.message.send(text);
return;
}
// command with a mention or username
if (cmd === '!info') {
const target = message.mentions(0) || message.args(0);
if (!target) {
await bot.message.send('Usage: !info @username');
return;
}
const found = await bot.room.users.find(target);
if (!found) {
await bot.message.send(`${target} is not in the room.`);
return;
}
await bot.message.send(`${found.user.username} is at ${found.position.x}, ${found.position.z}`);
return;
}
// command with multiple arguments
if (cmd === '!add') {
const a = Number(message.args(0));
const b = Number(message.args(1));
if (isNaN(a) || isNaN(b)) {
await bot.message.send('Usage: !add <number> <number>');
return;
}
await bot.message.send(`${a} + ${b} = ${a + b}`);
return;
}
});
Notice how storing message.command() in cmd at the top makes the
code cleaner. You call it once and use the result everywhere instead of
calling message.command() inside every single if statement.
The message object in Direct events
The Direct event also gives you a message object. It works the same
way, but message.content will be the content of the direct message
the user sent. The same command(), args(), and mentions() methods
are available so you can build command systems for direct messages too.
Error Handling
Nobody writes perfect code and networks are not perfectly reliable. Things go wrong. This page explains how highrise.bot handles errors and what you need to do about them.
The short version
You do not need try/catch blocks around your bot code. Every request
your bot makes catches its own errors and returns them to you in a
consistent, readable way. Your bot will not crash from a failed request.
How errors come back to you
When something goes wrong, the response object tells you about it
through the same ok and error properties you already know:
const result = await bot.message.send('Hello!');
result.ok // false if something went wrong
result.error // "ChatApi: message failed to send"
The error message always includes which part of the SDK it came from.
That prefix like "ChatApi:" or "GetRoomUsersResponse:" tells you
exactly where to look without having to dig through stack traces.
Two ways to check
const result = await bot.message.send('Hello!');
// option 1
if (!result.ok) {
console.log(result.error);
return;
}
// option 2
if (result.hasError()) {
console.log(result.error);
return;
}
Both do exactly the same thing. Use whichever reads more naturally to
you. Most developers use !result.ok because it is shorter.
When to check and when not to
You do not need to check every single response. Think about how important the operation is.
For things that do not really matter if they fail occasionally, just fire and forget:
// not critical, skip the check
await bot.message.send(`Welcome, ${user.username}!`);
For things that matter, always check:
const result = await bot.room.moderation.ban(userId, 3600);
if (!result.ok) {
log.error('Moderation', `Failed to ban ${userId}: ${result.error}`);
await bot.message.send('Something went wrong, the ban did not go through.');
return;
}
await bot.message.send(`${username} has been banned.`);
For purchases, always check and be specific about why it failed:
const result = await bot.inventory.item.buy(itemId);
if (result.insufficientFunds) {
await bot.message.send('Not enough gold to buy that item.');
return;
}
if (!result.ok) {
await bot.message.send('Could not complete the purchase right now.');
return;
}
await bot.message.send('Item purchased!');
BuyItemResponse and BuyRoomBoostResponse have two extra boolean
properties — success and insufficientFunds — because these are the
two outcomes worth handling differently. insufficientFunds lets you
give users a meaningful message instead of a generic error.
Validation errors
Before a request even reaches Highrise, the SDK checks that what you are passing makes sense. If you pass the wrong type of value or forget a required argument, you get back an error immediately without wasting a network request:
const result = await bot.room.moderation.mute(userId, -5);
result.ok // false
result.error // "duration must be a positive number"
The error message tells you exactly what is wrong and which value
caused it. This is especially helpful when building commands that take
user input, because users will type unexpected things. If someone types
!mute yahya abc, the validation catches it and tells you duration must be a positive number before it even tries to talk to Highrise.
Connection errors and reconnection
If the connection to Highrise drops, your bot reconnects automatically. You do not handle this yourself. The reconnect logic uses exponential backoff, which means it waits a little longer between each attempt so it does not spam the server:
1st attempt 5 seconds
2nd attempt 10 seconds
3rd attempt 20 seconds
4th attempt 40 seconds
(this is the maximum wait, will always retry after 60s from this point)
5th attempt 60 seconds
While the bot is reconnecting, any requests you try to make will return an error:
result.ok // false
result.error // "Websocket is not connected"
Once the bot reconnects, everything goes back to normal on its own.
Fatal errors
Some errors mean the bot cannot connect at all and retrying would not help. If your token is invalid or the room does not exist, the bot stops trying to reconnect and logs the reason:
[MyBot] │ 12:00:00 │ [ERROR] │ Connection │ Server requested no reconnect, stopping
The three fatal errors that trigger this are:
API token not found— your token is wrong or was deletedRoom not found— the room ID is incorrect or the room was deletedInvalid room id— the room ID format is wrong
When you see this, fix your .env file and restart the bot. No amount
of retrying will fix a bad token.
A complete example
Here is a command handler that covers the full error handling flow from validation to response checking:
bot.on('Chat', async (user, message) => {
if (message.command() === '!tip') {
// get the target from a mention or plain username
const target = message.mentions(0) || message.args(0);
if (!target) {
await bot.message.send('Usage: !tip @username <amount>');
return;
}
// validate the amount argument
const amount = Number(message.args(1) || message.args(0));
if (isNaN(amount) || amount <= 0) {
await bot.message.send('Please provide a valid gold amount.');
return;
}
// find the user in the room
const found = await bot.room.users.find(target);
if (!found) {
await bot.message.send(`${target} is not in the room.`);
return;
}
// attempt the tip
const result = await bot.player.tip(found.user.id, amount);
if (!result.ok) {
log.error('Tip', result.error);
await bot.message.send('Could not send the tip right now.');
return;
}
await bot.message.send(`Tipped ${target} ${amount} gold!`);
}
});
Each step checks what it needs to and gives the user a clear message about what went wrong. Nothing crashes. The bot keeps running. That is the pattern you want everywhere in your bot.
The Logger
The Logger is a simple but important tool that gives you clean, colored,
timestamped output in your terminal. Instead of raw console.log()
calls scattered everywhere, the Logger gives every message a consistent
format so you always know what happened, when it happened, and where it
came from.
Creating a logger
const { Logger } = require('highrise.bot');
const log = new Logger("MyBot");
The "MyBot" is what shows up in square brackets at the start of
every line. Change it to whatever makes sense for your bot. If you have
a bot called BananaBot, use "BananaBot". If you are running multiple
bots, give each one a different prefix so you can tell them apart in the
terminal.
The four log levels
The Logger has four methods, one for each type of message:
log.info()
For general information. Things that are working normally and you want to know about:
log.info('Bot', `Online in ${metadata.room.room_name}`);
log.info('Bot', `Running as ${metadata.bot_id}`);
Output looks like:
[MyBot] │ 12:00:01 │ [INFO] │ Bot │ Online in My Room
log.warn()
For things that are not errors but worth paying attention to. Unexpected but non-critical situations:
log.warn('Chat', `${failed}/${total} message parts failed to send`);
log.warn('Bot', 'Could not move to starting position');
Output looks like:
[MyBot] │ 12:00:05 │ [WARN] │ Chat │ 1/3 message parts failed to send
log.error()
For things that went wrong. Failed requests, unexpected states, anything that needs attention:
log.error('Moderation', `Failed to kick ${userId}: ${result.error}`);
log.error('Wallet', 'Could not fetch wallet balance');
Output looks like:
[MyBot] │ 12:00:10 │ [ERROR] │ Moderation │ Failed to kick user
log.debug()
For detailed information useful while you are building and testing. Debug logs also show the file and line number where they were called, which is helpful for tracing exactly where something happened:
log.debug('Chat', `Command received: ${message.command()}`);
log.debug('Room', `User count: ${count}`);
Output looks like:
[MyBot] │ 12:00:15 │ [DEBUG] │ Chat │ Command received: !ping
↳ /home/user/my-bot/index.js:25:9
The file path shown below debug messages tells you exactly which line of code produced that log. This is very useful when you have a large bot and need to find where something is coming from.
The format
Every log line follows the same format:
[prefix] │ HH:MM:SS │ [LEVEL] │ category │ message
The category is the second argument you pass to each method. Use it
to describe which part of your bot the message is coming from. Good
categories are short and descriptive: 'Bot', 'Chat', 'Moderation',
'Tip', 'Room'.
Logging multiple values
You can pass multiple values after the category and they will be joined
with a · separator:
log.info('Close', `code: ${code}`, `reason: ${reason}`, `attempts: ${attempts}`);
Output:
[MyBot] │ 12:00:20 │ [INFO] │ Close │ code: 1006 · reason: null · attempts: 2
Objects are automatically converted to JSON strings so you can log them
directly without calling JSON.stringify() yourself:
log.debug('Data', { userId: 'abc123', username: 'yahya' });
// logs the object as JSON automatically
Using the logger in your bot
Here is a realistic example of how to use all four levels throughout a bot:
const { Highrise, Logger } = require('highrise.bot');
require('dotenv').config();
const log = new Logger("MyBot");
const bot = new Highrise();
bot.once('Ready', async (metadata) => {
// info: normal startup message
log.info('Bot', `Online in ${metadata.room.room_name}`);
log.info('Bot', `Bot ID: ${metadata.bot_id}`);
});
bot.on('Chat', async (user, message) => {
// debug: useful while building, remove in production
log.debug('Chat', `${user.username}: ${message.content}`);
if (message.command() === '!kick') {
const target = message.args(0);
if (!target) {
await bot.message.send('Usage: !kick <username>');
return;
}
const found = await bot.room.users.find(target);
if (!found) {
await bot.message.send(`${target} is not in the room.`);
return;
}
const result = await bot.room.moderation.kick(found.user.id);
if (!result.ok) {
// error: something went wrong
log.error('Kick', `Failed to kick ${target}: ${result.error}`);
await bot.message.send('Could not kick that user.');
return;
}
// info: action completed successfully
log.info('Kick', `${user.username} kicked ${target}`);
await bot.message.send(`${target} was kicked.`);
}
});
bot.on('UserJoined', async (user, position) => {
log.debug('Join', `${user.username} joined the room`);
await bot.message.send(`Welcome, ${user.username}!`);
});
bot.login(process.env.BOT_TOKEN, process.env.ROOM_ID);
Using the right log level for each situation makes your terminal output much easier to read. Info tells you what is happening normally. Warn flags things that need attention. Error marks real problems. Debug gives you the detail you need when actively working on something.
The Validator
The SDK exports a Validator class you can use in your own bot code to
validate user input before acting on it. The same validator is used
internally by every API method in the SDK, so you already know it works
the way you expect.
Creating a validator
const { Validator } = require('highrise.bot');
const validate = new Validator();
How it works
Every method on the validator throws an Error if the value does not
pass the check. This means you wrap your validation calls in a
try/catch block and handle the error in one place instead of writing
if/else checks everywhere:
bot.on('Chat', async (user, message) => {
if (message.command() === '!mute') {
const target = message.args(0);
const duration = Number(message.args(1));
try {
validate
.required(target, 'target')
.string(target, 'target')
.required(duration, 'duration')
.positive(duration, 'duration')
} catch (error) {
await bot.message.send(`Invalid input: ${error.message}`);
return;
}
// safe to use target and duration here
const found = await bot.room.users.find(target);
if (!found) {
await bot.message.send(`${target} is not in the room.`);
return;
}
await bot.room.moderation.mute(found.user.id, duration);
await bot.message.send(`${target} has been muted for ${duration} seconds.`);
}
});
Method chaining
Every method returns the Validator instance so you can chain multiple
checks together in one block:
validate
.required(username, 'username')
.string(username, 'username')
.minLength(username, 3, 'username')
.match(username, /^[a-z0-9]+$/, 'username');
The chain stops at the first method that fails. If string() throws,
minLength() and match() never run.
All methods
Highrise specific
These two methods validate Highrise-specific data structures in a single call so you do not have to validate each coordinate or anchor property individually:
| Method | What it checks |
|---|---|
.isCoordinates(x, y, z, facing) | All four position values including a valid facing direction |
.isAnchor(entity_id, anchor_ix) | An anchor entity ID and its seat index |
facing must be one of "FrontRight", "FrontLeft", "BackRight",
or "BackLeft". Anything else throws.
Type and existence
| Method | What it checks |
|---|---|
.required(val, field) | Value is not null or undefined |
.string(val, field) | Value is a non-empty string |
.number(val, field) | Value is a valid number and not NaN |
.boolean(val, field) | Value is true or false |
.array(val, field) | Value is an array |
.object(val, field) | Value is a plain object, not an array, not null |
Numbers
| Method | What it checks |
|---|---|
.integer(val, field) | Value is a whole number with no decimal |
.positive(val, field) | Value is greater than zero |
.nonNegative(val, field) | Value is zero or greater |
.range(val, min, max, field) | Value falls between min and max inclusive |
Strings
| Method | What it checks |
|---|---|
.minLength(val, min, field) | String is at least min characters long |
.maxLength(val, max, field) | String is no longer than max characters |
.match(val, regex, field) | String matches the regular expression |
Arrays
| Method | What it checks |
|---|---|
.nonEmptyArray(val, field) | Value is an array with at least one item |
.oneOf(val, options, field) | Value exists in the provided array of options |
The field name
Every method takes a field argument as its last parameter. This is the
name that appears in the error message when the check fails. Always use
a name that makes sense to whoever reads the error:
validate.string(userId, 'userId');
// throws: "userId must be a non-empty string"
validate.positive(amount, 'amount');
// throws: "amount must be a positive number"
validate.range(page, 1, 100, 'page number');
// throws: "page number must be between 1 and 100"
Use descriptive field names. 'userId' is better than 'id'.
'tip amount' is better than 'val'. The clearer the field name, the
more useful the error message is to whoever reads it, next page we will explain each event and what it gives you to work with.
Ready Event
The Ready event is the very first event your bot receives after it successfully connects to a Highrise room. Think of it as the bot waking up and saying “I am here, I am connected, what should I do?” Everything else in your bot depends on this moment happening first.
Before the Ready event fires, your bot has no active connection to Highrise. After it fires, your bot is fully online and can send messages, listen to other events, and interact with users.
Event structure
bot.once('Ready', async (metadata) => {
});
Notice we use bot.once here and not bot.on. This is one of the most
important things to understand about the Ready event.
Your bot reconnects automatically whenever the connection drops, which
means the Ready event fires again every single time that happens. If you
use bot.on, everything inside that handler runs again on every
reconnect. If you use bot.once, it runs only the very first time the
bot connects.
Ask yourself: do I want this code to run every time the bot reconnects,
or just once when it first comes online? For most setup code the answer
is once, which is why bot.once is almost always the right choice here.
The metadata object
The Ready event gives you a metadata object containing information
about your bot and the room it connected to. Here is everything inside
it:
bot.once('Ready', async (metadata) => {
metadata.bot_id // the bot's own user ID
metadata.room.room_name // the display name of the room
metadata.room.owner_id // the user ID of whoever owns the room
// other property you rarely touch
metadata.connection.id // unique current connection id
metadata.connection.rate_limits // rate limit instruction
});
You can also access this same object at any time through the getter on the bot after Ready has fired:
// available anywhere in your code after Ready fires
bot.metadata.bot_id
bot.metadata.room.room_name
bot.metadata.room.owner_id
This is useful when you need the bot’s ID or room name inside another event handler and do not want to store them in a variable yourself.
The three most commonly used properties are:
metadata.bot_id — the bot’s own user ID. Useful when you need to know
whether a tip was sent to the bot specifically, or for anything that
needs to reference the bot as a user.
metadata.room.room_name — the display name of the room the bot
connected to. Great for logging so you always know which room is which
in your terminal.
metadata.room.owner_id — the user ID of the person who owns the room.
Useful if you want to automatically give the room owner special
permissions without hardcoding their ID.
A complete example
Here is a Ready handler that covers everything you would typically want to do when your bot first comes online, with every line explained:
bot.once('Ready', async (metadata) => {
// log that the bot is online and which room it is in
// this is the first thing you will see in your terminal every time you start the bot
log.info('Bot', `Online in ${metadata.room.room_name}`);
// log the bot's own user ID
// you might need this later when checking if a tip was sent to the bot
log.info('Bot', `Bot ID: ${metadata.bot_id}`);
// send a connect message to the room
// this only runs once because we used bot.once, not bot.on
// so it will not spam "Bot is online!" every time the connection drops and recovers
const result = await bot.message.send('Bot is online!');
if (!result.ok) {
log.warn('Bot', `Could not send connect message: ${result.error}`);
}
// move the bot to a specific position in the room on startup
// useful if you want the bot to always stand in the same place
await bot.player.walk(5, 0, 3, 'FrontRight');
});
Note
We’re using the
Loggerwe covered in the Fundamentals chapter to keep our terminal clean.
What to put here
-
The Ready event is for setup code that should run once when the bot
-
comes online. Good things to put here are:
-
Logging that the bot is online with its room name and bot ID.
-
Sending a connect message to the room if you want one.
-
Moving the bot to a starting position using
bot.player.walk(x, y, z, facing). -
Fetching initial data you need before the bot starts responding to users.
-
Starting
setIntervalorsetTimeout(remember to store their id for cleanup)
What not to put here
Command handling, tip responses, welcome messages, and anything else
that needs to respond to ongoing events all belong in their own event
handlers like Chat, Tip, and UserJoined. Putting those things
inside Ready would mean they only run once when the bot connects and
never again.
Also avoid making many API requests all at once right after connecting.
The bot just established its connection and things are still settling.
If you need room data, it is better to wait until a user action
triggers the fetch or use setTimeout rather than doing it immediately in Ready.
The reconnect behavior
When the connection drops, your bot reconnects automatically using exponential backoff. Each attempt waits a little longer than the previous one so it does not hammer the server:
1st attempt ~5 seconds
2nd attempt ~10 seconds
3rd attempt ~20 seconds
4th attempt ~40 seconds
5th attempt ~60 seconds (this is the maximum)
When the reconnect succeeds, the Ready event fires again. Here is what that looks like in your terminal:
[Highrise] │ 14:23:00 │ [INFO] │ Connected to Highrise Bot Server
[MyBot] │ 14:23:01 │ [INFO] │ Bot │ Online in My Room
[MyBot] │ 14:35:17 │ [WARN] │ Closing... │ code: 1006 · reason: null · connection Id: c5a6fc01-64a6-4dfa-8520-6c36ca977a01
[MyBot] │ 14:35:21 │ [INFO] │ Reconnecting... │ 5.3 seconds delay
[Highrise] │ 14:35:27 │ [INFO] │ Connected to Highrise Bot Server
Because we used bot.once, our handler does not run again on the
second connect. The bot is back online and everything works normally
without sending another “Bot is online!” message to the room.
The reconnect counter resets to zero every time a successful connection is made, so a bot that stays connected will always start from a short wait if it ever does drop.
Fatal errors
Some errors stop the reconnect loop entirely because retrying would never help. If your token is invalid or the room does not exist, the bot stops and logs why:
[Highrise] │ 11:59:59 │ [WARN] │ Websocket │ API token not found
[Highrise] │ 12:00:00 │ [ERROR] │ Connection │ Server requested no reconnect, stopping
When you see this, the cause is always one of these three things:
Your bot token is wrong or was deleted from the developer portal.
Your room ID is incorrect or the room no longer exists.
The room ID format is invalid.
Fix the credentials in your .env file and restart the bot. The Ready
event will fire normally once the connection succeeds.
Summary
- Use
bot.oncefor setup code that should only execute during the bot’s initial connection - Use
bot.ononly for tasks that must repeat every time the bot reconnects - The
metadataobject provides the bot ID, room name, and room owner ID - Access room and bot details globally at any time using
bot.metadata - The Ready event is the ideal place for logging, connection messages, and initial positioning
- Keep command handling and user interactions in their respective event handlers
Chat Event
The Chat event fires every time someone sends a message in the room and it is where almost all of your bot’s commands and responses will live. If you want your bot to react when someone types something, this is the event you need.
Event structure
bot.on('Chat', async (user, message) => {
});
We use bot.on here instead of bot.once because we want this to run
every single time someone sends a message, not just once. Every message,
every user, every time.
The user object
The user object tells you who sent the message. It has two properties:
bot.on('Chat', async (user, message) => {
console.log(user.id); // their unique Highrise user ID
console.log(user.username); // their display name
});
The id is a unique string that permanently identifies that user across
all of Highrise. The username is their display name which can change
over time. If you are storing information about users, always store
their id and use username only for displaying to people. A user’s
name might be different next week but their ID will always be the same.
The message object
The message object is more than just the text they sent. It has
methods built into it that make working with commands feel natural. We
covered the message object in detail in the
Message Object chapter, but here is a
quick reminder of what it gives you:
bot.on('Chat', async (user, message) => {
message.content // the full message text, trimmed of whitespace
message.command() // the first word — "!ping hello" → "!ping"
message.args() // everything after — "!ping hello world" → ["hello", "world"]
message.args(0) // first argument — "yahya"
message.mentions() // any @mentions — ["Unfairly"]
message.mentions(0) // first mention — "Unfairly"
});
Bot messages are filtered automatically
You do not need to check whether the message came from the bot itself.
The SDK filters out the bot’s own messages before the Chat event ever
fires. If your bot sends “Hello everyone!” in the room, that will not
trigger your Chat handler. This is handled for you so you never have to
write if (user.id === botId) return.
Building your first command
Here is the most straightforward way to respond to a command:
bot.on('Chat', async (user, message) => {
if (message.command() === '!ping') {
await bot.message.send('Pong! 🏓');
return;
}
});
The return after sending the reply is important. It tells JavaScript
to stop running the rest of the handler after this command matches.
Without it the code keeps going and checks every other condition even
though a command already matched. As your list of commands grows this
becomes more and more significant.
A complete example
Let’s build something realistic. Here is a Chat handler with several commands that cover the most common things bots do, with every single line explained so you know exactly what is happening and why:
bot.on('Chat', async (user, message) => {
// store the command once at the top so we only call message.command() once
// then use cmd throughout instead of calling message.command() in every if statement
const cmd = message.command();
// !ping
// the simplest command — just checks if the bot is alive and responding
if (cmd === '!ping') {
await bot.message.send('Pong! 🏓');
return;
}
// !uptime
// bot.utils.uptime() reads the connection timestamp from the bot's internal state
// and returns a formatted string like "2h 30m 15s"
// you do not need to track this yourself, it is built in
if (cmd === '!uptime') {
const uptime = bot.utils.uptime()
await bot.message.send(`Online for ${uptime}`);
return;
}
// !users
// bot.room.users.count() is a shorthand that fetches the room users
// and immediately returns how many there are without needing the full response
if (cmd === '!users') {
const count = await bot.room.users.count();
await bot.message.send(`There are ${count} users in the room right now.`);
return;
}
// !find
// this command accepts either a plain username or an @mention
// message.mentions(0) checks for @Unfairly style input first
// message.args(0) falls back to a plain "Unfairly" style input if there is no mention
// this means both "!find Unfairly" and "!find @Unfairly" work exactly the same way
if (cmd === '!find') {
const target = message.mentions(0) || message.args(0);
if (!target) {
await bot.message.send('Usage: !find <username> or !find @Unfairly');
return;
}
// bot.room.users.position() accepts either a username or a user ID
// it returns the position object if the user is in the room, or null if they are not
const pos = await bot.room.users.position(target);
if (!pos) {
await bot.message.send(`${target} is not in the room.`);
return;
}
// pos.x is left/right, pos.y is height, pos.z is forward/back, pos.facing is direction
await bot.message.send(
`${target} is at ${pos.x}, ${pos.y}, ${pos.z} facing ${pos.facing}`
);
return;
}
// !gold
// fetches the bot's wallet from Highrise and shows the gold balance
// always check result.ok before using the data
// if the request failed, result.error tells you what went wrong
if (cmd === '!gold') {
const wallet = await bot.inventory.wallet.get();
if (!wallet.ok) {
await bot.message.send('Could not fetch the wallet right now, try again.');
return;
}
// wallet.gold is the bot's current gold balance
// wallet.boostToken and wallet.voiceToken are also available
await bot.message.send(`I have ${wallet.gold} gold.`);
return;
}
// !hi
// responds to the user who sent the message by name
// user.username is their current display name
if (cmd === '!hi') {
await bot.message.send(`Hey ${user.username}! 👋`);
return;
}
// !room
// bot.metadata is a getter that reads from the bot's internal state
// it is available after the Ready event fires and gives you the room name and owner ID
if (cmd === '!room') {
const meta = bot.metadata;
if (!meta) {
await bot.message.send('Still connecting, try again in a moment.');
return;
}
await bot.message.send(`This room is called ${meta.room.room_name}`);
return;
}
});
There are a few things worth noticing in this example that will make your own bots cleaner.
We store message.command() in cmd at the very top so we call that
method only once. Every command after that uses cmd instead of calling
message.command() again inside each if statement.
For the !find command we use message.mentions(0) || message.args(0)
which means the user can type either !find Unfairly or
!find @Unfairly and both work exactly the same way. This makes your
bot feel much more natural to use.
We always check wallet.ok before trying to use wallet.gold. If the
request failed for any reason, wallet.gold would not be set and your
bot would either crash or send a confusing message. Checking first and
handling the error case is the right pattern every time.
We use bot.metadata rather than storing metadata in a variable during
the Ready event. Since bot.metadata is a getter that reads from the
bot’s internal state, it is always available after Ready fires and you
do not need to manage it yourself.
Responding to specific users
Sometimes you only want to respond to certain users. The most reliable way to check is by user ID, not username, because IDs never change:
const OWNER_ID = 'the_room_owners_user_id';
bot.on('Chat', async (user, message) => {
if (message.command() === '!shutdown') {
// check by ID, not username, because usernames can change
if (user.id !== OWNER_ID) {
await bot.message.send('You do not have permission to do that.');
return;
}
await bot.message.send('Shutting down. Goodbye!');
process.exit(0);
}
});
If you want to use the room owner’s ID without hardcoding it, you can
read it from bot.metadata which is populated after the Ready event:
bot.on('Chat', async (user, message) => {
if (message.command() === '!shutdown') {
const ownerId = bot.metadata?.room.owner_id;
if (user.id !== ownerId) {
await bot.message.send('Only the room owner can do that.');
return;
}
await bot.message.send('Shutting down. Goodbye!');
process.exit(0);
}
});
Whispers vs chat messages
The Chat event only fires for public room messages. Whispers are a separate event. If you want the same command to work in both public chat and whispers, you need to listen to both events separately. We will cover that in the Whisper page.
Summary
- The Chat event fires every time someone sends a public room message
- You get a
userobject (id,username) and amessageobject (content,command(),args(),mentions()) - Store
message.command()in a variable at the top of your handler to avoid multiple calls - Always use
returnafter handling a command to stop further code execution - Use
message.mentions(0) || message.args(0)to support both @mentions and plain text usernames - Always check
result.okbefore using data retrieved from any API request - The bot’s own messages are automatically filtered out to prevent infinite loops
- Access room and bot information via
bot.metadatainstead of creating your own variables
Whisper Event
The Whisper event fires when a user sends a private whisper message to
the bot. Whispers are only visible to the sender and the receiver, so
anything the bot sends back using bot.whisper.send() will only be
seen by that user.
The Whisper event works almost identically to the Chat event. You get
the same user and message objects with the same command(),
args(), and mentions() methods. Everything you learned in the
Chat page applies here too. The only real difference is
how you respond.
Event structure
bot.on('Whisper', async (user, message) => {
});
Replying to a whisper
To send a whisper back, use bot.whisper.send() with the user’s ID:
bot.on('Whisper', async (user, message) => {
if (message.command() === '!ping') {
await bot.whisper.send(user.id, 'Pong! 🏓');
return;
}
});
Notice you pass user.id not user.username. The whisper API requires
the user’s ID to make sure the message goes to the right person even if
their username has changed.
When to use whispers vs chat messages
This is worth thinking about when designing your bot. The rule is simple. Use a chat message when the information is relevant to everyone in the room. Use a whisper when the information is personal to that specific user.
A good example is a balance command. The user asks publicly but the answer only matters to them:
bot.on('Chat', async (user, message) => {
if (message.command() === '!balance') {
const wallet = await bot.inventory.wallet.get();
if (!wallet.ok) {
await bot.whisper.send(user.id, 'Could not fetch balance right now.');
return;
}
// whisper the result so only they see it
// nobody else in the room needs to know the bot's gold balance
await bot.whisper.send(user.id, `Bot balance: ${wallet.gold} gold.`);
return;
}
});
The command is typed in public chat but the response goes privately to that user. This keeps the room chat clean while still giving them the information they asked for.
Long whispers
Just like bot.message.send(), bot.whisper.send() handles messages
longer than 256 characters automatically by splitting them into multiple
parts and sending them in order. You never need to worry about hitting
the character limit.
Summary
- The Whisper event provides the same
userandmessageobjects as the Chat event - Use
bot.whisper.send(user.id, text)to reply privately to the user - Avoid using
bot.message.send(text)if the response is intended to be private - Ideal for handling personal or sensitive information without cluttering the room
- Public messages should be reserved for content intended for all participants
- Long whispers are automatically split into multiple parts, similar to chat messages
User Joined Event
The UserJoined event fires every time someone enters your Highrise room. It gives you the user who joined and where they spawned in the room.
Event structure
bot.on('UserJoined', async (user, position) => {
});
The user object
bot.on('UserJoined', async (user, position) => {
user.id // their unique Highrise user ID
user.username // their display name
});
The position object
The position tells you where in the room they appeared, position has x, y, z, and facing:
bot.on('UserJoined', async (user, position) => {
if (position) {
position.x // left/right
position.y // height
position.z // forward/back
position.facing // "FrontRight", "FrontLeft", "BackRight", "BackLeft"
}
});
useful if you are building something like a game that depends on starting positions.
A complete example
Here is a UserJoined handler that welcomes users and whispers them a help message, which is a very common pattern:
bot.on('UserJoined', async (user, position) => {
// announce the join in public chat
await bot.message.send(`Welcome to the room, ${user.username}! 👋`);
// whisper them something useful privately so the room chat stays clean
// this way every user gets a personal message without flooding the room
await bot.whisper.send(
user.id,
`Hey ${user.username}! Type !help to see what I can do.`
);
});
Important things to know
The UserJoined event fires for every single user who enters the room,
including users who leave and come back. If your room is busy, welcome
messages can add up quickly. Keep them short or consider only welcoming
users once per session using a Set to track who you have already seen:
const welcomed = new Set();
bot.on('UserJoined', async (user, position) => {
if (welcomed.has(user.id)) {
return;
}
welcomed.add(user.id);
await bot.message.send(`Welcome for the first time, ${user.username}! 🎉`);
});
Note that welcomed is stored in memory so it resets every time your
bot restarts. If you need it to persist across restarts you would need
to save it somewhere outside of memory.
Summary
- The UserJoined event fires every time someone enters the room
- You get the
userwho joined and their startingposition positioncontainsx,y,z, andfacingcoordinates- This event triggers for every entry, including users returning after leaving
- Use a
Setto track seen users if you need to distinguish first-timers from returning users
User Left Event
The UserLeft event fires every time someone leaves your Highrise room. It is simpler than UserJoined because you only get the user object. There is no position because they are already gone by the time the event fires.
Event structure
bot.on('UserLeft', async (user) => {
});
The user object
bot.on('UserLeft', async (user) => {
user.id // their unique Highrise user ID
user.username // their display name
});
That is all you get. Just the user. No position, no reason, no additional context. They left and this is who it was.
A complete example
Here is a realistic UserLeft handler that tracks session time and cleans up any data stored for that user:
const joinTimes = new Map();
bot.on('UserJoined', async (user, position) => {
// record when they joined so we can calculate how long they stayed
joinTimes.set(user.id, Date.now()); // storing timestamp the moment they joined
await bot.message.send(`Welcome, ${user.username}!`);
});
bot.on('UserLeft', async (user) => {
// calculate how long they were in the room
const joinedAt = joinTimes.get(user.id);
if (joinedAt) {
// since joinedAt is less than current one we will get the diff
const ms = Date.now() - joinedAt;
const minutes = Math.floor(ms / 60000);
const seconds = Math.floor((ms % 60000) / 1000);
// log their session time in the terminal
log.info('Session', `${user.username} was here for ${minutes}m ${seconds}s`);
// clean up so the Map does not grow forever as users come and go
joinTimes.delete(user.id);
}
});
Important things to know
Be careful with goodbye messages in busy rooms. If the UserJoined event can flood the chat with welcome messages, UserLeft can do the same with goodbyes. In an active room with users constantly coming and going, a goodbye message for every single person gets annoying fast. It works well in smaller quieter rooms, but in busy rooms it is better to skip it or only say goodbye to specific users you care about.
Always clean up data when a user leaves. If your bot stores anything per-user like cooldowns, warnings, or session data, the UserLeft event is the right place to delete it so your Maps and objects do not grow indefinitely throughout the day.
Summary
- The UserLeft event fires every time someone leaves the room
- You only get the
userobject containing theiridandusername - No position data is provided for this event
- Common uses include logging session time and sending conditional goodbye messages
- Use this event to clean up stored user data to prevent memory growth
- Pair it with the UserJoined event to maintain an accurate list of current room occupants
Movement Event
The Movement event fires every time a user taps somewhere in the room to walk there, or taps a piece of furniture to sit on it. Every tap is a movement event. In an active room with many users this event fires constantly, which makes it one of the most important events to handle carefully.
Event structure
bot.on('Movement', async (user, position, anchor) => {
});
You get three things: the user who moved, their new position if they
walked somewhere, and their anchor if they sat down. Only one of
position or anchor will have a value at any time. The other will
always be null.
The user object
bot.on('Movement', async (user, position, anchor) => {
console.log(user.id) // their unique Highrise user ID
console.log(user.username) // their display name
});
The position object
When a user taps the floor to walk somewhere, position has a value and
anchor is null:
bot.on('Movement', async (user, position, anchor) => {
if (position) {
console.log(
position.x // left/right
position.y // height
position.z // forward/back
position.facing // "FrontRight", "FrontLeft", "BackRight", "BackLeft"
)
}
});
The anchor object
When a user taps a chair, couch, or any sitting furniture, anchor has
a value and position is null:
bot.on('Movement', async (user, position, anchor) => {
if (anchor) {
console.log(
anchor.entity_id // the ID of the furniture they sat on
anchor.anchor_ix // which specific seat on that furniture
)
}
});
Checking which one you have
Since only one will ever be set, a simple check is enough:
bot.on('Movement', async (user, position, anchor) => {
if (position) {
// user tapped the floor and is walking
} else {
// user tapped furniture and is sitting
// anchor is guaranteed to have a value here
}
});
A simple example ( 20x20 Room )
let’s create a VIP Lounge. It uses a simple if/else if structure to give users different statuses based on where they are in the room.
bot.on('Movement', async (user, position) => {
// 1. Safety check: If the user is sitting, we can't get their position
if (!position) return;
// 2. Define our area using simple coordinate checks
const isInVIPLounge = position.x > 15 && position.z > 15;
// 3. Logic: Check where the user is and respond
if (isInVIPLounge) {
// This only triggers if the user is in the far corner
await bot.whisper.send(user.id, "Welcome to the VIP Lounge!");
} else {
// This triggers everywhere else (the "General" area)
await bot.whisper.send(user.id, "You are walking through the main lobby.");
}
});
Important things to know
This event fires a lot. In a room with 20+ active users all moving around, this event can fire hundreds of times per minute. Keep your handler as fast as possible. If the first thing you do is check a condition and return early, do that check before anything else so the expensive code never runs unnecessarily:
bot.on('Movement', async (user, position, anchor) => {
// cheap check first — exit immediately if we do not care about this user
if (!trackedUsers.has(user.id)) return;
// only reaches here for users we actually care about
// do the expensive work now
});
Do not make API requests inside this event without a guard. Calling
bot.room.users.get() or any other request every time someone moves
will make your bot generate a huge number of requests very quickly.
Always narrow down with a cheap check before making any network call.
Summary
- The Movement event fires when a user taps the floor to walk or taps furniture to sit
- You get
user,position, andanchorobjects, but only one of the latter two will have a value positionincludesx,y,z, andfacingfor walking usersanchorincludesentity_idandanchor_ixfor sitting users- This event fires very frequently, so always use early returns to optimize performance
- Never make API requests inside this handler without a strict guard condition
- Clean up tracking data in the
UserLeftevent to prevent memory leaks
Tip Event
The Tip event fires whenever a gold transaction occurs. This includes transfers between two players in a room, between a player and a bot, or via DM bot tips. It is commonly used to automate gold-based subscriptions.
Event structure
bot.on('Tip', async (sender, receiver, currency) => {
});
You get three things: the sender who sent the tip, the receiver who received it, and the currency object containing the specific tip information like the amount and type.
The sender and receiver objects
Both the sender and receiver objects inherit from the User object, which contains the id and username.
bot.on('Tip', async (sender, receiver, currency) => {
console.log(sender.id) // sender unique Highrise ID
console.log(sender.username) // sender display name
console.log(receiver.id) // receiver unique Highrise ID
console.log(receiver.username) // receiver display name
});
the currency object
bot.on('Tip', async (sender, receiver, currency) => {
console.log(currency.amount) // currency amount that got tipped
console.log(currency.type) // type of the currency that got tipped (usually gold)
});
Gold tips in Highrise can only be sent in specific denominations. You will always get one of these values for currency.amount:
| Amount | Name |
|---|---|
| 1 | 1 gold bar |
| 5 | 5 gold bars |
| 10 | 10 gold bars |
| 50 | 50 gold bars |
| 100 | 100 gold bars |
| 500 | 500 gold bars |
| 1000 | 1k gold bars |
| 5000 | 5k gold bars |
| 10000 | 10k gold bars |
Common patterns
Thanking tippers
The most basic use of this event is acknowledging tips in chat:
bot.on('Tip', async (sender, receiver, currency) => {
await bot.message.send(
`Thank you ${sender.username} for the ${currency.amount} gold!`
);
});
Detecting tips to the bot specifically
If you only want to react when someone tips the bot and not when users tip each other:
bot.on('Tip', async (sender, receiver, currency) => {
if (receiver.id !== bot.metadata.bot_id) return;
await bot.message.send(
`${sender.username} just tipped the bot ${currency.amount} gold!`
);
});
Summary
- The Tip event fires every time someone tips in the room
- You get the
sender,receiver, andcurrencyobjects currency.amountis always one of the valid gold bar denominations- Check
receiver.id === bot.metadata.bot_idif you only want to react to tips sent to the bot - Common uses are thank you messages, leaderboards, tip rewards, and voting systems
- Use
rewardedUserssets or similar guards to prevent rewards from triggering multiple times
Moderation Event
The Moderation event fires whenever a moderation action occurs in-room. This includes kick, mute, ban, unmute, and unban. It is commonly used to log staff actions or notify other moderators via bot.direct.send(), which we cover on the Direct event page.
Event structure
bot.on('Moderation', async (moderator, target, action) => {
});
You get three things: The moderator who did the action, the target who got moderated, and the action object which have type and duration.
Important
While
ModeratorandTargetclasses inherit from theUserclass, the Highrise WebSocket server emits moderation events containing only the user ID. as a result, theusernamefield will always benull, since you already knowUserclass structure at this point, and no need to explain what it has.
The action object
bot.on('Moderation', async (moderator, target, action) => {
console.log(action.type); // The type of moderation action performed
console.log(action.duration); // The length of the action (in seconds), or null if not applicable
});
There are 5 types of moderation actions. Note that only mute and ban include a duration; for others, the duration will be null.
| Action Type | Has Duration? |
|---|---|
| kick | false |
| mute | true |
| ban | true |
| unmute | false |
| unban | false |
Common patterns
Logging all moderation actions
A simple way to keep a record of everything that happens in your room:
bot.on('Moderation', async (moderator, target, action) => {
const duration = action.duration
? `for ${action.duration} seconds`
: '';
log.info(
'Moderation',
`${moderator.id} [${action.type}] ${target.id} ${duration}`
);
});
Tracking moderation history
Keep a log of every action taken against each user:
const moderationLog = new Map();
bot.on('Moderation', async (moderator, target, action) => {
// get existing history or create new one for this target
const history = moderationLog.get(target.id) || [];
// add details to his history
history.push({
type: action.type,
duration: action.duration,
moderatorId: moderator.id,
timestamp: Date.now(),
});
// set the new updated history to the target in Map()
moderationLog.set(target.id, history);
// log it
log.info(
'Moderation',
`${moderator.id} ${action.type} ${target.id}. Total actions against the user: ${history.length}`
);
});
Summary
- The Moderation event fires on kicks, bans, mutes, unmutes, and unbans
- You get the
moderatorwho acted, thetargetwho was moderated, and theactiondetails action.typeis always one of"kick","ban","mute","unmute", or"unban"action.durationis set for mutes and bans,nullfor everything else- Common uses are logging, public announcements, escalation systems, and user protection
- You can react to moderation actions and even undo them if needed
Direct Event
The Direct event fires when a user sends the bot a direct message from outside the room. Unlike Chat and Whisper which happen inside the room, direct messages are private conversations between the bot and a user that exist across the entire Highrise platform.
Event structure
bot.on('Direct', async (user, message, conversation) => {
});
The user object
bot.on('Direct', async (user, message, conversation) => {
console.log(user.id) // the sender's user ID
console.log(user.username) // always null — see below
});
Why username is always null
When Highrise sends the Direct event over the WebSocket, it only
includes the sender’s user ID. The username is never provided. This
means user.username will always be null in this event and you
should never rely on it.
If you need the username, you can look the user up in the room:
bot.on('Direct', async (user, message, conversation) => {
const found = await bot.room.users.find(user.id);
const username = found?.user.username || 'Unknown';
await bot.direct.send(conversation.id, `Hey ${username}!`);
});
Keep in mind that bot.room.users.find() only works if the user is
currently in the same room as the bot. If they sent the DM from a
different room, found will be null, we will cover how to fetch from outside the room later on so stick around.
the message object
for more details about this object read The Message Object page
bot.on('Direct', async (user, message, conversation) => {
console.log(message.content) // what they sent
console.log(message.command()) // first word
console.log(message.args()) // everything after
console.log(message.mentions()) // all mentions in the message (@ stripped)
});
the conversation object
bot.on('Direct', async (user, message, conversation) => {
console.log(conversation.id) // the conversation ID needed to reply
console.log(conversation.is_new_conversation) // true if this is the first message ever
});
Replying to a direct message
To reply, use bot.direct.send() with the conversation ID:
bot.on('Direct', async (user, message, conversation) => {
const convId = conversation.id
await bot.direct.send(convId, 'Hey! I got your message.');
});
The conversation ID is what ties your reply to the right conversation.
Always use conversation.id from the event itself rather than
constructing one yourself.
A complete example
Here is a Direct handler that responds to commands sent through DMs, including a greeting that handles the null username gracefully:
bot.on('Direct', async (user, message, conversation) => {
const cmd = message.command();
const convId = conversation.id;
// !ping works in DMs too
if (cmd === '!ping') {
await bot.direct.send(convId, 'Pong! 🏓');
return;
}
// !gold — show the bot's wallet balance privately
if (cmd === '!gold') {
const wallet = await bot.inventory.wallet.get();
if (!wallet.ok) {
await bot.direct.send(convId, 'Could not fetch the wallet right now.');
return;
}
await bot.direct.send(convId, `I have ${wallet.gold} gold.`);
return;
}
});
Summary
- The
Directevent fires on any private message sent to the bot. - Use
bot.direct.send(conversation.id, text)to reply. message.command()andmessage.args()work exactly like they do in room chat.user.usernameis alwaysnull; use theuser.idto identify the sender.
Voice Event
Warning
Unfortunately Voice event is no longer supported for unknown reason after Highrise
4.25.3Free Live Voice update
Channel Event
The Channel event fires whenever a bot sends a message using the bot.channel.send() method. This is used for hidden communication between bots in the same room, allowing them to coordinate actions without cluttering the public chat for players.
Event structure
bot.on("Channel", async (botId, message, tags) => {
// botId: The ID of the bot that sent the message
// message: The raw content sent
// tags: An array of strings used for filtering
});
You get three things: the botId of the sender, the message content, and tags, which is an array of strings.
Note
The sender can’t see his own message
Important
Unlike room chat, the
messagehere is not an instance of theMessageclass. It is a raw string. This means you cannot use.command(),.args(), or.mentions(). If you are sending structured data (like JSON), you will need to parse it manually usingJSON.parse(message).
Using Tags for Filtering
Tags are a powerful way to organize your bot-to-bot communication. Instead of parsing every message, you can check if a message belongs to a specific category:
bot.on("Channel", async (botId, message, tags) => {
if (tags.includes("moderation_sync")) {
console.log("Received a moderation update from another bot.");
}
});
Summary
- Messages sent here are invisible to regular players.
- The message is a generic string, not a
Messageobject. - Use
tagsto categorize or filter messages easily. - Best used for syncing state or triggering actions between multiple bots.
Important
You now have a solid grasp of the core events! You are officially ready to start building functional bots. If you want to dive deeper into the full capabilities of the SDK or explore specific API details, head over to the API Reference.
bot.message
The bot.message API handles everything related to sending messages in
the room. It is the most common way your bot will communicate with users.
This page covers what it can do and how to use it properly.
What lives on bot.message
bot.message.send() // send a public message to everyone in the room
That is it. One method. But it is the one you will use more than any other.
send()
Sends a message that everyone in the room can see.
await bot.message.send('Hello everyone!');
Parameters
| Parameter | Type | Description |
|---|---|---|
message | string | The text you want the bot to say |
Returns
An AcknowledgmentResponse. This is the same
response object covered in the Requests and Responses
chapter. It tells you whether the message was sent successfully.
Basic usage
The simplest possible use is sending a single message:
await bot.message.send('Hello, Highrise!');
This is what you will do most of the time. Call it and move on.
Real world example
Here is a complete Chat handler that uses bot.message.send() to
respond to a command:
bot.on('Chat', async (user, message) => {
const cmd = message.command();
if (cmd === '!ping') {
await bot.message.send('Pong! 🏓');
return;
}
if (cmd === '!users') {
const count = await bot.room.users.count();
await bot.message.send(`There are ${count} users in the room right now.`);
return;
}
if (cmd === '!say') {
const text = message.args().join(' ');
if (!text) {
await bot.message.send('Usage: !say <message>');
return;
}
await bot.message.send(text);
return;
}
});
Important things to know
Long messages are split for you. Highrise has a 256 character limit
per message. If your message is longer than that, bot.message.send()
splits it into chunks and sends each one in order. You do not need to
handle this yourself.
AcknowledgmentResponse
Every call to bot.message.send() returns this object:
{
ok: boolean; // true if the message was sent
error: string | null; // what went wrong if it failed
hasError(): boolean; // helper method, same as !ok
}
Summary
bot.message.send()is the only method you need to send public messages- It returns an
AcknowledgmentResponsewithokanderrorproperties - Messages over 256 characters are automatically split and sent in order
- Always use
awaitwhen callingbot.message.send() - The bot’s own messages are filtered out of the Chat event automatically
- Check
result.okfor important messages, skip it for casual ones
bot.whisper
The bot.whisper API handles sending private messages to specific users
in the room. Whispers are only visible to the sender and the receiver.
No one else in the room can see them.
What lives on bot.whisper
bot.whisper.send() // send a private whisper to a specific user
One method. It works almost exactly like bot.message.send() except you
also tell it who to send the message to.
send()
Sends a private whisper that only the target user can see.
await bot.whisper.send(userId, 'This is just between us.');
Parameters
| Parameter | Type | Description |
|---|---|---|
userId | string | The ID of the user to whisper |
message | string | The text you want the bot to whisper |
Returns
An AcknowledgmentResponse with ok and
error properties.
Basic usage
// whisper a welcome message to a new user
bot.on('UserJoined', async (user) => {
await bot.whisper.send(user.id, 'Welcome! Type !help to see commands.');
});
// respond to a command with whisper
bot.on('Chat', async (user, message) => {
const cmd = message.command()
if (cmd === '!ping') {
await bot.whisper.send(user.id, 'Pong! 🏓');
return;
}
});
When to whisper vs send to chat
Use bot.message.send() when… | Use bot.whisper.send() when… |
|---|---|
| Everyone should see it | Only one person needs to see it |
| Announcing something | Giving private instructions |
| Responding to public commands | Responding to whisper commands |
| Welcoming someone publicly | Sharing personal information |
A common pattern is listening for a command in public chat but responding privately:
bot.on('Chat', async (user, message) => {
const cmd = message.command()
if (cmd === '!balance') {
const wallet = await bot.inventory.wallet.get();
if (!wallet.ok) {
await bot.whisper.send(user.id, 'Could not fetch balance.');
return;
}
// whisper the result so only they see it
await bot.whisper.send(user.id, `Bot balance: ${wallet.gold} gold.`);
return;
}
});
The user types !balance publicly but the answer comes to them
privately. This keeps the room chat clean.
Important things to know
Use the user ID, not the username. bot.whisper.send() requires the
user’s unique ID.
Long messages are split automatically. Just like bot.message.send(),
whispers over 256 characters are split into multiple messages.
AcknowledgmentResponse
Same response type as bot.message.send():
{
ok: boolean; // true if the whisper was sent
error: string | null; // what went wrong if it failed
hasError(): boolean; // returns true if error exists
}
Summary
bot.whisper.send(userId, message)sends a private message to one user- Use the user’s
idfrom the event, not theirusername - Long messages are automatically split like
bot.message.send() - Whisper responses keep the room chat clean when information is personal
- The return type is identical to
bot.message.send()
bot.direct
The bot.direct API handles everything related to private conversations
between your bot and users outside of the room. Unlike whispers which
happen inside a room, direct messages exist across the entire Highrise
platform. A user can message your bot from anywhere and your bot can
reply back to that same conversation thread.
What lives on bot.direct
bot.direct.conversations // list conversations, leave conversations
bot.direct.messages // fetch message history from a conversation
bot.direct.send() // send a message to a conversation
bot.direct.broadcast() // send the same message to multiple users
bot.direct.inviteRoom() // invite a user to a room
bot.direct.inviteWorld() // invite a user to a world
bot.direct.broadcastInvite() // invite multiple users at once
This is a larger API than bot.message or bot.whisper. Take it one
method at a time.
send()
Sends a direct message to an existing conversation.
await bot.direct.send(convId, 'Hey! I got your message.');
| Parameter | Type | Description |
|---|---|---|
convId | string | The conversation ID from the Direct event |
message | string | The text you want to send |
Returns: AcknowledgmentResponse
Where to get the conversation ID
The Direct event gives you the conversation object which contains the ID:
bot.on('Direct', async (user, message, conversation) => {
const convId = conversation.id;
await bot.direct.send(convId, 'Thanks for reaching out!');
});
Always use the conversation.id from the event itself or save it for later usage. Do not try to
construct one manually.
broadcast()
Sends the same direct message to multiple users at once.
await bot.direct.broadcast(
['user_id_1', 'user_id_2', 'user_id_3'],
'Your daily reward is ready!'
);
| Parameter | Type | Description |
|---|---|---|
userIds | string[] | Array of user IDs (1-100 users) |
message | string | The text you want to send |
Returns: AcknowledgmentResponse
When to use broadcast
Use this when you need to notify multiple users of the same thing. Examples include event announcements, reward distributions, or system alerts. Keep the list under 100 users per call.
inviteRoom()
Invites a user to a specific room through a direct message conversation.
await bot.direct.inviteRoom(convId, 'room_id_here');
| Parameter | Type | Description |
|---|---|---|
convId | string | The conversation ID |
roomId | string | The ID of the room to invite them to |
Returns: AcknowledgmentResponse
inviteWorld()
Invites a user to a specific 3D world through a direct message conversation.
await bot.direct.inviteWorld(convId, 'world_id_here');
| Parameter | Type | Description |
|---|---|---|
convId | string | The conversation ID |
worldId | string | The ID of the world to invite them to |
Returns: AcknowledgmentResponse
broadcastInvite()
Sends the same room or world invite to multiple users.
await bot.direct.broadcastInvite(
['user_id_1', 'user_id_2'],
{ roomId: 'room_id_here' }
);
// or for a world
await bot.direct.broadcastInvite(
['user_id_1', 'user_id_2'],
{ worldId: 'world_id_here' }
);
| Parameter | Type | Description |
|---|---|---|
userIds | string[] | Array of user IDs (1-100 users) |
inviteDetails | object | Either { roomId: string } or { worldId: string } |
Returns: AcknowledgmentResponse
conversations.list()
Fetches a list of the bot’s direct message conversations.
const inbox = await bot.direct.conversations.list();
// with pagination
const nextPage = await bot.direct.conversations.list(lastId);
// including conversations the bot hasn't joined yet
const withUnjoined = await bot.direct.conversations.list(null, true);
| Parameter | Type | Description |
|---|---|---|
lastId | string | null | Cursor for pagination, fetches next 20 conversations |
notJoined | boolean | Include conversations the bot hasn’t joined (default: false) |
Returns: GetConversationsResponse
Pagination
The response includes a .next() method for fetching the next page:
const inbox = await bot.direct.conversations.list();
if (inbox.ok) {
console.log(`Found ${inbox.conversations.length} conversations`);
// fetch the next 20 conversations
if (inbox.next) {
const nextPage = await inbox.next();
console.log(`Next page has ${nextPage.conversations.length} more`);
}
}
conversations.leave()
Removes the bot from a conversation.
await bot.direct.conversations.leave(convId);
| Parameter | Type | Description |
|---|---|---|
convId | string | The conversation ID to leave |
Returns: AcknowledgmentResponse
messages.list()
Fetches message history from a specific conversation.
const messages = await bot.direct.messages.list(convId);
// with pagination
const olderMessages = await bot.direct.messages.list(convId, lastMessageId);
| Parameter | Type | Description |
|---|---|---|
convId | string | The conversation ID |
lastMessageId | string | Optional cursor for older messages |
Returns: GetMessagesResponse
Pagination
Works the same as conversations:
const response = await bot.direct.messages.list(convId);
if (response.ok) {
console.log(`Loaded ${response.messages.length} messages`);
// fetch older messages
if (response.next) {
const older = await response.next();
console.log(`Found ${older.messages.length} older messages`);
}
}
Complete example
Here is a Direct handler that responds to commands and uses several parts of the API:
bot.on('Direct', async (user, message, conversation) => {
const cmd = message.command();
const convId = conversation.id;
if (cmd === '!ping') {
await bot.direct.send(convId, 'Pong! 🏓');
return;
}
if (cmd === '!invite') {
const roomId = message.args(0);
if (!roomId) {
await bot.direct.send(convId, 'Usage: !invite <roomId>');
return;
}
const result = await bot.direct.inviteRoom(convId, roomId);
if (!result.ok) {
await bot.direct.send(convId, 'Could not send that invite.');
return;
}
await bot.direct.send(convId, 'Invite sent!');
return;
}
if (cmd === '!broadcast') {
const text = message.args().join(' ');
// in a real bot you would get these IDs from somewhere
const userIds = ['user1', 'user2', 'user3'];
await bot.direct.broadcast(userIds, text);
await bot.direct.send(convId, `Broadcast sent to ${userIds.length} users.`);
return;
}
});
Response Types
AcknowledgmentResponse
Returned by send(), broadcast(), inviteRoom(), inviteWorld(),
broadcastInvite(), and conversations.leave():
{
ok: boolean;
error: string | null;
hasError(): boolean;
}
GetConversationsResponse
Returned by conversations.list():
{
ok: boolean;
error: string | null;
hasError(): boolean;
conversations: Conversation[]; // array of conversation objects
notJoined: number; // count of unjoined conversations
recentMessage: MessageSummary | null;
lastId: string | null; // cursor for next page
next(): GetConversationsResponse | null;
}
GetMessagesResponse
Returned by messages.list():
{
ok: boolean;
error: string | null;
hasError(): boolean;
messages: MessageSummary[]; // array of message objects
recentMessage: MessageSummary | null;
lastId: string | null; // cursor for next page
next(): GetMessagesResponse | null;
}
Important things to know
Conversation IDs come from the Direct event. Always use
conversation.id from the event handler. Do not guess or construct IDs.
Broadcast is limited to 100 users per call. If you need to message
more than 100 users, split them into batches and call broadcast()
multiple times.
Messages over 2000 characters are split automatically. The same
behavior as bot.message.send() applies here.
Leaving a conversation is permanent. Once the bot leaves, it cannot message that user again unless the user starts a new conversation.
Username is always null in Direct events. The Direct event only
provides the user ID. If you need the username, you must look it up
separately using bot.room.users.find() if they are in the room.
Summary
bot.direct.send(convId, message)replies to a DM conversationbot.direct.broadcast(userIds, message)messages up to 100 users at oncebot.direct.conversations.list()fetches the bot’s inbox with paginationbot.direct.messages.list(convId)fetches message history from a conversationbot.direct.inviteRoom()andbot.direct.inviteWorld()send invites- Always get the conversation ID from the Direct event
- All send methods return
AcknowledgmentResponse - List methods return responses with built-in
.next()pagination
bot.channel
The bot.channel API handles hidden communication between bots in the
same room. Messages sent here are invisible to regular players. Only
other bots in the room can see them. This is useful for coordinating
multiple bots without cluttering the public chat.
What lives on bot.channel
bot.channel.send() // send a hidden message to all other bots in the room
One method. Simple.
send()
Sends a message to the hidden channel that only other bots can see.
await bot.channel.send('Player 1 wins', ['announcement']);
| Parameter | Type | Description |
|---|---|---|
message | string | The content to send (can be plain text or stringified JSON) |
tags | string[] | Array of strings for filtering and categorization |
Returns: AcknowledgmentResponse
What tags are for
Tags let you categorize messages so receiving bots can filter what they care about without parsing every message:
// sending bot
await bot.channel.send('User Unfairly was muted', ['moderation', 'log']);
await bot.channel.send('New high score: 9999', ['game', 'leaderboard']);
await bot.channel.send(JSON.stringify({ action: 'sync', data: {} }), ['state']);
The receiving bot can then check if a message is relevant:
bot.on('Channel', (botId, message, tags) => {
// only care about moderation messages
if (tags.includes('moderation')) {
console.log(`Mod action from ${botId}: ${message}`);
}
// ignore everything else
});
Receiving channel messages
Listen to the Channel event to receive messages from other bots:
bot.on('Channel', (botId, message, tags) => {
console.log(`[${botId}]: ${message}`);
console.log(`Tags: ${tags.join(', ')}`);
});
| Parameter | Type | Description |
|---|---|---|
botId | string | The ID of the bot that sent the message |
message | string | The raw message content |
tags | string[] | The tags the sender attached |
Important
The sender bot does not receive its own messages. You do not need to filter out your own bot ID.
Important
The
messagehere is a raw string, not aMessageobject. Methods like.command(),.args(), and.mentions()do not exist. If you send structured data, parse it manually withJSON.parse().
Basic usage
Syncing state between bots
// Bot A: announces when it mutes someone
bot.on('Chat', async (user, message) => {
if (message.command() === '!mute') {
const target = message.args(0);
// ... mute logic ...
await bot.channel.send(`${target} was muted by ${user.username}`, ['moderation']);
}
});
// Bot B: listens for moderation actions from other bots
bot.on('Channel', (botId, message, tags) => {
if (tags.includes('moderation')) {
console.log(`Bot ${botId} reports: ${message}`);
}
});
Sending structured data
Tags help with filtering but the message itself can be JSON for complex data:
// Sending bot
bot.on("Tip", async (sender, receiver, currency) => {
const data = {
event: 'tip_received',
from: sender.username,
amount: currency.amount,
timestamp: Date.now()
};
await bot.channel.send(JSON.stringify(data), ['economy', 'tip']);
})
// Receiving bot
bot.on('Channel', (botId, message, tags) => {
if (tags.includes('economy') && tags.includes('tip')) {
const data = JSON.parse(message);
console.log(`${data.from} tipped ${data.amount} gold`);
}
});
Using tags for different categories
A single bot can handle multiple types of channel messages:
bot.on('Channel', async (botId, message, tags) => {
if (tags.includes('announcement')) {
// forward important announcements to chat
await bot.message.send(`[System] ${message}`);
return;
}
if (tags.includes('moderation')) {
// log moderation actions from other bots
log.info('Channel', `Mod action from ${botId}: ${message}`);
return;
}
if (tags.includes('debug')) {
// only log debug messages in development
log.debug('Channel', `[${botId}] ${message}`);
return;
}
});
Complete example
Here is a bot that uses the channel to coordinate a simple game between multiple bots:
// Bot 1: Game host
bot.on('Chat', (user, message) => {
if (message.command() === '!startgame') {
await bot.message.send('Game starting in 3...');
await bot.channel.send('countdown:3', ['game', 'countdown']);
await bot.utils.sleep(1000);
await bot.message.send('2...');
await bot.channel.send('countdown:2', ['game', 'countdown']);
await bot.utils.sleep(1000);
await bot.message.send('1...');
await bot.channel.send('countdown:1', ['game', 'countdown']);
await bot.utils.sleep(1000);
await bot.message.send('GO!');
await bot.channel.send('game:started', ['game', 'start']);
}
});
// Bot 2: Game participant
bot.on('Channel', async (botId, message, tags) => {
if (tags.includes('game')) {
if (tags.includes('countdown')) {
const [, seconds] = message.split(':');
console.log(`Countdown: ${seconds}`);
}
if (tags.includes('start')) {
await bot.message.send('I am ready!');
}
}
});
Important things to know
Channel messages are bot-only. Regular players cannot see them. This makes the channel perfect for bot coordination, logging, and state synchronization without spamming users.
The sender does not see their own message. This is intentional. You
do not need to check if (botId === myBotId) return because you will
never receive your own messages.
Tags are your filtering system. Without tags, every bot would have to parse every message to decide if it cares. Tags make this efficient. Choose consistent tag names across all your bots.
Messages can be anything. Plain text, JSON, or any string format your bots agree on. The SDK does not enforce any structure.
There is no built-in reply system. Unlike DMs where you have a conversation ID to reply to, channel messages are broadcast to all bots. If you need a specific bot to respond, include a target bot ID in your message payload.
AcknowledgmentResponse
{
ok: boolean; // true if the message was sent
error: string | null; // what went wrong if it failed
hasError(): boolean; // returns true if error exists
}
Summary
bot.channel.send(message, tags)sends a hidden message to all other bots in the room- Tags are used for filtering and categorizing messages
- Listen with
bot.on('Channel', (botId, message, tags) => {}) - The sender never receives their own messages
- The message is a raw string, not a
Messageobject - Perfect for bot-to-bot coordination, logging, and state sync
- Use JSON for structured data when needed
bot.room
The bot.room API handles everything related to the room your bot is
currently in. Users, voice chat, privileges, and moderation all live
here. This is the overview page. Each sub-api has its own dedicated page
with full details.
What lives on bot.room
bot.room.users // fetch users, find by name or ID, check positions
bot.room.voice // check voice status, invite speakers, remove speakers
bot.room.privilege // check moderator/designer status of any user
bot.room.moderator // grant or remove moderator privileges
bot.room.designer // grant or remove designer privileges
When to use each
| API | Use when you need to… |
|---|---|
bot.room.users | Know who is in the room, find a user’s ID, check someone’s position |
bot.room.voice | Check who is speaking, invite someone to voice, remove a speaker |
bot.room.privilege | Check if a user is a moderator or designer |
bot.room.moderator | Promote or demote moderators |
bot.room.designer | Promote or demote designers |
Quick examples
Get all users in the room
const room = await bot.room.users.get();
console.log(`There are ${room.count()} users in the room`);
Check if someone is a moderator
const isMod = await bot.room.privilege.isModerator(userId);
if (isMod.value) {
console.log('This user is a moderator');
}
Promote someone to moderator
const result = await bot.room.moderator.add(userId);
if (result.ok) {
await bot.message.send('User is now a moderator');
}
Invite someone to voice chat
const result = await bot.room.voice.invite(userId);
if (result.ok) {
await bot.whisper.send(userId, 'You can now speak in voice chat');
}
Important things to know
All data is current as of the moment you call the method. Room state
changes constantly as users join, leave, and move. Each call to
bot.room.users.get() fetches fresh data.
Shorthand methods exist for common lookups. Instead of calling
.get() and then a method on the result, you can call the method
directly:
// these do the same thing
const room = await bot.room.users.get();
const count = room.count();
// shorthand
const count = await bot.room.users.count();
Use shorthand when you only need one piece of information. Use .get()
when you need multiple pieces so you are not making multiple network
requests.
Moderator and designer changes require proper permissions. The bot
must be made by the room owner to have permission. If the bot lacks permission, the response
will have ok: false with an error message.
bot.room.users
The bot.room.users API handles everything related to users currently
in the room. Fetch the full user list, find specific users, check
positions, and get user counts.
Methods
get()
Fetches all users currently in the room with their positions.
const room = await bot.room.users.get();
Returns: GetRoomUsersResponse
The response contains the user array and helper methods for working with the data.
count()
Returns the number of users currently in the room.
const total = await bot.room.users.count();
console.log(`${total} users online`);
Returns: Promise<number> — 0 if an error occurs
This is a shorthand. It fetches the room and returns just the count.
has(identifier)
Checks if a user is in the room by their ID or username.
const inRoom = await bot.room.users.has('Unfairly');
if (inRoom) {
await bot.message.send('Unfairly is here!');
}
| Parameter | Type | Description |
|---|---|---|
identifier | string | User ID or username |
Returns: Promise<boolean>
find(identifier)
Finds a user in the room and returns their full entry.
const entry = await bot.room.users.find('Unfairly');
if (entry) {
console.log(entry.user.id);
console.log(entry.user.username);
console.log(entry.position.x, entry.position.y, entry.position.z);
}
| Parameter | Type | Description |
|---|---|---|
identifier | string | User ID or username |
Returns: Promise<UserEntry | null>
username(userId)
Gets a user’s username from their ID.
const name = await bot.room.users.username('user_id_here');
console.log(name); // "Unfairly" or null if not found
| Parameter | Type | Description |
|---|---|---|
userId | string | The user’s ID |
Returns: Promise<string | null>
userId(username)
Gets a user’s ID from their username.
const id = await bot.room.users.userId('Unfairly');
console.log(id); // "user_id_here" or null if not found
| Parameter | Type | Description |
|---|---|---|
username | string | The user’s username |
Returns: Promise<string | null>
position(identifier)
Gets a user’s current position by their ID or username.
const pos = await bot.room.users.position('Unfairly');
if (pos) {
console.log(`${pos.x}, ${pos.y}, ${pos.z} facing ${pos.facing}`);
}
| Parameter | Type | Description |
|---|---|---|
identifier | string | User ID or username |
Returns: Promise<Position | AnchorPosition | null>
If the user is sitting, this returns an AnchorPosition with
entity_id and anchor_ix. If walking or standing, it returns a
Position with x, y, z, and facing.
Complete example
bot.on('Chat', async (user, message) => {
const cmd = message.command();
if (cmd === '!users') {
const total = await bot.room.users.count();
await bot.message.send(`There are ${total} users in the room.`);
return;
}
if (cmd === '!find') {
const target = message.mentions(0) || message.args(0);
if (!target) {
await bot.message.send('Usage: !find @username');
return;
}
const entry = await bot.room.users.find(target);
if (!entry) {
await bot.message.send(`${target} is not in the room.`);
return;
}
if ('entity_id' in pos) {
await bot.message.send(`${entry.user.username} are sitting on ${pos.entity_id}`);
} else {
await bot.message.send(
`${entry.user.username} is at ${entry.position.x}, ${entry.position.z}`
);
}
return;
}
if (cmd === '!pos') {
const pos = await bot.room.users.position(user.id);
if (!pos) {
await bot.message.send('Could not find your position.');
return;
}
if ('entity_id' in pos) {
await bot.message.send(`You are sitting on ${pos.entity_id}`);
} else {
await bot.message.send(`You are at ${pos.x}, ${pos.y}, ${pos.z}`);
}
return;
}
});
GetRoomUsersResponse
Returned by bot.room.users.get():
{
ok: boolean;
error: string | null;
hasError(): boolean;
users: UserEntry[]; // array of users and positions
count(): number; // number of users
has(identifier: string): boolean; // check if user is present
find(identifier: string): UserEntry | null;
position(identifier: string): Position | AnchorPosition | null;
username(userId: string): string | null;
userId(username: string): string | null;
}
Important things to know
All methods accept either a user ID or username. You can pass
whichever you have. The API figures out which one you meant, except username() and userId().
Usernames are case-insensitive in lookups. 'Unfairly' and 'unfairly' are
the same.
Sitting users have AnchorPosition. When a user is sitting, their
position object has entity_id and anchor_ix instead of x, y, z.
Check for the presence of entity_id or x to know which type you have.
Shorthand methods make a network request each time. Calling
bot.room.users.count() three times makes three separate requests to
Highrise. If you need multiple pieces of information, call .get() once
and use the response object.
await bot.room.voice
The await bot.room.voice API manages voice chat in the room. Check who is
currently speaking, invite users to speak, and remove users from the
voice chat.
Methods
check()
Gets the current voice chat status and active speakers.
const voice = await bot.room.voice.check();
if (voice.ok) {
console.log(`Seconds left: ${voice.secondsLeft}`);
console.log(`Speakers: ${voice.speakers.join(', ')}`);
}
Returns: CheckVoiceChatResponse
invite(userId)
Invites a user to speak in the voice chat.
const result = await bot.room.voice.invite(userId);
if (result.ok) {
await bot.whisper.send(userId, 'You can now speak in voice chat');
}
| Parameter | Type | Description |
|---|---|---|
userId | string | The ID of the user to invite |
Returns: AcknowledgmentResponse
remove(userId)
Removes a user from speaking in the voice chat.
const result = await bot.room.voice.remove(userId);
if (result.ok) {
await bot.whisper.send(userId, 'You have been removed from voice chat');
}
| Parameter | Type | Description |
|---|---|---|
userId | string | The ID of the user to remove |
Returns: AcknowledgmentResponse
Complete example
bot.on('Chat', async (user, message) => {
const cmd = message.command();
if (cmd === '!voice') {
const voice = await bot.room.voice.check();
if (!voice.ok) {
await bot.message.send('Could not check voice status.');
return;
}
if (voice.speakers.length === 0) {
await bot.message.send('No one is speaking right now.');
} else {
await bot.message.send(`Speakers: ${voice.speakers.length} users`);
}
await bot.message.send(`Voice time remaining: ${voice.secondsLeft} seconds`);
return;
}
if (cmd === '!speak') {
const result = await bot.room.voice.invite(user.id);
if (!result.ok) {
await bot.whisper.send(user.id, 'Could not invite you to speak.');
return;
}
await bot.whisper.send(user.id, 'You can now speak in voice chat!');
return;
}
});
// Auto-invite moderators to voice
bot.on('UserJoined',async (user) => {
const isMod = await bot.room.privilege.isModerator(user.id);
if (isMod.value) {
await bot.room.voice.invite(user.id);
}
});
CheckVoiceChatResponse
{
ok: boolean;
error: string | null;
hasError(): boolean;
secondsLeft: number; // time remaining in current voice session
speakers: string[]; // array of user IDs currently speaking
}
AcknowledgmentResponse
Returned by invite() and remove():
{
ok: boolean;
error: string | null;
hasError(): boolean;
}
Important things to know
Voice chat requires a live chat enabled Rooms have a limited amount of voice time. When the time runs out, voice chat ends until more extends.
Only moderators and the room owner can invite speakers. Your bot
must have appropriate privileges to use invite() and remove().
secondsLeft is the time until voice chat ends. When this reaches
zero, all speakers are removed and new invitations will fail until more
voice time is added.
speakers contains user IDs, not usernames. If you need usernames,
use bot.room.users.username() to look them up.
bot.room.privilege
The bot.room.privilege API checks the room privileges of any user.
Find out if someone is a moderator or designer without making changes.
Methods
get(userId)
Gets all privileges for a specific user.
const priv = await bot.room.privilege.get(userId);
if (priv.ok) {
console.log(`Moderator: ${priv.moderator}`);
console.log(`Designer: ${priv.designer}`);
}
| Parameter | Type | Description |
|---|---|---|
userId | string | The ID of the user to check |
Returns: GetRoomPrivilegeResponse
isModerator(userId)
Checks if a user is a moderator.
const isMod = await bot.room.privilege.isModerator(userId);
if (isMod.error) {
console.log(`Error: ${isMod.error}`);
} else if (isMod.value) {
console.log('This user is a moderator');
} else {
console.log('This user is not a moderator');
}
| Parameter | Type | Description |
|---|---|---|
userId | string | The ID of the user to check |
Returns: Promise<{ value: boolean, error: string | null }>
isDesigner(userId)
Checks if a user is a designer.
const isDes = await bot.room.privilege.isDesigner(userId);
if (isDes.error) {
console.log(`Error: ${isDes.error}`);
} else if (isDes.value) {
console.log('This user is a designer');
} else {
console.log('This user is not a designer');
}
| Parameter | Type | Description |
|---|---|---|
userId | string | The ID of the user to check |
Returns: Promise<{ value: boolean, error: string | null }>
Complete example
bot.on('Chat', async (user, message) => {
const cmd = message.command();
if (cmd === '!priv') {
const target = message.mentions(0) || message.args(0)
const priv = await bot.room.privilege.get(target);
if (!priv.ok) {
await bot.message.send('Could not check privileges.');
return;
}
const roles = [];
if (priv.moderator) roles.push('Moderator');
if (priv.designer) roles.push('Designer');
const roleText = roles.length ? roles.join(', ') : 'Member';
await bot.message.send(`${target} is a ${roleText}`);
return;
}
if (cmd === '!ismod') {
const target = message.mentions(0)
const isMod = await bot.room.privilege.isModerator(target);
if (isMod.error) {
await bot.message.send('Could not check moderator status.');
return;
}
await bot.message.send(
isMod.value
? `${target} is a moderator.`
: `${target} is not a moderator.`
);
return;
}
});
// Only allow moderators to use certain commands
bot.on('Chat', (user, message) => {
if (message.command() === '!kick') {
const isMod = await bot.room.privilege.isModerator(user.id);
if (!isMod.value) {
await bot.message.send('Only moderators can use this command.');
return;
}
// proceed with kick logic
}
});
GetRoomPrivilegeResponse
{
ok: boolean;
error: string | null;
hasError(): boolean;
moderator: boolean; // true if user is a moderator
designer: boolean; // true if user is a designer
}
Important things to know
Privileges are per-room. A user might be a moderator in one room but not in another. These methods only check privileges in the current room.
Room owners are automatically moderators. The room owner has
special permissions, isModerator() returns true
Use isModerator() and isDesigner() for simple checks. These
shorthand methods are cleaner than calling .get() when you only need
one piece of information.
The return value is an object with value and error. Unlike most
API responses, isModerator() and isDesigner() return an object with
value instead of ok. Check error first, then use value.
bot.room.moderator
The bot.room.moderator API grants and removes moderator privileges
in the current room. Moderators can kick, mute, and ban users. They can
also invite others to voice chat.
Important
The bot must be made my room owner to grant or remove moderator privileges. If the bot lacks permission, requests will fail.
Methods
add(userId)
Grants moderator privileges to a user.
const result = await bot.room.moderator.add(userId);
if (result.ok) {
await bot.message.send('User is now a moderator');
} else {
console.log(`Failed: ${result.error}`);
}
| Parameter | Type | Description |
|---|---|---|
userId | string | The ID of the user to promote |
Returns: AcknowledgmentResponse
remove(userId)
Removes moderator privileges from a user.
const result = await bot.room.moderator.remove(userId);
if (result.ok) {
await bot.message.send('User is no longer a moderator');
}
| Parameter | Type | Description |
|---|---|---|
userId | string | The ID of the user to demote |
Returns: AcknowledgmentResponse
Complete example
bot.on('Chat', async (user, message) => {
const cmd = message.command();
if (cmd === '!promote') {
// only room owner can promote
if (user.id !== bot.metadata?.room.owner_id) {
await bot.message.send('Only the room owner can use this command.');
return;
}
const target = message.mentions(0) || message.args(0);
if (!target) {
await bot.message.send('Usage: !promote @username');
return;
}
const found = await bot.room.users.find(target);
if (!found) {
await bot.message.send(`${target} is not in the room.`);
return;
}
const result = await bot.room.moderator.add(found.user.id);
if (!result.ok) {
await bot.message.send(`Could not promote ${target}: ${result.error}`);
return;
}
await bot.message.send(`${target} is now a moderator.`);
return;
}
if (cmd === '!demote') {
if (user.id !== bot.metadata?.room.owner_id) {
await bot.message.send('Only the room owner can use this command.');
return;
}
const target = message.mentions(0) || message.args(0);
if (!target) {
await bot.message.send('Usage: !demote @username');
return;
}
const found = await bot.room.users.find(target);
if (!found) {
await bot.message.send(`${target} is not in the room.`);
return;
}
const result = await bot.room.moderator.remove(found.user.id);
if (!result.ok) {
await bot.message.send(`Could not demote ${target}: ${result.error}`);
return;
}
await bot.message.send(`${target} is no longer a moderator.`);
return;
}
});
AcknowledgmentResponse
{
ok: boolean; // true if the privilege was changed
error: string | null; // error message if failed
hasError(): boolean;
}
bot.room.designer
The bot.room.designer API grants and removes designer privileges in
the current room. Designers can edit the room’s furniture and layout.
Important
The bot must be made by room owner to grant or remove designer privileges. If the bot lacks permission, requests will fail.
Methods
add(userId)
Grants designer privileges to a user.
const result = await bot.room.designer.add(userId);
if (result.ok) {
await bot.message.send('User is now a designer');
}
| Parameter | Type | Description |
|---|---|---|
userId | string | The ID of the user to promote |
Returns: AcknowledgmentResponse
remove(userId)
Removes designer privileges from a user.
const result = await bot.room.designer.remove(userId);
if (result.ok) {
await bot.message.send('User is no longer a designer');
}
| Parameter | Type | Description |
|---|---|---|
userId | string | The ID of the user to demote |
Returns: AcknowledgmentResponse
Complete example
bot.on('Chat', async (user, message) => {
const cmd = message.command();
if (cmd === '!designer') {
// only room owner can grant designer
if (user.id !== bot.metadata?.room.owner_id) {
await bot.message.send('Only the room owner can use this command.');
return;
}
const target = message.mentions(0) || message.args(0);
if (!target) {
await bot.message.send('Usage: !designer @username');
return;
}
const found = await bot.room.users.find(target);
if (!found) {
await bot.message.send(`${target} is not in the room.`);
return;
}
// check if already a designer
const isDes = await bot.room.privilege.isDesigner(found.user.id);
if (isDes.value) {
const result = await bot.room.designer.remove(found.user.id);
if (!result.ok) {
await bot.message.send(`Could not remove designer: ${result.error}`);
return;
}
await bot.message.send(`${target} is no longer a designer.`);
} else {
const result = await bot.room.designer.add(found.user.id);
if (!result.ok) {
await bot.message.send(`Could not add designer: ${result.error}`);
return;
}
await bot.message.send(`${target} is now a designer.`);
}
return;
}
});
AcknowledgmentResponse
{
ok: boolean; // true if the privilege was changed
error: string | null; // error message if failed
hasError(): boolean;
}
Important things to know
Designer privileges allow furniture editing. Users with designer can move, add, and remove furniture in the room. Be careful who you grant this to.
bot.player
The bot.player API handles everything related to player actions and
interactions. Move the bot, teleport users, send emotes, react to users,
tip gold, and moderate players. This is the overview page. Each sub-api
has its own dedicated page with full details.
What lives on bot.player
bot.player.walk() // move the bot to coordinates
bot.player.sit() // make the bot sit on furniture
bot.player.teleport() // teleport a user to coordinates
bot.player.emote() // send an emote
bot.player.react() // send a reaction to a user
bot.player.tip() // tip a user with gold
bot.player.splitTip() // tip large amounts split into valid denominations
bot.player.transport() // send a user to another room
bot.player.moderation // kick, mute, ban, unmute, unban users
bot.player.outfit // fetch a player's current outfit
When to use each
| API | Use when you need to… |
|---|---|
bot.player.walk() | Move the bot to a specific spot in the room |
bot.player.sit() | Make the bot sit on a chair or couch |
bot.player.teleport() | teleport a user to a specific location |
bot.player.emote() | Make the bot or the user to perform any emote |
bot.player.react() | Send a heart, clap, or other reaction to a user |
bot.player.tip() | Send gold to a user (valid denominations only) |
bot.player.splitTip() | Send any amount of gold (auto-splits into valid amounts) |
bot.player.transport() | Send a user to a different room |
bot.player.moderation | Kick, mute, or ban users from the room |
bot.player.outfit | See what items a user is wearing |
Quick examples
Move the bot
await bot.player.walk(10, 0, 5, 'FrontRight');
Send a reaction
await bot.player.react(userId, 'heart');
Tip a user
const result = await bot.player.tip(userId, 100);
if (result.result === 'success') {
await bot.message.send('Tip sent!');
}
Kick a user
await bot.player.moderation.kick(userId);
Check a user’s outfit
const outfit = await bot.player.outfit.get(userId);
if (outfit.has('shirt-f_marchingband')) {
console.log('They are wearing the marching band shirt');
}
Important things to know
Coordinates are room-specific. The valid coordinate range depends on the room size. Most rooms are 20x20 or larger.
Tipping uses specific denominations. Gold can only be sent in amounts
of 1, 5, 10, 50, 100, 500, 1000, 5000, or 10000. Use splitTip() to
send arbitrary amounts.
Moderation requires permissions. Your bot must be a moderator to kick, mute, or ban users.
All methods return responses with ok and error. Always check
result.ok for important operations.
bot.player (Actions)
Movement, emotes, reactions, and teleportation. Everything the bot or users can physically do in the room.
walk()
Moves the bot to specific coordinates with an optional facing direction.
await bot.player.walk(10, 0, 5, 'FrontLeft');
| Parameter | Type | Description |
|---|---|---|
x | number | X coordinate (must be >= 0) |
y | number | Y coordinate (must be >= 0) |
z | number | Z coordinate (must be >= 0) |
facing | Facing | Direction to face (default: ‘FrontRight’) |
Returns: AcknowledgmentResponse
Facing options: 'FrontRight', 'FrontLeft', 'BackRight', 'BackLeft'
sit()
Makes the bot sit on a furniture anchor.
await bot.player.sit('entity_id_here');
// sit on a specific seat
await bot.player.sit('entity_id_here', 2);
| Parameter | Type | Description |
|---|---|---|
entity_id | string | The furniture entity ID |
anchor_ix | number | Which seat to sit on (default: 0) |
Returns: AcknowledgmentResponse
teleport()
Moves a user to specific coordinates.
await bot.player.teleport(userId, 5, 0, 5);
// with facing direction
await bot.player.teleport(userId, 5, 0, 5, 'BackRight');
| Parameter | Type | Description |
|---|---|---|
userId | string | ID of the user to teleport |
x | number | X coordinate |
y | number | Y coordinate |
z | number | Z coordinate |
facing | Facing | Direction to face (default: ‘FrontRight’) |
Returns: AcknowledgmentResponse
emote()
Sends an emote from the bot or a specific user.
// bot performs the emote
await bot.player.emote('dance-macarena');
// specific user performs the emote
await bot.player.emote('wave', userId);
| Parameter | Type | Description |
|---|---|---|
userId | string | User to perform the emote (default: bot’s own ID) |
emoteId | string | The emote ID to perform |
Returns: AcknowledgmentResponse
react()
Sends a reaction to a user.
await bot.player.react(userId, 'heart');
await bot.player.react(userId, 'clap');
| Parameter | Type | Description |
|---|---|---|
userId | string | ID of the user to react to |
reaction | Reactions | Reaction type (default: ‘heart’) |
Returns: AcknowledgmentResponse
Reaction options: 'clap', 'heart', 'thumbs', 'wave', 'wink'
transport()
move a user to a different room the room owner has.
await bot.player.transport(userId, 'room_id_here');
| Parameter | Type | Description |
|---|---|---|
userId | string | ID of the user to transport |
roomId | string | ID of the destination room |
Returns: AcknowledgmentResponse
Complete example
bot.on('Chat', async (user, message) => {
const cmd = message.command();
if (cmd === '!grab') {
const target = message.mentions(0) || message.args(0);
const targetId = target ? await bot.room.users.userId(target) : null;
if (!targetId) {
return; // silent fail or add an error message
}
const myPos = await bot.room.users.position(user.id);
if (!myPos) return;
await bot.player.teleport(targetId, myPos.x, myPos.y, myPos.z);
}
if (cmd === '!dance') {
await bot.player.emote('dance-macarena');
return;
}
if (cmd === '!wave') {
const target = message.mentions(0) || message.args(0);
if (target) {
const found = await bot.room.users.find(target);
if (found) {
await bot.player.react(found.user.id, 'wave');
return;
}
}
}
if (cmd === '!move') {
const x = Number(message.args(0));
const z = Number(message.args(1));
if (isNaN(x) || isNaN(z)) {
await bot.message.send('Usage: !move <x> <z>');
return;
}
await bot.player.walk(x, 0, z);
await bot.message.send(`Moving to ${x}, ${z}`);
return;
}
});
AcknowledgmentResponse
All action methods return this response:
{
ok: boolean; // true if the action succeeded
error: string | null; // error message if failed
hasError(): boolean;
}
Important things to know
Coordinates must be non-negative. Passing negative values will cause validation errors before the request is sent.
Some emotes are paid The user must have them to be performed on them
bot.player.moderation
The bot.player.moderation API handles all in-room moderation actions.
Kick, mute, ban, unmute, and unban users.
Important
The bot must be a moderator or made by room owner to use these methods. If the bot lacks permission, requests will fail with
ok: false.
Methods
kick(userId)
Removes a user from the room. They can rejoin immediately.
const result = await bot.player.moderation.kick(userId);
if (result.ok) {
await bot.message.send('User has been kicked.');
}
| Parameter | Type | Description |
|---|---|---|
userId | string | ID of the user to kick |
Returns: AcknowledgmentResponse
mute(userId, duration)
Prevents a user from sending chat messages for a duration.
// mute for 60 seconds
await bot.player.moderation.mute(userId, 60);
// mute for 1 hour
await bot.player.moderation.mute(userId, 3600);
| Parameter | Type | Description |
|---|---|---|
userId | string | ID of the user to mute |
duration | number | Duration in seconds (must be positive) |
Returns: AcknowledgmentResponse
unmute(userId)
Removes an active mute from a user.
await bot.player.moderation.unmute(userId);
| Parameter | Type | Description |
|---|---|---|
userId | string | ID of the user to unmute |
Returns: AcknowledgmentResponse
ban(userId, duration)
Bans a user from the room for a duration.
// ban for 1 hour
await bot.player.moderation.ban(userId, 3600);
// ban for 24 hours
await bot.player.moderation.ban(userId, 86400);
| Parameter | Type | Description |
|---|---|---|
userId | string | ID of the user to ban |
duration | number | Duration in seconds (must be positive) |
Returns: AcknowledgmentResponse
unban(userId)
Removes an active ban from a user.
Note
This requires the bot to be created by the room owner.
await bot.player.moderation.unban(userId);
| Parameter | Type | Description |
|---|---|---|
userId | string | ID of the user to unban |
Returns: AcknowledgmentResponse
Complete example
const mutedUsers = new Map();
bot.on('Chat', async (user, message) => {
const cmd = message.command();
if (cmd === '!kick') {
const target = message.mentions(0) || message.args(0);
if (!target) {
await bot.message.send('Usage: !kick @username');
return;
}
const found = await bot.room.users.find(target);
if (!found) {
await bot.message.send(`${target} is not in the room.`);
return;
}
const result = await bot.player.moderation.kick(found.user.id);
if (!result.ok) {
await bot.message.send(`Could not kick ${target}: ${result.error}`);
return;
}
await bot.message.send(`${target} has been kicked.`);
return;
}
if (cmd === '!mute') {
const target = message.mentions(0) || message.args(0);
const duration = Number(message.args(1)) || 60;
if (!target) {
await bot.message.send('Usage: !mute @username [seconds]');
return;
}
const found = await bot.room.users.find(target);
if (!found) {
await bot.message.send(`${target} is not in the room.`);
return;
}
const result = await bot.player.moderation.mute(found.user.id, duration);
if (!result.ok) {
await bot.message.send(`Could not mute ${target}: ${result.error}`);
return;
}
await bot.message.send(`${target} has been muted for ${duration} seconds.`);
return;
}
if (cmd === '!unmute') {
const target = message.mentions(0) || message.args(0);
if (!target) {
await bot.message.send('Usage: !unmute @username');
return;
}
const found = await bot.room.users.find(target);
if (!found) {
await bot.message.send(`${target} is not in the room.`);
return;
}
const result = await bot.player.moderation.unmute(found.user.id);
if (!result.ok) {
await bot.message.send(`Could not unmute ${target}: ${result.error}`);
return;
}
await bot.message.send(`${target} has been unmuted.`);
return;
}
});
// Log all moderation actions
bot.on('Moderation', (moderator, target, action) => {
console.log(`${moderator.id} performed ${action.type} on ${target.id}`);
});
AcknowledgmentResponse
{
ok: boolean; // true if the action succeeded
error: string | null; // error message if failed
hasError(): boolean;
}
Important things to know
Your bot must be a moderator or made by room owner. If the bot is not a moderator, all
methods will return ok: false with an error explaining the lack of
permission.
Durations are in seconds. Pass 60 for one minute, 3600 for one
hour, 86400 for one day.
Unban requires the bot to be created by the room owner. This is a
Highrise limitation. If your bot was not created by the room owner,
unban() will fail.
The Moderation event fires for all these actions. You can listen to
bot.on('Moderation') to log or respond to moderation events from any
source, including other moderators.
bot.player.outfit
The bot.player.outfit API fetches the current outfit of any user in
the room. See what items they are wearing.
Methods
get(userId)
Gets the full outfit of a specific user.
const result = await bot.player.outfit.get(userId);
if (result.ok) {
console.log(`User is wearing ${result.count} items`);
result.outfit.forEach(item => {
console.log(`${item.type}: ${item.id}`);
});
}
| Parameter | Type | Description |
|---|---|---|
userId | string | ID of the user whose outfit to fetch |
Returns: GetUserOutfitResponse
Complete example
bot.on('Chat', async (user, message) => {
const cmd = message.command();
if (cmd === '!outfit') {
const target = message.mentions(0) || message.args(0);
const found = await bot.room.users.find(target);
if (!found) {
bot.message.send(`${target} is not in the room.`);
return;
}
const result = await bot.player.outfit.get(found.user.id);
if (!result.ok) {
await bot.message.send('Could not fetch outfit.');
return;
}
await bot.message.send(`${target} is wearing ${result.count} items.`);
return;
}
if (cmd === '!hasshirt') {
const target = message.mentions(0)
if (!target) {
}
const found = await bot.room.users.find(target);
if (!found) {
await bot.message.send(`${target} is not in the room.`);
return;
}
const result = await bot.player.outfit.get(found.user.id);
if (!result.ok) {
await bot.message.send('Could not check outfit.');
return;
}
if (result.has('shirt-f_marchingband')) {
await bot.message.send(`${target} is wearing the marching band shirt!`);
} else {
await bot.message.send(`${target} is not wearing that shirt.`);
}
return;
}
});
bot.on('UserJoined', (user) => {
const result = await bot.player.outfit.get(user.id);
if (result.ok && result.has('hat-crown')) {
await bot.message.send(`All hail ${user.username}, wearer of the crown!`);
}
});
GetUserOutfitResponse
{
ok: boolean;
error: string | null;
hasError(): boolean;
outfit: OutfitItem[]; // array of items the user is wearing
count: number; // number of items
has(itemId: string): boolean; // check if wearing specific item
find(itemId: string): OutfitItem; // get specific item by ID
}
OutfitItem
{
type: string; // item type
amount: number; // quantity owned
id: string; // unique item identifier
account_bound: boolean; // whether item is bound to account
active_palette: number; // color palette index
}
Important things to know
Use has() for quick checks. Instead of looping through the outfit
array, use outfit.has('item-id') to check for a specific item.
Use find() to get item details. If you need the full item object
including active_palette or account_bound, use outfit.find('item-id').
Item IDs are unique strings. They follow patterns like
'shirt-f_marchingband'. You can find item IDs in the
Highrise shop or by inspecting a user’s outfit.
bot.inventory
The bot.inventory API handles everything related to the bot’s owned
items, wallet balance, and purchases. Check gold, manage outfits, buy
items from the shop, and purchase room boosts.
What lives on bot.inventory
bot.inventory.wallet // check gold, boost tokens, voice tokens
bot.inventory.outfit // get and set the bot's current outfit
bot.inventory.item // buy items from the Highrise shop
bot.inventory.boost // buy room boosts
bot.inventory.get() // get the bot's full clothing inventory
When to use each
| API | Use when you need to… |
|---|---|
bot.inventory.wallet.get() | Check the bot’s gold or token balance |
bot.inventory.outfit.get() | See what the bot is currently wearing |
bot.inventory.outfit.set() | Change the bot’s outfit |
bot.inventory.item.buy() | Purchase an item from the shop |
bot.inventory.boost.buy() | Purchase room boosts |
bot.inventory.get() | Get a list of all clothing the bot owns |
Quick examples
Check gold balance
const wallet = await bot.inventory.wallet.get();
console.log(`Gold: ${wallet.gold}`);
Change the bot’s outfit
await bot.inventory.outfit.set(); // if no outfit passed, set back to default
Buy an item
const result = await bot.inventory.item.buy('shirt-f_marchingband');
if (result.success) {
console.log('Item purchased!');
}
Get full inventory
const inventory = await bot.inventory.get();
console.log(`Bot owns ${inventory.count} items`);
Important things to know
Purchases use the bot’s gold. Make sure the bot has enough gold before attempting to buy items or boosts.
Outfit changes are visible to everyone. When you change the bot’s outfit, all users in the room will see the change immediately.
Inventory is all owned clothing. bot.inventory.get() returns every
clothing item the bot owns, not just what it is currently wearing.
bot.inventory.wallet
The bot.inventory.wallet API checks the bot’s currency balances.
Gold, boost tokens, and voice tokens.
Methods
get()
Gets the bot’s current wallet balance.
const wallet = await bot.inventory.wallet.get();
if (wallet.ok) {
console.log(`Gold: ${wallet.gold}`);
console.log(`Boost tokens: ${wallet.boostToken}`);
console.log(`Voice tokens: ${wallet.voiceToken}`);
}
Returns: GetWalletResponse
Complete example
bot.on('Chat', async (user, message) => {
const cmd = message.command();
if (cmd === '!gold') {
const wallet = await bot.inventory.wallet.get();
if (!wallet.ok) {
await bot.message.send('Could not fetch wallet.');
return;
}
await bot.message.send(`I have ${wallet.gold} gold.`);
return;
}
if (cmd === '!tokens') {
const wallet = await bot.inventory.wallet.get();
if (!wallet.ok) {
await bot.message.send('Could not fetch tokens.');
return;
}
await bot.message.send(
`Boost tokens: ${wallet.boostToken}\nVoice tokens: ${wallet.voiceToken}`
);
return;
}
if (cmd === '!balance') {
const wallet = await bot.inventory.wallet.get();
if (!wallet.ok) {
await bot.message.send('Could not fetch balance.');
return;
}
await bot.message.send(
`Gold: ${wallet.gold}\n` +
`Boosts: ${wallet.boostToken}\n` +
`Voice: ${wallet.voiceToken}`
);
return;
}
});
GetWalletResponse
{
ok: boolean;
error: string | null;
hasError(): boolean;
gold: number; // current gold balance
boostToken: number; // available room boosts
voiceToken: number; // available voice time tokens
}
bot.inventory.outfit
The bot.inventory.outfit API manages the bot’s current appearance.
Get what the bot is wearing, change outfits, add or remove individual
items, and change item colors.
Methods
get()
Gets the bot’s current outfit. The result is cached after the first call.
const result = await bot.inventory.outfit.get();
if (result.ok) {
console.log(`Bot is wearing ${result.count} items`);
result.outfit.forEach(item => console.log(item.id));
}
Returns: GetBotOutfitResponse
Note: This method requires no parameters. It automatically uses the bot’s own ID from the connection metadata.
set(outfit)
Changes the bot’s entire outfit at once. Pass an array of OutfitItem
objects or plain objects matching the structure.
const { OutfitItem } = require('highrise.bot');
// Using OutfitItem helper class
const newOutfit = [
new OutfitItem('shirt-f_marchingband', 2),
new OutfitItem('hat-beanie'),
new OutfitItem('pants-jeans')
];
await bot.inventory.outfit.set(newOutfit);
// Reset to default appearance
await bot.inventory.outfit.set();
| Parameter | Type | Description |
|---|---|---|
outfit | OutfitItem[] | Array of items to equip (optional, resets if omitted) |
Returns: AcknowledgmentResponse
add(outfitItem)
Adds a single item to the bot’s current outfit without affecting other items.
const { OutfitItem } = require('highrise.bot');
const newHat = new OutfitItem('hat-beanie', 1);
const result = await bot.inventory.outfit.add(newHat);
if (!result.ok) {
console.log(`Failed to add item: ${result.error}`);
}
| Parameter | Type | Description |
|---|---|---|
outfitItem | OutfitItem | The item to add (must be an OutfitItem instance) |
Returns: AcknowledgmentResponse
remove(itemId)
Removes a specific item from the bot’s current outfit.
const result = await bot.inventory.outfit.remove('hat-beanie');
if (result.ok) {
console.log('Hat removed from outfit');
}
| Parameter | Type | Description |
|---|---|---|
itemId | string | The ID of the item to remove |
Returns: AcknowledgmentResponse
color(itemId, colorIndex)
Changes the color palette of a specific item the bot is currently wearing.
// Change shirt to palette index 3
const result = await bot.inventory.outfit.color('shirt-f_marchingband', 3);
if (result.ok) {
console.log('Color changed');
}
| Parameter | Type | Description |
|---|---|---|
itemId | string | The ID of the item to recolor |
colorIndex | number | The new palette index (default: 0) |
Returns: AcknowledgmentResponse
Complete example
const { Highrise, OutfitItem, Logger } = require('highrise.bot');
const log = new Logger("FashionBot");
const bot = new Highrise();
bot.once('Ready', async () => {
const result = await bot.inventory.outfit.get();
if (result.ok) {
log.info('Bot', `Wearing ${result.count} items`);
}
});
bot.on('Chat', async (user, message) => {
const cmd = message.command();
if (cmd === '!outfit') {
const result = await bot.inventory.outfit.get();
if (!result.ok) {
await bot.message.send('Could not fetch outfit.');
return;
}
const items = result.outfit
await bot.message.send(`I am wearing: ${items.join(', ')}`);
return;
}
if (cmd === '!addhat') {
const hatId = message.args(0) || 'hat-beanie';
const color = Number(message.args(1)) || 0;
const newHat = new OutfitItem(hatId, color);
const result = await bot.inventory.outfit.add(newHat);
if (!result.ok) {
await bot.message.send(`Could not add hat: ${result.error}`);
return;
}
await bot.message.send(`Added ${hatId} to my outfit!`);
return;
}
if (cmd === '!remove') {
const itemId = message.args(0);
if (!itemId) {
await bot.message.send('Usage: !remove <itemId>');
return;
}
const result = await bot.inventory.outfit.remove(itemId);
if (!result.ok) {
await bot.message.send(`Could not remove item: ${result.error}`);
return;
}
await bot.message.send(`Removed ${itemId} from my outfit.`);
return;
}
if (cmd === '!recolor') {
const itemId = message.args(0);
const color = Number(message.args(1));
if (!itemId || isNaN(color)) {
await bot.message.send('Usage: !recolor <itemId> <colorIndex>');
return;
}
const result = await bot.inventory.outfit.color(itemId, color);
if (!result.ok) {
await bot.message.send(`Could not recolor: ${result.error}`);
return;
}
await bot.message.send(`Changed ${itemId} to palette ${color}`);
return;
}
if (cmd === '!royal') {
const royalOutfit = [
new OutfitItem('hat-crown', 2),
new OutfitItem('shirt-royal', 1),
new OutfitItem('pants-royal', 0)
];
await bot.inventory.outfit.set(royalOutfit);
await bot.message.send('Dressed as royalty!');
return;
}
if (cmd === '!reset') {
await bot.inventory.outfit.set();
await bot.message.send('Back to default outfit.');
return;
}
});
bot.login(process.env.BOT_TOKEN, process.env.ROOM_ID);
OutfitItem
The OutfitItem class is available as a named export from highrise.bot.
It provides a clean way to create outfit items without writing raw objects.
const { OutfitItem } = require('highrise.bot');
Constructor
new OutfitItem(id, palette?, amount?, isBound?)
| Parameter | Type | Default | Description |
|---|---|---|---|
id | string | required | The item identifier (e.g., "shirt-f_marchingband") |
palette | number | 0 | The color palette index |
amount | number | 1 | Quantity of the item |
isBound | boolean | false | Whether the item is account bound |
Properties
| Property | Type | Description |
|---|---|---|
id | string | The unique item identifier |
type | string | Extracted from the ID (e.g., "shirt", "hat") |
amount | number | Quantity of the item |
account_bound | boolean | Whether bound to the account |
active_palette | number | The color palette index |
Examples
// Basic usage
const hat = new OutfitItem('hat-beanie');
// With custom color palette
const shirt = new OutfitItem('shirt-f_marchingband', 2);
// Full options
const special = new OutfitItem('shoes-rare', 1, 1, true);
// Use in outfit changes
await bot.inventory.outfit.set([hat, shirt, special]);
await bot.inventory.outfit.add(new OutfitItem('pants-jeans'));
Response Types
GetBotOutfitResponse
{
ok: boolean;
error: string | null;
hasError(): boolean;
outfit: OutfitItem[]; // items currently equipped
count: number; // number of items
has(itemId: string): boolean;
find(itemId: string): OutfitItem;
}
AcknowledgmentResponse
{
ok: boolean;
error: string | null;
hasError(): boolean;
}
Important things to know
The outfit is cached. The first call to get() fetches from the API.
Subsequent calls return the cached outfit until set(), add(),
remove(), or color() is called.
Gold paid Items must be owned. You can only equip items the bot has in its inventory. Attempting to equip unowned items will fail.
Reset with no arguments. Calling set() with no arguments resets
the bot to its default appearance.
Changes are immediate. Everyone in the room sees outfit changes as soon as the request completes.
color() only works on currently worn items. The item must already
be part of the bot’s current outfit.
bot.inventory.item
The bot.inventory.item API handles purchasing items directly from the
Highrise shop using the bot’s gold.
Methods
buy(itemId)
Purchases a specific shop item using the bot’s gold.
const result = await bot.inventory.item.buy('shirt-f_marchingband');
if (result.insufficientFunds) {
console.log('Not enough gold');
} else if (result.success) {
console.log('Item purchased!');
}
| Parameter | Type | Description |
|---|---|---|
itemId | string | The unique shop item ID to purchase |
Returns: BuyItemResponse
Example
bot.on('Chat', async (user, message) => {
const cmd = message.command()
if (cmd === '!buy') {
const itemId = message.args(0);
if (!itemId) {
await bot.message.send('Usage: !buy <itemId>');
return;
}
const result = await bot.inventory.item.buy(itemId);
if (result.insufficientFunds) {
await bot.message.send('Not enough gold!');
return;
}
if (!result.ok) {
await bot.message.send('Purchase failed.');
return;
}
await bot.message.send(`Purchased ${itemId}!`);
return;
}
});
BuyItemResponse
{
ok: boolean;
error: string | null;
hasError(): boolean;
result: "success" | "insufficient_funds" | "only_token_bought";
success: boolean; // true if purchase succeeded
insufficientFunds: boolean; // true if failed due to low gold
}
Important things to know
Item IDs are unique strings. Find them in the Highrise shop. Common
format: 'type-name' like 'shirt-f_marchingband'.
Purchased items go to inventory. After a successful purchase, the item is added to the bot’s inventory and can be equipped.
bot.inventory.boost
The bot.inventory.boost API handles purchasing room boosts. Boosts
increase your room’s visibility in the Highrise room browser.
Caution
Might not work in future updates, will be tested after each update.
Methods
buy(amount)
Purchases room boosts using the bot’s gold.
// Buy 1 boost (default)
const result = await bot.inventory.boost.buy();
// Buy multiple boosts
const result = await bot.inventory.boost.buy(5);
| Parameter | Type | Description |
|---|---|---|
amount | number | Number of boosts to purchase (default: 1) |
Returns: BuyRoomBoostResponse
Example
bot.on('Chat', async (user, message) => {
if (message.command() === '!boost') {
const amount = Number(message.args(0)) || 1;
const result = await bot.inventory.boost.buy(amount);
if (result.insufficientFunds) {
await bot.message.send('Not enough gold!');
return;
}
if (!result.ok) {
await bot.message.send('Boost purchase failed.');
return;
}
await bot.message.send(`Purchased ${amount} boost(s)!`);
return;
}
});
BuyRoomBoostResponse
{
ok: boolean;
error: string | null;
hasError(): boolean;
result: "success" | "insufficient_funds";
success: boolean; // true if purchase succeeded
insufficientFunds: boolean; // true if failed due to low gold
}
Important things to know
Boosts cost 100 gold. Make sure the bot has enought gold
Boosts increase room visibility. Rooms with active boosts appear higher in the room browser, attracting more users.
Default amount is 1. Calling buy() with no arguments purchases
a single boost.
bot.utils
The bot.utils API provides helper methods for common tasks like delays,
time formatting, message splitting, and gold calculations. It also
exposes the built-in validator.
What lives on bot.utils
bot.utils.sleep() // pause execution for a duration
bot.utils.uptime() // get bot uptime as formatted string
bot.utils.formatTime() // format milliseconds to readable string
bot.utils.splitMessages() // split long text into chunks
bot.utils.sequencingGoldBars() // break down gold into valid denominations
bot.utils.validator // built-in validator instance
Methods
sleep(ms)
Pauses execution for the specified duration. Useful for rate limiting or creating artificial delays between actions.
// Basic delay
await bot.utils.sleep(1000); // Wait 1 second
// Between messages
await bot.message.send("First message");
await bot.utils.sleep(500);
await bot.message.send("Second message");
// In loops
for (let i = 0; i < 3; i++) {
await bot.message.send(`Message ${i + 1}`);
await bot.utils.sleep(1000);
}
| Parameter | Type | Description |
|---|---|---|
ms | number | Delay duration in milliseconds |
Returns: Promise<boolean>
uptime()
Returns the bot’s uptime as a formatted string. The timer starts when the WebSocket connection is established and resets upon reconnection.
const uptime = bot.utils.uptime();
console.log(uptime); // "2h 30m 15s"
Returns: string — Formatted uptime or "Offline" if not connected
formatTime(ms)
Converts milliseconds into a human-readable duration string.
bot.utils.formatTime(3665000); // "1h 1m 5s"
bot.utils.formatTime(65000); // "1m 5s"
bot.utils.formatTime(5000); // "5s"
| Parameter | Type | Description |
|---|---|---|
ms | number | Milliseconds to format |
Returns: string — Formatted like "2d 5h 13m 42s"
splitMessages(text, limit)
Splits long text into chunks of specified length, respecting word boundaries. Useful when you need to send messages longer than the 256 character limit but want to split them manually.
const longText = "This is a very long message...";
const chunks = bot.utils.splitMessages(longText, 200);
for (const chunk of chunks) {
await bot.message.send(chunk);
}
| Parameter | Type | Description |
|---|---|---|
text | string | The text to split |
limit | number | Maximum characters per chunk |
Returns: string[] — Array of text chunks
Note:
bot.message.send()andbot.whisper.send()already split long messages automatically. Use this method only when you need custom splitting logic.
sequencingGoldBars(amount)
Breaks down a gold amount into valid Highrise gold bar denominations. The input is rounded down to the nearest whole number.
bot.utils.sequencingGoldBars(454); // [100, 100, 100, 100, 50]
bot.utils.sequencingGoldBars(7500); // [5000, 1000, 1000, 500]
bot.utils.sequencingGoldBars(1234); // [1000, 100, 100, 10, 10, 10, 1, 1, 1, 1]
| Parameter | Type | Description |
|---|---|---|
amount | number | Total gold amount (rounded down) |
Returns: number[] — Valid gold bar denominations in descending order
Valid denominations: 10000, 5000, 1000, 500, 100, 50, 10, 5, 1
Note:
bot.player.splitTip()uses this internally. Use this method when you need the breakdown without actually sending tips.
validator
The built-in validator instance for validating user input. This is the same validator used internally by all SDK methods.
const { Validator } = require('highrise.bot');
// Using the bot's built-in validator
bot.utils.validator
.required(userId, 'userId')
.string(userId, 'userId');
// Or create your own instance
const validate = new Validator();
validate.positive(amount, 'amount');
For full validator documentation, see The Validator.
Complete example
bot.on('Chat', async (user, message) => {
const cmd = message.command();
if (cmd === '!uptime') {
const uptime = bot.utils.uptime();
await bot.message.send(`I've been online for ${uptime}`);
return;
}
if (cmd === '!countdown') {
await bot.message.send('Starting in 3...');
await bot.utils.sleep(1000);
await bot.message.send('2...');
await bot.utils.sleep(1000);
await bot.message.send('1...');
await bot.utils.sleep(1000);
await bot.message.send('GO!');
return;
}
if (cmd === '!breakdown') {
const amount = Number(message.args(0));
if (isNaN(amount) || amount <= 0) {
await bot.message.send('Usage: !breakdown <amount>');
return;
}
const bars = bot.utils.sequencingGoldBars(amount);
await bot.message.send(`${amount} gold breaks down to: ${bars.join(' + ')}`);
return;
}
if (cmd === '!format') {
const ms = Number(message.args(0));
if (isNaN(ms)) {
await bot.message.send('Usage: !format <milliseconds>');
return;
}
const formatted = bot.utils.formatTime(ms);
await bot.message.send(`${ms}ms = ${formatted}`);
return;
}
});
// Rate-limited announcements
async function announceWinners(winners) {
for (const winner of winners) {
await bot.message.send(`Congratulations ${winner}!`);
await bot.utils.sleep(2000); // Wait 2 seconds between announcements
}
}
Important things to know
sleep() is non-blocking. It only pauses the current async function.
Other events continue to process normally.
uptime() resets on reconnect. If the bot disconnects and reconnects,
the uptime counter starts over.
sequencingGoldBars() rounds down. Passing 454.9 returns the same
as 454.
Messages auto-split. You rarely need splitMessages() since
bot.message.send() handles long messages automatically.
Great ending
If you’ve read through all of these pages, we’re done here. You now have everything you need to build bots that are solid, clean, and well-structured. You’ve learned how to handle events properly, validate input before acting, check responses for errors, and use every part of the API the way it was designed to be used.
You’re at a great point. The foundation is there. What you build from here is up to you.
You can always come back to these pages anytime to check something. And if you get stuck or want feedback, the community is always there to help in discord.gg/highrise in the bot API section.
Good luck with your bot.