traderepublic-sync
Unofficial Python client for Trade Republic. Handles AWS WAF token acquisition, phone+PIN login with 2FA, WebSocket data fetching, and parsing of the timeline-detail responses into structured Python dicts.
⚠️ Unofficial. Trade Republic does not publish an API. This library reverse-engineers the web app's WebSocket protocol; it can break at any time and is not endorsed by Trade Republic. Use at your own risk.
a showcase of the capability in trdump
Install
# Base install (websockets + requests only)
pip install -e .
# With Playwright for WAF token acquisition (recommended)
pip install -e .[playwright]
playwright install chromium
# Or with Selenium
pip install -e .[selenium]
Python ≥ 3.11.
Quickstart
import asyncio
from traderepublic_sync import TRClient
client = TRClient(locale="fr")
# 1. WAF token (uses headless browser)
client.acquire_waf_token("playwright")
# 2. Login - Trade Republic will push a 2FA prompt to your phone
login = client.login(phone_number="+33612121212", pin="1234")
print(f"2FA code requested. You have {login['countdown']}s.")
# Optional: ask for SMS instead of the in-app push
# client.request_sms(login["process_id"])
# 3. Verify 2FA (read the code from wherever the user enters it)
code = input("2FA code: ")
session_token = client.verify_2fa(login["process_id"], code)
# 4. Fetch data
result = asyncio.run(client.fetch_transactions(session_token))
print(f"{len(result['transactions'])} transactions, "
f"{len(result['raw_items'])} raw items")
balance = asyncio.run(client.fetch_cash_balance(session_token))
print("Cash:", balance)
Persisting session state
ConnectionState is a plain dataclass - pickle it, JSON-encode it, or store
it in your own DB. The WAF token + session token can be reused across
processes until they expire (typically a few hours).
from dataclasses import asdict
import json
from traderepublic_sync import ConnectionState, TRClient
# Save after a successful login
state = ConnectionState(
phone_number="+33612121212",
pin="1234",
waf_token=client.waf_token,
device_info=client.device_info,
session_token=session_token,
auth_status="authenticated",
)
with open("tr_state.json", "w") as f:
json.dump(asdict(state), f)
# Restore later
with open("tr_state.json") as f:
state = ConnectionState(**json.load(f))
client = TRClient(waf_token=state.waf_token, device_info=state.device_info)
asyncio.run(client.fetch_transactions(state.session_token))
Portfolio API (traderepublic_sync.v1)
A versioned facade that returns typed, EUR-correct portfolio data — the
computation layer most consumers would otherwise hand-roll. It wraps
TRClient and adds: valuation in EUR (bonds divided by 100 + FX,
averageBuyIn kept as EUR), unrealized and realized P&L (sales +
dividends via GET /api/v2/taxes/pnl, with a timeline fallback for
crypto/bonds), fully-sold-asset discovery, and an FX source backed by TR's
own LSX ticker rates.
import asyncio
from traderepublic_sync import TRClient
from traderepublic_sync.v1 import Portfolio
async def main():
client = TRClient(session_token=token) # or TRClient.from_state(state)
pf = Portfolio(client)
snap = await pf.snapshot() # include_committed=False by default
print(snap.total_value_eur, snap.total_unrealized_pnl_eur)
print(snap.total_realized_pnl_eur, snap.total_dividends_eur)
for s in await pf.sold_assets():
print(s.isin, s.realized_pnl, s.source) # "tr" or "timeline"
asyncio.run(main())
The low-level
fetch_asset_listreturns metrics in the instrument's quote currency (no FX, no bond ÷100) — fine as raw data, but use thev1Positionfor EUR-correct value/P&L.
Private Markets: held and committed are separate. Position.value_eur
is the invested book only; committed_eur / committed_schedule carry
uncalled capital. snapshot(include_committed=True) adds committed to both
value and cost (so it shows in the value total but nets to 0 in P&L).
Live equivalents are on Portfolio.stream() (prices / positions /
cash / transactions / fx). See docs/tr-undocumented-api.md for the
underlying REST/WS endpoints and scripts/probe_rest.py to probe them.
Dual-legged transactions (optional)
The mapping layer shapes TR events into a double-entry transaction schema (PURCHASE / SELL / DIVIDEND / TRANSFER / …) with explicit credit / debit / fee / tax legs. It lives in a separate submodule so generic users can ignore it:
from traderepublic_sync.dual_legged import (
build_dual_legged_transaction,
deduplicate_pea,
EVENT_TYPE_MAP,
)
# Given a raw TR item + its parsed detail (use parse_detail_sections from
# the main package), produce a dual-legged transaction dict:
tx = build_dual_legged_transaction(raw_item, parsed_detail)
fetch_transactions() already applies this mapping and returns both forms
under "transactions" (dual-legged) and "raw_items" (raw TR items with the
parsed detail attached as _detail / _detail_raw).
Live subscriptions (TRSession)
TRClient exposes one-shot helpers (fetch_transactions, fetch_cash_balance,
fetch_ticker, …) that open a WebSocket, send a single request, and close.
For streaming use cases — live ticker, live portfolio updates, instrument
search — open a long-lived session via client.open_session().
import asyncio
from traderepublic_sync import TRClient
client = TRClient(waf_token=..., device_info=...)
# ...login + verify_2fa as in the quickstart...
async def watch_apple():
async with client.open_session(session_token) as session:
def on_tick(data):
last = (data.get("last") or {}).get("price")
print(f"AAPL = {last}")
sub_id = await session.subscribe_ticker("US0378331005", on_tick)
await asyncio.sleep(60) # stream for a minute
await session.unsubscribe(sub_id) # optional — __aexit__ also cleans up
asyncio.run(watch_apple())
Convenience subscriptions
| Method | What it streams |
|---|---|
subscribe_ticker(isin, cb) |
Live price (last, bid, ask, open, pre) — resolves the home exchange for you |
subscribe_portfolio(sec_acc_no, cb) |
Live positions list (quantity + cost basis) |
subscribe_cash(cash_acc_no, cb) |
Available cash balance |
subscribe_transactions(cash_acc_no, cb) |
Timeline transactions as they appear |
All callbacks may be plain functions or coroutines. They receive the parsed JSON payload of each incoming frame; errors in the callback are logged and swallowed so one bad frame doesn't kill the stream.
Searching for instruments (securities)
search_instrument(query, instrument_type=None, limit=20) queries TR's
neonSearch endpoint — the same one the web app uses for the asset
picker. It accepts a free-text query (name, ticker, ISIN fragment) and
returns the raw result list.
async with client.open_session(session_token) as session:
# By asset class
btc = await session.search_instrument("bitcoin", instrument_type="crypto")
apple = await session.search_instrument("apple", instrument_type="stock")
msci = await session.search_instrument("MSCI World", instrument_type="etf")
# Without a type filter, results span all asset classes
mixed = await session.search_instrument("tesla", limit=5)
# By ISIN (or fragment)
by_isin = await session.search_instrument("US0378331005")
Parameters
| Name | Type | Notes |
|---|---|---|
query |
str |
Free-text search — name, ticker, partial ISIN |
instrument_type |
str | None |
One of "crypto", "stock", "etf", "bond", "derivative", "fund" — or None to search everything |
limit |
int |
Max results (default 20) |
Result shape — each item is a raw TR dict, typically including:
{
"isin": "XF000BTC0017", # ISIN or pseudo-ISIN for crypto
"name": "Bitcoin",
"type": "crypto",
"exchanges": [{"slug": "BTC", "name": "Bitcoin"}],
# ...additional fields vary by asset class
}
Use the returned isin to feed subscribe_ticker(), fetch_ticker(),
or any other instrument-keyed API on the client.
Lower-level primitives
If a TR subscription type isn't covered by the convenience helpers, use
subscribe() / request() directly:
async with client.open_session(session_token) as session:
# One-shot: subscribe, take the first frame, unsubscribe.
data = await session.request("availableCash", {"id": cash_acc_no})
# Streaming: keep receiving until you unsubscribe.
sub_id = await session.subscribe(
"compactPortfolio",
{"secAccNo": sec_acc_no},
callback=lambda d: print(d["positions"]),
)
The WebSocket token is injected for you; you don't need to pass it in
params.
Downloading files listed in transactions
The key points:
- Cookie: tr_session=<session_token> — this is how TR authenticates document downloads (same mechanism as login).
- Headers: reuse client._headers() for x-aws-waf-token and x-tr-device-info — TR's WAF will reject requests without a valid token.
- The document URLs are absolute https:// URLs already, no base URL manipulation needed.
Examples:
import requests
def download_tr_documents(client, session_token: str, transactions: list, output_dir: str = "."):
"""Download all PDF documents from a list of dual-legged transactions."""
import os
headers = client._headers() # includes x-aws-waf-token + x-tr-device-info
cookies = {"tr_session": session_token}
for tx in transactions:
for doc in tx.get("document_urls", []):
url = doc["url"]
title = doc["title"].replace("/", "-")
tr_id = tx.get("tr_id", "unknown")
filename = f"{tx['date'][:10]}_{tr_id}_{title}.pdf"
filepath = os.path.join(output_dir, filename)
resp = requests.get(url, headers=headers, cookies=cookies)
resp.raise_for_status()
with open(filepath, "wb") as f:
f.write(resp.content)
print(f"Saved: {filepath}")
result = asyncio.run(client.fetch_transactions(session_token))
download_tr_documents(client, session_token, result["transactions"], output_dir="/tmp/tr_docs")
Or if you want to pull from raw_items instead (same document URLs, accessible before the dual-legged mapping):
for item in result["raw_items"]:
for doc in item["_detail"].get("document_urls", []):
...
Pure parsing utilities
These are dependency-free helpers you can use without authenticating:
from traderepublic_sync import (
parse_currency_amount, # "1 000,00 EUR" → 1000.0
parse_detail_sections, # timelineDetailV2 dict → structured dict
extract_isin_from_icon, # "logos/FR0011550672/v2" → "FR0011550672"
)
Layout
src/traderepublic_sync/
├── client.py # TRClient (login, 2FA, one-shot websocket fetches)
├── session.py # TRSession (long-lived ws, callback-based subscriptions)
├── waf.py # AWS WAF token via Playwright or Selenium
├── parsing.py # parse_currency_amount, parse_detail_sections, ISIN extraction
├── state.py # ConnectionState dataclass
├── constants.py # API URLs, default headers, WS connect payload
├── exceptions.py # TRAuthError
└── dual_legged/ # Optional dual-legged transaction mapping
└── mapping.py
Reporting bugs
The TR API is undocumented and locale-sensitive — the most useful thing you can attach to a bug report is a redacted copy of your own dump, so we can reproduce the parsing path that misbehaved. Two scripts make this safe:
-
Dump everything with
examples/smoke_fetch_all.py— writesaccounts.json,assets.json,transactions_raw.jsonandtransactions_dl.jsonunderexamples/out/. The raw file contains the full TR responses (_detail+_detail_raw) which is what we need for parser fixes. -
Anonymize the whole folder with
scripts/redact_dump.pybefore sharing. It keeps every item and preserves the data shape, but:- replaces
sender/iban/holderName/email/phonefield values wholesale, - regex-scrubs IBANs, JWTs, emails, phone numbers, AWS pre-signed URL query strings, and any 10+ digit run,
- maps TR cash account numbers to consistent placeholders
(
9000000001,9000000002, …) so cross-file references still match, - takes
--also-redact "<string>"for anything the regexes can't infer (your real name and its variants, account labels, etc.). Matched case-insensitively. Repeat the flag per term.
# Default in/out: examples/out/ → examples/out_redacted/ python scripts/redact_dump.py \ --also-redact "Jane Doe" \ --also-redact "Jane-Doe" \ --also-redact "DOE-J"
The script prints a per-rule hit count and the cash-account-number mapping at the end. Open the redacted JSONs and search for any remaining real names, IBAN fragments, or labels — name variants (truncations, capitalizations, hyphenations) are the classic blind spot. Re-run with extra
--also-redactflags until clean. - replaces
-
Open an issue on github.com/hdecreis/libtrsync describing what you expected vs. what happened, and attach the relevant file(s) from
examples/out_redacted/. A single offendingeventTypeis often enough — pulling one item withscripts/extract_fixture.py(which also sanitizes) lets us turn it straight into a regression test.
License
MIT. See LICENSE.
Metadata
Release files for traderepublic-sync 0.5.6
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| traderepublic_sync-0.5.6.tar.gz | 82.7 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| traderepublic_sync-0.5.6-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 146.7 kB
Release files / traderepublic_sync-0.5.6.tar.gz
| Download URL | traderepublic_sync-0.5.6.tar.gz |
|---|---|
| Size | 82.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
64b851e203a2c8e53f35a0c27a60722af1c38c4a6afd6b883a40e613e807a32a
|
|
BLAKE2b-256 checksum How to use checksums |
65667904606d17db07619036db3af8954ac41d1970afac36698f3c3da300b0a9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.13
|
Release files / traderepublic_sync-0.5.6-py3-none-any.whl
| Download URL | traderepublic_sync-0.5.6-py3-none-any.whl |
|---|---|
| Size | 64.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8b0c9a0df7a76eec2a5246607fc007dc1d644512df92e9c9a4fdbb8faff0c156
|
|
BLAKE2b-256 checksum How to use checksums |
e70a47942232f5637fb8229d7cd26567fc62ddb2e4ba6dea1b4f0ea9330e875d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.13
|
Security for traderepublic-sync 0.5.6
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.