개발자 포털
실험적 봇 및 앱 플랫폼은 활발히 개발 중입니다. 자체 호스팅 서버 지원은 2026-08-13에 출시되었으며 클라우드보다 기능 범위가 좁습니다. 지원 항목 보기.
← 문서

GameVox로 로그인

사용자가 GameVox 계정으로 여러분의 웹사이트나 앱에 로그인할 수 있게 합니다. Discord와 같은 형태의 표준 OAuth2 인가 코드 흐름이며 봇이 필요 없습니다. 앱이 사용자 계정 정보의 일부를 요청하고, 사용자가 승인하면, 여러분의 서버가 토큰을 받습니다.

용도

  • 로그인과 계정 연결. 사용자가 누구인지(변하지 않는 GameVox 사용자 ID, 사용자 이름, 아바타) 알 수 있고, 사용자가 허락하면 인증된 이메일도 받을 수 있습니다.
  • 서버 선택기와 대시보드. guilds로 사용자가 속한 서버와 설정을 관리할 수 있는 서버를 확인할 수 있습니다.
  • 사용자 대신 행동하기 위한 기능이 아닙니다. 사용자 토큰으로는 메시지를 보내거나, 음성에 참여하거나, GameVox에서 무엇이든 변경할 수 없습니다. GameVox 안에서 동작하는 기능에는 봇이 필요합니다.

시작하기 전에

  1. developers.gamevox.com에서 애플리케이션을 만드세요. OAuth2 탭의 클라이언트 ID가 client_id입니다.
  2. OAuth2 탭에서 앱이 사용할 리디렉션 URI를 모두 추가하세요(최대 10개). https://여야 하고(개발용으로는 http://localhost / http://127.0.0.1도 가능), #fragment가 없어야 합니다. 보내는 redirect_uri는 쿼리 문자열까지 포함해 그중 하나와 정확히 일치해야 합니다.
  3. 클라이언트 시크릿을 복사하세요. 앱을 만들 때나 시크릿을 재설정할 때 한 번만 표시됩니다. 모든 토큰 요청에 필요하므로 서버에 보관하세요.

엔드포인트

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

/oauth2/authorize/의 끝 슬래시를 빼지 마세요. 토큰 및 사용자 엔드포인트는 /api/v10/ 아래에서도 응답하므로, 경로에 버전을 넣는 라이브러리도 그대로 동작합니다. 역할 연결은 app-api가 아니라 api.gamevox.com에 있습니다.

1. 사용자를 GameVox로 보내기

브라우저를 인가 URL로 리디렉션하세요(팝업이나 iframe이 아닌 일반 페이지 이동으로):

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
매개변수필수설명
response_type 예 항상 code.
client_id 예 앱 OAuth2 탭의 클라이언트 ID.
scope 예 공백으로 구분합니다(URL 인코딩 시 %20 또는 +). 스코프를 참고하세요. bot은 넣지 마세요. bot이 있으면 이 URL은 서버 설치가 됩니다.
redirect_uri 예 등록한 리디렉션 URI 중 하나와 정확히 같은 값.
state 아니요 사용자 세션에 묶인, 추측할 수 없는 값(최대 1024자). 그대로 돌아오므로, 확인해서 사이트 간 요청 위조를 막으세요.
code_challenge 아니요 선택 사항인 PKCE 챌린지. 43자에서 128자 사이.
code_challenge_method 아니요 챌린지를 보낼 때는 S256. plain은 거부됩니다.
prompt 아니요 none을 지정하면, 사용자가 이 스코프 전부에 대해 이미 앱을 승인한 경우 동의 화면을 건너뜁니다. 새로운 스코프가 있으면 여전히 묻습니다.

사용자에게 보이는 화면

  • gamevox.com의 동의 화면에 앱 이름, 아이콘, 설명, 로그인된 계정, 앱이 볼 수 있게 되는 정보를 쉬운 말로 정리한 목록, 그리고 이동할 사이트가 표시됩니다.
  • 로그인하지 않은 상태라면 Log in을 누르면 GameVox 웹 앱을 거쳐 같은 화면으로 바로 돌아옵니다.
  • 일반 정보 탭에서 서비스 약관과 개인정보 처리방침 링크를 설정하면 버튼 아래에 표시됩니다.

2. 콜백 처리하기

사용자가 Authorize를 클릭하면 GameVox가 코드와 함께 리디렉션 URI로 보냅니다:

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

