npm SDK Docs

Developer docs

npm SDK

Full control from your Node.js server. Add "Log in with Vortex" 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)
  • 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)

OptionDefaultDescription
clientIdRequiredYour client ID.
clientSecretRequiredYour client secret.
baseUrlhttps://vtx.chatThe Vortex address.
fetchglobalThis.fetchA custom fetch implementation.

Methods

MethodReturnsDescription
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 })UserSessionAct as a person. tokens can also be a bare access token string.
app({ scopes })AppSessionAct as your app. scopes defaults to guild.read and guild.announce.
asset(path)stringTurn 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

MethodReturnsDescription
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 }.
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 })PromiseBan someone from your guild. Leave out minutes for a permanent ban.
unban(userId)PromiseLift a ban.
timeout(userId, minutes, reason)PromiseStop someone sending messages for up to 28 days.
removeTimeout(userId)PromiseEnd a timeout early.
addRole(userId, roleId) / removeRole(userId, roleId)PromiseGive 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)MessageTurn a raw server message into a Message.

UserSession

MethodReturnsDescription
me()Promise<User>The person's profile.
send(channelId, content, { replyTo })Promise<Message>Send a message, up to 2000 characters.
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.
tokensTokensThe 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.

AppSession

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

Realtime

MemberDescription
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.
socketThe 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
  }
}
StatusUsually means
400Bad input, an expired code or a reused refresh token.
401Wrong client secret, a disallowed site, or the session ended. Ask the person to sign in again.
403Missing scope, no permission in that channel, or banned from your guild.
404The channel or message doesn't exist or isn't in your guild.
429Slowmode 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.