OAuth 2.0 authorization code flow
STX uses the OAuth 2.0 authorization code flow with PKCE (Proof Key for Code Exchange). The member signs in to STX, approves your scopes once, and your backend exchanges a short-lived code for tokens.
A Web app exchanges the code with its client_secret, which must stay on your server. A Browser app or Mobile or desktop app has no secret: it sends its client_id in the form and proves itself with the PKCE verifier alone. See application types.
The endpoints are listed in the overview. Discover them from the server metadata rather than hard-coding them; see Discovery. $STX in the examples below is the base URL STX gives you with your invite.
The flow, step by step
Section titled “The flow, step by step”1. Create a PKCE verifier and challenge
Section titled “1. Create a PKCE verifier and challenge”Generate a high-entropy code_verifier, 43 to 128 characters. The challenge is its base64url SHA-256. Keep the verifier on your server, keyed to this pending sign-in. Only S256 is accepted.
code_verifier=$(openssl rand -base64 64 | tr -d '=+/\n' | cut -c1-64)code_challenge=$(printf '%s' "$code_verifier" \ | openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '=')state=$(openssl rand -hex 16)2. Redirect the member to /oauth/authorize
Section titled “2. Redirect the member to /oauth/authorize”GET $STX/oauth/authorize ?response_type=code &client_id=<your client_id> &redirect_uri=<one you registered, exactly> &scope=profile.read%20balance.read%20orders.read &state=<opaque, unguessable, per request> &code_challenge=<the challenge> &code_challenge_method=S256state is yours: store it against the member’s session and verify it on return. scope is narrowed to your client’s allow-list; see how a request is narrowed.
A member who is not signed in is sent to the STX login, including any two-factor step, and returned to this request when they sign in. A person without an account signs up on /player/register, then logs in in the same browser session and is returned to consent. The member then approves the scopes on the consent screen; see Hosted pages.
3. Receive the code at your redirect URI
Section titled “3. Receive the code at your redirect URI”On Allow, STX redirects the browser to your redirect_uri:
<redirect_uri>?code=stxapp_code_…&state=<the state you sent>Verify state. On Deny you get ?error=access_denied&state=…. A malformed request (bad response_type, missing PKCE, missing or unusable scope) also comes back to your redirect_uri with an RFC 6749 error; see authorize errors. An unknown client_id, or a redirect_uri that does not match one on file, is shown an error page on STX and never redirected, so the flow cannot be turned into an open redirector.
The code is single use and expires in 60 seconds; exchange it immediately.
4. Exchange the code for tokens
Section titled “4. Exchange the code for tokens”From your server, POST to /oauth/token with HTTP Basic authentication (client_secret_basic): username client_id, password client_secret.
curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$STX/oauth/token" \ -d grant_type=authorization_code \ -d code="$code" \ -d redirect_uri=http://localhost:8787/callback \ -d code_verifier="$code_verifier"STX checks the code, the redirect URI and the PKCE verifier, then returns:
{ "access_token": "stxapp_at_…", "refresh_token": "stxapp_rt_…", "token_type": "bearer", "expires_in": 3600, "scope": "profile.read balance.read orders.read"}Store both tokens against the member, on your server if you have one. Presenting the same code twice revokes the member’s grant to your app.
5. Call STX for the member
Section titled “5. Call STX for the member”Send the access token as a bearer on /api/v1:
curl -s "$STX/api/v1/account/balance" -H "Authorization: Bearer $ACCESS_TOKEN"On the WebSocket, send it in the handshake header x-stx-oauth-token. A call succeeds only within the effective scope. Outside it you get 403 insufficient_scope; an expired or revoked token gets 401; a call from an address outside your app’s IP allowlist gets 403 ip_not_allowed. All three are described under API errors.
Returning members: silent re-authorization
Section titled “Returning members: silent re-authorization”A member consents once. When a member whose active grant already covers the requested scopes returns, /oauth/authorize issues a code without showing the consent screen.
For a background reconnection with no interaction at all, add prompt=none:
- an active grant covering the request returns a
code; - a member not signed in to STX returns
?error=login_required; - a member without a covering grant returns
?error=consent_required.
All three arrive at your redirect_uri with state echoed, and none shows a screen.
App tokens: client credentials
Section titled “App tokens: client credentials”An app token authenticates the application itself, with no member and no consent, for the market and event catalogue. Only an app with a client secret (Web app or Server-to-server) can mint one.
curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$STX/oauth/token" \ -d grant_type=client_credentials \ -d scope=market_data{ "access_token": "stxapp_at_…", "token_type": "bearer", "expires_in": 3600, "scope": "market_data" }Omit scope to get every app scope your client is allow-listed for. A member scope, or an app scope your client is not allow-listed for, is invalid_scope. There is no refresh token: mint a new one when it expires. A member token reads the same catalogue, so you only need this with no member involved; see app scopes.
Building on Node.js or Bun? The TypeScript SDK wraps these requests; see With the TypeScript SDK.