대신 Cancel을 클릭한 경우:

https://yourapp.example.com/auth/gamevox?error=access_denied
  &error_description=The+user+denied+the+request&state=Zx81kq0vT2
  • state가 이 브라우저에 발급한 값이 아니면 콜백을 거부하세요.
  • 코드는 10분 후 만료되며 한 번만 사용할 수 있습니다. 서버에서 바로 교환하세요.

3. 코드를 토큰으로 교환하기

서버에서 폼 인코딩된 본문(JSON이 아닌 application/x-www-form-urlencoded)을 토큰 엔드포인트로 POST하세요:

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"
}
  • 위와 같이 HTTP Basic(client_id:client_secret)으로 인증하거나, client_id와 client_secret을 폼 필드로 보내세요.
  • redirect_uri는 1단계에서 보낸 값과 같아야 합니다. PKCE를 사용했다면 code_verifier를 추가하세요.
  • 응답에는 Cache-Control: no-store가 붙습니다. 클라이언트 시크릿이나 토큰을 브라우저 코드나 URL에 절대 넣지 마세요.

4. API 호출하기

액세스 토큰을 Authorization: Bearer로 보내세요. GET /users/@me에는 identify 스코프가 필요합니다:

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는 사용자의 변하지 않는 GameVox ID입니다. 계정 레코드의 키로 사용하세요. 사용자 이름은 바뀔 수 있습니다. gamevox_id는 같은 사용자를 GameVox 고유의 UUID 형식으로 나타낸 것입니다.
  • email과 verified는 email 스코프가 있고 사용자가 이메일을 등록한 경우에만 포함됩니다. GameVox는 인증된 주소만 저장하므로, email이 있으면 verified는 항상 true입니다.
  • avatar는 항상 null입니다(Discord 라이브러리가 이 값으로 Discord CDN 링크를 만들기 때문입니다). 대신 gamevox_avatar_url을 사용하세요. 사용자에게 사진이 없으면 생략됩니다.

사용자의 서버

GET /users/@me/guilds에는 guilds 스코프가 필요합니다:

