Context for AI assistants. Humans: see README.md. Cross-repo context: read
../TR-GBM-Project/docs first —sc-apiis the third library in the same family astr-apiandgbm-mx-apiand inherits all of its conventions.
sc-api is the canonical Python library for talking to Scalable Capital's
backend (GraphQL + WebSocket). Two downstream projects depend on it:
Scalable-Capital-Dashboard— local single-user dashboardScalable-Capital-owncloud— multi-user ownCloud port
This repo is upstream. Any change that touches the Scalable protocol (endpoints, GraphQL operations, auth flow, WS topics) lands here first, then downstreams adopt it. Workflow rule #1 from ../TR-GBM-Project/WORKFLOW.md.
Third trio alongside [[project-tr-trio]] (Trade Republic) and the GBM trio.
Cross-repo docs in ../TR-GBM-Project/ are the
source of truth for ALL three trios — architecture, workflow, design system,
ownCloud patches, technical patterns. Don't reinvent — copy and adapt.
gbm-mx-api tr-api sc-api ← this repo
↓ ↓ ↓
gbm-dashboard TR-Dashboard SC-Dashboard
↓ ↓ ↓
gbm-owncloud TR-owncloud SC-owncloud
- ffischbach/unofficial-scalable-capital-api (TypeScript) requires a
HEADED Chromium for login. We solved that cleanly in
tr-api(Playwright headless for the WAF challenge, pure HTTP for the actual login) and apply the same pattern here. - ScalableCapital/scalable-cli (Rust, official, 2026-04) covers only
Broker, requires manual email allowlist to
[email protected], and uses DPoP token binding with hardware HSMs. Not worth porting to Python for €5500+€3000 at stake.
We build our own: pure Python, both auth modes side-by-side (mirroring
tr-api), and one library covering Broker + Wealth in one session because
Scalable's GraphQL backend serves both products under the same login.
- Cookie-import (
sc_api.cookies.import_from_chrome) — read thede.scalable.capitalsession cookies from a real Chrome on the user's machine viapycookiecheat. No Playwright. Used on workstations. - Programmatic login (
sc_api.auth.initiate_login/complete_login) — email+password, 2FA push approved on the user's linked phone. Used on headless servers (ownCloud, CI).
If Scalable uses a bot-protection challenge (Cloudflare / AWS-WAF / similar),
the JS challenge runs through Playwright headless, mirroring
tr_api.waf — we never open a Chromium window the user sees.
| sc-api / Scalable | |
|---|---|
| Initial login | Auth0 email + password + push approval (no TOTP) |
| Access lifetime | session cookie, ceiling ~8h |
| Refresh model | re-auth on 401/403 (→ exit 10); no GBM-style proactive Bearer refresh |
| Long-lived secret | session cookie {"user":{"userId":personId}} |
| Re-MFA cadence | when the ~8h cookie dies → new push |
| Endpoint | de.scalable.capital + /cockpit/graphql + WS |
CONTRASTE con GBM: SC no sufre el burn-down del access token de GBM. No apliques aquí el fix de refresh proactivo de gbm-mx-api. El "Update failed" que vimos fue de config/ruta (
readRoutes()sin exponerbenchmark, o cuenta equivocada), no de sesión. Ver ADR2026-06-16 — SCen ../Portfolio-Master/DECISIONS.md.
UN solo login Auth0, DOS productos en arrays paralelos: Broker y Wealth (roboadvisor). Ninguno es "solo posiciones" — a diferencia de la cuenta Trading USA de GBM, no hay aquí una cuenta con ese gap:
- Broker:
inventory.json(+fifoPrice),cash.json,transactions.json(BUY/SELL/DEPOSIT/WITHDRAWAL/DISTRIBUTION/TAX/INTEREST…, discriminador__typename).SECURITY_TRANSACTIONtraeside+amount→ sin gap de total de orden. - Wealth:
wealth.json+wealth_detail.json(TWR,valuationHistory, allocation — datos ricos). - Opcionales:
broker_overview,savings,watchlist,crypto,interest. - Moneda EUR.
- Gaps menores: yield-on-cost cruza
fifoPrice;wealth_detailse trae una vez (puede ir atrasado); paginación por cursor desavingssin confirmar; quotes realtime por WS sin cablear.
→ La nota de UI "solo posiciones" es exclusiva de GBM; SC no la lleva.
- GraphQL endpoint at
de.scalable.capitalbackend. Both Broker and Wealth positions show up in the sameportfolio/inventoryqueries. - WebSocket for live data: valuation stream, quotes per ISIN.
- 2FA: push notification on linked smartphone (mandatory since March 2024, no SMS or TOTP option for ongoing logins).
- Native CSV transaction export exists — ground-truth for validation.
- See
docs/protocol.md(filled from HAR capture + reading ffischbach'ssrc/scalable/client.ts).
../TR-GBM-Project/TECHNICAL-PATTERNS.md#2)
When the ownCloud port invokes a sc_api wrapper, use the exact same
exit codes as tr-api and gbm-mx-api. PHP and Python sides BOTH define
them; a smoke test asserts they stay in sync.
| Code | Name | Meaning |
|---|---|---|
| 0 | EXIT_OK | Success; data written |
| 10 | EXIT_MFA_REQUIRED | Session expired, user must approve push |
| 11 | EXIT_MFA_INVALID | Push rejected / timed out |
| 12 | EXIT_AUTH_FAILED | Email/password rejected |
| 20 | EXIT_API_ERROR | Upstream Scalable returned 5xx / unexpected |
| 21 | EXIT_TIMEOUT | Wrapper hung past PHP timeout |
| 30 | EXIT_CONFIG_ERROR | Misconfigured (lib missing, paths wrong) |
Note: Scalable uses push approval, not TOTP — EXIT_MFA_REQUIRED /
_INVALID map to push state in our wrapper.
Pattern #5 from
../TR-GBM-Project/TECHNICAL-PATTERNS.md:
login_or_refresh() that tries persisted session → full login (push
required). Persist atomically (tmp → fsync → rename) to
~/.sc-api/session.json mode 0600. Confirmed 2026-06-16: la sesión es una
cookie con techo ~8h; el patrón es re-auth on-demand ante 401/403 (no
keepalive como tr-api, no refresh-Bearer proactivo como gbm-mx-api). Poll a
validate2faOnLogin ~120s durante el login con push.
src/sc_api/
├── __init__.py ← public re-exports
├── auth.py ← initiate_login / complete_login (programmatic mode)
├── cli.py ← `sc-api ...` command
├── client.py ← ScalableClient (authenticated GraphQL)
├── cookies.py ← import_from_chrome / save / load / validate
├── documents.py ← bulk PDF download (confirm Scalable exposes this)
├── exceptions.py ← hierarchy: ScApiError → AuthError / ApiError / ...
├── portfolio.py ← inventory (Broker + Wealth combined in one call)
├── profiles.py ← multi-account profile management
├── protocol.py ← ScalableWebSocket (async, low-level)
├── savings.py ← Tagesgeld
├── transactions.py ← paginated transaction history
├── waf.py ← bot-challenge token via headless Playwright (if needed)
└── watchlist.py ← watchlist securities
docs/
├── auth-modes.md ← cookie-import vs programmatic-login
├── cli-contract.md ← CLI surface (mirrors tr-api/docs/cli-contract.md)
├── events.md ← Scalable transaction-type vocabulary
├── protocol.md ← endpoints + payload shapes (Phase 0)
└── troubleshooting.md
- Upstream first. Library → Dashboard → ownCloud. Bump version + CHANGELOG on every protocol change.
- Don't break the public surface.
sc_api.transactions,sc_api.portfolio,sc_api.authare used by downstreams. Function- signature changes go through add-new + deprecate-old, never rename. - Verify against CSV export. Scalable's native CSV is ground truth.
When
sc_api.transactions.fetch_allreturns N items, the CSV must agree. - Tests: validate manually by running the downstream Dashboard against this library and diffing the result against the CSV export.
- Privacy hard rules (same as TR/GBM):
.env,credentials,cookies.txt,session.jsonalways gitignored.experiments/anddiscovery/gitignored. Tokens redacted in logs. Public docs use synthetic ISINs/amounts.
- Conversations with Carlos: Spanish
- Code, identifiers, docstrings, commits: English
- UI strings on Dashboard: English (matches TR — Scalable's audience is German/EU English-speaking, not Spanish like GBM)
requires-python = ">=3.10"— same floor as tr-api and gbm-mx-api.- Server
cloud.damken.com(snoopy5, Ubuntu 20.04 LTS) ships Python 3.8.10 as the system interpreter — that's BELOW our floor. The system Python cannot be upgraded (Ubuntu 20.04 sticks with 3.8 for security updates). Workaround already in place by Carlos:- Python 3.11.15 built from source at
/opt/python-3.11/(NOT apt, NOT deadsnakes; just a vanilla configure/make install). - All app venvs live next to it:
/opt/tr-venv/,/opt/gbm-venv/, and we'll add/opt/sc-venv/for this trio. Each was created withsudo /opt/python-3.11/bin/python3.11 -m venv /opt/<app>-venv. - The internal
damken8-php84box has Python 3.14 native (Ubuntu 26.04) but ownCloud doesn't live there — IGNORE it for sc-api.
- Python 3.11.15 built from source at
- Local dev: pin
sc-api/.venv/to 3.11.15 (Homebrew[email protected]) so your local dev catches any 3.12+-specific issue before it hits the server. Carlos's Mac defaultpython3is 3.14 — never use it for this venv. Recreate:rm -rf .venv $(brew --prefix [email protected])/bin/python3.11 -m venv .venv .venv/bin/pip install -e . - Audited: no PEP 695 generic syntax, no
tomllib, no@override, noasyncio.Runner, noTypeVarTuple— nothing that breaks on 3.11. All modules usefrom __future__ import annotationssoX | Nonetype unions are evaluated as strings (safe on any 3.7+).
Mirror of what Carlos already did for tr-venv/gbm-venv:
ssh snoopy5
sudo /opt/python-3.11/bin/python3.11 -m venv /opt/sc-venv
sudo /opt/sc-venv/bin/pip install --upgrade pip
sudo /opt/sc-venv/bin/pip install \
"sc-api @ git+https://github.com/cdamken/sc-api.git"
sudo /opt/sc-venv/bin/python -c "import sc_api; print(sc_api.__version__)"After this one-time setup, the ownCloud deploy script
(Scalable-Capital-owncloud/scripts/deploy.sh) handles all subsequent
reinstalls via --force-reinstall --no-deps.
- Phase 0 (Discovery) — done via open-source mining, not via HAR.
Combined ffischbach (TS, last commit 2026-06-01) + ScalableCapital/
scalable-cli (Rust, official) into
docs/protocol.md. 19 GraphQL operations + WS topics extracted verbatim. - Phase 1 (Skeleton) — done. 14 Python modules, runnable CLI, installs editable cleanly on 3.11. ~3200 LOC.
- Phase 2 (Core endpoints) — done in scaffold form. Every operation documented in protocol.md has a Python wrapper. Verification against Carlos's real account is the only thing pending.