Authorize

Send the user to haunt.gg to sign in and approve your app.

The start of the authorization code flow. Redirect the user's browser here — this is not an endpoint you call from your server. haunt.gg handles the login, shows its consent screen, and sends the browser back to your redirect_uri with a one-time code.

GET/api/auth/oauth2/authorize

PKCE is mandatory for every haunt.gg app, and S256 is the only accepted method. Generate a random code_verifier, send its SHA-256 hash as code_challenge, and keep the verifier for the token exchange.

Query Parameters

client_idquerystringrequired
Your app's client ID from the Developer page.
redirect_uriquerystringrequired
One of your app's registered redirect URLs. Matched exactly, path included. The single exception is the loopback IP 127.0.0.1 (and ::1), where the port is ignored so a native app can listen on a random one — see the note below.
response_typequerystringrequired
Must be code. Anything else is rejected before the flow starts.
code_challengequerystringrequired
Base64url of the SHA-256 hash of your code_verifier. Required — PKCE is not optional here.
code_challenge_methodquerystringrequired
Must be S256. plain is rejected.
scopequerystring
Space-separated permissions, e.g. openid identify email. Every entry must be one your app is registered for. Omit it entirely and haunt.gg grants your app's full registered scope set.
statequerystring
Opaque value echoed back to your redirect URL. Use it to tie the callback to the browser session that started the flow — this is your CSRF defence.
noncequerystring
Echoed into the id_token as the nonce claim. Verify it after the exchange to bind the token to this request.
promptquerystring
One of none, login, consent, create, login consent. login and create force a fresh sign-in; consent always re-shows the approval screen; none never shows any UI and instead fails with a redirect error. select_account is advertised in discovery but not supported.

Native apps: register 127.0.0.1, not localhost. The port is only ignored for loopback IP literals (RFC 8252 §7.3). Register http://localhost:3000/callback and come back on port 51734 and the request is refused with invalid_redirect; register http://127.0.0.1:3000/callback and any port works. Both are accepted at registration time, so this only surfaces once you are testing.

Unknown query parameters are dropped, not passed through. login_hint, id_token_hint, max_age, display, ui_locales, acr_values and resource are all ignored at this endpoint — resource is only honoured at the token endpoint.

Example Request

Request
// 1. PKCE
const verifier = base64url(crypto.getRandomValues(new Uint8Array(32)));
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
const challenge = base64url(new Uint8Array(digest));

// Keep `verifier` and `state` in the user's session — you need them later.
const state = base64url(crypto.getRandomValues(new Uint8Array(16)));

const url = new URL("https://haunt.gg/api/auth/oauth2/authorize");
url.searchParams.set("client_id", CLIENT_ID);
url.searchParams.set("redirect_uri", "https://example.com/auth/callback");
url.searchParams.set("response_type", "code");
url.searchParams.set("scope", "openid identify offline_access");
url.searchParams.set("state", state);
url.searchParams.set("code_challenge", challenge);
url.searchParams.set("code_challenge_method", "S256");

redirect(url.toString());

Response

This endpoint never returns JSON. It answers with a 302 to your redirect URL — either with a code, or with an OAuth error.

Response
Approved. `state` comes back exactly as you sent it, and `iss` is always appended — verify it equals https://haunt.gg before you exchange the code. The code is single-use and expires after 10 minutes.
https://example.com/auth/callback
  ?code=1a2B3c4D5e6F7g8H9i0J1k2L3m4N5o6P
  &state=RANDOM_STATE
  &iss=https://haunt.gg

Errors your app receives

errorWhen
access_deniedThe user pressed Cancel on the consent screen.
invalid_scopeA requested scope is not registered on your app. error_description names them.
invalid_requestPKCE problem — the challenge and method were not both sent, or the method was not S256.
login_requiredprompt=none but the user is not signed in.
consent_requiredprompt=none but the user has not approved these permissions yet.
interaction_requiredprompt=none but the flow needs a UI step.
account_selection_requiredprompt=none and the account could not be determined.

Errors your app never sees

Some failures happen before the redirect URL is validated, and OAuth forbids redirecting to an unverified URL. Those send the user to https://haunt.gg/login?error=…&error_description=… and your app simply never gets a callback. This is what you are looking at when a test flow appears to hang on the haunt.gg login page:

  • invalid_client — unknown or missing client_id
  • invalid_redirect — the redirect_uri is not registered, character for character
  • client_disabled — the app was suspended by haunt.gg staff
  • unauthorized_client — the app is not registered for the authorization code grant
  • unsupported_response_typeresponse_type was missing or not code

haunt.gg shows its own consent screen with your app's name, the owner's handle, and your website, terms and privacy links if you registered them. It lists exactly the permissions in your scope.

If the same user already approved the same set of permissions, the screen is skipped and the browser bounces straight back with a code. Adding a scope later re-prompts. prompt=consent always re-prompts.

Rate limit

100 requests per minute per IP address, counted for this endpoint alone.