Tokens and security
The token lifecycle and the security properties you can rely on. Read it before you go to production. $STX in the examples is the base URL STX gives you with your invite.
The tokens
Section titled “The tokens”STX issues opaque tokens. They are not JWTs, carry no readable claims, and their meaning lives only in STX. Treat them as strings.
| Token | Prefix | Lifetime | Use |
|---|---|---|---|
| Access token | stxapp_at_ |
expires_in, 3600 seconds |
Bearer on every call |
| Refresh token | stxapp_rt_ |
Expires after 14 days unused, and capped (see Refresh lifetime) | Get a new pair, no member involved |
| Authorization code | stxapp_code_ |
60 seconds, single use | Exchanged once for the first pair |
Every STX credential has the form stxapp_<type>_ followed by 43 base62 characters and a 6-character checksum: stxapp_at_ access token, stxapp_rt_ refresh token, stxapp_code_ authorization code, stxapp_cs_ client secret, stxapp_client_ client id. The fixed shape lets secret scanners spot a leaked credential. Store the whole string.
An app token is a stxapp_at_ access token from client_credentials, with no refresh token and no member.
Hold tokens on your server where you have one, encrypted at rest, per member. A Browser app or Mobile or desktop app keeps them in memory or the platform’s secure storage, never in a URL, a log or a shipped bundle.
Refreshing
Section titled “Refreshing”curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$STX/oauth/token" \ -d grant_type=refresh_token \ -d refresh_token="$REFRESH_TOKEN"You receive a new access token and a new refresh token; the old refresh token is retired. Always store the new refresh token.
Rotation and reuse detection
Section titled “Rotation and reuse detection”A retired refresh token presented again is treated as stolen: STX revokes the whole grant. Both the thief and your app lose access, and the member must reconnect. A 60-second grace window lets an ordinary retry of the same refresh return the same successor instead of tripping this. Never refresh with a token you have already rotated past.
Refresh lifetime
Section titled “Refresh lifetime”A refresh token not used for 14 days expires. Every refresh chain also ends at a fixed cap counted from the member’s approval: 90 days for a Web app or Server-to-server app, 30 days for a Browser app or Mobile or desktop app. Refreshing never extends it. Past either limit a refresh returns 400 invalid_grant; send the member through the authorization flow again to re-approve.
When a refresh fails
Section titled “When a refresh fails”A failed refresh is 400 with error: "invalid_grant" and an error_description saying why (reused, expired, or invalid); see token endpoint errors. Treat it as terminal for that member: clear the stored pair and send them back through consent. A 503 with temporarily_unavailable means the refresh was not judged; retry with the same token.
Revoking
Section titled “Revoking”To disconnect a member from your side (sign-out, account deletion):
curl -s -u "$CLIENT_ID:$CLIENT_SECRET" "$STX/oauth/revoke" -d token="$ACCESS_TOKEN"- Revoking either token of a pair kills both. Other pairs under the same grant (another device of yours) are untouched.
- The answer is
200for a known, unknown, or another client’s token alike, so it never reveals whether a token was valid.503means retry. - A Browser app or Mobile or desktop app revokes with
client_idin the form instead of a secret.
A member can also revoke your app from Connected apps in their STX account. That revokes the whole grant and every token under it; your next call for that member returns 401.
If STX suspends, closes, bans or otherwise restricts a member’s account, your tokens for that member stop working at once and the grant is revoked; the member must approve your app again once reinstated. A temporary cool-off (the cool_off account status) only pauses access until it lifts.
When a token is revoked, rotated or expires, STX closes any WebSocket opened with it. The member’s own sessions are unaffected.
Checking a token
Section titled “Checking a token”POST /oauth/introspect with token= answers {"active": true, "scope": "…", "client_id": "…", "token_type": "bearer"} for a live access token of your own, and {"active": false} for anything else, including another client’s token. It requires client_secret_basic. The reported scope is the effective scope. Use it for debugging, not the hot path; trust the 401 and 403 from the API.
Apps without a client secret
Section titled “Apps without a client secret”A Browser app or Mobile or desktop app (see application types) has token_endpoint_auth_method: "none". It has no secret, sends no Authorization header to /oauth/token or /oauth/revoke, and identifies itself with a client_id form field; its proof is the PKCE verifier. It can redeem codes and refresh, and cannot use client_credentials or introspection.
Dynamic registration
Section titled “Dynamic registration”Dynamic client registration (RFC 7591) is POST /oauth/register, unauthenticated, JSON body. It is off by default: while off it answers 404 and the metadata has no registration_endpoint.
| Field | |
|---|---|
client_name |
required |
redirect_uris |
https, or a reverse-domain custom scheme or http://localhost / 127.0.0.1 / [::1] (any port) for an installed app; none needed for client_credentials only. No fragment |
scope |
optional; space-separated known scopes, member or app |
token_endpoint_auth_method |
client_secret_basic or none. If omitted: none when any callback is local or a custom scheme, otherwise client_secret_basic. The response echoes the method chosen |
grant_types |
subset of authorization_code refresh_token client_credentials, default authorization_code refresh_token; refresh_token needs authorization_code; client_credentials needs a secret |
response_types |
["code"] |
client_uri, logo_uri |
optional absolute https URLs |
software_id, software_version |
optional strings |
Without scope, the client is allow-listed for the read-only default profile.read balance.read portfolio.read orders.read, plus the app scopes when grant_types includes client_credentials. It cannot request a write scope (orders.write, terms.write): those need an IP allowlist, which only STX sets. Register with read scopes and ask STX to add the write scope. A request that names one fails with 400 invalid_client_metadata. 201 returns client_id, client_id_issued_at, the stored metadata and, for an app with a secret, a one-time client_secret with client_secret_expires_at: 0. Errors are listed under registration errors.
Security properties you can rely on
Section titled “Security properties you can rely on”- PKCE is mandatory,
S256only. An intercepted code is useless without the verifier your server holds. - Codes are single use. Presenting a code twice revokes the member’s grant to your app.
- Callbacks are matched exactly, byte for byte, at both the authorize and token steps, except a local callback (
localhost,127.0.0.1,[::1]), which matches on any port. An unknown client or callback gets an error page on STX and is never redirected. - An IP allowlist guards writes. When your app has an IP allowlist, token requests, REST calls and WebSocket connections from any other address are refused with
403 ip_not_allowed. Any write scope requires one. - Tokens are bound to your client. Refresh, revoke and introspect check that the token belongs to the authenticating client.
- Scope is re-checked on every call against the member’s current grant and your client’s current allow-list; see effective scope.
- Suspending a client cuts its tokens. A client STX suspends or revokes stops resolving on its members’ live tokens and its app tokens at once.
- Two-factor stays with STX. It is enforced at STX sign-in; your app only receives tokens after the member has authenticated.
- No scope moves money. See Scopes.
A checklist for production
Section titled “A checklist for production”- Store
client_secret, access and refresh tokens server-side and encrypted where you have a server. - Verify
stateon every redirect back; keep the PKCE verifier per pending sign-in. - Refresh proactively; store the rotated refresh token every time; never present one twice.
- Handle
401(refresh, then reconnect) and403(ask for the named scope) distinctly; see API errors. - Handle
403 ip_not_allowedseparately from403 insufficient_scope: call from an address on your allowlist. - Revoke on sign-out, and expect members to revoke you.
- Request the fewest scopes that do the job.

