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.
server.jsThe whole app: sign-in, channels, messagespackage.jsonDependencies and the start script.env.exampleCopy to.envand fill inREADME.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
redirectUrimust 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 }. |
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. |
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. |
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.
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.