The (Unofficial) Guide to Charles Schwab’s Trader APIs

Carsten Savage
6 min readApr 12, 2024

--

For retail traders, there are two main groups of Charles Schwab (CS) APIs: “Accounts and Trading Production”, and “Market Data Production”. In a single app (application), you may want only one, or both. “Accounts and Trading Production” enables your application to get an account’s positions, cash available for trading, and make a trade, for example, while “Market Data Production” focuses on getting current quotes, price history, and daily movers in a given index.

Before I get started, I would like to thank Tyler Bowers for his fantastic YouTube video and the GitHub repo which are another tremendous resource for getting started with Schwab’s Trader APIs. This guide will be primarily in Python, but the methodology can be adapted to other programming languages.

Please check out my YouTube videos, Build Your Own Trading Bot Part 1 — Charles Schwab API Foundation and Build Your Own Trading Bot Part 2 — Charles Schwab API Foundation, where I teach you how to implement this in-depth.

To get started, your first step will be creating a new account in the Schwab Developer Portal. This account needs to be separate from your Charles Schwab portfolio account. You’ll be able to link them later on in the process, however.

Once you have a Developer Portal account, your next step will be creating a registered application.

  • In the Dashboard, click “Create App”.
  • In the API products dropdown — Choose one or both of “Market Data Production” and “Accounts and Trading Production”.
  • Create an app name.
  • Enter this app callback URL: https://127.0.0.1
  • The callback URL above is the localhost IP address for your local machine. This IP address is needed so that you can get the first batch of authentication tokens on your local machine (in your browser, which I’ll touch on later).

Once you submit your app registration, it will take a few days to process. Initially, it will have an “Approved — Pending” Status label. Once it is approved, the Dashboard Status will be “Ready for Use”.

Once your application is approved, you need to get the first batch of authentication tokens. Below is a modified version of the code from Tyler Bowers’ YouTube video:

import os
import base64
import requests
import webbrowser
from loguru import logger


def construct_init_auth_url() -> tuple[str, str, str]:

app_key = "your-app-key"
app_secret = "your-app-secret"

auth_url = f"https://api.schwabapi.com/v1/oauth/authorize?client_id={app_key}&redirect_uri=https://127.0.0.1"

logger.info("Click to authenticate:")
logger.info(auth_url)

return app_key, app_secret, auth_url


def construct_headers_and_payload(returned_url, app_key, app_secret):
response_code = f"{returned_url[returned_url.index('code=') + 5: returned_url.index('%40')]}@"

credentials = f"{app_key}:{app_secret}"
base64_credentials = base64.b64encode(credentials.encode("utf-8")).decode(
"utf-8"
)

headers = {
"Authorization": f"Basic {base64_credentials}",
"Content-Type": "application/x-www-form-urlencoded",
}

payload = {
"grant_type": "authorization_code",
"code": response_code,
"redirect_uri": "https://127.0.0.1",
}

return headers, payload


def retrieve_tokens(headers, payload) -> dict:
init_token_response = requests.post(
url="https://api.schwabapi.com/v1/oauth/token",
headers=headers,
data=payload,
)

init_tokens_dict = init_token_response.json()

return init_tokens_dict


def main():
app_key, app_secret, cs_auth_url = construct_init_auth_url()
webbrowser.open(cs_auth_url)

logger.info("Paste Returned URL:")
returned_url = input()

init_token_headers, init_token_payload = construct_headers_and_payload(
returned_url, app_key, app_secret
)

init_tokens_dict = retrieve_tokens(
headers=init_token_headers, payload=init_token_payload
)

logger.debug(init_tokens_dict)

return "Done!"


if __name__ == "__main__":
main()

When you run the code above, the code will open a Charles Schwab login screen in your browser. Here, you need to log in using your existing Charles Schwab portfolio credentials, not your Developer Portal credentials. From there, you will select the brokerage account(s) to which you elect to give the APIs access.

From there, you’ll be routed to an empty page, which contains a URL in the search bar with an access code embedded in it. You’ll need to copy the entire URL. The code will prompt you to paste it in terminal, and once you do so, the code will make a request for the first batch of authentication tokens, and logger will print the tokens out for you in terminal.

At this point, you have all of the tokens you need. The access token, however, expires after 30 minutes, and your application needs this access token every time it makes a request to the Trader APIs. To keep the access token refreshed, you need to exchange your current refresh token with Schwab and get a new access token (while the refresh token stays constant), etc. every 29 minutes to ensure you have a working access token at all times — or at least during market hours. You need to execute the initial authentication every 7 days because Schwab forces the refresh token expiration after 7 days. The next step, then, is setting up a refresh token system.

import os
from flask import Request
import base64
import requests
from loguru import logger


def refresh_tokens():
logger.info("Initializing...")

app_key = "your-app-key"
app_secret = "your-app-secret"

# You can pull this from a local file,
# Google Cloud Firestore/Secret Manager, etc.
refresh_token_value = "your-current-refresh-token"

