Developer Portal
Experimental The bot & app platform is in active development. Self-hosted server support shipped on 2026-08-13 with a smaller feature surface than cloud. See what's supported.
← Docs

Log in with GameVox

Let people sign in to your website or app with their GameVox account. This is the standard OAuth2 authorization-code flow, shaped like Discord's, and it needs no bot: your app asks for parts of the user's account, the user approves, and your server gets a token.

What it is for

  • Sign-in and account linking. Learn who the user is (a stable GameVox user ID, username, avatar) and, with their permission, their verified email.
  • Server pickers and dashboards. With guilds, see which servers the user is in and where they can manage settings.
  • Not for acting as the user. A user token cannot send messages, join voice, or change anything on GameVox. Anything that acts inside GameVox needs a bot.

Before you start

  1. Create an application at developers.gamevox.com. The Client ID on its OAuth2 tab is your client_id.
  2. On the OAuth2 tab, add every redirect URI your app will use (up to 10). They must be https://, or http://localhost / http://127.0.0.1 for development, with no #fragment. The redirect_uri you send must match one of them exactly, query string included.
  3. Copy the client secret. It is shown once, when the app is created or the secret is reset. Keep it on your server: every token request needs it.

Endpoints

GET  https://gamevox.com/oauth2/authorize/
POST https://app-api.gamevox.com/api/oauth2/token
POST https://app-api.gamevox.com/api/oauth2/token/revoke
GET  https://app-api.gamevox.com/api/users/@me
GET  https://app-api.gamevox.com/api/users/@me/guilds
GET  https://app-api.gamevox.com/api/oauth2/@me
PUT  https://api.gamevox.com/api/users/@me/applications/{application.id}/role-connection

Keep the trailing slash on /oauth2/authorize/. The token and user endpoints also answer under /api/v10/, so libraries that put a version in the path work unchanged. Role connections live on api.gamevox.com, not app-api.

1. Send the user to GameVox

Redirect the browser (a normal navigation, not a popup or iframe) to the authorize URL:

https://gamevox.com/oauth2/authorize/
  ?response_type=code
  &client_id=123456789012345678
  &scope=identify%20email
  &redirect_uri=https%3A%2F%2Fyourapp.example.com%2Fauth%2Fgamevox
  &state=Zx81kq0vT2
ParameterRequiredDescription
response_type Yes Always code.
client_id Yes The Client ID from your app's OAuth2 tab.
scope Yes Space-separated (URL-encoded as %20 or +). See Scopes. Leave out bot: with bot this URL becomes a server install instead.
redirect_uri Yes One of your registered redirect URIs, exactly.
state No An unguessable value tied to the user's session (up to 1024 characters). It comes back unchanged; check it to stop cross-site request forgery.
code_challenge No Optional PKCE challenge, 43 to 128 characters.
code_challenge_method No S256 when you send a challenge. plain is refused.
prompt No none skips the consent screen when the user already approved your app for all of these scopes. Anything new still asks.

What the user sees

  • A consent screen on gamevox.com with your app name, icon and description, the account they are signed in as, a plain-language list of what you will be able to see, and the site they will be sent to.
  • If they are signed out, Log in takes them through the GameVox web app and straight back to the same screen.
  • Your Terms of Service and Privacy Policy links appear under the buttons when you set them on the General Information tab.

2. Handle the callback

After the user clicks Authorize, GameVox sends them to your redirect URI with a code:

https://yourapp.example.com/auth/gamevox?code=3vR0b...&state=Zx81kq0vT2

If they click Cancel instead:

https://yourapp.example.com/auth/gamevox?error=access_denied
  &error_description=The+user+denied+the+request&state=Zx81kq0vT2
  • Reject the callback if state is not the one you issued for this browser.
  • A code expires after 10 minutes and works once. Exchange it right away, from your server.

3. Exchange the code for tokens

From your server, POST a form-encoded body (application/x-www-form-urlencoded, not JSON) to the token endpoint:

curl -X POST https://app-api.gamevox.com/api/oauth2/token \
  -u "123456789012345678:CS.your-client-secret" \
  -d grant_type=authorization_code \
  -d code=3vR0b... \
  -d redirect_uri=https://yourapp.example.com/auth/gamevox
{
  "access_token": "GVO.mJ4c...",
  "token_type": "Bearer",
  "expires_in": 604800,
  "refresh_token": "GVR.9tQe...",
  "scope": "identify email"
}
  • Authenticate with HTTP Basic (client_id:client_secret) as above, or send client_id and client_secret as form fields.
  • redirect_uri must be the same value you sent in step 1. Add code_verifier if you used PKCE.
  • Responses carry Cache-Control: no-store. Never put the client secret or tokens in browser code or URLs.

