diff --git a/DOCS.md b/DOCS.md new file mode 100644 index 0000000..6a4ef51 --- /dev/null +++ b/DOCS.md @@ -0,0 +1,689 @@ +# Aurum API Documentation + +Aurum exposes the same REST API its own frontend uses. Every action available in the UI — adding a +transaction, creating an account, tagging an expense, importing a CSV statement, tracking an asset, +setting a budget — can be done directly over HTTP. This makes it possible to script Aurum, feed it +from another program (a bank-sync job, a bot, a shortcut on your phone), or pull your data into your +own tools. + +There is no SDK — it's a plain JSON REST API, callable with `curl`, any HTTP client library, or tools +like Postman/Insomnia. + +> Interactive, always-up-to-date docs are also built into the backend itself: once your instance is +> running, open `http://:/api/docs` (Swagger UI) or `http://:/api/redoc` +> (ReDoc) for a live, "try it out" version of everything below. + +## Table of Contents + +- [Base URL & Authentication](#base-url--authentication) +- [Conventions](#conventions) +- [Errors](#errors) +- [Health Check](#health-check) +- [Accounts](#accounts) +- [Categories](#categories) +- [Tags](#tags) +- [Transactions](#transactions) +- [Recurring Transactions](#recurring-transactions) +- [Budgets](#budgets) +- [Goals](#goals) +- [Assets & Net Worth](#assets--net-worth) +- [Dashboard, Cash Flow & Reports](#dashboard-cash-flow--reports) +- [Insights & Advice](#insights--advice) +- [Settings](#settings) +- [Backup & Restore](#backup--restore) +- [Recipes](#recipes) + +## Base URL & Authentication + +Aurum ships as three containers (Postgres, FastAPI backend, nginx-served frontend). The frontend +container reverse-proxies `/api/*` straight through to the backend, so **the API and the web UI share +the same host and port** — whatever you set `AURUM_WEB_PORT` to in `.env` (default `3000`): + +``` +http://:/api +``` + +For example, on a local install: `http://localhost:3000/api`. All endpoints below are relative to +this base URL — e.g. `GET /transactions` means `GET http://localhost:3000/api/transactions`. + +### Auth + +Aurum has no built-in login system or API keys — it's designed for one person to self-host one +private instance. Access control is whatever you put in front of it: + +- **Nothing set:** if `AURUM_BASIC_AUTH_USER` / `AURUM_BASIC_AUTH_PASSWORD` are empty in `.env` + (the default), the API is completely open to anyone who can reach the host — no credentials + needed. Fine for `localhost`-only or a private network; **not** fine on the public internet. +- **HTTP Basic Auth:** set both `AURUM_BASIC_AUTH_USER` and `AURUM_BASIC_AUTH_PASSWORD` in `.env` + and restart (`docker compose up -d`). Every request — UI and API alike — then requires an + `Authorization: Basic ` header, or the equivalent `-u user:password` flag + in curl. + +```bash +# No auth configured +curl http://localhost:3000/api/accounts + +# Basic Auth configured +curl -u myuser:mypassword http://localhost:3000/api/accounts +``` + +`GET /api/health` is always open (no auth), even with Basic Auth configured — it exists for Docker +healthchecks and uptime monitors. + +There's no per-endpoint permission model beyond this: whoever can authenticate can read, create, +update, and delete everything. + +## Conventions + +- **Format:** all request and response bodies are JSON (`Content-Type: application/json`). +- **IDs:** integer, auto-incrementing, assigned by the server. +- **Money fields** (`amount`, `monthly_limit`, `value`, ...): decimal numbers as JSON numbers or + strings, up to 14 digits with 2 decimal places. Aurum is single-currency per account/asset — there + is no built-in FX conversion. +- **Dates:** `YYYY-MM-DD` (ISO 8601 date, no time component). Timestamps (e.g. backup `exported_at`) + are full ISO 8601 datetimes. +- **Colors:** 6-digit hex strings, e.g. `"#4f46e5"`. +- **Partial updates:** every `PATCH` endpoint only touches the fields you actually send — omit a + field and it's left unchanged. This matters for fields whose "clear this" value is `null`: sending + `{"parent_id": null}` explicitly clears a category's parent, while omitting `parent_id` entirely + leaves it as-is. +- **Enums** are plain lowercase strings — see each resource's table below for the valid values. + +## Errors + +Standard HTTP status codes: + +| Code | Meaning | +|---|---| +| `200` / `201` | Success (`201` on `POST` that creates a resource) | +| `204` | Success, no response body (deletes) | +| `400` | Invalid request — a business rule was violated (e.g. wrong category kind, duplicate budget) | +| `401` | Missing/invalid Basic Auth credentials (only when Basic Auth is configured) | +| `404` | Resource not found | +| `422` | Request body failed schema validation (wrong type, missing required field, out-of-range value) | + +Error bodies follow FastAPI's default shape: + +```json +{ "detail": "Category 'Salary' is a income category and cannot be used for a expense transaction" } +``` + +`422` validation errors carry a `detail` array with one entry per invalid field instead of a single +string. + +## Health Check + +### `GET /api/health` + +No auth required. Returns `{"status": "ok"}`. Use this to check the backend is up before hitting +anything else. + +## Accounts + +An account is a place money lives — a bank account, a card, a cash wallet. Every transaction belongs +to exactly one account (transfers touch two). Balances are **derived** from transactions, not +stored — there's no "set balance" endpoint. + +**`AccountType`:** `checking` · `savings` · `credit_card` · `cash` · `investment` · `other` + +| Method | Path | Description | +|---|---|---| +| `GET` | `/accounts` | List accounts, with live balance. `?include_archived=true` to include archived ones (excluded by default). | +| `POST` | `/accounts` | Create an account. | +| `PATCH` | `/accounts/{id}` | Update an account (partial). Set `is_archived: true` to archive instead of deleting. | +| `DELETE` | `/accounts/{id}` | Delete an account **and every transaction on it** — irreversible. | + +**Create/update body:** + +```json +{ + "name": "Checking", + "type": "checking", + "currency": "USD", + "color": "#4f46e5" +} +``` + +- `name`: required, 1–100 chars. +- `type`: optional, defaults to `checking`. +- `currency`: optional 3-letter code, defaults to `USD`. Purely a display label — not validated + against ISO 4217 and not used for conversion. +- `color`: optional hex color. +- `is_archived` (update only): archive/unarchive without deleting. An archived account is hidden + from the default account list but its transactions and balance remain intact. + +**Response** (`AccountWithBalance`): + +```json +{ + "id": 1, + "name": "Checking", + "type": "checking", + "currency": "USD", + "color": "#4f46e5", + "is_archived": false, + "balance": "1523.40" +} +``` + +## Categories + +Categories classify transactions as income or expense, with one optional level of subcategories +(a subcategory's `parent_id` points at a top-level category of the *same* kind — no deeper nesting). +A handful of default categories are seeded on first run and can't be deleted (`is_default: true`), +though they can be renamed, recolored, and reordered. + +**`CategoryKind`:** `income` · `expense` + +| Method | Path | Description | +|---|---|---| +| `GET` | `/categories` | List categories, sorted by `sort_order`. `?kind=income` or `?kind=expense` to filter. | +| `POST` | `/categories` | Create a category (or subcategory, via `parent_id`). | +| `PATCH` | `/categories/{id}` | Update a category (partial). | +| `DELETE` | `/categories/{id}` | Delete a category. Fails with `400` if it's a default category. | + +**Create body:** + +```json +{ + "name": "Groceries", + "kind": "expense", + "icon": "shopping-cart", + "color": "#22c55e", + "sort_order": 0, + "parent_id": null +} +``` + +- `name`: required, 1–100 chars. +- `kind`: required, `income` or `expense`. Fixed at creation — there's no way to flip a category's + kind afterward (create a new one instead). +- `icon`: optional, free-form string (the frontend maps these to an icon set). +- `color`: required hex color. +- `sort_order`: optional int controlling display order, defaults to `0`. +- `parent_id`: optional. Rules enforced server-side: the parent must itself be top-level (no + grandchildren), must share the child's `kind`, and a category can't be its own parent. + +**Update** accepts the same fields, all optional. Note: to turn a subcategory back into a top-level +category, send `"parent_id": null` explicitly — omitting the field leaves the existing parent as-is. +A category that already has subcategories of its own can't be turned into a subcategory. + +**Response** (`CategoryRead`): same fields as create, plus `id` and `is_default`. + +## Tags + +Free-form labels a transaction can carry any number of. Case-insensitive dedup on creation: posting +`"Georgia"` when `"georgia"` already exists returns the existing tag instead of making a duplicate. + +| Method | Path | Description | +|---|---|---| +| `GET` | `/tags` | List all tags, alphabetically. | +| `POST` | `/tags` | Create a tag (or return the existing one, case-insensitive match). | +| `DELETE` | `/tags/{id}` | Delete a tag — removes it from every transaction that had it. | + +**Create body:** `{"name": "Vacation"}` (1–50 chars). + +**Response** (`TagRead`): `{"id": 1, "name": "Vacation"}`. + +## Transactions + +The core resource. A transaction is `income`, `expense`, or `transfer`. + +**`TransactionType`:** `income` · `expense` · `transfer` + +| Method | Path | Description | +|---|---|---| +| `GET` | `/transactions` | Paginated, filterable list. See query params below. | +| `GET` | `/transactions/years` | Every year with data (plus the current year), for a year picker. Returns `[2024, 2025, 2026]`. | +| `POST` | `/transactions` | Create one transaction. | +| `POST` | `/transactions/bulk` | Create up to 5000 transactions in one all-or-nothing batch (CSV import). | +| `PATCH` | `/transactions/{id}` | Update a transaction (partial). | +| `DELETE` | `/transactions/{id}` | Delete a transaction. | + +### `GET /transactions` query params + +| Param | Type | Notes | +|---|---|---| +| `year` | int | `2000`–`2100` | +| `month` | int | `1`–`12` | +| `start_date` / `end_date` | date | Inclusive range, combinable with `year`/`month` | +| `account_id` | int | | +| `category_id` | int | | +| `tag_id` | int | | +| `type` | `income`\|`expense`\|`transfer` | | +| `search` | string | Substring match (case-insensitive) against description, merchant, and notes | +| `sort` | `date_desc`\|`amount_desc`\|`amount_asc` | Default `date_desc` | +| `page` | int | Default `1` | +| `page_size` | int | Default `20`, max `200` | + +**Response** (`TransactionPage`): + +```json +{ + "items": [ /* TransactionRead[] */ ], + "total": 143, + "page": 1, + "page_size": 20 +} +``` + +### Create body (`TransactionCreate`) + +```json +{ + "account_id": 1, + "category_id": 4, + "transfer_account_id": null, + "type": "expense", + "amount": 42.50, + "description": "Groceries", + "merchant": "Whole Foods", + "notes": null, + "date": "2026-08-29", + "tag_ids": [3, 7] +} +``` + +Field rules, enforced server-side: + +- `account_id`: required, the account the money moves through. +- `type`: required. Drives which other fields are valid: + - `income` / `expense`: `category_id` optional but **must** point at a category of the matching + `kind` if given (an `income` transaction can't use an `expense` category). `transfer_account_id` + must be omitted. + - `transfer`: `transfer_account_id` is **required** and must differ from `account_id`. + `category_id` must be omitted — transfers aren't categorized. +- `amount`: required, `> 0` (transfers/expenses aren't signed negative — direction comes from + `type`), up to 14 digits, 2 decimal places. +- `description`: required, 1–255 chars. Auto-capitalized server-side (first letter uppercased). +- `merchant`: optional, up to 150 chars. +- `notes`: optional, free text. +- `date`: required. +- `tag_ids`: optional list of existing tag IDs. Unknown IDs return `400`. + +**Update** (`TransactionUpdate`) accepts the same fields, all optional, plus the same +type/category/transfer consistency rules applied to the *effective* (merged) values. `tag_ids`: omit +to leave tags untouched; send (even `[]`) to replace the full tag set. + +**Response** (`TransactionRead`): the input fields plus `id`, and the nested `account` +(`AccountRead`), `category` (`CategoryRead` or `null`), `tags` (`TagRead[]`). + +### Bulk create (`POST /transactions/bulk`) + +Used by the CSV import wizard, but callable directly for any bulk load (e.g. syncing from a bank +export tool). All rows are validated **before** any is inserted — one bad row fails the whole +request with no partial import. + +```json +{ "items": [ /* 1–5000 TransactionCreate objects */ ] } +``` + +Response: `{"created": 250}`. + +## Recurring Transactions + +Templates for bills/income that repeat on a schedule. **Nothing posts automatically in the +background** — a transaction is only created when you call the `/post` endpoint (or click "Post" in +the UI). This is deliberate: a missed week never silently back-fills a pile of transactions. + +**`RecurringFrequency`:** `weekly` · `monthly` · `yearly` + +| Method | Path | Description | +|---|---|---| +| `GET` | `/recurring` | List all recurring templates, with computed due-date info. | +| `POST` | `/recurring` | Create a template. | +| `PATCH` | `/recurring/{id}` | Update a template (partial). Set `is_active: false` to pause. | +| `DELETE` | `/recurring/{id}` | Delete a template (does not touch already-posted transactions). | +| `POST` | `/recurring/{id}/post` | Create a real transaction from the template, dated today, and advance the schedule. | + +**Create body:** same shape as `TransactionCreate` minus `date`/`tag_ids`, plus: + +```json +{ + "account_id": 1, + "category_id": 4, + "type": "expense", + "amount": 15.99, + "description": "Netflix", + "frequency": "monthly", + "anchor_date": "2026-01-05" +} +``` + +`anchor_date` is the first due date; each `/post` call advances `last_posted_date` and recomputes +`next_due_date` (monthly clamps to the shortest month, e.g. day 31 → day 28/29/30; yearly Feb 29 +falls back to Feb 28 in non-leap years). + +**Response** (`RecurringTransactionRead`) adds computed fields: `next_due_date`, `is_due` (boolean), +`days_until_due` (negative if overdue), plus denormalized `account_name`/`category_name`/etc. for +display without extra lookups. + +## Budgets + +One monthly spending limit per **expense** category (income categories can't be budgeted; each +category can have at most one budget). + +| Method | Path | Description | +|---|---|---| +| `GET` | `/budgets` | List all budgets. | +| `GET` | `/budgets/status` | Budgets vs. actual spend for a month. `?year=&month=` (default: current month). | +| `POST` | `/budgets` | Create a budget. `400` if the category already has one, or isn't an expense category. | +| `PATCH` | `/budgets/{id}` | Update `monthly_limit`. | +| `DELETE` | `/budgets/{id}` | Delete a budget. | + +**Create body:** `{"category_id": 4, "monthly_limit": 500}`. + +**`GET /budgets/status` response:** + +```json +{ + "year": 2026, + "month": 8, + "items": [ + { + "budget_id": 1, + "category_id": 4, + "category_name": "Groceries", + "category_color": "#22c55e", + "category_icon": "shopping-cart", + "monthly_limit": "500.00", + "spent": "612.30", + "remaining": "-112.30", + "percent": 122.46, + "is_over_budget": true + } + ] +} +``` + +`percent` can exceed `100` on purpose — clamp it client-side if you're rendering a progress bar. + +## Goals + +Savings goals with a running contribution log. `current_amount` is always the sum of every logged +contribution — there's no separate "set balance" call. + +| Method | Path | Description | +|---|---|---| +| `GET` | `/goals` | List all goals with computed progress. | +| `POST` | `/goals` | Create a goal. | +| `PATCH` | `/goals/{id}` | Update `name`/`target_amount`/`target_date` (partial). | +| `DELETE` | `/goals/{id}` | Delete a goal and its contribution log. | +| `POST` | `/goals/{id}/contributions` | Log a contribution (or a withdrawal). | + +**Create body:** `{"name": "Emergency fund", "target_amount": 10000, "target_date": "2027-01-01"}` +(`target_date` optional). + +**Add contribution:** `{"amount": 250, "date": "2026-08-29", "note": "Bonus"}`. `amount` may be +negative (a withdrawal against the goal) but not zero. + +**Response** (`GoalRead`): + +```json +{ + "id": 1, + "name": "Emergency fund", + "target_amount": "10000.00", + "target_date": "2027-01-01", + "current_amount": "3250.00", + "remaining": "6750.00", + "percent": 32.5, + "is_reached": false +} +``` + +## Assets & Net Worth + +Assets are manually tracked, non-cash net-worth components — investments, crypto, real estate, +vehicles, precious metals, etc. Cash is **not** an asset; it's derived automatically from account +balances and shows up in the net worth summary alongside assets, not as an `Asset` row. + +Each asset has a value **history** (`AssetValuation` rows, one per date) rather than a single +number — creating an asset seeds its first valuation, and you add more over time to track +appreciation/depreciation. + +**`AssetClass`:** `investments` · `crypto` · `real_estate` · `vehicles` · `precious_metals` · `other` +**`CapitalRole`** (how it behaves month to month — user-tagged, not inferred): `income` (e.g. a +rented-out apartment) · `neutral` (e.g. a laptop used for work) · `drain` (e.g. a depreciating +personal vehicle) +**`RiskLevel`** (risk of loss — user-tagged): `low` · `medium` · `high` + +| Method | Path | Description | +|---|---|---| +| `GET` | `/assets` | List assets, each with its current (latest) value. | +| `POST` | `/assets` | Create an asset, seeding its first valuation. | +| `PATCH` | `/assets/{id}` | Update asset metadata (not its value — use the valuations endpoint). | +| `GET` | `/assets/{id}/valuations` | Full value history, oldest first. | +| `POST` | `/assets/{id}/valuations` | Record a value as of a date. Re-posting the same date **updates** that day's value (upsert) instead of erroring. | +| `DELETE` | `/assets/{id}` | Delete an asset and its whole valuation history. | +| `GET` | `/net-worth/summary` | Aggregated net worth: timeline, breakdown by class, by capital role, by risk level. | + +**Create asset body:** + +```json +{ + "name": "Brokerage account", + "asset_class": "investments", + "currency": "USD", + "notes": null, + "capital_role": "income", + "monthly_cash_flow": 0, + "risk_level": "high", + "value": 25000, + "as_of_date": "2026-08-29" +} +``` + +**Add valuation body:** `{"value": 26500, "as_of_date": "2026-08-30"}`. + +**`GET /net-worth/summary`** — `?range=` one of `7d`, `30d`, `90d`, `180d`, `365d`, `all` (default +`30d`): + +```json +{ + "range": "30d", + "current": "48250.00", + "change_amount": "1200.00", + "change_percent": 2.55, + "series": [ { "date": "2026-08-01", "value": "47050.00" } ], + "breakdown": [ + { "key": "cash", "name": "Cash", "color": "#...", "icon": "wallet", "amount": "5250.00", "percent": 10.88 } + ], + "capital_roles": [ + { "role": "income", "label": "Income", "color": "#...", "total_value": "25000.00", "monthly_cash_flow": "0.00", "count": 1 } + ], + "risk_levels": [ + { + "risk_level": "low", "label": "Low", "color": "#...", + "total_value": "5250.00", "percent": 10.88, + "items": [ { "key": "cash", "name": "Cash", "amount": "5250.00", "percent": 100.0 } ] + } + ] +} +``` + +## Dashboard, Cash Flow & Reports + +Read-only aggregation endpoints — the numbers behind Aurum's charts. Useful for pulling summary data +into an external dashboard without recomputing it yourself. + +| Method | Path | Description | +|---|---|---| +| `GET` | `/dashboard/summary` | One month's headline numbers + spending by category. `?year=&month=` (default: current month). | +| `GET` | `/cash-flow` | Income vs. expense, month by month. `?start_date=&end_date=` (default: no bound, i.e. all history). | +| `GET` | `/reports/category-spending` | One category's spend over time. `?category_id=` (required) `&start_date=&end_date=`. | +| `GET` | `/reports/category-ranking` | All categories ranked by total spend over a period. `?kind=expense\|income` (default `expense`) `&start_date=&end_date=`. | + +**`GET /dashboard/summary` response:** + +```json +{ + "year": 2026, "month": 8, + "real_income": "5200.00", "spent": "3120.45", "net": "2079.55", "transferred_out": "500.00", + "spending_by_category": [ + { "category_id": 4, "name": "Groceries", "color": "#22c55e", "icon": "shopping-cart", "amount": "612.30", "percent": 19.6 } + ] +} +``` + +**`GET /cash-flow` response:** + +```json +{ + "start_date": null, "end_date": null, + "points": [ { "year": 2026, "month": 7, "income": "5200.00", "expense": "3400.00", "net": "1800.00" } ], + "total_income": "62400.00", "total_expense": "40800.00", "total_net": "21600.00" +} +``` + +## Insights & Advice + +Rules-based, computed on read — no ML, no background jobs. Both read from the same transaction/ +account/asset data as everything else; thresholds are configurable via [Settings](#settings). + +| Method | Path | Description | +|---|---|---| +| `GET` | `/insights/alerts` | Active proactive alerts (negative cash flow streak, declining net worth, over-budget category, risky allocation, idle cash). | +| `GET` | `/advice` | Plain-language observations (rising spending categories, unbudgeted top expenses, savings-rate trend). | + +Both return a `key` + machine-readable `params` per item rather than pre-rendered text — the +frontend interpolates a localized message client-side, so build your own message from `key`/`params` +if you're consuming this programmatically rather than trying to parse rendered strings. + +```json +// GET /insights/alerts +{ "alerts": [ { "key": "idle_cash", "severity": "warning", "params": { "account_id": 2, "days": 75 } } ] } + +// GET /advice +{ "items": [ { "key": "rising_category", "tone": "warning", "params": { "category": "Dining", "percent": 34.2 } } ] } +``` + +## Settings + +App-wide configuration: display currency and every alert threshold used by `/insights/alerts`. +Single row, created automatically on first run — there's nothing to create, only to read/update. + +| Method | Path | Description | +|---|---|---| +| `GET` | `/settings` | Read current settings. | +| `PATCH` | `/settings` | Update settings (partial). | + +```json +{ + "currency": "USD", + "negative_cash_flow_threshold_months": 2, + "net_worth_decline_threshold_months": 2, + "risky_allocation_threshold_percent": 20, + "idle_cash_threshold_amount": "1000.00", + "idle_cash_threshold_days": 60 +} +``` + +- `currency`: 3-letter uppercase code — display-only, no conversion. +- `negative_cash_flow_threshold_months` / `net_worth_decline_threshold_months`: consecutive months + before the corresponding alert fires (`1`–`24`). +- `risky_allocation_threshold_percent`: max % of total capital allowed in medium/high risk tiers + before `risky_allocation_exceeded` fires (`1`–`100`). +- `idle_cash_threshold_amount` / `idle_cash_threshold_days`: balance + days of no activity a + depository account needs to hit before `idle_cash` fires. + +## Backup & Restore + +A full snapshot of every table as one JSON document — the same mechanism the in-app Settings → +Backup & Restore uses. Good for scripted off-site backups, or for migrating data programmatically. + +| Method | Path | Description | +|---|---|---| +| `GET` | `/backup/export` | Download a full backup as JSON. | +| `POST` | `/backup/import` | Restore from a backup file — **replaces existing data**, all-or-nothing. | + +```bash +# Export +curl -u user:pass http://localhost:3000/api/backup/export -o aurum-backup.json + +# Restore +curl -u user:pass -X POST http://localhost:3000/api/backup/import \ + -H "Content-Type: application/json" \ + -d @aurum-backup.json +``` + +The payload includes an `aurum_backup_version` field checked on import — a file from an incompatible +future format is rejected outright rather than partially applied. Treat `/backup/import` as +destructive: back up your current data first if you're experimenting. + +## Recipes + +### Add an expense transaction + +```bash +curl -u user:pass -X POST http://localhost:3000/api/transactions \ + -H "Content-Type: application/json" \ + -d '{ + "account_id": 1, + "category_id": 4, + "type": "expense", + "amount": 12.50, + "description": "Coffee", + "date": "2026-08-30" + }' +``` + +### Add income with a tag + +```bash +# 1. Find or create the tag +curl -u user:pass -X POST http://localhost:3000/api/tags \ + -H "Content-Type: application/json" -d '{"name": "Freelance"}' +# -> {"id": 9, "name": "Freelance"} + +# 2. Create the transaction referencing it +curl -u user:pass -X POST http://localhost:3000/api/transactions \ + -H "Content-Type: application/json" \ + -d '{ + "account_id": 1, + "category_id": 2, + "type": "income", + "amount": 800, + "description": "Client invoice", + "date": "2026-08-30", + "tag_ids": [9] + }' +``` + +### Move money between two of your own accounts + +```bash +curl -u user:pass -X POST http://localhost:3000/api/transactions \ + -H "Content-Type: application/json" \ + -d '{ + "account_id": 1, + "transfer_account_id": 2, + "type": "transfer", + "amount": 500, + "description": "Move to savings", + "date": "2026-08-30" + }' +``` + +### Pull this month's spend by category (for an external dashboard) + +```bash +curl -u user:pass "http://localhost:3000/api/dashboard/summary" | jq '.spending_by_category' +``` + +### Bulk-import transactions from your own data source + +```bash +curl -u user:pass -X POST http://localhost:3000/api/transactions/bulk \ + -H "Content-Type: application/json" \ + -d '{ + "items": [ + { "account_id": 1, "type": "expense", "amount": 9.99, "description": "Spotify", "date": "2026-08-01" }, + { "account_id": 1, "type": "expense", "amount": 45.00, "description": "Gas", "date": "2026-08-03" } + ] + }' +``` + +--- + +For self-hosting, environment variables, and running Aurum itself, see [README.md](README.md). diff --git a/README.md b/README.md index 0da3e21..48a3fa9 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Aurum is a source-available personal finance operating system — a complete, gr [![License: PolyForm Noncommercial 1.0.0](https://img.shields.io/badge/license-PolyForm%20Noncommercial%201.0.0-blue.svg)](LICENSE) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md) -[Features](#-core-features) • [Screenshots](#-screenshots) • [Getting Started](#-getting-started) • [Security](#-security--self-hosting) • [Contributing](#-contributing) • [License](#-license) +[Features](#-core-features) • [Screenshots](#-screenshots) • [Getting Started](#-getting-started) • [API Docs](DOCS.md) • [Security](#-security--self-hosting) • [Contributing](#-contributing) • [License](#-license) @@ -159,6 +159,13 @@ docker compose pull && docker compose up -d --build # update to newer code Your data lives in a Docker named volume (`aurum_pgdata`), not in the repo folder — it survives `docker compose down`, image rebuilds, and `git pull`. It's only gone if you explicitly run `docker compose down -v` or delete the volume yourself. For anything short of that, use the in-app **Settings → Backup & Restore** to export a JSON snapshot of everything before making risky changes. +## 🔌 API + +Everything Aurum's UI can do — adding transactions, managing accounts and budgets, importing a CSV, +tracking assets, exporting a backup — is also available as a plain JSON REST API at `/api`, so you +can script Aurum or connect it to other programs. See **[DOCS.md](DOCS.md)** for the full reference, +or open `/api/docs` on your running instance for interactive Swagger docs. + ## 🔒 Security & Self-Hosting **Aurum has no built-in login system.** It's built for one person to self-host one private instance of their own financial data — not as a multi-tenant service with per-user accounts. That's a deliberate trade-off, not an oversight, but it means: