# OAuth 2.0 authorization code flow


Source: https://docs.stxapp.io/oauth/authorization-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](/isv/#application-types).

<OAuthFlowDiagram />

The endpoints are listed in the [overview](/oauth/#the-endpoints-at-a-glance). Discover them from the server metadata rather than hard-coding them; see [Discovery](/oauth/discovery-and-errors/#discovery). `$STX` in the examples below is the base URL STX gives you with your invite.

## The flow, step by step

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

```bash
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`

```
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](/oauth/scopes/#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](/isv/hosted-pages/).

### 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](/oauth/discovery-and-errors/#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

From your server, POST to `/oauth/token` with HTTP Basic authentication (`client_secret_basic`): username `client_id`, password `client_secret`.

```bash
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:

```json
{
  "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

Send the access token as a bearer on `/api/v1`:

```bash
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](/oauth/scopes/#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](/isv/#get-demo-access) gets `403 ip_not_allowed`. All three are described under [API errors](/oauth/discovery-and-errors/#api-errors).

## 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

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.

```bash
curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$STX/oauth/token" \
  -d grant_type=client_credentials \
  -d scope=market_data
```

```json
{ "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](/oauth/scopes/#app-scopes).

Building on Node.js or Bun? The TypeScript SDK wraps these requests; see [With the TypeScript SDK](/isv/typescript-sdk/).
