API Reference

Developer docs

API reference

The OAuth endpoints, REST API and realtime events behind every integration type. The CDN script and npm SDK wrap all of this, so you only need it for custom clients or other languages.

Basics

Sign-in page %ORIGIN%/oauth/authorize
OAuth API %ORIGIN%/api/partner/oauth
REST API %ORIGIN%/api/partner/v1
Realtime Socket.IO namespace %ORIGIN%/partner
  • Send JSON with Content-Type: application/json.
  • Authenticate with Authorization: Bearer <access_token>.
  • From a browser, add ?client_id=vtx_your_client_id to every request so Vortex can check that your site is allowed. Cookies are never used.
  • REST errors look like { "message": "..." }. OAuth errors look like { "error": "...", "error_description": "..." }.
  • IDs are 24-character hex strings.

OAuth

Sign-in page

GET/oauth/authorize

Send the person here in their browser. They sign in to Vortex if needed and approve your guild.

Parameter Required Description
client_id Yes Your client ID.
response_type Yes Always code.
redirect_uri Yes An HTTPS URL on an allowed site.
scope No Space-separated user scopes. Defaults to all of them.
state Recommended A random value you check on return. Up to 500 characters.
code_challenge CDN: yes
SDK: recommended
Base64url SHA-256 of your code verifier (PKCE).
code_challenge_method With challenge Always S256.

Vortex redirects back to redirect_uri with ?code=...&state=..., or ?error=access_denied&state=... if they cancel. Codes last 2 minutes and work once. Embeds use their own built-in flow and can't use this page directly.

Get tokens

POST/api/partner/oauth/token

Send client_id in the body. SDK integrations also send client_secret (or use HTTP Basic auth). CDN and embed integrations must not send a secret.

grant_type Other fields Use
authorization_code code, redirect_uri, code_verifier Finish a sign-in. redirect_uri must match exactly.
refresh_token refresh_token Get a new access token. Returns a new refresh token; the old one stops working.
client_credentials scope (optional) SDK only. An app token with guild.read and/or guild.announce.
{
  "access_token": "eyJ...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "vtxr_...",
  "scope": "identify chat.read chat.write",
  "user": { "id": "...", "username": "cherry", ... }
}

refresh_token is left out for app tokens. user is only included when finishing a sign-in with identify.

Reusing a refresh token or a code that was already used signs out that whole session, in case it was stolen.

Error Meaning
invalid_client Unknown or disabled client, wrong secret, a secret sent by a browser integration, or a disallowed site.
invalid_grant The code or refresh token is wrong, expired or used, the redirect_uri or code_verifier doesn't match, or the account can't sign in.
unauthorized_client Only SDK integrations can use client_credentials.
invalid_scope An unknown app scope was requested.
unsupported_grant_type The grant_type isn't one of the three above.

Sign out

POST/api/partner/oauth/revoke

Body: client_id, token (access or refresh), plus client_secret for SDK integrations. Ends the whole session. Always returns { "ok": true }.

Who is this token for?

GET/api/partner/oauth/userinfoidentify

Returns { "user": User }. Same as GET /api/partner/v1/me.

REST API

All paths below are relative to %ORIGIN%/api/partner/v1. The pill after each path is the scope it needs.

Account and guild

GET/meidentify

The signed-in person: { "user": User }.

GET/guildchat.read or guild.read