[
  { "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는 서버를 고르기 위한 요약이며 완전한 계산 결과가 아닙니다. 소유자와 관리자에게는 모든 비트(ADMINISTRATOR 포함), 서버 설정을 편집할 수 있는 사용자에게는 MANAGE_GUILD(32)와 VIEW_CHANNEL(1024), 그 밖의 사용자에게는 VIEW_CHANNEL이 들어갑니다.
  • 응답 하나에 최대 limit개의 서버가 오래된 순서로 들어갑니다(1에서 200 사이, 기본값 200). Discord와 마찬가지로 after나 before에 이전 페이지에 있는 서버의 id를 지정해 페이지를 넘깁니다.

현재 인가

GET /oauth2/@me는 어떤 스코프로도 사용할 수 있으며, 토큰이 어느 앱의 것인지, 무엇이 허용되었는지, 언제 만료되는지 알려 줍니다. identify가 있으면 user도 포함됩니다.

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

연결된 역할

role_connections.write로 사용자가 여러분의 서비스에서 어떤 계정을 쓰는지 기록할 수 있습니다. 호스트에 주의하세요: 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": {}}'

사용자는 Settings > Authorized Apps에서 “Linked as ShadowMage on RaidPlanner”로 보게 됩니다. metadata 키는 앱의 역할 연결 메타데이터에 정의한 것이어야 합니다. 같은 URL에 GET하면 다시 읽을 수 있습니다.

토큰 갱신

액세스 토큰은 7일 동안 유효합니다(expires_in은 초 단위). 리프레시 토큰으로 새 쌍을 받으세요:

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...
  • 리프레시 토큰은 교체됩니다. 응답에 새 리프레시 토큰이 담기고, 이전 리프레시 토큰과 그 액세스 토큰은 즉시 동작을 멈추므로, 사용하기 전에 새 쌍을 저장하세요.
  • scope를 보내 새 토큰의 스코프를 줄일 수 있습니다. 사용자가 허용한 적 없는 스코프를 요청하면 invalid_scope로 실패합니다.
  • 사용되지 않은 리프레시 토큰은 90일 후 만료됩니다. 사용자 한 명이 앱 하나에 대해 가질 수 있는 유효한 토큰 쌍은 최대 25개이며, 26번째를 만들면 가장 오래된 것이 삭제됩니다.

취소(로그아웃)

사용자가 앱에서 로그아웃하면 토큰을 취소하세요. 쌍 중 어느 토큰으로든 쌍 전체가 취소됩니다. 유효한 클라이언트라면 토큰의 존재 여부와 관계없이 엔드포인트는 항상 200을 반환합니다.

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

PKCE

선택 사항이며, 다른 사람이 볼 수 있는 곳에서 인가 URL을 만드는 경우 권장합니다. GameVox는 S256만 지원하며, 토큰 요청에는 여전히 클라이언트 시크릿이 필요합니다. 시크릿 없는 퍼블릭 클라이언트는 없습니다.

// 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)

스코프

스코프앱이 할 수 있는 것
identify 사용자의 ID, 사용자 이름, 아바타를 읽습니다: GET /users/@me, 그리고 GET /oauth2/@me의 user.
email /users/@me에 사용자의 인증된 이메일을 추가합니다.
guilds 사용자의 서버 목록을 읽습니다: GET /users/@me/guilds.
role_connections.write 앱에 대한 사용자의 연결된 역할 정보를 읽고 씁니다.

그 밖의 스코프(connections, guilds.join, bot 없는 applications.commands 포함)는 동의 화면에서 그 사실을 알리는 메시지와 함께 거부됩니다.

오류

토큰 및 취소 엔드포인트는 표준 OAuth2 형식인 {"error": "...", "error_description": "..."}로 오류를 반환하므로, OAuth 라이브러리가 그대로 읽을 수 있습니다:

errorHTTP의미
invalid_client 401 클라이언트 ID나 시크릿이 틀렸거나, 앱이 삭제되었거나, 앱 소유자의 계정이 정지되었습니다.
invalid_grant 400 코드나 리프레시 토큰을 알 수 없거나, 만료되었거나, 이미 사용되었거나, 다른 앱에 발급되었거나, redirect_uri 또는 code_verifier가 일치하지 않습니다. 사용자가 앱의 인가를 해제한 뒤에도 반환됩니다.
invalid_request 400 필수 필드가 없거나 본문이 폼 인코딩되지 않았습니다.
invalid_scope 400 갱신 요청이 원래 허용에 포함되지 않은 스코프를 요청했습니다.
unsupported_grant_type 400 authorization_code와 refresh_token만 지원합니다.
slow_down 429 여러분의 IP에서 클라이언트 인증 실패가 너무 많습니다(분당 30회). 1분 기다리세요.

잘못되었거나 만료되었거나 취소된 토큰으로 API를 호출하면 401을 받고, 필요한 스코프가 없는 토큰은 403(50001 Missing Access)을 받습니다. 응답에는 일반적인 X-RateLimit-* 헤더가 붙으며 사용자별, 앱별로 집계됩니다.

접근이 끝나는 경우

토큰은 만료 전에 동작을 멈출 수 있습니다. 다음과 같은 경우입니다:

  • 사용자가 Settings > Authorized Apps에서 Deauthorize를 클릭한 경우(해당 사용자에 대해 보유한 모든 토큰이 즉시 무효화되고, 그 사용자의 연결된 역할 항목도 삭제됩니다)
  • 사용자의 계정이 비활성화된 경우
  • 애플리케이션이 삭제된 경우
  • 개발자 계정이 정지된 경우

401이나 invalid_grant는 “로그아웃됨”으로 간주하고, 사용자를 1단계부터 다시 진행하게 하세요.

전체 예제(Node.js + Express)

state 확인, 코드 교환, 사용자 정보 읽기를 포함한 로그인 예제입니다. 내장 fetch를 쓰므로 Node 18 이상이 필요합니다.

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);

Discord와의 차이

  • 인가 코드 그랜트만 지원합니다. 암시적 그랜트(response_type=token)와 client_credentials는 없습니다.
  • 퍼블릭 클라이언트는 없습니다. PKCE 사용 여부와 관계없이 모든 토큰 요청에 클라이언트 시크릿이 필요합니다.
  • 사용자 설치(bot 없는 applications.commands, integration_type=1)는 아직 사용할 수 없습니다.
  • connections, guilds.join, guilds.members.read, rpc 스코프는 없습니다.
  • /users/@me/guilds의 permissions는 서버를 고르기 위한 요약이며 완전한 계산 결과가 아닙니다(위 참고).

← 문서로 돌아가기