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.
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 from your invite. 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.
Link a member
Section titled “Link a member”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.
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.
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. |
STXOAuthException |
The callback or token endpoint returned an error; err.error holds the code, such as access_denied when the member declines. |
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 |
|---|---|
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 app built on this module: linking and unlinking, calls and live channels for the member, and public market data as the app.

