Skip to main content

Access your Wealthsimple account using their (GraphQL) API.

Project description

Unofficial Wealthsimple API Library for Python

This library allows you to access your own Wealthsimple account using the Wealthsimple (GraphQL) API.

Python 3.10+ required

Features

  • Retrieve all accounts (RRSP, TFSA, FHSA, cash, margin, crypto, credit card)
  • Get account balances and positions
  • Fetch historical performance (net value, deposits, gains)
  • Access transaction/activity history
  • Search securities and get market data
  • Historical price quotes
  • Handle 2FA (TOTP) authentication
  • Automatic session refresh
  • Optional security data caching

Installation

uv add ws-api
# or
pip install ws-api

Basic Example

from ws_api import WealthsimpleAPI

# Login (will prompt for username/password/TOTP)
session = WealthsimpleAPI.login(username="you@example.com", password="yourpassword")
ws = WealthsimpleAPI.from_token(session)

# Get your accounts
accounts = ws.get_accounts()
for account in accounts:
    print(f"{account['description']}: {account['number']}")

Full Example

Note: You'll need the keyring package to run the code below. Install with: uv add keyring (or pip install keyring)

from datetime import datetime
import json
import keyring
import os
import tempfile
from ws_api import WealthsimpleAPI, OTPRequiredException, LoginFailedException, WSAPISession

class WSApiTest:
    def main(self):
        # 1. Define a function that will be called when the session is created or updated. Persist the session to a safe place, like in the keyring
        keyring_service_name = "foo.bar"
        persist_session_fct = lambda sess, uname: keyring.set_password(f"{keyring_service_name}.{uname}", "session", sess)
        # The session contains tokens that can be used to empty your Wealthsimple account, so treat it with respect!
        # i.e. don't store it in a Git repository, or anywhere it can be accessed by others!

        # If you want, you can set a custom User-Agent for the requests to the WealthSimple API:
        WealthsimpleAPI.set_user_agent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36")
    
        # 2. If it's the first time you run this, create a new session using the username & password (and TOTP answer, if needed). Do NOT save those infos in your code!
        username = input("Wealthsimple username (email): ")
        session = keyring.get_password(f"{keyring_service_name}.{username}", "session")
        if session:
            try:
                # 3a. Use the save session to instantiate the API object
                ws = WealthsimpleAPI.from_token(session, persist_session_fct, username)
                # persist_session_fct is needed here, because the session may be updated if the access token expired, and thus this function will be called to save the new session
            except Exception as e:
                print(f"Error trying to use stored tokens: {e}")
                session = None
                # Continue below with a regular log-in

        if not session:
            # 3b. Need to log-in interactively
            username = None
            password = None
            otp_answer = None
            while True:
                try:
                    if not username:
                        username = input("Wealthsimple username (email): ")
                        session = keyring.get_password(f"{keyring_service_name}.{username}", "session")
                        if session:
                            session = WSAPISession.from_json(session)
                            break
                    if not password:
                        password = input("Password: ")
                    WealthsimpleAPI.login(username, password, otp_answer, persist_session_fct=persist_session_fct)
                    # The above will throw exceptions if login failed
                    # So we break (out of the login "while True" loop) on success:
                    session = WSAPISession.from_json(keyring.get_password(keyring_service_name, "session"))
                    break
                except OTPRequiredException:
                    otp_answer = input("TOTP code: ")
                except LoginFailedException:
                    print("Login failed. Try again.")
                    username = None
                    password = None
            # Use the new session object to instantiate the API object
            ws = WealthsimpleAPI.from_token(session, persist_session_fct, username)
        
        # Optionally define functions to cache market data, if you want transactions' descriptions and accounts balances to show the security's symbol instead of its ID
        # eg. sec-s-e7947deb977341ff9f0ddcf13703e9a6 => TSX:XEQT
        def sec_info_getter_fn(ws_security_id: str):
            cache_file_path = os.path.join(tempfile.gettempdir(), f"ws-api-{ws_security_id}.json")
            if os.path.exists(cache_file_path):
                return json.load(open(cache_file_path, 'r'))
            return None
        def sec_info_setter_fn(ws_security_id: str, market_data: object):
            cache_file_path = os.path.join(tempfile.gettempdir(), f"ws-api-{ws_security_id}.json")
            # noinspection PyTypeChecker
            json.dump(market_data, open(cache_file_path, 'w'))
            return market_data
        ws.set_security_market_data_cache(sec_info_getter_fn, sec_info_setter_fn)
        
        # 4. Use the API object to access your WS accounts
        accounts = ws.get_accounts()

        print("All Accounts Historical Value & Gains:")
        historical_fins = ws.get_identity_historical_financials([a['id'] for a in accounts])
        for hf in historical_fins:
            value = float(hf['netLiquidationValueV2']['amount'])
            deposits = float(hf['netDepositsV2']['amount'])
            gains = value - deposits
            print(f"  - {hf['date']} = ${value:,.0f} - {deposits:,.0f} (deposits) = {gains:,.0f} (gains)")
        
        for account in accounts:
            print(f"Account: {account['description']} ({account['number']})")
            if account['description'] == account['unifiedAccountType']:
                # This is an "unknown" account, for which description is generic; please open an issue on https://github.com/gboudreau/ws-api-python/issues and include the following:
                print(f"    Unknown account: {account}")

            if 'credit-card' in account['id']:
                cc_infos = ws.get_creditcard_account(account['id'])
                balance = cc_infos['balance']['current']
                print(f"  Credit card balance: {balance} CAD")
            else:
                if account['currency'] == 'CAD':
                    value = account['financials']['currentCombined']['netLiquidationValue']['amount']
                    print(f"  Net worth: {value} {account['currency']}")    
                # Note: For USD accounts, value is the CAD value converted to USD
                # For USD accounts, only the balance & positions are relevant
        
                # Cash and positions balances
                balances = ws.get_account_balances(account['id'])
                for cash_balance_key in [ 'sec-c-usd', 'sec-c-cad']:
                    cash_balance = float(balances.get(cash_balance_key, 0))
                    print(f"  Available (cash) balance: {cash_balance} {cash_balance_key.split('-')[-1].upper()}")
        
                if len(balances) > 1:
                    print("  Assets:")
                    for security, bal in balances.items():
                        if security in ['sec-c-cad', 'sec-c-usd']:
                            continue
                        print(f"  - {security} x {bal}")
        
            print("  Historical Value & Gains:")
            historical_fins = ws.get_account_historical_financials(account['id'], account['currency'])            
            for hf in historical_fins:
                value = hf['netLiquidationValueV2']['cents'] / 100
                deposits = hf['netDepositsV2']['cents'] / 100
                gains = value - deposits
                print(f"  - {hf['date']} = ${value:,.0f} - {deposits:,.0f} (deposits) = {gains:,.0f} (gains)")
            
            # Fetch activities (transactions)
            acts = ws.get_activities(account['id'])
            if acts:
                print("  Transactions:")
                acts.reverse()  # Activities are sorted by OCCURRED_AT_DESC by default

                for act in acts:
                    if act['type'] == 'DIY_BUY':
                        act['amountSign'] = 'negative'
        
                    # Print transaction details
                    print(
                        f"  - [{datetime.strptime(act['occurredAt'].replace(':', ''), '%Y-%m-%dT%H%M%S.%f%z')}] [{act['canonicalId']}] {act['description']} "
                        f"{'+' if act['amountSign'] == 'positive' else '-'}{act['amount']} {act['currency']}")

                    if act['description'] == f"{act['type']}: {act['subType']}":
                        # This is an "unknown" transaction, for which description is generic; please open an issue on https://github.com/gboudreau/ws-api-python/issues and include the following:
                        print(f"    Unknown activity: {act}")
    
            print()

