Skip to content

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 STX OAuth authorization code flow with PKCEThree lanes: the member's browser, your server-side client, and STX. Your server builds a PKCE challenge, redirects the member to STX to sign in and approve scopes, receives a single-use code at your redirect URI, exchanges that code and the verifier for an access and refresh token, then calls the API as the member with a bearer token.Member · browserYour server · clientSTX1Build PKCE verifier +challenge (S256)2Redirect to /oauth/authorizeMember signs in andapproves scopes at STX3redirect_uri?code=…&state=…4POST /oauth/token (code + verifier)access_token + refresh_token5GET /api/v1/… (Bearer access_token)member data, or 403 out of scopeReturning member: a prior grant skips the consent screen. Add prompt=none to reconnect silently.

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.

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=S256

state 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.

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.

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.

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.

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.

v1.5.9Changelogllms.txtllms-full.txt