4. Call the API

Send the access token as Authorization: Bearer. GET /users/@me needs the identify scope:

curl https://app-api.gamevox.com/api/users/@me \
  -H "Authorization: Bearer GVO.mJ4c..."
{
  "id": "1557709980780474368",
  "gamevox_id": "edf412bc-a7b8-4f2a-a810-8b9eb8c02462",
  "username": "alice",
  "discriminator": "0",
  "global_name": "alice",
  "avatar": null,
  "gamevox_avatar_url": "https://...",
  "email": "alice@example.com",
  "verified": true
}
  • id is the user's stable GameVox ID. Key your account records on it; usernames can change. gamevox_id is the same user in GameVox's native UUID form.
  • email and verified are present only with the email scope, and only if the user has an email on file. GameVox only stores verified addresses, so verified is always true when email is set.
  • avatar is always null (Discord libraries would build a Discord CDN link from it). Use gamevox_avatar_url, which is omitted when the user has no picture.

The user's servers

GET /users/@me/guilds needs the guilds scope:

[
  { "id": "1557445672742031360", "name": "Raid Night", "icon": null,
    "owner": true,  "permissions": "2251799813685247", "features": [] },
  { "id": "1557445672742031361", "name": "Guild Hall", "icon": null,
    "owner": false, "permissions": "1056", "features": [] }
]
  • permissions is a summary for picking servers, not a full calculation: every bit (including ADMINISTRATOR) for the owner and admins, MANAGE_GUILD (32) plus VIEW_CHANNEL (1024) where the user can edit server settings, and VIEW_CHANNEL for everyone else.
  • Up to limit servers per response (1 to 200, default 200), oldest first. Page with after or before set to a server id from the previous page, as on Discord.

The current authorization

GET /oauth2/@me works with any scope and tells you which app the token belongs to, what it was granted, and when it expires. user is included with identify.

{
  "application": { "id": "123456789012345678", "name": "RaidPlanner", ... },
  "scopes": ["identify", "email"],
  "expires": "2026-10-15T15:03:20Z",
  "user": { "id": "1557709980780474368", "username": "alice", ... }
}

Linked roles

With role_connections.write you can record which account the user has on your side. Note the host: api.gamevox.com.

curl -X PUT \
  https://api.gamevox.com/api/users/@me/applications/123456789012345678/role-connection \
  -H "Authorization: Bearer GVO.mJ4c..." \
  -H "Content-Type: application/json" \
  -d '{"platform_name": "RaidPlanner", "platform_username": "ShadowMage", "metadata": {}}'

The user sees it under Settings > Authorized Apps as “Linked as ShadowMage on RaidPlanner”. metadata keys must be ones you defined in your app's role-connection metadata. GET on the same URL reads it back.

Refreshing

Access tokens last 7 days (expires_in is in seconds). Get a new pair with the refresh token:

curl -X POST https://app-api.gamevox.com/api/oauth2/token \
  -u "123456789012345678:CS.your-client-secret" \
  -d grant_type=refresh_token \
  -d refresh_token=GVR.9tQe...
  • Refresh tokens rotate. The response holds a new refresh token, and the old refresh token and its access token stop working immediately, so save the new pair before using it.
  • You may send scope to narrow the new token to fewer scopes. Asking for a scope the user never granted fails with invalid_scope.
  • An unused refresh token expires after 90 days. Each user keeps at most 25 live token pairs per app; making a 26th drops the oldest.

Revoking (logging out)

Revoke when the user signs out of your app. Either token of a pair revokes the pair. The endpoint always answers 200 for a valid client, whether or not the token existed.

curl -X POST https://app-api.gamevox.com/api/oauth2/token/revoke \
  -u "123456789012345678:CS.your-client-secret" \
  -d token=GVO.mJ4c...

PKCE

Optional, and recommended if the authorize URL can be built where someone else could see it. GameVox supports S256 only, and a token request still needs your client secret: there are no secret-less public clients.

// Before redirecting: keep the verifier with the user's pending login.
const verifier = base64url(crypto.randomBytes(32));
const challenge = base64url(crypto.createHash('sha256').update(verifier).digest());
// authorize URL: ...&code_challenge=CHALLENGE&code_challenge_method=S256
// token request: ...&code_verifier=VERIFIER (client secret still required)

Scopes

ScopeLets your app
identify Read the user's ID, username and avatar: GET /users/@me, and user in GET /oauth2/@me.
email Add the user's verified email to /users/@me.
guilds Read the user's server list: GET /users/@me/guilds.
role_connections.write Read and write the user's linked role details for your app.

