npm SDK Docs

Developer docs

npm SDK

Full control from your Node.js server. Add the fixed "Log in with Vortex" button with the Vortex icon to your own accounts, read and send messages for your users, and run bots that post announcements as your app.

Node 18+ TypeScript types included Server only

Install

npm install @vortex-chat/sdk socket.io-client

socket.io-client is only needed for live updates. Set your integration type to npm SDK to get a client secret. It's shown once, so store it in an environment variable straight away.

const { Vortex } = require("@vortex-chat/sdk");

const vortex = new Vortex({
  clientId: "vtx_your_client_id",
  clientSecret: process.env.VORTEX_CLIENT_SECRET,
});

Never ship the client secret to a browser or commit it. If it leaks, rotate it from your dashboard. Rotating ends your app's own sessions but doesn't sign your users out.

Demo app

A complete Express app that signs people in with Vortex, shows your guild's channels and recent messages, and lets them send messages. Download it, put your client ID and secret in .env, add http://localhost:3000 to your Domains, then run npm install and npm start.

Download all files (.zip)
  • index.htmlHTML shell and styling served by the Express app
  • server.jsThe whole app: sign-in, channels, messages
  • package.jsonDependencies and the start script
  • .env.exampleCopy to .env and fill in
  • README.mdStep-by-step setup

Log in with Vortex

The standard OAuth flow: send people to Vortex, they approve, and Vortex sends them back to your redirectUri with a code you swap for tokens.

const express = require("express");
const session = require("express-session");
const { Vortex } = require("@vortex-chat/sdk");

const app = express();
app.use(session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false }));

const vortex = new Vortex({
  clientId: "vtx_your_client_id",
  clientSecret: process.env.VORTEX_CLIENT_SECRET,
});
const redirectUri = "https://yoursite.com/auth/vortex/callback";

app.get("/auth/vortex", (req, res) => {
  const { url, state, codeVerifier } = vortex.getAuthorizeUrl({ redirectUri });
  req.session.vortex = { state, codeVerifier };
  res.redirect(url);
});

app.get("/auth/vortex/callback", async (req, res) => {
  const saved = req.session.vortex;
  if (!saved || req.query.state !== saved.state) return res.status(400).send("Invalid state");
  if (req.query.error) return res.redirect("/?login=cancelled");

  const tokens = await vortex.exchangeCode({
    code: req.query.code,
    redirectUri,
    codeVerifier: saved.codeVerifier,
  });
  // tokens.user is the Vortex profile: { id, username, avatar, rank, color, ... }
  await db.users.upsert({ vortexId: tokens.user.id, username: tokens.user.username, tokens });
  req.session.userId = tokens.user.id;
  res.redirect("/");
});
  • The redirectUri must be on an allowed site. For servers, add your domain under Domains or your server's IP under Server IPs. See Site access.
  • Always check state. It stops other sites from finishing a sign-in on your users' behalf.
  • If someone cancels, Vortex returns ?error=access_denied.

Acting as a user

Wrap a person's tokens in a session. It refreshes access tokens automatically and tells you when to save new ones.

const user = vortex.forUser(savedTokens, {
  onRefresh: (tokens) => db.users.update({ vortexId }, { tokens }),
});

const { guild, channels } = await user.guild();
const { messages } = await user.messages(channels[0].id, { limit: 25 });
await user.send(channels[0].id, "Hello from my site!");

Refresh tokens change every time they're used. Save the new tokens in onRefresh every time, or the next refresh will fail and the person will need to sign in again.

Acting as your app

App sessions don't need a person. Use them for bots, syncing and announcements.

const bot = vortex.app();

const { channels } = await bot.guild();
const { messages } = await bot.messages(channels[0].id);
await bot.announce(channels[0].id, "Server restarts in 10 minutes!");

Announcements appear as highlighted system messages with your app's name. They're limited to 10 per minute.

Live updates

Both user and app sessions can connect for live events. By default you join every channel the session can see.

const live = await vortex.app().connect();

live.on("ready", () => console.log("Connected"));
live.on("message", (msg) => {
  console.log(`#${msg.channelId} ${msg.author.username}: ${msg.content}`);
});
live.on("error", (err) => console.error(err.message));

// Later
live.leave(channelId);
live.close();

Connections reconnect and renew their tokens on their own.

Reference: Vortex

new Vortex(options)

Option Default Description
clientId Required Your client ID.
clientSecret Required Your client secret.
baseUrl https://vtx.chat The Vortex address.
fetch globalThis.fetch A custom fetch implementation.

Methods

