Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

highrise.bot logo
NPM Downloads NPM Type Definitions NPM License

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.

  1. Go to the Highrise developer portal
  2. Sign in with your Highrise account
  3. Click Create API Key
  4. Give it a name so you can recognize it later
  5. 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.

  1. Go to the Rooms tab in the developer portal
  2. 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:

EventWhen it fires
ReadyBot connects to the room
ChatSomeone sends a room message
WhisperSomeone sends a whisper to the bot
UserJoinedSomeone enters the room
UserLeftSomeone leaves the room
MovementSomeone moves or sits
TipSomeone tips in the room
ModerationA moderation action happens
VoiceVoice chat status changes
DirectA direct message is received
ChannelA 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 deleted
  • Room not found — the room ID is incorrect or the room was deleted
  • Invalid 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:

MethodWhat 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

MethodWhat 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

MethodWhat 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

MethodWhat 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

MethodWhat 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 Logger we 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 setInterval or setTimeout (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.once for setup code that should only execute during the bot’s initial connection
  • Use bot.on only for tasks that must repeat every time the bot reconnects
  • The metadata object 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 user object (id, username) and a message object (content, command(), args(), mentions())
  • Store message.command() in a variable at the top of your handler to avoid multiple calls
  • Always use return after 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.ok before 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.metadata instead 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 user and message objects 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 user who joined and their starting position
  • position contains x, y, z, and facing coordinates
  • This event triggers for every entry, including users returning after leaving
  • Use a Set to 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 user object containing their id and username
  • 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, and anchor objects, but only one of the latter two will have a value
  • position includes x, y, z, and facing for walking users
  • anchor includes entity_id and anchor_ix for 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 UserLeft event 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:

AmountName
11 gold bar
55 gold bars
1010 gold bars
5050 gold bars
100100 gold bars
500500 gold bars
10001k gold bars
50005k gold bars
1000010k 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, and currency objects
  • currency.amount is always one of the valid gold bar denominations
  • Check receiver.id === bot.metadata.bot_id if you only want to react to tips sent to the bot
  • Common uses are thank you messages, leaderboards, tip rewards, and voting systems
  • Use rewardedUsers sets 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 Moderator and Target classes inherit from the User class, the Highrise WebSocket server emits moderation events containing only the user ID. as a result, the username field will always be null, since you already know User class 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 TypeHas Duration?
kickfalse
mutetrue
bantrue
unmutefalse
unbanfalse

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 moderator who acted, the target who was moderated, and the action details
  • action.type is always one of "kick", "ban", "mute", "unmute", or "unban"
  • action.duration is set for mutes and bans, null for 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 Direct event fires on any private message sent to the bot.
  • Use bot.direct.send(conversation.id, text) to reply.
  • message.command() and message.args() work exactly like they do in room chat.
  • user.username is always null; use the user.id to identify the sender.

Voice Event

Warning

Unfortunately Voice event is no longer supported for unknown reason after Highrise 4.25.3 Free 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 message here is not an instance of the Message class. 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 using JSON.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 Message object.
  • Use tags to 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

ParameterTypeDescription
messagestringThe 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 AcknowledgmentResponse with ok and error properties
  • Messages over 256 characters are automatically split and sent in order
  • Always use await when calling bot.message.send()
  • The bot’s own messages are filtered out of the Chat event automatically
  • Check result.ok for 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

ParameterTypeDescription
userIdstringThe ID of the user to whisper
messagestringThe 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 itOnly one person needs to see it
Announcing somethingGiving private instructions
Responding to public commandsResponding to whisper commands
Welcoming someone publiclySharing 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 id from the event, not their username
  • 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.');
ParameterTypeDescription
convIdstringThe conversation ID from the Direct event
messagestringThe 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!'
);
ParameterTypeDescription
userIdsstring[]Array of user IDs (1-100 users)
messagestringThe 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');
ParameterTypeDescription
convIdstringThe conversation ID
roomIdstringThe 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');
ParameterTypeDescription
convIdstringThe conversation ID
worldIdstringThe 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' }
);
ParameterTypeDescription
userIdsstring[]Array of user IDs (1-100 users)
inviteDetailsobjectEither { 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);
ParameterTypeDescription
lastIdstring | nullCursor for pagination, fetches next 20 conversations
notJoinedbooleanInclude 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);
ParameterTypeDescription
convIdstringThe 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);
ParameterTypeDescription
convIdstringThe conversation ID
lastMessageIdstringOptional 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 conversation
  • bot.direct.broadcast(userIds, message) messages up to 100 users at once
  • bot.direct.conversations.list() fetches the bot’s inbox with pagination
  • bot.direct.messages.list(convId) fetches message history from a conversation
  • bot.direct.inviteRoom() and bot.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']);
