API reference
Capitrack exposes a RESTful JSON API for every operation in the app. Everything you can do in the UI you can automate — managing accounts, recording transactions, importing CSVs, tracking goals, fetching prices and reading settings.
Base URL & conventions
All endpoints are mounted under /api on the host you run Capitrack on. In the normal Docker setup the browser calls them on the same origin as the web app (port 3000) and nginx proxies them to the API container.
http://localhost:3000/api
- JSON is snake_case. Request and response bodies use snake_case (
base_currency,account_id,tag_ids,change_percent…). The one exception is the change-password endpoint, which uses camelCase (currentPassword,newPassword). Dictionary-keyed responses (the/prices/quotesmap keyed by symbol) keep their original keys. - IDs in paths are integers; dates are
YYYY-MM-DDstrings. - Error shape: failures return
{ "error": "message" }; simple successes often return{ "message": "..." }.
Authentication
Capitrack uses a session cookie. Call POST /api/auth/login to start a session; the server sets the capitrack.sid cookie (HttpOnly, 7-day sliding expiry). Send that cookie with every subsequent request. All endpoints require it except POST /auth/login, POST /auth/logout, GET /auth/session and GET /health, which are anonymous. Unauthenticated calls return 401 (not a redirect).
| Method | Endpoint | Description |
|---|---|---|
| post | /auth/login | Authenticate with username & password (anonymous) |
| post | /auth/logout | Destroy the current session (anonymous) |
| get | /auth/session | Return the signed-in user (anonymous) |
| put | /auth/password | Change the password (camelCase body) |
| put | /auth/currency | Set the base currency |
Log in
$ curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-c cookies.txt \
-d '{ "username": "admin", "password": "your-password" }'
{
"username": "admin",
"base_currency": "EUR"
}
Returns 401 { "error": "Invalid credentials" } for a bad username/password.
Change password
This endpoint is the exception that uses a camelCase body.
{
"currentPassword": "your-password",
"newPassword": "N3w!Secret"
}
! @ # $ % ^ & *).Accounts
| Method | Endpoint | Description |
|---|---|---|
| get | /accounts | List all accounts (newest first, with tags) |
| post | /accounts | Create an account (only name required) |
| get | /accounts/{id} | Get a single account |
| put | /accounts/{id} | Partial update (tag_ids replaces the tag set) |
| del | /accounts/{id} | Delete an account (cascades to its transactions) |
| get | /accounts/{id}/holdings | Per-symbol holdings summary |
| del | /accounts/purge/all | Danger zone — wipe all accounts, transactions, goals, tags & cached prices |
Create an account
$ curl -X POST http://localhost:3000/api/accounts \
-H "Content-Type: application/json" -b cookies.txt \
-d '{ "name": "Stocks", "type": "stock", "currency": "USD",
"icon": "chart-line", "color": "#10b981", "tag_ids": [] }'
Omitted fields default: type=general, currency=EUR, icon=wallet, color=#6366f1. Returns 201 with the created account, or 400 if name is empty.
Transactions
A transaction's type is one of buy, sell, transfer_in, transfer_out, dividend, interest, fee.
| Method | Endpoint | Description |
|---|---|---|
| get | /transactions | List, filterable by account_id, symbol, limit, offset |
| post | /transactions | Record a transaction (account_id, symbol, type, date required) |
| get | /transactions/{id} | Get a single transaction |
| put | /transactions/{id} | Partial update |
| del | /transactions/{id} | Delete a transaction |
| get | /transactions/export/csv | Export transactions as CSV (optionally per account_id) |
| post | /transactions/import/csv | Import a CSV file (multipart/form-data) |
| post | /transactions/import/detect | Detect a CSV's format without importing |
List transactions
$ curl -b cookies.txt \
"http://localhost:3000/api/transactions?account_id=1&limit=50"
[
{
"id": 12,
"account_id": 1,
"account_name": "Stock Portfolio",
"symbol": "AAPL",
"type": "buy",
"quantity": 10.0,
"price": 150.0,
"fee": 1.0,
"currency": "USD",
"date": "2024-02-01",
"tags": []
}
]
Import a CSV
multipart/form-data with file (required), account_id (required) and an optional format to force a parser. The same response shape is returned by the UI:
$ curl -X POST http://localhost:3000/api/transactions/import/csv \
-b cookies.txt \
-F "[email protected]" -F "account_id=1"
{ "imported": 8, "skipped": 2, "total": 10, "errors": [], "format": "revolut-stocks" }
See CSV import for the supported formats and field mapping.
Goals
| Method | Endpoint | Description |
|---|---|---|
| get | /goals | List goals (ordered by target date), filterable by category_id / tag_id |
| post | /goals | Create a goal (title and target_date required) |
| get | /goals/{id} | Get a single goal |
| put | /goals/{id} | Partial update (incl. achieved) |
| del | /goals/{id} | Delete a goal |
| del | /goals | Delete all goals |
Tags
| Method | Endpoint | Description |
|---|---|---|
| get | /tags | List tags (alphabetical) |
| post | /tags | Create a tag (name required; color defaults to #6366f1) |
| get | /tags/{id} | Get a single tag |
| put | /tags/{id} | Update name / color |
| del | /tags/{id} | Delete a tag |
Currencies
| Method | Endpoint | Description |
|---|---|---|
| get | /currencies | List manual FX rates |
| post | /currencies | Upsert a rate by (from_currency, to_currency) |
| put | /currencies/{id} | Update a rate by id |
| del | /currencies/{id} | Delete a rate |
| get | /currencies/convert | Convert an amount — query from, to, amount |
{ "result": 92.0, "rate": 0.92 }
If from == to the amount is returned unchanged with rate: 1. Returns 404 if no rate exists for the pair.
Prices
Quotes come from Yahoo Finance and carry only the fields the quote/chart source provides (symbol, price, currency, name, change_percent); richer fields like P/E or market cap are not fetched. Quotes are cached for 5 minutes with a stale fallback.
| Method | Endpoint | Description |
|---|---|---|
| get | /prices/quote/{symbol} | Live quote for one symbol |
| post | /prices/quotes | Batch quotes — body { "symbols": [...] }; returns a map keyed by symbol |
| get | /prices/history/{symbol} | Historical OHLCV — query period (1w…5y, max) |
| get | /prices/search/{query} | Symbol search via Yahoo |
| get | /prices/dashboard/summary | Total wealth, cost & gain in the base currency, plus per-account breakdown |
| get | /prices/portfolio/history | Portfolio value over time — query account_id, period |
| get | /prices/daily-wealth | Saved daily-wealth snapshots between start & end |
| post | /prices/daily-wealth | Compute & upsert today's snapshot from cached prices |
Dashboard summary
{
"total_wealth": 25340.55,
"total_cost": 21000.0,
"total_gain": 4340.55,
"total_gain_percent": 20.67,
"base_currency": "EUR",
"accounts": [ { "account_id": 1, "account_name": "Crypto Portfolio", "market_value": 12000.0, "cost_basis": 9000.0, "holdings_count": 2 } ],
"holdings_count": 5
}
Settings
| Method | Endpoint | Description |
|---|---|---|
| get | /settings | App info — db path, version, repository, license |
| get | /settings/database | Current SQLite path and whether the file exists |
| put | /settings/database | Set the SQLite path (takes effect on restart) |
| post | /settings/refresh | Refresh hook |
| get | /settings/about | Name, description, version, license, author, repository |
Health
| Method | Endpoint | Description |
|---|---|---|
| get | /health | (anonymous, not under /api) — returns { "status": "ok" }; used by the API container's healthcheck |
Status codes
| Code | Meaning |
|---|---|
200 | Success (reads, updates, deletes) |
201 | Resource created |
400 | Missing or invalid fields |
401 | Not authenticated / bad credentials |
404 | Resource or symbol not found |
500 | Upstream / price error |