Method Returns Description
getAuthorizeUrl({ redirectUri, state, scopes, pkce }) { url, state, codeVerifier } Build the sign-in URL. scopes defaults to all user scopes. pkce defaults to true; keep it on.
exchangeCode({ code, redirectUri, codeVerifier }) Promise<Tokens> Swap the callback code for tokens. Includes user when identify was granted.
refresh(refreshToken) Promise<Tokens> Get new tokens. Sessions do this for you.
revoke(token) Promise<true> Sign a session out. Pass the access or refresh token.
getUser(accessToken) Promise<User> Look up who an access token belongs to.
forUser(tokens, { onRefresh }) UserSession Act as a person. tokens can also be a bare access token string.
app({ scopes }) AppSession Act as your app. scopes defaults to guild.read and guild.announce.
asset(path) string Turn a Vortex image path into a full URL.

Tokens

{
  accessToken: "eyJ...",
  refreshToken: "vtxr_...",   // null for app tokens
  expiresAt: 1767225600000,   // ms timestamp
  scopes: ["identify", "chat.read", "chat.write"],
  user: { ... }               // only from exchangeCode
}

Reference: sessions

Both sessions

Method Returns Description
guild() Promise<{ guild, channels, app }> Your guild and the channels this session can see.
channels() Promise<Channel[]> Shortcut for guild().channels.
emotes() Promise<object> Emotes by name: { name, src, pack, inPack: true }. Unauthorized custom emotes are rendered as :unknown-emote:.
members({ q, limit, after }) Promise<{ members, nextCursor }> Search members, 25 at a time, with their roles, timeout and ban status. Pass nextCursor as after for the next page.
bans({ limit, after }) Promise<{ bans, nextCursor, total }> Active bans with reason and expiry, a page at a time.
ban(userId, { reason, minutes }) Promise Ban someone from your guild. Leave out minutes for a permanent ban.
unban(userId) Promise Lift a ban.
timeout(userId, minutes, reason) Promise Stop someone sending messages for up to 28 days.
removeTimeout(userId) Promise End a timeout early.
addRole(userId, roleId) / removeRole(userId, roleId) Promise Give or remove a guild role.
messages(channelId, { before, limit }) Promise<{ messages, hasMore }> Up to 100 messages, oldest first. Page back with before.
connect({ channels }) Promise<Realtime> Open a live connection. Pass channel IDs to limit what you listen to.
request(method, path, body) Promise<any> Call any REST endpoint under /api/partner/v1.
normalize(raw, channelId) Message Turn a raw server message into a Message.

UserSession

Method Returns Description
me() Promise<User> The person's profile, including banner and ownedEmotePacks.
send(channelId, content, { replyTo }) Promise<Message> Send a message, up to 2000 characters. The person must accept the guild rules first.
edit(channelId, messageId, content) Promise<Message> Edit one of their messages.
delete(channelId, messageId) Promise<true> Delete one of their messages.
markRead(channelId) Promise<true> Mark a channel as read.
report(channelId, messageId, reason, note) Promise<true> Report someone else's message. reason: spam, harassment, hate_speech, nsfw, self_harm or other.
tokens Tokens The current tokens.

Moderation methods need guild.moderate on app sessions (included by default), or the right guild role on user sessions. See the moderation API for the rules. Display vortexStaff, timeout status and Vortex mute status when showing profiles.

Partners must show a rules popup with an accept button, keep the rules viewable, report messages through report(), and never send a message as a user who did not write it.

AppSession

Method Returns Description
announce(channelId, content) Promise<{ _id, content, createdAt }> Post an announcement. Needs guild.announce.

Realtime

Member Description
on("ready") Connected and listening.
on("message" | "edit", msg) A new or edited Message.
on("delete", { id, channelId }) A message was deleted.
on("typing", { channelId, userId, username, typing }) Typing started or stopped.
on("join" | "leave", { channelId, user }) Someone joined or left.
on("disconnect", reason) / on("error", err) Connection problems. Reconnects on its own.
join(channelId) / leave(channelId) Start or stop listening to a channel.
close() Disconnect for good.
socket The underlying Socket.IO client.

The Message, Channel, Guild and User shapes are in the API reference. The SDK's Message matches the CDN script's, minus html and mentionsMe.

Errors

Everything throws VortexError, with the HTTP status and response body when there is one.

const { VortexError } = require("@vortex-chat/sdk");

try {
  await user.send(channelId, text);
} catch (err) {
  if (err instanceof VortexError && err.status === 429) {
    // slowmode or rate limit, try again later
  }
}
Status Usually means
400 Bad input, an expired code or a reused refresh token.
401 Wrong client secret, a disallowed site, or the session ended. Ask the person to sign in again.
403 Missing scope, no permission in that channel, or banned from your guild.
404 The channel or message doesn't exist or isn't in your guild.
429 Slowmode or a rate limit.

Troubleshooting

"This site can't use this integration yet" on the Vortex sign-in page

Your redirectUri's domain isn't allowed. Add it under Domains.

"redirect_uri mismatch"

Pass exactly the same redirectUri to exchangeCode as you did to getAuthorizeUrl.

"Invalid or expired refresh token"

An old refresh token was reused. Make sure onRefresh saves new tokens, and that two servers don't refresh the same tokens at once.

"Realtime needs socket.io-client"

Run npm install socket.io-client.