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
- Create an application at developers.gamevox.com. The Client ID on its OAuth2 tab is your
client_id. - On the OAuth2 tab, add every redirect URI your app will use (up to 10). They must be
https://, orhttp://localhost/http://127.0.0.1for development, with no#fragment. Theredirect_uriyou send must match one of them exactly, query string included. - 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 | Parameter | Required | Description |
|---|---|---|
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
stateis 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 sendclient_idandclient_secretas form fields. redirect_urimust be the same value you sent in step 1. Addcode_verifierif 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
} idis the user's stable GameVox ID. Key your account records on it; usernames can change.gamevox_idis the same user in GameVox's native UUID form.emailandverifiedare present only with theemailscope, and only if the user has an email on file. GameVox only stores verified addresses, soverifiedis alwaystruewhenemailis set.avataris alwaysnull(Discord libraries would build a Discord CDN link from it). Usegamevox_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": [] }
] permissionsis a summary for picking servers, not a full calculation: every bit (includingADMINISTRATOR) for the owner and admins,MANAGE_GUILD(32) plusVIEW_CHANNEL(1024) where the user can edit server settings, andVIEW_CHANNELfor everyone else.- Up to
limitservers per response (1 to 200, default 200), oldest first. Page withafterorbeforeset to a serveridfrom 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
scopeto narrow the new token to fewer scopes. Asking for a scope the user never granted fails withinvalid_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
| Scope | Lets 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:
| error | HTTP | Meaning |
|---|---|---|
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 noclient_credentials. - No public clients: every token request needs the client secret, with or without PKCE.
- User installs (
applications.commandswithoutbot,integration_type=1) are not available yet. - No
connections,guilds.join,guilds.members.readorrpcscopes. - In
/users/@me/guilds,permissionsis a summary for picking servers, not a full calculation (see above).