Standard Structure for Authenticated Requests

View as MarkdownOpen in Claude

This is the culmination of the entire OAuth 1.0a / Diffie-Hellman handshake documented across the preceding five steps. Having derived and verified the Live Session Token (LST), this function represents the general-purpose authenticated request pattern used to call IBKR’s actual trading/portfolio API endpoints — in this example, /portfolio/accounts, though the function is written generically enough to serve any authenticated IBKR endpoint.

This marks a shift in signing methodology from every previous step: whereas the OAuth handshake steps (Request Token, Access Token, Live Session Token request) all signed requests using RSA-SHA256 with the application’s private key, all authenticated API calls going forward use HMAC-SHA256, keyed by the Live Session Token. This is the expected and correct behavior, and should be documented as the clear dividing line between the one-time handshake phase and the ongoing operational phase of the integration.

1

Prerequisites

RequirementDescription
live_session_tokenThe verified LST from the previous derivation/verification steps
access_tokenThe Access Token (aToken) obtained from /oauth/access_token or the Interactive Brokers Self Service Portal
consumer_keyIssued by IBKR upon API application registration
realmFor TESTCONS, use “test_realm”. For all other consumer keys, use “limited_poa”
method, endpoint, baseUrlCaller-supplied values identifying the specific API call (e.g., GET, /portfolio/accounts)
query_params, contentOptional caller-supplied query string parameters and JSON request body, respectively

Generic function design: Unlike the previous handshake steps, which were each dedicated to a single fixed endpoint, this function is parameterized by method and endpoint, making it the reusable core for all authenticated IBKR API traffic post-handshake. This should be documented as the primary, ongoing integration point that most consumers of this library will actually call — the handshake steps are a one-time (or infrequent) setup cost, while this function runs on every API interaction.

2

Construct the Request URL

1url = f'https://{baseUrl}{endpoint}'

Unlike the fixed OAuth endpoint URLs used in earlier steps, endpoint is caller-supplied, allowing this function to target any path on IBKR’s API gateway (e.g., /portfolio/accounts, /iserver/account/orders, etc.).

3

Assemble OAuth Parameters

1oauth_params = {
2 "oauth_consumer_key": consumer_key,
3 "oauth_nonce": hex(random.getrandbits(128))[2:],
4 "oauth_signature_method": "HMAC-SHA256",
5 "oauth_timestamp": str(int(datetime.now().timestamp())),
6 "oauth_token": access_token
7}

This parameter set is notably leaner than every prior step — it omits oauth_callback and oauth_verifier (relevant only to the initial handshake) and, critically, declares oauth_signature_method as HMAC-SHA256 rather than RSA-SHA256.

ParameterPurpose
oauth_consumer_keyIdentifies the application, as in all prior steps
oauth_nonceFreshly generated per request, as always
oauth_signature_methodHMAC-SHA256 — signals the shift from asymmetric (RSA) to symmetric (HMAC) signing, now that both client and server share the LST as a common secret
oauth_timestampFreshly generated per request
oauth_tokenThe Access Token, not the Request Token — this identifies the authorized user session for this and all future API calls
4

Build the Signature Base String

1params_string = "&".join([f"{k}={v}" for k, v in sorted(oauth_params.items())])
2base_string = f"{method}&{quote_plus(url)}&{quote(params_string)}"

Follows the same structural pattern as the Request Token and Access Token steps: sorted key=value pairs joined with &, then combined into METHOD&URL&PARAMS, with the URL encoded via quote_plus() and the parameter string via quote().

Query parameters excluded from signature base string: Note that query_params (passed separately to the final requests call in Step 6) are not included in this signature base string. Per OAuth 1.0a’s core specification, query string parameters that will be sent with the request are normally required to be included in the signature base string alongside the OAuth parameters.

5

Sign the Base String with HMAC-SHA256

1bytes_hmac_hash = HMAC.new(
2 key=base64.b64decode(live_session_token),
3 msg=base_string.encode("utf-8"),
4 digestmod=SHA256
5).digest()
6
7b64_str_hmac_hash = base64.b64encode(bytes_hmac_hash).decode("utf-8")
8oauth_params["oauth_signature"] = quote_plus(b64_str_hmac_hash)

The signature is computed as:

signature=HMAC-SHA256(key=base64_decode(live_session_token), message=base_string)\text{signature} = \text{HMAC-SHA256}(\text{key}=\text{base64\_decode}(\text{live\_session\_token}),\ \text{message}=\text{base\_string})

ParameterValue
keyThe raw bytes of the Live Session Token, obtained by Base64-decoding it
msgThe UTF-8 encoded signature base string from Step 3
digestmodSHA256

The resulting digest is Base64-encoded, then percent-encoded (quote_plus) for safe inclusion in the Authorization header — consistent with every prior signing step in this series.

This is the moment the LST is actually put to use. All prior documentation in this series has been building toward this single line: live_session_token, once decoded from Base64 back into raw bytes, becomes the symmetric signing key for every authenticated request.

6

Construct the Authorization Header

1oauth_params["realm"] = realm
2oauth_header = "OAuth " + ", ".join([f'{k}="{v}"' for k, v in sorted(oauth_params.items())])
3headers = {"Authorization": oauth_header}
4headers["User-Agent"] = "python/3.12"
5headers["Accept"] = "*/*"
6headers["Connection"] = "keep-alive"

realm is added to oauth_params (and thus rendered quoted) only after signature computation.

New headers introduced at this step:

HeaderPurpose
Accept: */*Signals acceptance of any response content type — reasonable default for a generic multi-endpoint function, though specific endpoints may benefit from a more precise Accept: application/json if IBKR’s API is JSON-only
Connection: keep-aliveRequests persistent connection reuse — sensible given this function is likely called repeatedly against the same host during a session
7

Execute the Request

1try:
2 with s.request(method=method, url=url, headers=headers, params=query_params, json=content, stream=True) as req:
3 if print_data == "y":
4 print(pretty_request_response(req))
5 if not req.ok:
6 logger.error(f"Request to {url} failed: {req.status_code} - {req.text}")
7 raise IBKRApiError(f"Request failed with status {req.status_code}: {req.text}")
8 return req
9except requests.exceptions.RequestException as e:
10 logger.exception(f"Failed to submit request to {url}")
11 raise IBKRApiError(f"Request submission failed: {e}") from e

This is the most operationally significant step, as it is executed on every authenticated API call rather than once during setup.