payload = {
"grant_type": "refresh_token",
"refresh_token": refresh_token_value,
}
headers = {
"Authorization": f'Basic {base64.b64encode(f"{app_key}:{app_secret}".encode()).decode()}',
"Content-Type": "application/x-www-form-urlencoded",
}

refresh_token_response = requests.post(
url="https://api.schwabapi.com/v1/oauth/token",
headers=headers,
data=payload,
)
if refresh_token_response.status_code == 200:
logger.info("Retrieved new tokens successfully using refresh token.")
else:
logger.error(
f"Error refreshing access token: {refresh_token_response.text}"
)
return None

refresh_token_dict = refresh_token_response.json()

logger.debug(refresh_token_dict)

logger.info("Token dict refreshed.")

return "Done!"

if __name__ == "__main__":
refresh_tokens()

As you can see in the above, the code prints the new refresh token dictionary in the logs in terminal. What is not shown here, for the sake of simplicity, is a way to store and access the tokens.

To implement this, you can store the initial authentication tokens somewhere like Google Cloud Firestore or Secret Manager. Then, you can retrieve them when it’s time to refresh, use the current refresh token to get a new batch of tokens, and then overwrite them in Firestore/Secret Manager, etc. so that they’re ready for the next refresh.

Most of the Trader APIs are relatively straightforward. One especially tricky detail that’s not very well explained in the API documentation is that any API requests which include a particular brokerage account number need to use the hash value of the account number, not the account number itself.

To make things a bit easier, you can create a class (e.g., “AccountsTrading”), which instantiates with your current access token and the hash value of your account number, as seen below:

class AccountsTrading:
def __init__(self):
# Initialize access token during class instantiation
self.access_token = None
self.account_hash_value = None
self.refresh_access_token()
self.base_url = "https://api.schwabapi.com/trader/v1"
self.headers = {"Authorization": f"Bearer {self.access_token}"}
self.get_account_number_hash_value()

def refresh_access_token(self):
# Custom function to retrieve access token from Firestore
self.access_token = retrieve_firestore_value(
collection_id="your-collection-id",
document_id="your-doc-id",
key="your-access-token",
)

def get_account_number_hash_value(self):
response = requests.get(
self.base_url + f"/accounts/accountNumbers", headers=self.headers
)
response_frame = pandas.json_normalize(response.json())
self.account_hash_value = response_frame["hashValue"].iloc[0]

From here, if you want to use your account number in a request, you can feed in the hash value:

    def create_order(self, order_payload):
order_payload = json.dumps(order_payload)
response = requests.post(
self.base_url + f"/accounts/{self.account_hash_value}/orders",
data=order_payload,
headers={
"Authorization": f"Bearer {self.access_token}",
"accept": "*/*",
"Content-Type": "application/json",
},
)
# Custom function to create dataframe from response
response_frame = return_dataframe_from_response(response)
return response_frame

You’ll notice above that the headers for creating an order need to be structured with an “accept” key and “Content-Type” as well.

The Create/Post Order API lacks documentation, and in my mind, this is the most difficult API to post a request to because there are so many parameters.

Below is the dictionary structure for ‘designing’ a buy or sell order:

from typing import Optional


def design_order(
symbol,
order_type,
instruction,
quantity,
leg_id,
order_leg_type,
asset_type,
price: Optional[str] = None,
session="NORMAL",
duration="DAY",
complex_order_strategy_type="NONE",
tax_lot_method="FIFO",
position_effect="OPENING",
# special_instruction="ALL_OR_NONE",
order_strategy_type="SINGLE",
):

post_order_payload = {
"price": price,
"session": session,
"duration": duration,
"orderType": order_type,
"complexOrderStrategyType": complex_order_strategy_type,
"quantity": quantity,
"taxLotMethod": tax_lot_method,
"orderLegCollection": [
{
"orderLegType": order_leg_type,
"legId": leg_id,
"instrument": {
"symbol": symbol,
"assetType": asset_type,
},
"instruction": instruction,
"positionEffect": position_effect,
"quantity": quantity,
}
],
"orderStrategyType": order_strategy_type,
}

return post_order_payload

You can use the ‘design_order’ function like this:

# Sell one share of an equity, market order.
post_order_payload = design_order(
symbol,
# price="5000",
order_type="MARKET",
instruction="SELL",
quantity=f"1",
leg_id="1",
order_leg_type="EQUITY",
asset_type="EQUITY",
)

Then, you can use this payload as the data in the post request, such as in the create_order method a few chunks above.

Please check out Part 2, “Cloud-Deploy Your Trading Bot — Charles Schwab APIs + Google Cloud”, where you can follow along with me as I implement this code and deploy it on Google Cloud.

I specialize in creating cloud-deployed trading algorithms based on Charles Schwab’s APIs. For inquiries, please reach out to me at carsten.savage.consulting@gmail.com.

You can also find me on LinkedIn and GitHub.

--

--