# With the TypeScript SDK

> Link a member, act for them, read market data as your app, and unlink, with @stxapp/stx-typescript/oauth.

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

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.

:::note
Your app's client credentials come with demo access; see [Get demo access](/isv/#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 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](/oauth/scopes/).

## Link a member

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/).

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.

## 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. |
| `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](/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 |
| --- | --- |
| `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 app built on this module: linking and unlinking, calls and live channels for the member, and public market data as the app.