Any other scope, including connections, guilds.join and applications.commands without bot, is refused on the consent screen with a message saying so.

Errors

The token and revoke endpoints answer errors in the standard OAuth2 shape, {"error": "...", "error_description": "..."}, so OAuth libraries read them as-is:

errorHTTPMeaning
invalid_client 401 Wrong client ID or secret, deleted app, or the app owner's account is suspended.
invalid_grant 400 The code or refresh token is unknown, expired, already used, issued to another app, or its redirect_uri or code_verifier does not match. Also returned after the user deauthorizes your app.
invalid_request 400 A required field is missing, or the body is not form-encoded.
invalid_scope 400 A refresh asked for a scope the original grant does not include.
unsupported_grant_type 400 Only authorization_code and refresh_token are supported.
slow_down 429 Too many failed client authentications from your IP (30 per minute). Wait a minute.

API calls with a bad, expired or revoked token get 401; a token without the needed scope gets 403 (50001 Missing Access). Responses carry the usual X-RateLimit-* headers, counted per user per app.

When access ends

A token can stop working before it expires. That happens when:

  • the user clicks Deauthorize under Settings > Authorized Apps (every token you hold for them dies at once, and your linked-role entry for them is deleted)
  • the user's account is disabled
  • your application is deleted
  • your developer account is suspended

Treat a 401 or invalid_grant as “signed out” and send the user through step 1 again.

Complete example (Node.js + Express)

Sign-in with state checking, the code exchange, and reading the user. Node 18+ for the built-in fetch.

import express from 'express';
import crypto from 'node:crypto';

const CLIENT_ID = process.env.GAMEVOX_CLIENT_ID;
const CLIENT_SECRET = process.env.GAMEVOX_CLIENT_SECRET;
const REDIRECT_URI = 'https://yourapp.example.com/auth/gamevox';

// state must be tied to THIS browser, or someone can hand a victim a callback
// URL carrying the attacker's own code and sign them in as the attacker.
// A short-lived HttpOnly cookie does that without a session store.
const STATE_COOKIE = 'gv_oauth_state';
const readCookie = (req, name) =>
  (req.headers.cookie || '').split(';').map((c) => c.trim().split('='))
    .find(([k]) => k === name)?.[1] || '';

const app = express();

app.get('/login', (req, res) => {
  const state = crypto.randomBytes(16).toString('hex');
  res.cookie(STATE_COOKIE, state, {
    httpOnly: true, secure: true, sameSite: 'lax', maxAge: 10 * 60 * 1000, path: '/auth/gamevox',
  });
  const url = new URL('https://gamevox.com/oauth2/authorize/');
  url.search = new URLSearchParams({
    response_type: 'code',
    client_id: CLIENT_ID,
    scope: 'identify email',
    redirect_uri: REDIRECT_URI,
    state,
  }).toString();
  res.redirect(url.toString());
});

app.get('/auth/gamevox', async (req, res) => {
  const { code, state, error } = req.query;
  const expected = readCookie(req, STATE_COOKIE);
  res.clearCookie(STATE_COOKIE, { path: '/auth/gamevox' });
  if (!expected || typeof state !== 'string' || state !== expected) {
    return res.status(400).send('Login expired or started in another browser');
  }
  if (error) return res.redirect('/?login=cancelled');
  if (typeof code !== 'string') return res.status(400).send('Missing code');

  const tokenRes = await fetch('https://app-api.gamevox.com/api/oauth2/token', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      Authorization: 'Basic ' + Buffer.from(CLIENT_ID + ':' + CLIENT_SECRET).toString('base64'),
    },
    body: new URLSearchParams({ grant_type: 'authorization_code', code, redirect_uri: REDIRECT_URI }),
  });
  if (!tokenRes.ok) return res.status(502).send('GameVox login failed');
  const tokens = await tokenRes.json();

  const me = await fetch('https://app-api.gamevox.com/api/users/@me', {
    headers: { Authorization: 'Bearer ' + tokens.access_token },
  }).then((r) => r.json());

  // me.id is the stable GameVox user id: key your account record on it.
  // Store tokens.refresh_token server-side if you need access later.
  res.send('Signed in as ' + me.username);
});

app.listen(3000);

Differences from Discord

  • Only the authorization-code grant. No implicit grant (response_type=token) and no client_credentials.
  • No public clients: every token request needs the client secret, with or without PKCE.
  • User installs (applications.commands without bot, integration_type=1) are not available yet.
  • No connections, guilds.join, guilds.members.read or rpc scopes.
  • In /users/@me/guilds, permissions is a summary for picking servers, not a full calculation (see above).

← Back to docs