Web API Reference
40+ REST endpoints + SSE exposed by FastAPI. Same-origin deployment, CF Access edge authentication. This chapter is a quick reference for endpoints grouped by "Read / Write / Committee / System / Transparency" + the frontend type synchronization workflow.
← 05-data-model · Wiki Index · 07-extending →
1. Startup
# Local development
uv run uvicorn connectors.web_api:app --host 127.0.0.1 --port 8765
# Swagger auto-generation
open http://127.0.0.1:8765/docs
# OpenAPI schema (pulled by frontend gen-types)
curl http://127.0.0.1:8765/openapi.jsonProduction deployment: see 08-deployment.md.
2. Read Endpoints (GET)
Holdings / Cash
| Method | Path | Purpose |
|---|---|---|
| GET | /api/portfolio | Simplified holdings snapshot (compatible with legacy frontend) |
| GET | /api/holdings | Complete holdings list + cash dict (v2 main interface) |
| GET | /api/portfolio/total_value?base=CNY | Multi-currency converted total market value |
| GET | /api/cash | Returns only the cash dict |
| GET | /api/gold | Gold holdings + real-time price + floating PnL (compatible with legacy interface) |
| GET | /api/ndq | NDQ.AX holdings + real-time quote (compatible with legacy interface) |
/api/holdings response example:
{
"cash": {"CNY": 50000, "AUD": 1000},
"holdings": [
{
"symbol": "NDQ.AX",
"kind": "etf",
"units": 50,
"unit_label": "shares",
"avg_cost": 38.50,
"cost_currency": "AUD",
"channel": "CommSec",
"display_name": "BetaShares Nasdaq 100 ETF",
"quote": {
"price": 42.30,
"currency": "AUD",
"is_stale": false,
"extra": {"day_change_pct": 1.2}
},
"market_value": 2115.00,
"pnl": 190.00,
"is_tracking_only": false
}
]
}History / Transactions
| Method | Path | Purpose |
|---|---|---|
| GET | /api/history?limit=200&symbol=NDQ.AX | Transaction history |
| GET | /api/daily?since=N | Daily decision snapshots for the last N days |
| GET | /api/pnl_history?since=N | PnL time series points (written by jobs/pnl_snapshot) |
| GET | /api/pnl_chart.svg | Backend-generated SVG (viewable without JS) |
Strategy / Configuration
| Method | Path | Purpose |
|---|---|---|
| GET | /api/strategy | Complete strategy + target_assets |
| GET | /api/config | Currently effective values of API-settable tunables, whether overridden, and metadata (ADR-017) |
| GET | /api/symbols/search?q=apple&limit=8 | yfinance Search for symbols (used by GUI for adding assets) |
| GET | /api/regime/{symbol} | Current regime for the symbol + algorithm inputs |
| GET | /api/regime_rules | All hard rules + full prompts for the 4 roles |
3. Write Endpoints (POST/PUT/DELETE)
Cash
| Method | Path | Body |
|---|---|---|
| POST | /api/cash/{currency}/deposit | {amount, source?} |
| POST | /api/cash/{currency}/withdraw | {amount, source?} |
curl -X POST http://127.0.0.1:8765/api/cash/CNY/deposit \
-H "Content-Type: application/json" \
-d '{"amount": 1000, "source": "Salary"}'Holdings CRUD (v2 Generic)
| Method | Path | Purpose |
|---|---|---|
| POST | /api/holdings | Add a holding (any yfinance symbol) |
| PUT | /api/holdings/{symbol} | Update specific fields |
| DELETE | /api/holdings/{symbol} | Delete (rejected if units > 0) |
# Add AAPL tracking position
curl -X POST http://127.0.0.1:8765/api/holdings -d '{
"symbol": "AAPL",
"kind": "stock",
"units": 0,
"unit_label": "shares",
"avg_cost": 0,
"cost_currency": "USD",
"channel": "Robinhood",
"is_tracking_only": true
}'Smart Holdings Import (Free Text / CSV)
| Method | Path | Purpose |
|---|---|---|
| POST | /api/holdings/import | {content, commit} — Free text/CSV → backend LLM parses into structured holdings |
commit:false only parses and returns a preview (parsed.{cash,holdings}), commit:true performs a non-destructive write: Only adds symbols not already in the portfolio; only fills cash currencies currently at 0; skips existing entries (summary.{added_holdings,skipped_holdings,cash_set,cash_skipped}). Returns 400 if no LLM key is configured. Single source of truth: services/holdings_import.py. Shared by GUI "Import Holdings", CLI skill.py import, and onboarding.
curl -X POST http://127.0.0.1:8765/api/holdings/import \
-d '{"content": "510300 ETF 3000 shares cost 4.2 yuan (Alipay)\nYu'ebao 50k", "commit": false}'Legacy Dedicated Endpoints (Retained for Compatibility)
POST /api/deposit POST /api/withdraw
POST /api/gold/buy POST /api/gold/sell POST /api/gold/set POST /api/gold/offsetNew code should use the generic holdings interface; dedicated endpoints are only for legacy frontend compatibility.
Strategy Write
| Method | Path | Purpose |
|---|---|---|
| PUT | /api/strategy/allocations | Modify stock/cash target ratios |
| POST | /api/strategy/asset | Add a target_asset |
| PUT | /api/strategy/asset/{symbol} | Modify target_asset cap / fee rate |
| DELETE | /api/strategy/asset/{symbol} | Delete a target_asset |
| PUT | /api/config | Set a whitelisted config override (body {key, value}, persisted to disk, priority > env; ADR-017) |
| DELETE | /api/config/{key} | Delete a config override, fallback to env/yaml/default |
config-via-API (ADR-017): The whitelist
API_SETTABLE(incore/config/_loader.py) only contains user-safe behavioral switches (verdict.concentration_lens_enabled/verdict.risk_profile/verdict.gold_defense_dca_enabled/dreaming.llm_verify_enabled). Persisted tomemory/.state/config_overrides.json, merged byload_config()on top of env. Read by web/cron/skill processes. Secrets + deployment guidance still only via env;locked.pyis never exposed. CLI equivalent:skill config [--set K V] [--clear K].
4. Committee
Trigger + Status
| Method | Path | Purpose |
|---|---|---|
| POST | /api/committee/run | Async trigger, immediately returns task_id (upon completion, result.by_asset[sym] contains verdict + cio_memo) |
| GET | /api/committee/{task_id} | Single status snapshot |
| GET | /api/committee/live/{task_id} | SSE stream (25s keepalive to prevent CF 5min idle timeout) |
| GET | /api/committee_sessions?limit=N | Historical decision archive (reverse chronological) |
| GET | /api/committee_sessions/{date}/{symbol} | Complete markdown for a single decision |
| POST | /api/committee/prepare | Coordinator path RPC: returns a self-contained brief (6 inline prompts), used by remote clients to spawn subagents. Body {symbol} |
| POST | /api/committee/save | Coordinator path RPC: transcript parsing + defensive degradation post-processing + persistence. Body {symbol, transcript} |
# Trigger
curl -X POST http://127.0.0.1:8765/api/committee/run \
-d '{"symbols": ["NDQ.AX"], "max_debate_rounds": 4}'
# → {"task_id": "abc123...", "status": "queued", "poll_url": "..."}
# SSE stream
curl -N http://127.0.0.1:8765/api/committee/live/abc123
# event: progress
# data: {"phase": "round_1_done", "round": 1, ...}
# event: progress
# data: {"phase": "round_2_done", ...}
# event: done
# data: {"result": {...}}POST body schema:
{
symbols?: string[] // If omitted, runs for all strategy.target_assets
max_debate_rounds?: number // Default 4, range 1-8
note?: string
}See 02-agents.md for the meaning of each phase.
5. Transparency Endpoints (v3)
LLM Telemetry
| Method | Path | Purpose |
|---|---|---|
| GET | /api/llm/usage?since=N | Historical LLM call list (token/latency/cost) |
| GET | /api/llm/summary?period=7d | Period aggregation (total tokens / total cost / role distribution) |
Tool Audit Trail
| Method | Path | Purpose |
|---|---|---|
| GET | /api/agents/run/{run_id}/tool_calls | All tool calls for a single run + input/output/duration |
Verdict Hit Rate
| Method | Path | Purpose |
|---|---|---|
| GET | /api/verdict_review/summary | 1d / 7d / 30d hit rate × verdict type |
| GET | /api/verdict_review/data | Raw data points |
| GET | /api/verdict_review/report | Full markdown from docs/verdict_accuracy.md |
Discipline Ledger (ADR-023)
| Method | Path | Purpose |
|---|---|---|
| GET | /api/discipline | Committee discipline quantification: inaction rate + intervention count + counterfactual PnL |
Returns {summary:{inaction:{total_verdicts,by_verdict,hold,hold_rate}, interventions:{total,by_family,...}}, markdown}. Honest positioning (no alpha claims, quantifying "doing fewer wrong things"), see adr/023. Consumed by the GUI "Discipline" page.
Data Source Health
| Method | Path | Purpose |
|---|---|---|
| GET | /api/data_sources/health | Last successful time + is_stale for all data sources (yfinance / DB / CommSec) |
CommSec Manual Import (Replaces Legacy Cron)
| Method | Path | Purpose |
|---|---|---|
| GET | /api/commsec/preview?lookback_days=180 | Preview trades fetched from emails (no write) |
| POST | /api/commsec/apply | Write after user confirmation |
See 05-data-model.md for why the mode was changed to manual.
6. System / Internal State
| Method | Path | Purpose |
|---|---|---|
| GET | /api/health | Container health check |
| GET | /api/jobs/status | List of all cron jobs + next run time + enabled status |
| GET | /api/insights | Dreaming long-term insights |
| GET | /api/dreams/state | Dreaming short-term memory state |
| GET | /api/dreams/buckets | Regime bucket group hit rates (v3) |
7. Authentication Model
Browser → CF Access (email verification) → CF proxy → Caddy → Backend 127.0.0.1:8765Backend does not perform auth:
- Backend binds only to 127.0.0.1, unreachable from the public internet
- Caddy only reverse-proxies /api/* to port 8765
- CF Access verifies JWT at the edge (only authorized email passes through)
- All paths are trusted → backend does not re-verify
→ Adding JWT validation is over-engineering, but only if no one can directly connect to the origin server IP. → Hardening plan: Add @cloudflare matcher in Caddy to only allow CF IP ranges (future).
Optional Application-Layer Token (2026-06, Remote Mode): When INVEST_API_TOKEN is set, non-loopback sources accessing /api/* (/api/health is exempt for health checks) must include Authorization: Bearer <token> (constant-time comparison via secrets.compare_digest). If not set, behavior is completely unchanged, and the edge authentication model above remains in effect. Loopback exemption ensures the Caddy→127.0.0.1 link and local GUI are unaffected; the token is for scenarios like "binding to 0.0.0.0 / direct intranet connection / LAN hub without CF". The token is never logged or included in response bodies.
See 08-deployment.md#cloudflare-access.
8. Frontend Type Synchronization
Auto-generation
# In the invest-gui repository
pnpm gen-types
# = openapi-typescript http://127.0.0.1:8765/openapi.json -o src/lib/api-types.ts→ Backend changes an endpoint / Pydantic model → Frontend runs pnpm gen-types to sync types in one line.
Workflow
- Backend modifies an endpoint / Pydantic schema
- Start uvicorn locally exposing port :8765
- Run
pnpm gen-typesin the invest-gui repository - TS compilation errors indicate fields that need updating in the frontend code
- After fixing, commit the type artifact (
src/lib/api-types.ts) - CI does not force running gen-types (to avoid blocking builds when the backend is not running)
api-client.ts Wrapper
Wraps fetcher (for SWR) + postJSON (for mutations) + ApiError class. All routing code uses hooks for calls, not direct fetch().
import useSWR from "swr";
import { fetcher, type HoldingsListResponse } from "../lib/api-client";
const { data, error, isLoading } = useSWR<HoldingsListResponse>(
"/api/holdings",
fetcher,
{ refreshInterval: 30_000 },
);9. Error Protocol
Backend raises HTTPException(status_code=N, detail=msg). Frontend ApiError:
class ApiError extends Error {
status: number
detail: string
}
try {
await postJSON(...)
} catch (err) {
setError(err instanceof ApiError ? err.detail : String(err))
}Common error codes:
400Input validation failure (amount ≤ 0 / incorrect currency format)404Symbol not found / task_id not found409Holding symbol already exists (when POSTing holdings)503External dependency failure (IMAP / yfinance)
10. CORS / Same-Origin
Production: Fully same-origin (/api/* and /* on the same host) → No CORS headers needed.
Development: Vite dev server on :5173 calls local :8765 → Backend uses INVEST_WEB_DEV_CORS=1 env to allow:
INVEST_WEB_DEV_CORS=1 uv run uvicorn ...→ Backend injects CORS middleware allowing only http://localhost:5173.
11. Skill-Parity Endpoints (Remote Mode Hub-and-Spoke)
When scripts/skill.py has INVEST_API_BASE set, CLI subcommands are forwarded to these endpoints via scripts/remote_dispatch.py. Output shape is identical to the local CLI (shares services/skill_views.py / PortfolioManager methods), so the agent protocol (SKILL.md) is completely unaware.
| Method | Path | CLI Equivalent | Description |
|---|---|---|---|
| GET | /api/doctor | doctor | Hub-perspective health self-check (memory/.env/LLM reachability) |
| GET | /api/skill/status | status | Cash + holdings + real-time prices + total assets |
| GET | /api/skill/strategy | strategy | Strategy + Dreaming insights |
| GET | /api/skill/history?n= | history -n | Trades + debates (/api/history only has trades) |
| POST | /api/skill/what_if | what_if | Scenario simulation, body fields correspond one-to-one with CLI parameters |
| POST | /api/skill/buy / /api/skill/sell | buy / sell | Writes holdings via with_portfolio_tx, history source: skill_remote |
| POST | /api/skill/deposit / /api/skill/withdraw | deposit / withdraw | Same output as CLI (/api/cash/* returns WriteResponse shape, which doesn't match CLI, hence separate endpoints) |
| POST | /api/skill/delete_holding | delete_holding | Supports force (DELETE /api/holdings lacks this semantics) |
Error semantics align with CLI: Domain errors (e.g., what_if symbol not in holdings) return 200 + {"status": "error", ...} (client prints as-is); invalid parameters return 400, uninitialized memory returns 503, wrong token returns 401—the remote dispatch maps these back to CLI-equivalent error JSON + exit 1.
Deployment topology is described in the hub-and-spoke section of 08-deployment.md.
Next Steps
→ 07-extending.md — What to modify to add a new endpoint
→ 08-deployment.md — Complete production deployment chain
→ 09-troubleshooting.md — How to debug endpoint failures