# Apps: connect members

> Let STX members connect their account to your app, then trade and read their account for them, with stx-python.

Source: https://docs.stxapp.io/sdks/python/apps/

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

An app acts for STX members who connect their account to it: they sign in on STX, approve the scopes you ask for, and your server gets tokens for them. `stx.oauth` runs the whole flow, from the redirect to refreshing and revoking tokens, and gives you an `STX` client acting for each member. What an app is, how to register one and how the protocol works are on [Apps](/apps/overview/) and [OAuth](/oauth/). Everything here is server-side: the client secret and the tokens never go to a browser.

An app can also act as itself, with no member, to read the market catalog and stream market data: see [App tokens and market data](/sdks/python/app-tokens/). To sign members in to your app with their STX account (an ID token saying who they are), see [Sign in with STX](/sdks/python/sign-in-with-stx/).

Connecting members needs an exchange that offers member sign-in. [What you can do](/sdks/python/capabilities/) lists which exchanges do; elsewhere the calls below raise `STXNotAvailableError`.

## The client

Register your app in STX Apps (see [Get demo access](/apps/overview/#get-demo-access)) to get a `client_id`, a `client_secret` and your callback URL. Then build one `OAuthClient` per app, from the exchange's discovery document:

```python
from stx import oauth

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

`discover` reads `/.well-known/oauth-authorization-server`, takes every endpoint from it, and from then on refuses a scope the exchange does not offer before a member is sent anywhere. `client_id`, `client_secret` and `redirect_uri` can also come from `STX_OAUTH_CLIENT_ID`, `STX_OAUTH_CLIENT_SECRET` and `STX_OAUTH_REDIRECT_URI`. A Browser app or Mobile app has no secret: leave `client_secret` out and PKCE is its proof.

For asyncio, `oauth.AsyncOAuthClient` has the same methods to `await`; `await oauth.AsyncOAuthClient.discover(...)` builds it.

## Two stores

The SDK keeps two kinds of state through stores you provide:

- a **pending sign-in store**, keyed by the `state` of each redirect: the PKCE verifier and your own data, kept for ten minutes and used once;
- a **token store**, keyed by any stable id you choose for a member (your user id): their access and refresh tokens.

`oauth.MemoryPendingAuthorizationStore` and `oauth.MemoryTokenStore` keep them in memory, which suits one process and tests. In production, implement the same methods over your database or cache, plain or `async`, and encrypt tokens at rest:

```python
from typing import Optional

from stx import oauth

class DbTokenStore:
    def get(self, member_key: str) -> Optional[oauth.StoredTokens]:
        row = db.fetch_one("select tokens from stx_links where user_id = %s", member_key)
        return oauth.StoredTokens.from_dict(decrypt(row["tokens"])) if row else None

    def set(self, member_key: str, tokens: oauth.StoredTokens) -> None:
        db.upsert("stx_links", user_id=member_key, tokens=encrypt(tokens.to_dict()))

    def delete(self, member_key: str) -> None:
        db.execute("delete from stx_links where user_id = %s", member_key)
```

STX rotates refresh tokens, and a refresh token used twice ends the member's connection. Within one process the SDK refreshes each member once at a time. When several processes share a token store, give it a `lock(member_key)` method returning an async context manager (a database row lock, a Redis lock) so only one of them refreshes a member at a time.

## Connect a member

Two routes in your web app: one sends the member to STX, the other receives them back.

```python
pending = DbPendingStore()   # or oauth.MemoryPendingAuthorizationStore()
tokens = DbTokenStore()      # or 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)
    return redirect("/portfolio")
```

`begin_authorization` makes a fresh PKCE pair and `state`, saves them in `pending` with your `data`, and returns the URL to send the member to:

```text
https://demo.stxapp.io/oauth/authorize?response_type=code&client_id=stxapp_client_g2XU2Kyg3EJoOhC9sndPe2bPvZENsbkkFkzCQFgkWHa0emOUt&redirect_uri=https%3A%2F%2Fyour.app%2Fcallback&scope=profile.read+balance.read+orders.read&code_challenge=vDagE4Ezn74P2Avdk9vJ0tDRgxnYxmIA4mCiE--0TTI&code_challenge_method=S256&state=Qcbwvbf3IJ2ydAth8v8i8IUU8F3gUwPW
```

STX signs the member in, shows them your app and the scopes, and sends them back to your callback. `complete_authorization` checks and uses up the `state`, exchanges the code with the stored verifier, and saves the tokens under the key you give. `done.scopes` is what the member granted, which can be narrower than what you asked for.

`begin_authorization` also takes `scope=` (this sign-in only), `connection=` (`google`, `apple` or `x`, to skip STX's login page), `login_hint=` (the member's email), `prompt=` and `ttl=` (seconds the sign-in stays open).

### When the member does not connect

Every way the callback can fail is a typed exception, and the `state` is always checked first, so an error only counts for a sign-in your app started:

| Exception | When |
|---|---|
| `oauth.STXAccessDeniedException` | The member declined, or their account cannot be connected (`error=access_denied`) |
| `oauth.STXAccountPendingException` | The member's STX account is still being verified (`error=account_pending`). See [Members still being verified](/oauth/members-being-verified/) |
| `oauth.STXTermsNotAcceptedException` | The member left the STX terms page without agreeing (`error=terms_required`) |
| `oauth.STXOAuthException` | Anything else; `.error` is `invalid_state` (unknown, used or expired state), `invalid_request` (no code), `invalid_grant` (a used or expired code) or the exchange's code |

Nothing is granted in any of these cases; show your own "not connected" state and let the member start again.

## Act for the member

`member_client` gives an `STX` client for one member, over your token store:

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

Every member method works the same way it does with an API key: `balance`, `positions`, `orders`, `fills`, `settlements`, `place_order`, `place_orders`, `cancel_order`, and the rest in [Trading](/sdks/python/trading/) and [Portfolio](/sdks/python/portfolio/). Each call is limited to the scopes the member granted; see [Scopes](/oauth/scopes/).

The client sends the member's access token, refreshes it 30 seconds before it expires and once more if the exchange answers `401`, and stores the rotated pair. Pass `on_refresh=` to be told after each refresh.

For the member's channels, use `oauth.AsyncOAuthClient` and the socket of its member client:

```python
async with oauth_client.member_client(tokens, "u1") as stx:
    async with stx.websocket() as ws:
        view = await ws.account_view(on_change=lambda change: print(change.kind))
        print(view.balance["available_balance"], len(view.open_orders))
        await ws.run_forever()
```

The socket sends the member's token on every connect, fresh after a refresh. `ws.account_view()` keeps a live copy of the balance, open orders, fills and positions from the four account channels; every channel is in [WebSockets](/sdks/python/websockets/). A member token without `profile.read` still works: the SDK reads the member's id from their balance.

A member token you already hold, from your own OAuth code, is enough on its own: `STX(region="us", env="demo", access_token=token)`. It is not refreshed.

## When access ends or changes

| Raised from a member call | Meaning | What to do |
|---|---|---|
| `oauth.STXGrantRevokedException` | The member disconnected your app in STX, or a refresh token was reused. The SDK has deleted their tokens | Show "not connected"; they can connect again |
| `oauth.STXAccountPendingException` | The member's account is being verified. Their tokens are kept | Try again once STX approves them |
| `stx.STXTermsRequiredException` | New STX terms are in effect and the member has not accepted them. Reads and cancels still work; orders are refused | Send the member to `exc.terms_url` (below) |
| `stx.STXInsufficientScopeException` | The call needs a scope the member did not grant; `exc.required_scopes` names it | Ask the member to connect again with that scope |
| `stx.STXIpNotAllowedException` | A write (placing or cancelling orders) 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/) |

Pass `on_grant_revoked=` and `on_terms_required=` to `member_client` to be called back when those happen.

### New terms

A member accepts the STX terms on an STX page, signed in to their own account; an app cannot accept them for anyone. Send the member there and give the page your return address:

```python
from stx import STXTermsRequiredException, oauth

try:
    stx.place_order(market_id, "buy", "limit", price="0.40", quantity="1")
except STXTermsRequiredException as exc:
    return redirect(oauth.terms_url_with_return(exc.terms_url, "https://your.app/portfolio"))
```

The return address must be one of your callback URLs or on the same origin as one. Once the member agrees, the same call works with the same tokens. See [Hosted pages](/apps/hosted-pages/).

## 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. `app.revoke(token)` revokes a single token: a refresh token ends the member's whole connection, an access token only its pair.

## The pieces, one by one

`begin_authorization` and `complete_authorization` are built from smaller calls you can use directly, for example when one callback URL serves several apps:

| Call | Does |
|---|---|
| `app.authorization_url(scope=...)` | A PKCE pair, a `state` and the authorize URL, without storing anything |
| `app.read_callback(pending, url)` | Checks the callback and uses up its pending sign-in, without calling STX |
| `app.redeem_authorization(result, store=..., member_key=...)` | Exchanges a callback already read |
| `app.exchange_code(code, code_verifier)` | The token request for a code |
| `app.refresh_token(refresh_token)` | One refresh; store what comes back |
| `app.session(tokens, key)` | The auth provider behind `member_client`, for `STX(auth=...)` |

Every class and method is in the [API reference](/sdks/python/reference/oauth/).
