# With the Python SDK

> Connect a member's STX account or sign them in with it, act for them, read market data as your app, and handle typed errors, with stx-python.

Source: https://docs.stxapp.io/apps/python-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 [Python SDK](/sdks/python/), `stx-python`, runs the whole OAuth flow for you on your server: `stx.oauth` creates the PKCE verifier and `state`, exchanges the code, refreshes and rotates tokens, and revokes them, and hands you an ordinary `STX` client for each member or for your app. How the flow works, what each scope allows and how to handle tokens safely are covered in the [OAuth](/oauth/) section. This page is the short tour; each step links to the Python SDK guide page with the full detail.

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

```bash
pip install stx-python
```

## Set up the client

Build one `OAuthClient` per registered app, on your server. `discover` reads the exchange's [server metadata](/oauth/discovery-and-errors/#authorization-server-metadata) and takes every endpoint from it:

```python
from stx import oauth

app = oauth.OAuthClient.discover(
    client_id="stxapp_client_g2XU2Kyg3EJoOhC9sndPe2bPvZENsbkkFkzCQFgkWHa0emOUt",
    client_secret=CLIENT_SECRET,  # from your secret store, never sent to a browser
    redirect_uri="https://your.app/callback",  # exactly as registered
    scope=["profile.read", "balance.read", "orders.read", "orders.write"],
    region="us",
    env="demo",
)
```

`client_id`, `client_secret` and `redirect_uri` can also come from `STX_OAUTH_CLIENT_ID`, `STX_OAUTH_CLIENT_SECRET` and `STX_OAUTH_REDIRECT_URI`. For asyncio, `await oauth.AsyncOAuthClient.discover(...)` gives the same methods to `await`.

The SDK keeps two kinds of state in stores you provide: sign-ins in progress, and each member's tokens. `oauth.MemoryPendingAuthorizationStore` and `oauth.MemoryTokenStore` suit one process; in production, implement the same methods over your database and encrypt tokens at rest. See [Two stores](/sdks/python/apps#two-stores).

## Connect a member

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.

```python
pending = oauth.MemoryPendingAuthorizationStore()
tokens = oauth.MemoryTokenStore()

@route("/connect-stx")
def connect_stx(request):
    auth = app.begin_authorization(pending, data={"user_id": request.user.id})
    return redirect(auth.url)

@route("/callback")
def callback(request):
    done = app.complete_authorization(
        pending, request.url, store=tokens, member_key=lambda data: data["user_id"]
    )
    print(done.member_key, done.scopes)  # what the member granted, which can be less than you asked for
    return redirect("/portfolio")
```

