All API endpoints are prefixed with /api/v1/. The prefix is omitted in the endpoint tables below for brevity.
Authentication: all endpoints except POST /auth/login require a bearer token (see Authentication).
Status codes: successful POST requests return 201 Created; all other successful requests return 200 OK. Errors are described in Error Responses.
| Method | Endpoint | Description | Request Body | Response Body |
|---|---|---|---|---|
| POST | /auth/login |
Authenticate and get a bearer token | See below | See below |
| GET | /auth/refresh_token |
Gets a new token with extended expiry | - | See below |
Authenticate with username and password to receive a JWT bearer token. Returns 201 Created on success; invalid credentials return 403 with type: "Auth".
Request:
{
"username": "string (required, non-empty)",
"password": "string (required, non-empty)"
}Response:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshBefore": 518400
}Get a new bearer token with extended expiry without credentials (requires valid token in Authorization header).
Response:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshBefore": 518400
}| Method | Endpoint | Description | Request Body | Response Body |
|---|---|---|---|---|
| GET | /profile |
Get current user profile | - | See below |
| PUT | /profile |
Update current user profile | See below | See below |
| POST | /profile |
Update current user password | See below | See below |
| DELETE | /profile |
Delete current user profile | - | See below |
Retrieve the current authenticated user's profile information.
Response:
{
"id": 1,
"username": "admin",
"admin": true,
"login_attempts": 0,
"locked": false,
"created": "2026-02-14T21:20:21.000Z",
"modified": "2026-02-14T21:20:21.000Z"
}Update the current user's profile information (username, admin status, etc.).
Request:
{
"username": "admin_updated",
"admin": true,
"login_attempts": 0,
"locked": false
}Response: (same as Get Profile)
Update the current user's password. The oldPassword is verified — a wrong value returns 400 BadRequest.
Request:
{
"oldPassword": "string (required, non-empty)",
"newPassword": "string (required, non-empty)",
"repeat": "string (required, must match newPassword)"
}Response: (same as Get Profile)
Delete the current user's profile and all associated data.
Response:
{
"id": 1
}| Method | Endpoint | Description | Request Body | Response Body |
|---|---|---|---|---|
| GET | /prefs |
Get current user prefs | - | See below |
| PUT | /prefs |
Update current user prefs | See below | See below |
Retrieve the current user's preferences (e.g., base currency, display options).
Response:
{
"id": 1,
"base_ccy": "GBP",
"additional": {
"altChart": false
}
}Update the current user's preferences.
Request:
{
"base_ccy": "EUR",
"additional": {
"altChart": true
}
}Supported currencies: USD, GBP, EUR, CAD, AUD, CHF, SEK, NOK, DKK, NZD, JPY, INR
Response: (same as Get Preferences)
| Method | Endpoint | Description | Request Body | Response Body |
|---|---|---|---|---|
| GET | /users |
Get all users (admin only) | - | See below |
| GET | /users/{user_id} |
Get user by ID (admin only) | - | See below |
| DELETE | /users/{user_id} |
Delete user by ID (admin only) | - | See below |
| PATCH | /users/{user_id} |
Reset user password (admin only) | See below | See below |
| POST | /users |
Create new user (admin only) | See below | See below |
| PUT | /users/{user_id} |
Update user (admin only) | See below | See below |
Retrieve all users in the system. Requires admin privileges.
Response:
[
{
"id": 1,
"username": "admin",
"admin": true,
"login_attempts": 0,
"locked": false,
"created": "2026-02-14T21:20:21.000Z",
"modified": "2026-02-14T21:20:21.000Z"
},
{
"id": 2,
"username": "jane_smith",
"admin": false,
"login_attempts": 0,
"locked": false,
"created": "2026-03-01T10:30:00.000Z",
"modified": "2026-04-21T14:22:00.000Z"
}
]Retrieve a specific user by ID. Requires admin privileges.
Response: (same structure as single user in List All Users)
Create a new user account. Requires admin privileges.
Request:
{
"username": "jane_smith",
"admin": false,
"password": "s3cret",
"locked": false
}passwordis required.login_attemptsis managed internally and cannot be set (the validator is exact and rejects unknown fields).
Response: (same as Get User)
Update an existing user's information (username, admin flag, lock state). Requires admin privileges. Does not change the password — see Reset Password below.
Request:
{
"username": "jane_smith",
"admin": false,
"login_attempts": 0,
"locked": false
}Response: (same as Get User)
Force-reset a user's password without knowing the current one. Requires admin privileges. oldPassword must be present in the body but is not verified for admin resets.
Request:
{
"oldPassword": "ignored",
"newPassword": "new-s3cret",
"repeat": "new-s3cret"
}Response: (same as Get User)
Delete a user account. Requires admin privileges.
Response:
{
"id": 2
}| Method | Endpoint | Description | Request Body | Response Body |
|---|---|---|---|---|
| GET | /summary |
Summary across all portfolios | - | See below |
Retrieve aggregated summary data across all portfolios, including total invested, realized P&L, and charts.
Query Parameters (optional):
range: Time range for chart data. Valid values:1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max. Default:1d
Example: GET /api/v1/summary?range=1y
Response:
{
"numPortfolios": 1,
"meta": {
"range": "1d",
"validRanges": ["1d", "5d", "1mo", "3mo", "6mo", "1y", "2y", "5y", "10y", "ytd", "max"],
"fiftyTwoWeekLow": 163533.54,
"fiftyTwoWeekHigh": 221598.82,
"volatilityRange": 58065.28,
"volatilityPct": 0.3015,
"currencies": ["USD", "GBp", "GBP"],
"exchanges": ["NMS", "NYQ", "LSE", "CCC"],
"types": ["EQUITY", "ETF", "CRYPTOCURRENCY"]
},
"chart": [
{
"timestamp": 1776729300,
"price": 32091.83,
"volume": 0,
"tx": null
},
{
"timestamp": 1776729600,
"price": 32094.20,
"volume": 5242880,
"tx": null
}
],
"multiChart": {
"USD": [...],
"GBP": [...]
},
"changes": {
"startPrice": 0,
"endPrice": 32091.83,
"returnValue": 1000.50,
"returnPct": 0.0312,
"startTs": 1776729300,
"endTs": 1776794139
},
"totals": {
"returnValue": 1000.50,
"returnPct": 0.0312
},
"invested": 32000.00,
"realizedPnl": 500.00,
"breakEven": 31500.00,
"fxImpact": -50.00
}| Method | Endpoint | Description | Request Body | Response Body |
|---|---|---|---|---|
| POST | /portfolios |
Create a new portfolio | See below | See below |
| GET | /portfolios |
List all portfolios | - | See below |
| GET | /portfolios/{portfolio_id} |
Get a portfolio by ID | - | See below |
| PUT | /portfolios/{portfolio_id} |
Update a portfolio | See below | See below |
| DELETE | /portfolios/{portfolio_id} |
Delete a portfolio | - | See below |
Create a new portfolio for organizing assets.
Request:
{
"name": "Test Portfolio",
"description": "Test Description"
}Response:
{
"id": 1733,
"user_id": 1,
"name": "Test Portfolio",
"description": "Test Description",
"num_assets": 0,
"created": "2026-04-21T16:55:49.000Z",
"modified": "2026-04-21T16:55:49.000Z",
"meta": {
"range": "1d",
"validRanges": ["1d", "5d", "1mo", "3mo", "6mo", "1y", "2y", "5y", "10y", "ytd", "max"],
"volatilityRange": 0,
"volatilityPct": 0,
"currencies": [],
"exchanges": [],
"types": [],
"fiftyTwoWeekLow": 0,
"fiftyTwoWeekHigh": 0
},
"weight": null,
"domestic": false,
"chart": [],
"multiChart": {},
"changes": {
"startPrice": 0,
"endPrice": 0,
"returnValue": 0,
"returnPct": 0,
"startTs": 0,
"endTs": 0
},
"totals": {
"returnValue": 0,
"returnPct": 0
},
"invested": 0,
"realizedPnl": 0,
"breakEven": 0,
"fxImpact": 0
}Retrieve all portfolios for the current user.
Query Parameters (optional):
range: Time range for chart data. Valid values:1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max. Default:1d
Example: GET /api/v1/portfolios?range=1y
Response:
[
{
"id": 1733,
"user_id": 1,
"name": "Test Portfolio",
"description": "Test Description",
"num_assets": 1,
"created": "2026-04-21T16:55:49.000Z",
"modified": "2026-04-21T16:55:49.000Z",
"meta": {
"range": "1d",
"validRanges": ["1d", "5d", "1mo", "3mo", "6mo", "1y", "2y", "5y", "10y", "ytd", "max"],
"volatilityRange": 95.37,
"volatilityPct": 0.3958,
"currencies": ["USD"],
"exchanges": ["NMS"],
"types": ["EQUITY"],
"fiftyTwoWeekLow": 193.25,
"fiftyTwoWeekHigh": 288.62
},
"weight": 0.0621,
"domestic": false,
"chart": [...],
"multiChart": {},
"changes": {
"startPrice": 0,
"endPrice": 1972.64,
"returnValue": 859.90,
"returnPct": 0.7723,
"startTs": 1776777900,
"endTs": 1776794239
},
"totals": {
"returnValue": 1355.82,
"returnPct": 0.6614
},
"invested": 2050,
"realizedPnl": 0,
"breakEven": 2050,
"fxImpact": 0
}
]Retrieve a specific portfolio by ID.
Query Parameters (optional):
range: Time range for chart data. Valid values:1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max. Default:1d
Example: GET /api/v1/portfolios/1733?range=1y
Response: (same structure as single portfolio in List All Portfolios)
Update portfolio name or description.
Request:
{
"name": "Tech Stocks Updated",
"description": "Updated growth-focused technology investments"
}Response: (same as Get Portfolio)
Delete a portfolio. This will also delete all assets and transactions within it.
Response:
{
"id": 1
}| Method | Endpoint | Description | Request Body | Response Body |
|---|---|---|---|---|
| POST | /portfolios/{portfolio_id}/assets |
Add an asset to a portfolio | See below | See below |
| GET | /portfolios/{portfolio_id}/assets |
List assets in a portfolio | - | See below |
| GET | /portfolios/{portfolio_id}/assets/{asset_id} |
Get an asset by ID | - | See below |
| PUT | /portfolios/{portfolio_id}/assets/{asset_id} |
Update an asset | See below | See below |
| DELETE | /portfolios/{portfolio_id}/assets/{asset_id} |
Delete an asset | - | See below |
| PATCH | /portfolios/{portfolio_id}/assets/{asset_id}/move/{new_portfolio_id} |
Move asset to another portfolio | - | See below |
Add a new asset (stock, ETF, etc.) to a portfolio.
Request:
{
"ticker": "AAPL",
"name": "Apple Inc."
}Response:
{
"id": 1,
"portfolio_id": 1733,
"ticker": "AAPL",
"name": "Apple Inc.",
"user_id": 1,
"holdings": 0,
"invested": 0,
"avg_price": 0,
"break_even": 0,
"realized_pnl": 0,
"num_txs": 0,
"last_activity": null,
"last_activity_ts": null,
"base_ccy": "USD",
"created": "2026-04-21T16:55:49.000Z",
"modified": "2026-04-21T16:55:49.000Z",
"meta": {
"currency": "USD",
"symbol": "AAPL",
"exchangeName": "NMS",
"fullExchangeName": "NasdaqGS",
"instrumentType": "EQUITY",
"regularMarketTime": 1776794146,
"regularMarketPrice": 266.29,
"fiftyTwoWeekHigh": 288.62,
"fiftyTwoWeekLow": 193.25,
"shortName": "Apple Inc.",
"longName": "Apple Inc.",
"previousClose": 265.1,
"chartPreviousClose": 265.1,
"scale": null,
"currentTradingPeriod": null,
"tradingPeriods": null,
"dataGranularity": "1m",
"validRanges": ["1d", "5d", "1mo", "3mo", "6mo", "ytd", "1y", "2y", "5y", "10y", "max"],
"range": "1d"
},
"weight": null,
"volatilityRange": 0,
"volatilityPct": 0,
"ccy": {
"chart": [],
"changes": {
"startPrice": 0,
"endPrice": 0,
"returnValue": 0,
"returnPct": 0,
"startTs": 0,
"endTs": 0
},
"totals": {
"returnValue": 0,
"returnPct": 0
}
},
"base": {
"domestic": false,
"invested": 0,
"fxImpact": 0,
"fxRate": 1,
"chart": [],
"changes": {
"startPrice": 0,
"endPrice": 0,
"returnValue": 0,
"returnPct": 0,
"startTs": 0,
"endTs": 0
},
"totals": {
"returnValue": 0,
"returnPct": 0
},
"avgPrice": null,
"breakEven": null,
"realizedPnl": 0
}
}Retrieve all assets in a portfolio.
Query Parameters (optional):
range: Time range for chart data. Valid values:1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max. Default:1d
Example: GET /api/v1/portfolios/1733/assets?range=1y
Response:
[
{
"id": 1,
"portfolio_id": 1733,
"ticker": "AAPL",
"name": "Apple Inc.",
"user_id": 1,
"holdings": 10,
"invested": 2050,
"avg_price": 205,
"break_even": 205,
"realized_pnl": 0,
"num_txs": 1,
"last_activity": "2026-04-21T16:55:49.000Z",
"last_activity_ts": 1776794549,
"base_ccy": "USD",
"created": "2026-04-21T16:55:49.000Z",
"modified": "2026-04-21T16:55:49.000Z",
"meta": {...},
"weight": 0.0621,
"volatilityRange": 95.37,
"volatilityPct": 0.3958,
"ccy": {...},
"base": {...}
}
]Retrieve details for a specific asset.
Query Parameters (optional):
range: Time range for chart data. Valid values:1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max. Default:1d
Example: GET /api/v1/portfolios/1733/assets/1?range=1y
Response: (same as single asset in List Assets)
Update asset details (ticker, name).
Request:
{
"ticker": "AAPL",
"name": "Apple Inc. - Updated"
}Response: (same as Get Asset)
Delete an asset and all its associated transactions.
Response:
{
"id": 10
}Move an asset from one portfolio to another.
Response:
{
"id": 10
}| Method | Endpoint | Description | Request Body | Response Body |
|---|---|---|---|---|
| POST | /portfolios/{portfolio_id}/assets/{asset_id}/tx |
Create a transaction for an asset | See below | See below |
| GET | /portfolios/{portfolio_id}/assets/{asset_id}/tx |
List all transactions for an asset | - | See below |
| GET | /portfolios/{portfolio_id}/assets/{asset_id}/tx/{tx_id} |
Get a transaction by ID | - | See below |
| PUT | /portfolios/{portfolio_id}/assets/{asset_id}/tx/{tx_id} |
Update a transaction | See below | See below |
| DELETE | /portfolios/{portfolio_id}/assets/{asset_id}/tx/{tx_id} |
Delete a transaction | - | See below |
| POST | /portfolios/{portfolio_id}/assets/{asset_id}/txs |
Bulk insert transactions (CSV upload) | See below | See below |
| DELETE | /portfolios/{portfolio_id}/assets/{asset_id}/txs |
Delete all transactions for an asset | - | See below |
Record a buy or sell transaction for an asset.
Request:
{
"type": "buy",
"quantity": 10,
"price": 205,
"date": "2026-04-21T16:55:49.000Z",
"comments": "Initial purchase"
}Response:
{
"id": 1,
"asset_id": 1,
"type": "buy",
"quantity": 10,
"price": 205,
"date": "2026-04-21T16:55:49.000Z",
"comments": "Initial purchase",
"quantity_ext": 10,
"stretch": 1,
"final_stretch": false,
"value": 2660,
"pnl": 610,
"pnl_pct": 0.2976,
"realized_pnl": 0,
"cost": 2050,
"cost_basis": 2050,
"contribution": 100,
"running_holding": 10,
"running_cost": 2050,
"running_average_price": 205,
"running_break_even": 205,
"running_contribution": 100,
"asset_name": "Apple Inc.",
"asset_ticker": "AAPL",
"portfolio_name": "Test Portfolio",
"portfolio_description": "Test Description",
"user_id": 1,
"user_base_ccy": "GBP",
"timestamp": 1776794549,
"created": "2026-04-21T16:55:49.000Z",
"modified": "2026-04-21T16:55:49.000Z"
}Retrieve all transactions for an asset.
Response:
[
{
"id": 45,
"asset_id": 10,
"type": "buy",
"quantity": 10,
"price": 150.25,
"date": "2023-10-15T14:30:00Z",
"comments": "Purchased at market open",
"quantity_ext": 10,
"asset_name": "Apple Inc.",
"asset_ticker": "AAPL"
},
{
"id": 46,
"asset_id": 10,
"type": "buy",
"quantity": 5,
"price": 152.00,
"date": "2023-10-18T09:00:00Z",
"comments": ""
}
]Retrieve details for a specific transaction.
Response: (same as single transaction in List Transactions)
Update an existing transaction.
Request:
{
"type": "buy",
"quantity": 12,
"price": 150.25,
"date": "2023-10-15T14:30:00Z",
"comments": "Updated quantity"
}Response: (same as Get Transaction)
Delete a specific transaction.
Response:
{
"id": 45
}Upload multiple transactions at once (e.g., from CSV file).
Request:
{
"replace": true,
"txs": [
{
"type": "buy",
"quantity": 10,
"price": 150.25,
"date": "2023-10-15T14:30:00Z",
"comments": ""
},
{
"type": "sell",
"quantity": 5,
"price": 160.00,
"date": "2023-10-18T10:00:00Z",
"comments": "Partial profit taking"
}
]
}replace: Iftrue, deletes all existing transactions and inserts the provided ones. Iffalse, appends to existing transactions.
Response: the full (enriched) list of all transactions for the asset after the upload — same structure as List Transactions.
Delete all transactions for a specific asset.
Response:
{
"id": 4
}id is the number of transactions deleted.
| Method | Endpoint | Description | Request/Query | Response Body |
|---|---|---|---|---|
| GET | /lookup/ticker |
Search for ticker details | term (query param) |
See below |
| GET | /lookup/quote/{ticker}/{date?} |
Get quote for a ticker (with optional date) | Path params | See below |
| GET | /lookup/fx/{base}/{ccy}/{date?} |
Get FX rates for base/currency pair (with optional date) | Path params | See below |
Search for ticker symbols and company names. Returns matching results from Yahoo Finance.
Query Parameters:
term(required): Search term (ticker symbol or company name, e.g., "AAPL" or "Apple")
Example: GET /api/v1/lookup/ticker?term=apple
Response:
{
"quotes": [
{
"symbol": "AAPL",
"exchange": "NMS",
"shortname": "Apple Inc.",
"longname": "Apple Inc.",
"quoteType": "EQUITY"
},
{
"symbol": "AAPL.SW",
"exchange": "EBS",
"shortname": "APPLE INC",
"longname": null,
"quoteType": "EQUITY"
},
{
"symbol": "AAPW",
"exchange": "BTS",
"shortname": "Roundhill AAPL WeeklyPay ETF",
"longname": null,
"quoteType": "ETF"
}
]
}Retrieve the current or historical quote for a ticker symbol.
Path Parameters:
ticker(required): Ticker symbol (e.g., "AAPL")date(optional): ISO 8601 date string (e.g., "2023-10-15"). If omitted, returns latest quote.
Example: GET /api/v1/lookup/quote/AAPL/2023-10-15
Response:
{
"timestamp": 1776794146,
"price": 266.2950134277344,
"volume": 27800253,
"tx": null
}Retrieve foreign exchange rate for a currency pair.
Path Parameters:
base(required): Base currency (e.g., "USD")ccy(required): Target currency (e.g., "EUR")date(optional): ISO 8601 date string. If omitted, returns latest rate.
Example: GET /api/v1/lookup/fx/USD/EUR/2023-10-15
Response:
{
"ccy": "EUR",
"base": "USD",
"rate": 0.8514999747276306,
"timestamp": 1776794139
}All endpoints return errors as JSON with a type and a message. The HTTP status code is determined by the error type:
| HTTP Status | type |
Description |
|---|---|---|
| 400 | BadRequest |
Validation failed — the request body or parameters do not match the expected schema |
| 403 | Auth |
Authentication or authorization failure — missing/invalid token, restricted (locked) user, admin privileges required, wrong credentials or password |
| 404 | NotFound |
Resource not found (e.g. an unknown ID) |
| 500 | General |
Internal server error |
HTTP 400 Bad Request:
{
"type": "BadRequest",
"message": "Invalid value undefined supplied to /username: string"
}HTTP 403 Auth (e.g. missing token):
{
"type": "Auth",
"message": "no token"
}HTTP 403 Auth (admin-only endpoint):
{
"type": "Auth",
"message": "Requires admin role."
}HTTP 404 Not Found:
{
"type": "NotFound",
"message": "Not Found"
}HTTP 500 General:
{
"type": "General",
"message": "Internal server error"
}The
messagestrings above are illustrative — exact wording is produced by the validation and error internals. Unmatched routes return a plain-text404body (route <url> does not exist).
All endpoints except POST /auth/login require authentication via bearer token — this includes /auth/refresh_token and all /lookup/* endpoints:
Authorization: Bearer eyJhbGc...
Obtain a token by calling /auth/login with credentials, then include it in the Authorization header for subsequent requests.