With the TypeScript SDK
The TypeScript SDK, @stxapp/stx-typescript, runs the whole OAuth flow for you on Node.js 18+ and Bun: it creates the PKCE verifier and state, exchanges the code, refreshes and rotates tokens, and revokes them. How the flow works, what each scope allows and how to handle tokens safely are covered in the OAuth section; this page shows the SDK calls for each starting point.
npm install @stxapp/stx-typescriptSet up the client
Section titled “Set up the client”Everything on this page is imported from @stxapp/stx-typescript/oauth. Create one OAuthClient per registered app, on your server:
import { MemoryPendingAuthorizationStore, MemoryTokenStore, OAuthClient } from "@stxapp/stx-typescript/oauth";
const oauth = new OAuthClient({ // baseUrl defaults to STX_HOST: the demo host. clientId: process.env.STX_CLIENT_ID!, clientSecret: process.env.STX_CLIENT_SECRET, // keep it on the server redirectUri: "https://yourapp.example/stx/callback", // exactly as registered scope: "profile.read balance.read portfolio.read orders.read orders.write",});
// Sign-ins in progress, and each member's tokens.const pending = new MemoryPendingAuthorizationStore();const tokens = new MemoryTokenStore();The in-memory stores suit one process. In production, implement TokenStore (get, set, delete) and PendingAuthorizationStore (put, take) over your database, and encrypt tokens at rest. If several processes share one token store, also implement withLock, so only one of them refreshes a member’s token at a time.
Scope names are also exported as constants: MemberScopes.ORDERS_WRITE, AppScopes.MARKET_DATA and so on. What each one allows is listed in Scopes.
Connect your STX account
Section titled “Connect your STX account”For an app with its own accounts, on any login system; see You have an app with its own accounts. Two routes on your server: one sends the member to STX, the other receives them back. Here with Express:
// 1. Send the member to STX to sign in and approve your app.app.get("/stx/link", async (req, res) => { const { url } = await oauth.beginAuthorization(pending, { data: { userId: req.user.id } }); res.redirect(url);});
// 2. STX sends them back with a code. Exchange it and store the tokens under your own user id.app.get("/stx/callback", async (req, res) => { const done = await oauth.completeAuthorization(pending, req.url, { store: tokens, memberKey: (data) => String(data?.userId), }); res.send(`Your STX account is linked (${done.scopes.join(", ")}).`);});beginAuthorization() creates the PKCE verifier and state and keeps them in pending; completeAuthorization() checks the state, exchanges the code and saves the tokens. done.scopes is what the member granted, which can be less than you asked for. The steps it runs are described in Authorization code flow.
A member who declines, or whose STX account is still being verified, comes back with an error and no code. completeAuthorization() throws it as an STXOAuthException, whose error holds the code:
import { STXAccessDeniedException, STXAccountPendingException } from "@stxapp/stx-typescript/oauth";
try { await oauth.completeAuthorization(pending, req.url, { store: tokens, memberKey: (data) => String(data?.userId) });} catch (err) { if (err instanceof STXAccountPendingException) { res.send("STX is still verifying your account. Connect again once it is ready."); } else if (err instanceof STXAccessDeniedException) { res.send("Your STX account was not connected."); } else { throw err; }}If one callback URL serves several apps, split the second step: readCallback(pending, req.url) returns the code and the pending entry (with your data), and redeemAuthorization() on the right app’s OAuthClient finishes it.
Act for the member
Section titled “Act for the member”memberClient() returns an ordinary STX client that acts for one member. Every method works as it does with an API key, within the scopes the member granted:
const member = oauth.memberClient(tokens, userId);
const balance = await member.balance();console.log(balance.available_balance);
const open = await member.orders({ status: ["open"] });console.log(`${open.length} open orders`);
await member.placeOrder(marketId, "buy", "limit", { price: "0.40", quantity: "1" });The client refreshes the access token before it expires and after a 401, and writes the new tokens to your store (STX rotates refresh tokens, so the old one stops working; see Refreshing). Pass { session: { onRefresh, onGrantRevoked } } to hear about either.
The member’s channels work the same way:
const ws = await member .websocket({ // Called if access ends while the socket is open (revoked, or the token expired). onAuthError: (err) => console.log("link again:", err.message), }) .connect();
await ws.orders({ onMessage: (msg) => console.log(msg.event, msg.payload) });Market data as your app
Section titled “Market data as your app”With a client secret, your app can read the market catalogue and market data channels as itself, with no member involved (an app token):
const api = oauth.appClient("market_data");
const page = await api.markets({ status: "open", limit: 5 });for (const market of page.items) console.log(market.symbol, market.bids?.[0]?.price ?? "-");The app token is cached and renewed for you.
Unlink a member
Section titled “Unlink a member”const revoked = await oauth.unlink(tokens, userId);unlink() revokes the member’s grant at STX and deletes their tokens from your store. It resolves false if STX could not be reached; the local tokens are deleted either way.
Sign in with your STX account
Section titled “Sign in with your STX account”For an app with no login of its own; see You’re building a new app with no login yet. createConnect reads the endpoints and signing keys from the STX host, adds openid to your scopes, and verifies the ID token for you.
import { createConnect } from "@stxapp/stx-typescript/oauth";
const connect = createConnect({ issuer: process.env.STX_BASE_URL!, // the STX host, e.g. the US demo clientId: process.env.STX_CLIENT_ID!, clientSecret: process.env.STX_CLIENT_SECRET!, // keep it on the server redirectUri: "https://yourapp.example/auth/stx/callback", // exactly as registered scopes: ["profile.read", "balance.read", "orders.read", "orders.write"],});
// 1. Send the member to STX. Keep state, codeVerifier and nonce in their session.app.get("/auth/stx/start", async (req, res) => { const signIn = await connect.start(); req.session.stxSignIn = signIn; res.redirect(signIn.url);});
// 2. STX sends them back. Exchange the code, verify the ID token, and sign them in.app.get("/auth/stx/callback", async (req, res) => { const saved = req.session.stxSignIn; delete req.session.stxSignIn; // one use, whether or not the sign-in succeeds const { tokens, identity } = await connect.finish(req.originalUrl, saved); await users.upsert({ stxMemberId: identity.sub, email: identity.email }); await saveTokens(identity.sub, tokens); res.redirect("/");});connect.start({ connection: "google" })goes straight to Google, Apple ("apple") or X ("x") sign-in;loginHintpasses the member’s email;prompt: "none"reconnects a member who has already approved your app without showing a screen.connect.finish()throwsSTXAccessDeniedExceptionwhen the member declines,STXAccountPendingExceptionwhen their account is still being verified, andSTXOAuthExceptionwitherrorset toinvalid_id_tokenwhen the ID token does not verify.connect.userInfo(accessToken, { sub })reads the same identity from UserInfo later, and checks it is still the member you expect.connect.refresh(refreshToken)returns a new token pair; store what comes back.connect.disconnect(tokens)ends the connection at STX.
users and saveTokens stand for your own user table and token storage. To call the API for the member, create a client with their access token: new STX({ auth: new OAuthBearerAuth(tokens.accessToken) }), with STX from @stxapp/stx-typescript and OAuthBearerAuth from @stxapp/stx-typescript/oauth. Every method then works as under Act for the member.
If you add STX as a login option inside a login service instead, the service makes these calls and the SDK is not involved; see You use a login service like Auth0 or Clerk.
Errors
Section titled “Errors”Every OAuth error extends STXAuthenticationException or STXForbiddenException, and they are exported from @stxapp/stx-typescript/oauth:
| Exception | When | What to do |
|---|---|---|
STXGrantRevokedException |
The member revoked your app, or the grant ended. Their tokens are already deleted from your store. | Show “Link your STX account” again. |
STXInsufficientScopeException |
The grant lacks a scope the call needs; err.requiredScopes names it. |
Link again, asking for that scope. |
STXAccountPendingException, STXAccessDeniedException |
The member came back without connecting: their account is still being verified, or they declined. | Show a not-connected state and let them try again. |
STXOAuthException |
The callback or token endpoint returned any other error; err.error holds the code. |
Depends on the code; see Discovery and errors. |
STXInvalidTokenException |
The access token was refused even after a refresh. | Link again. |
import { STXGrantRevokedException, STXInsufficientScopeException } from "@stxapp/stx-typescript/oauth";
try { await member.placeOrder(marketId, "buy", "limit", { price: "0.40", quantity: "1" });} catch (err) { if (err instanceof STXGrantRevokedException) { res.redirect("/stx/link"); } else if (err instanceof STXInsufficientScopeException) { console.log("needs", err.requiredScopes); } else { throw err; }}What the module exports
Section titled “What the module exports”The package ships full TypeScript declarations for @stxapp/stx-typescript/oauth, so your editor shows every option and its documentation. The main pieces:
| Export | What it is |
|---|---|
createConnect() |
Sign in with an STX account: start(), finish(), refresh(), revoke() and disconnect(). |
OAuthClient |
One registered app. beginAuthorization(), completeAuthorization() and redeemAuthorization() link a member; memberClient() and appClient() return an STX client; unlink() and revoke() disconnect; refreshToken(), clientCredentials() and introspect() call the token endpoints directly. OAuthClient.fromMetadata() builds one from the server metadata. |
TokenStore, MemoryTokenStore |
Where each member’s token pair is kept: get, set, delete, and optionally withLock. |
PendingAuthorizationStore, MemoryPendingAuthorizationStore |
Where a sign-in in progress keeps its PKCE verifier and state: put and take. |
readCallback() |
Reads the code and the pending entry from a callback URL, for a callback shared by several apps. |
MemberScopes, AppScopes |
The scope names as constants. |
discoverMetadata(), registerClient() |
Fetch the server metadata, and register a client where dynamic registration is enabled. |
STXOAuthException, STXGrantRevokedException, STXInvalidTokenException, STXInsufficientScopeException |
The errors above. |
A complete app
Section titled “A complete app”Sideline is a working demo app built on this module: linking and unlinking, calls and live channels for the member, and public market data as the app.

