# With the TypeScript SDK

> Connect a member's STX account or sign them in with it, act for them, read market data as your app, and disconnect, with @stxapp/stx-typescript/oauth.

Source: https://docs.stxapp.io/apps/typescript-sdk/

> **Availability.** Available on US demo now. Live access is coming soon in the US and Ontario. See [Environments](https://docs.stxapp.io/environments/).

The [TypeScript SDK](/sdks/typescript/), `@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](/oauth/) section; this page shows the SDK calls for each [starting point](/oauth/#start-from-where-you-are).

:::note
Your app's client credentials come with demo access; see [Get demo access](/apps/overview/#get-demo-access).
:::

```bash
npm install @stxapp/stx-typescript
```

## Set up the client

Everything on this page is imported from `@stxapp/stx-typescript/oauth`. Create one `OAuthClient` per registered app, on your server:

```ts
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](/oauth/scopes/).

## Connect your STX account

For an app with its own accounts, on any login system; see [You have an app with its own accounts](/oauth/connect-your-stx-account/). Two routes on your server: one sends the member to STX, the other receives them back. Here with Express:

```ts
// 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](/oauth/authorization-flow/).

A member who declines, or whose STX account is [still being verified](/oauth/members-being-verified/), comes back with an error and no code. `completeAuthorization()` throws it as an `STXOAuthException`, whose `error` holds the code:

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

`memberClient()` returns an ordinary [`STX`](/sdks/typescript/reference/stx/) client that acts for one member. Every method works as it does with an API key, within the scopes the member granted:

```ts
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](/oauth/tokens-and-security/#refreshing)). Pass `{ session: { onRefresh, onGrantRevoked } }` to hear about either.

The member's channels work the same way:

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

With a client secret, your app can read the market catalogue and market data channels as itself, with no member involved (an [app token](/oauth/authorization-flow/#app-tokens-client-credentials)):

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

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

For an app with no login of its own; see [You're building a new app with no login yet](/oauth/sign-in-with-your-stx-account/). `createConnect` reads the endpoints and signing keys from the STX host, adds `openid` to your scopes, and verifies the ID token for you.

```ts
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; `loginHint` passes the member's email; `prompt: "none"` reconnects a member who has already approved your app without showing a screen.
- `connect.finish()` throws `STXAccessDeniedException` when the member declines, `STXAccountPendingException` when their account is [still being verified](/oauth/members-being-verified/), and `STXOAuthException` with `error` set to `invalid_id_token` when the ID token does not verify.
- `connect.userInfo(accessToken, { sub })` reads the same identity from [UserInfo](/oauth/sign-in-with-your-stx-account/#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](#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](/oauth/sign-in-with-your-stx-account/login-vendor/).

## Errors

Every OAuth error extends [`STXAuthenticationException` or `STXForbiddenException`](/sdks/typescript/reference/errors/), 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](/oauth/discovery-and-errors/). |
| `STXInvalidTokenException` | The access token was refused even after a refresh. | Link again. |

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

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](/oauth/discovery-and-errors/#authorization-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](/oauth/tokens-and-security/#dynamic-registration) is enabled. |
| `STXOAuthException`, `STXGrantRevokedException`, `STXInvalidTokenException`, `STXInsufficientScopeException` | The errors above. |

## A complete app

[Sideline](https://github.com/stxapp/stx-isv-demo) 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.