if __name__ == "__main__":
    WSApiTest().main()

Projects Using It

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ws_api-0.33.0.tar.gz (53.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ws_api-0.33.0-py3-none-any.whl (46.2 kB view details)

Uploaded Python 3

File details

Details for the file ws_api-0.33.0.tar.gz.

File metadata

  • Download URL: ws_api-0.33.0.tar.gz
  • Upload date:
  • Size: 53.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for ws_api-0.33.0.tar.gz
Algorithm Hash digest
SHA256 4dc7b437656eebfaf6320f38192b6b1493162dd7d7d70d074e52089561838a35
MD5 3216a8007179bfe9a1fbc97f0ba5f55e
BLAKE2b-256 38679539c63c1fc520bc6ecf323125ce9bb96db3030e365a6395d8b4bf23b7a0

See more details on using hashes here.

Provenance

The following attestation bundles were made for ws_api-0.33.0.tar.gz:

Publisher: publish.yml on gboudreau/ws-api-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file ws_api-0.33.0-py3-none-any.whl.

File metadata

  • Download URL: ws_api-0.33.0-py3-none-any.whl
  • Upload date:
  • Size: 46.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for ws_api-0.33.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f239b42a7c188fe469716aaafe32d05b3619bb3002a2ee38cc25a491de297bff
MD5 c8da89ce09480182633db99662a932e9
BLAKE2b-256 c3797fe07c5ed6fa346c3232e58b5b6d43a76ee5dd875f5ad909ebc657e86c72

See more details on using hashes here.

Provenance

The following attestation bundles were made for ws_api-0.33.0-py3-none-any.whl:

Publisher: publish.yml on gboudreau/ws-api-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page