ParameterTypeDescription
messagestringThe content to send (can be plain text or stringified JSON)
tagsstring[]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(', ')}`);
});
ParameterTypeDescription
botIdstringThe ID of the bot that sent the message
messagestringThe raw message content
tagsstring[]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 message here is a raw string, not a Message object. Methods like .command(), .args(), and .mentions() do not exist. If you send structured data, parse it manually with JSON.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 Message object
  • 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

APIUse when you need to…
bot.room.usersKnow who is in the room, find a user’s ID, check someone’s position
bot.room.voiceCheck who is speaking, invite someone to voice, remove a speaker
bot.room.privilegeCheck if a user is a moderator or designer
bot.room.moderatorPromote or demote moderators
bot.room.designerPromote 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!');
}
ParameterTypeDescription
identifierstringUser 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);
}
ParameterTypeDescription
identifierstringUser 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
ParameterTypeDescription
userIdstringThe 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
ParameterTypeDescription
usernamestringThe 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}`);
}
ParameterTypeDescription
identifierstringUser 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');
}
ParameterTypeDescription
userIdstringThe 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');
}
ParameterTypeDescription
userIdstringThe 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}`);
}
ParameterTypeDescription
userIdstringThe 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');
}
ParameterTypeDescription
userIdstringThe 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');
}
ParameterTypeDescription
userIdstringThe 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}`);
}
ParameterTypeDescription
userIdstringThe 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');
}
ParameterTypeDescription
userIdstringThe 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');
}
ParameterTypeDescription
userIdstringThe 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');
}
ParameterTypeDescription
userIdstringThe 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

APIUse 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.moderationKick, mute, or ban users from the room
bot.player.outfitSee 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');
ParameterTypeDescription
xnumberX coordinate (must be >= 0)
ynumberY coordinate (must be >= 0)
znumberZ coordinate (must be >= 0)
facingFacingDirection 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);
ParameterTypeDescription
entity_idstringThe furniture entity ID
anchor_ixnumberWhich 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');
ParameterTypeDescription
userIdstringID of the user to teleport
xnumberX coordinate
ynumberY coordinate
znumberZ coordinate
facingFacingDirection 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);
ParameterTypeDescription
userIdstringUser to perform the emote (default: bot’s own ID)
emoteIdstringThe 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');
ParameterTypeDescription
userIdstringID of the user to react to
reactionReactionsReaction 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');
ParameterTypeDescription
userIdstringID of the user to transport
roomIdstringID 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.');
}
ParameterTypeDescription
userIdstringID 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);
ParameterTypeDescription
userIdstringID of the user to mute
durationnumberDuration in seconds (must be positive)

Returns: AcknowledgmentResponse

unmute(userId)

Removes an active mute from a user.

await bot.player.moderation.unmute(userId);
ParameterTypeDescription
userIdstringID 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);
ParameterTypeDescription
userIdstringID of the user to ban
durationnumberDuration 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);
ParameterTypeDescription
userIdstringID 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}`);
    });
}
ParameterTypeDescription
userIdstringID 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

APIUse 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();
ParameterTypeDescription
outfitOutfitItem[]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}`);
}
ParameterTypeDescription
outfitItemOutfitItemThe 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');
}
ParameterTypeDescription
itemIdstringThe 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');
}
ParameterTypeDescription
itemIdstringThe ID of the item to recolor
colorIndexnumberThe 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?)
ParameterTypeDefaultDescription
idstringrequiredThe item identifier (e.g., "shirt-f_marchingband")
palettenumber0The color palette index
amountnumber1Quantity of the item
isBoundbooleanfalseWhether the item is account bound

Properties

PropertyTypeDescription
idstringThe unique item identifier
typestringExtracted from the ID (e.g., "shirt", "hat")
amountnumberQuantity of the item
account_boundbooleanWhether bound to the account
active_palettenumberThe 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!');
}
ParameterTypeDescription
itemIdstringThe 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);
ParameterTypeDescription
amountnumberNumber 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);
}
ParameterTypeDescription
msnumberDelay 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"
ParameterTypeDescription
msnumberMilliseconds 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);
}
ParameterTypeDescription
textstringThe text to split
limitnumberMaximum characters per chunk

Returns: string[] — Array of text chunks

Note: bot.message.send() and bot.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]
ParameterTypeDescription
amountnumberTotal 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.