{ "guild": Guild, "channels": Channel[], "app": { "name", "type" } }. With a user token, only channels they can see are listed, canSend reflects their permissions, and permissions says which moderator actions they can take: { "manageMessages", "timeout", "ban", "manageRoles", "manageChannels" }. standing says whether they can talk right now: { "muted", "mutedUntil", "timedOut", "timeoutUntil" }, where muted is a Vortex-wide mute and timedOut is a timeout in your guild (a null date means it doesn't expire). rules is { "items": [{ "id", "title", "description", "locked"?, "links"? }], "accepted": boolean }: the first rule always points to the Vortex Community Guidelines and Terms of Service, then your own rules from the dashboard. accepted is only there for user tokens. Returns 403 if they're banned.

POST/guild/rules/acceptchat.read

Record that the person agreed to the rules. Show the rules and ask before they chat the first time, like the embed does.

GET/guild/roleschat.read or guild.read

Your guild's roles, highest first: { "roles": [{ "id", "name", "color", "icon", "position" }] }.

GET/guild/members/:userIdchat.read or guild.read

A member's guild profile: { "user", "member", "owner", "joinedAt", "roles": [{ "id", "name", "color", "icon" }] }. vortexStaff is true for Vortex staff, who can moderate every guild, and vortexMuted is { "until" } while they're muted across Vortex (otherwise null). Moderators also get timeoutUntil and banned.

GET/guild/users?ids=ID,IDchat.read or guild.read

Public profiles for up to 100 user IDs, handy for showing names in <@userId> mentions: { "users": { "<userId>": { "id", "username", "avatar", "rank", "color" } } }.

GET/guild/online?channelId=IDchat.read or guild.read

Who's in a channel right now, on your site or on Vortex, grouped by their top guild role: { "count": 3, "groups": [{ "role": { "id", "name", "color", "icon" } | null, "members": [{ "id", "username", "avatar", "rank", "color", "status" }] }] }. Invisible people aren't listed. Up to 100 members are returned. Refetch when you get channelPresenceChanged.

GET/guild/mentions?q=namechat.read or guild.read

Up to 8 guild members whose username starts with q, for mention autocomplete: { "users": [{ "id", "username", "avatar", "rank", "color" }] }. Send the pick as <@userId>.

GET/guild/commandschat.read or guild.read

The slash commands this person can use: { "commands": [{ "name", "usage", "description", "moderation" }] }. See Commands.

GET/guild/member-roles?ids=ID,IDchat.read or guild.read

Each member's highest guild role, for up to 100 user IDs: { "roles": { "<userId>": { "name", "color", "icon" } | null } }.

GET/emotesany token

Emotes by name: { "emotes": { "<name>": { "name", "src" } } }. Messages use them as :name:.

Messages (as a person)

GET/chat/channelschat.read

Channels with unread state: { "guildChannels": [...] }.

GET/chat/channels/:channelId/messages?limit=50&before=IDchat.read

Up to limit (1 to 100) messages before before, oldest first: { "messages": RawMessage[], "hasMore": boolean }.

GET/chat/channels/:channelId/messages/:messageIdchat.read

One message.

POST/chat/channels/:channelId/messageschat.write

Body: { "content": "Hi!", "replyToId": "ID" }. content is up to 2000 characters. Returns { "message": RawMessage }. Content starting with / runs a command instead.

Commands

Send a command as the message content, like /timeout @cherry 10m spam. Commands reply with { "command": true, "ok": true, "message": "...", "announced": boolean } (or a 4xx with ok: false) and nothing is posted as the person.

Command Needs
/help Anyone. The reply also has commands.
/timeout @user <duration> [reason], /untimeout @user Timeout members
/ban @user [duration|permanent] [reason], /unban @user Ban members
/purge <count> [reason] Manage messages (up to 100)
/role add|remove @user <role name> Manage roles

Durations look like 10m, 1h, 1d or 2w. Moderation commands post a notice in the channel (a system message with commandKind set to timeout, untimeout, ban, unban or purge) unless you add --private, and only in your guild's own channels.

PATCH/chat/channels/:channelId/messages/:messageIdchat.write

Body: { "content": "..." }. Only the author can edit.

DELETE/chat/channels/:channelId/messages/:messageIdchat.write

Delete one of the person's messages.

POST/chat/channels/:channelId/messages/:messageId/reportchat.write

Body: { "reason": "spam", "note": "optional, up to 500 characters" }. reason is spam, harassment, hate_speech, nsfw, self_harm or other. Returns 201, or 409 if they already reported it. People can't report their own messages.

POST/chat/channels/:channelId/readchat.read

Mark the channel as read for the person.

Moderation

Works with an SDK app token that has guild.moderate, or a person's token (chat.write) when their guild roles give them the matching permission. With a person's token, they can only act on members ranked below them and only manage roles below their own. Nobody can moderate the guild owner. Every action shows up in your guild's audit log. See the Partner Policy.

:userId can be a user ID or a username.

GET/guild/members?q=name&limit=25&after=CURSORany moderator permission

Search members, newest first: { "members": [{ "user", "roleIds", "joinedAt", "owner", "timeoutUntil", "banned" }], "nextCursor": "..." | null }. limit is 1 to 100 (default 25). Pass nextCursor as after to get the next page. For numbered pages, send page instead: you get total, page and pages, and can add sort (joined-desc, joined-asc, name-asc, name-desc or role) and filter (timedout, noroles or a role ID).

GET/guild/bans?limit=25&after=CURSORban

Active bans, newest first: { "bans": [{ "user", "reason", "bannedAt", "expiresAt", "by" }], "nextCursor": "..." | null, "total": 12 }. Pages work the same way as members, including page for numbered pages.

PUT/guild/bans/:userIdban

Body: { "reason": "...", "minutes": 0 }. minutes 0 or left out means permanent, otherwise up to a year. Banned people lose access to your guild everywhere and can't sign in to your integration. Add "channelId" to post a notice in one of your channels, like the /ban command does. The DELETE endpoints for bans and timeouts take ?channelId=ID for the same thing.

DELETE/guild/bans/:userIdban

Unban.

PUT/guild/timeouts/:userIdtimeout

Body: { "minutes": 60, "reason": "..." }. Up to 40320 minutes (28 days). They can still read but can't send messages in your guild. Takes "channelId" like bans.

DELETE/guild/timeouts/:userIdtimeout

End a timeout early.

PUT/guild/members/:userId/roles/:roleIdmanage roles
DELETE/guild/members/:userId/roles/:roleIdmanage roles

Give or remove a guild role. The person must already be a member. Returns { "ok": true, "user", "roleIds" }.

GET/guild/audit?limit=25&after=CURSOR&category=moderationany moderator permission

Your guild's audit log, newest first: { "entries": [{ "id", "action", "category", "message", "at", "by": { "id", "username", "avatar" } | null, "via" }], "nextCursor": "..." | null }. category is moderation or admin. via is "integration" for actions taken through your app, "staff" for Vortex staff, otherwise empty.

Moderation endpoints return { "ok": true, "user": { "id", "username" } }, or 403 when the token or person isn't allowed. Banning someone also signs them out of your integration right away.

Channels

Same tokens as moderation, with the manage channels permission. Your guild can have up to 50 channels, and the shared Vortex channel can't be changed here.

POST/guild/channelsmanage channels

Body: { "name": "general", "description": "", "category": "", "slowmodeSeconds": 0, "readOnly": false, "discordChannelId": "" }. Only name is required. readOnly stops everyone without a role that allows sending. Returns 201 with { "channel" }.

PATCH/guild/channels/:channelIdmanage channels

Send only the fields you want to change. Returns { "channel" }.

DELETE/guild/channels/:channelIdmanage channels

Deletes the channel and all of its messages.

GET/guild/discord-channelsmanage channels

Text channels in your linked Discord server: { "connected": boolean, "channels": [{ "id", "name", "category", "usable", "linkedTo": { "id", "name" } | null }] }. Pass an id as discordChannelId to mirror a channel, or "" to unlink it. usable is false when the bot can't manage webhooks there. If the webhook is deleted on Discord, Vortex re-creates it on the next message.

App endpoints (SDK only)

GET/guild/channels/:channelId/messages?limit=50&before=IDguild.read

Read any of your guild's channels as your app. Same response as above.

POST/guild/channels/:channelId/announceguild.announce

Body: { "content": "..." }. Posts a highlighted announcement with your app's name. 10 per minute. Returns 201 with { "message": { "_id", "content", "createdAt" } }.

Public endpoints

GET/site-check?client_id=...&page=URLnone

Call from a page to see if it's allowed: { "allowed": boolean, "reason": string, "type": "embed" | "cdn" | "sdk" }. See Site access.

GET/embed-optionsnone

Every embed style option with its type, limits and default.

Realtime

Connect with Socket.IO v4 to the /partner namespace, then join the channels you want.

import { io } from "socket.io-client";

const socket = io("%ORIGIN%/partner", {
  auth: { token: accessToken },
  query: { client_id: "vtx_your_client_id" },
  transports: ["websocket"],
});

socket.on("connect", () => socket.emit("joinChannelRoom", { channelId }));
socket.on("chatMessage", ({ channelId, message }) => render(message));
socket.on("connect_error", () => { /* refresh the token, update socket.auth, reconnect */ });

You send

Event Payload Description
joinChannelRoom { channelId } Start receiving a channel's events.
leaveChannelRoom { channelId } Stop receiving them.
chatTyping { channelId } Show the person as typing (user tokens only).

You receive

Event Payload
chatMessage { channelId, message: RawMessage }
chatMessageEdited { channelId, message: RawMessage }
chatMessageDeleted { channelId, messageId }
chatTyping { channelId, userId, username }
chatTypingStop { channelId, userId }
chatMessagesPurged { channelId, messageIds }: several messages were deleted at once
channelPresenceChanged { channelId }: someone opened or left the channel, so refetch /guild/online
channelSystemMessage { channelId, user, verb }: someone joined or left
channelAccessDenied { channelId }: you can't join that channel
guildChannelsUpdated { channelId?, deleted? }: a channel was added, changed or removed, so reload /guild
guildRolesUpdated {}: roles changed, so refresh role chips
guildBanned { reason, expiresAt }: the signed-in person was banned from your guild. Their tokens are already revoked and the socket closes right after, so sign them out
standingUpdate { muted, mutedUntil } or { timedOut, timeoutUntil }: the signed-in person was muted, timed out, or let back in (user tokens only)

The partner namespace only carries your guild's channel events. It never receives Vortex-wide presence or DMs.

Objects

User

{
  "id": "...",
  "username": "cherry",
  "avatar": "https://.../img/uploads/...",
  "avatarFrame": "",
  "rank": "VIP",
  "color": "#ff4f8b",
  "premium": 0,
  "level": 12
}

Guild

{
  "id": "...",
  "ownerId": "...",
  "name": "Cherri",
  "tag": "CHR",
  "description": "",
  "icon": "https://...",
  "logo": "https://...",
  "themeColor": "#bb00ff",
  "themeColorSecondary": ""
}

Channel

{
  "id": "...",
  "type": "text",          // or "global" for the shared Vortex channel
  "name": "general",
  "description": "",
  "icon": "",
  "category": "",
  "position": 0,
  "locked": false,
  "slowmodeSeconds": 0,
  "readOnly": false,
  "editable": true,        // false for the shared Vortex channel
  "canSend": true
}

RawMessage

{
  "_id": "...",
  "channelId": "...",
  "content": "Hello **world** :wave:",
  "authorId": { "_id": "...", "username": "cherry", "avatar": "...", "rank": "VIP", "color": "#ff4f8b", "premium": 0 },
  "createdAt": "2026-01-01T12:00:00.000Z",
  "editedAt": null,
  "replyToId": { "_id": "...", "content": "...", "authorId": { "username": "..." } } | null,
  "attachments": ["/img/uploads/..."],
  "gif": { "url": "...", "width": 320, "height": 240 } | null,
  "mentionedUserIds": ["..."],
  "mentionsEveryone": false,
  "mentionsHere": false,
  "system": false,
  "commandKind": "",       // for system messages: "timeout", "ban", "purge", "partnerAnnounce", ...
  "color": null,
  "source": "vortex"
}

Message formatting

Syntax Result
**bold** *italic* __underline__ ~~strike~~ Text styles
`code` and ```code blocks``` Code
<@userId>, <@everyone>, <@here> Mentions
<#channelId> Channel link
:name: Emote, from /emotes

Content is plain text. Always escape it before showing it as HTML. As a backup, Vortex removes common script tricks from content before sending it to partner apps (things like <script> tags, onerror= handlers and javascript: links), so HTML-like text in messages may look slightly different than it does on Vortex.

Rate limits

  • 150 API requests per minute per person or app. Over the limit you get 429.
  • Channel slowmode applies to messages sent through your integration.
  • App announcements: 10 per minute.
  • Site checks: 30 new checks per minute per integration.