Apps: connect members
Describes stx-python 0.8.0, the current release.
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 and 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. To sign members in to your app with their STX account (an ID token saying who they are), see Sign in with STX.
Connecting members needs an exchange that offers member sign-in. What you can do lists which exchanges do; elsewhere the calls below raise STXNotAvailableError.
The client
Section titled “The client”Register your app in STX Apps (see 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:
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
Section titled “Two stores”The SDK keeps two kinds of state through stores you provide:
- a pending sign-in store, keyed by the
stateof 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:
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
Section titled “Connect a member”Two routes in your web app: one sends the member to STX, the other receives them back.
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:
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=Qcbwvbf3IJ2ydAth8v8i8IUU8F3gUwPWSTX 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
Section titled “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.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
Section titled “Act for the member”member_client gives an STX client for one member, over your token store:
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 and Portfolio. Each call is limited to the scopes the member granted; see 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:
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. 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
Section titled “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 |
Pass on_grant_revoked= and on_terms_required= to member_client to be called back when those happen.
New terms
Section titled “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:
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.
Disconnect a member
Section titled “Disconnect a member”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
Section titled “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.