A member who declines, or whose account is [still being verified](/oauth/members-being-verified/), comes back without a code, and `complete_authorization` raises a typed exception: `oauth.STXAccessDeniedException`, `oauth.STXAccountPendingException` or `oauth.STXTermsNotAcceptedException`. Show your own "not connected" state and let them start again. The full flow, every option and every callback error are on [Apps: connect members](/sdks/python/apps#connect-a-member).

## Act for the member

`member_client` returns an `STX` client acting for one member. Every method works as it does with an API key, within the scopes the member granted:

```python
with app.member_client(tokens, "u1") as stx:
    print(stx.balance().available_balance)
    order = stx.place_order(market_id, "buy", "limit", price="0.40", quantity="1")
    stx.cancel_order(order.id)
```

The client refreshes the access token 30 seconds before it expires and once more after a `401`, and writes the rotated pair to your store (STX rotates refresh tokens; see [Refreshing](/oauth/tokens-and-security/#refreshing)). With `oauth.AsyncOAuthClient`, the member client's socket streams the member's channels, including `ws.account_view()`, a live copy of their balance, open orders, fills and positions. See [Act for the member](/sdks/python/apps#act-for-the-member).

## Market data as your app

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

```python
with app.app_client("market_data events") as catalog:
    for market in catalog.markets(status="open", trading=True, limit=3):
        print(market.symbol, market.title)
```

Sample output, from US demo:

```text
STXMLB-26OCT182000LADMIL-GAMELAD MLB Playoff LAD @ MIL
STXMLB-26OCT182000LADMIL-GAMEMIL MLB Playoff LAD @ MIL
STXMLB-26OCT192000LADMIL-GAMELAD MLB Playoff LAD @ MIL
```

The SDK mints the app token on first use and renews it before it expires. An app token cannot make member calls: `balance()` or `place_order()` raise `STXForbiddenException`. In Ontario, once live, an app reads public market data only, with the `market_data` and `events` scopes, and does not act for members. Streaming as your app, and what each scope can call, are on [App tokens and market data](/sdks/python/app-tokens).

An approved app holding `markets.write` can also list its own player-prop markets; see [Create markets](/sdks/python/create-markets) and [List your own markets on STX](/apps/creating-markets/).

## Disconnect a member

```python
app.unlink(tokens, "u1")
```

`unlink` revokes the member's grant at STX and deletes their tokens from your store. It returns `True` when STX confirmed the revoke, and deletes the local tokens 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/). `oauth.Connect` adds `openid` and a `nonce`, and verifies the ID token for you:

```python
import os

from stx import oauth

connect = oauth.Connect(
    issuer=os.environ["STX_BASE_URL"],  # the STX host, e.g. the US demo
    client_id=CLIENT_ID,
    client_secret=CLIENT_SECRET,
    redirect_uri="https://your.app/auth/stx/callback",
    scopes=["balance.read", "orders.read", "orders.write"],
)

@route("/auth/stx/start")
def start(request):
    started = connect.start(connection=request.args.get("connection"))  # "google", "apple", "x" or None
    request.session["stx_sign_in"] = started.saved().to_dict()
    return redirect(started.url)

@route("/auth/stx/callback")
def callback(request):
    saved = oauth.SavedSignIn.from_dict(request.session.pop("stx_sign_in"))
    done = connect.finish(request.url, saved)
    user = users.find_or_create(stx_sub=done.identity.sub, email=done.identity.email)
    tokens.set(user.id, done.tokens.to_stored())
    return redirect("/")
```

Key your user record on `done.identity.sub`, not the email. `users` stands for your own user table. A bad ID token raises `oauth.STXOAuthException` with `error == "invalid_id_token"`. See [Sign in with STX](/sdks/python/sign-in-with-stx).

## Errors

Every error is a typed exception you can catch by class. The ones an app meets:

| Exception | When | What to do |
| --- | --- | --- |
| `oauth.STXGrantRevokedException` | The member disconnected your app, or a refresh token was reused. Their tokens are already deleted from your store. | Show "Connect your STX account" again. |
| `stx.STXInsufficientScopeException` | The grant lacks a scope the call needs; `exc.required_scopes` names it. | Ask the member to connect again with that scope. |
| `stx.STXTermsRequiredException` | New STX terms are in effect and the member has not accepted them. Reads and cancels still work. | Send them to `oauth.terms_url_with_return(exc.terms_url, your_return_url)`. See [Hosted pages](/apps/hosted-pages/). |
| `stx.STXIpNotAllowedException` | A write from an address not on your app's IP allow-list. | Add the address in STX Apps. See [IP allow-list](/oauth/ip-allow-list/). |
| `stx.STXNotAvailableError` | The exchange does not offer the feature yet; `exc.feature` and `exc.host` name both. | Check `client.capabilities()` first. See [What you can do](/sdks/python/capabilities). |

```python
from stx import STXInsufficientScopeException, oauth

try:
    with app.member_client(tokens, "u1") as stx:
        stx.place_order(market_id, "buy", "limit", price="0.40", quantity="1")
except oauth.STXGrantRevokedException:
    return redirect("/connect-stx")
except STXInsufficientScopeException as exc:
    print("needs", exc.required_scopes)
```

On an exchange without OAuth, `discover` itself raises `STXNotAvailableError`. Captured on Ontario demo:

```text
OAuthClient.discover: STXNotAvailableError: OAuth is not available on demo.stxapp.ca yet.  feature='oauth' host='demo.stxapp.ca'
```

Every exception the SDK raises is on [Errors & retries](/sdks/python/errors-and-retries) and in the [API reference](/sdks/python/reference/errors).

## What `stx.oauth` provides

| Piece | What it is |
| --- | --- |
| `OAuthClient`, `AsyncOAuthClient` | One registered app. `begin_authorization()` and `complete_authorization()` connect a member; `member_client()` and `app_client()` return an `STX` client; `unlink()` and `revoke()` disconnect; `client_credentials()` and `refresh_token()` call the token endpoint directly. |
| `Connect`, `AsyncConnect` | Sign in with STX: `start()`, `finish()`, `refresh()`, `user_info()` and `disconnect()`. |
| `MemoryTokenStore`, `MemoryPendingAuthorizationStore` | In-memory stores for one process and tests. Your own stores implement the same methods, plain or `async`. |
| `terms_url_with_return()` | The STX terms page with your return address, for `STXTermsRequiredException`. |

Every class and method is in the API reference: [OAuth apps](/sdks/python/reference/oauth) and [Sign in with STX](/sdks/python/reference/connect).

## A complete app

The [Python sample app](https://github.com/stxapp/stx-python-demo/tree/main/examples/app) is a working FastAPI app built on `stx.oauth`, with two login modes: members sign in with their STX account (`LOGIN_MODE=stx`), or sign in to your app and then connect their STX account (`LOGIN_MODE=own`). It covers the member dashboard (balance, positions, open orders, fills and settlements), placing and cancelling an order, the market list and a live order book on the app token, live account updates, disconnecting, and a page for each typed error. Its README walks through getting credentials at STX Apps and running it against the demo environment.
