# LumaLedger API

> A double-entry personal-finance ledger you can drive from code or an AI agent.

Base URL: https://api.lumaledger.app/v1
Auth: `Authorization: Bearer ll_…` (a personal access token from Settings → API & MCP; read or write; bound to one ledger).
MCP server (Streamable HTTP): https://api.lumaledger.app/api/mcp — every operation below is also a tool named <resource>_<verb>.
OpenAPI: https://lumaledger.app/documentation/developers/openapi.json

## Conventions
- Money: {"amount": "1234.50", "currency": "USD"} — decimal strings, never floats; send amounts as strings too.
- Lists: {"data": [...], "has_more": bool, "next_cursor": str|null}; pass `cursor` and `limit` (1–200).
- Errors: {"error": {"type", "code", "message", "param", "request_id"}} — branch on `code`.
- Writes: send `Idempotency-Key` on creates; a retry replays the first response for 24h.
- Rate limit: 120 requests/minute per token; 429 with Retry-After.

## Account Types

### GET /v1/account-types — List account types (`account_types.list`, read)

The five account types, with the side each one's balance grows on and
the code prefix its accounts must carry. The same for every ledger.

Parameters:
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - X-Organization-Id (string | null, header)

Returns 200 AccountTypeList.

#### AccountType
  - code_prefixes (array of string, required): What an account code of this type must start with (`accounts.create`).
  - id (integer, required)
  - name (string, required): `asset`, `liability`, `equity`, `income` or `expense`. Accounts name their type by this, as `type`.
  - normal_balance (string, required): `debit` or `credit`: the side that increases it.

#### AccountTypeList
  - data (array of AccountType, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

## Accounts

### GET /v1/accounts — List accounts (`accounts.list`, read)

Every account in the ledger's chart of accounts, including the
categories transactions are filed under, with its current balance.

Parameters:
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - include_inactive (boolean, query): Also return closed accounts.
  - type (string | null, query): Only accounts of this type. `expense` and `income` are the categories transactions are filed under.
  - institution_id (integer | null, query): Only accounts held at this bank (see `institutions.list`).
  - X-Organization-Id (string | null, header)

Returns 200 AccountList.

### POST /v1/accounts — Create an account (`accounts.create`, write)

Add an account to the chart of accounts: a bank account, a card, a
loan, an equity account, or a category. It starts at a zero balance.
Send an `Idempotency-Key` so a retry cannot create it twice.

Parameters:
  - Idempotency-Key (string | null, header): Any unique string (a UUID is ideal). A retry with the same key returns the first attempt's response instead of doing the work twice. Keys are kept for 24 hours.
  - X-Organization-Id (string | null, header)

Body (AccountCreate):
  - code (string, required): Chart-of-accounts code, unique in the ledger. Must start with the type's prefix (see `account_types.list`): `1` asset, `2` liability, `3` equity, `4` income, `5` or `6` expense.
  - institution_id (integer | null): File it at this bank (see `institutions.list`).
  - is_placeholder (boolean): A grouping account that holds no postings of its own. Default: `false`.
  - name (string, required)
  - parent_id (integer | null): Nest it under this account: a group (`is_placeholder`) of the same type, or, for an income or expense category, a top-level category of its type.
  - type (string, required): `asset`, `liability`, `equity`, `income` or `expense`. Pattern: `^(asset|liability|equity|income|expense)$`.

Returns 201 Account.

### DELETE /v1/accounts/{account_id} — Delete an account (`accounts.delete`, write, destructive)

Delete an account that was never used: no journal lines and no
sub-accounts. An account with history cannot be deleted without
unbalancing the books; close it instead (`accounts.close`).

Parameters:
  - account_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 204.

### GET /v1/accounts/{account_id} — Get an account (`accounts.get`, read)

One account by id, closed or not.

Parameters:
  - account_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 200 Account.

### PATCH /v1/accounts/{account_id} — Update an account (`accounts.update`, write)

Rename or renumber an account, move it in the tree, file it at a bank,
hide it, leave it out of totals, mark it business, or set its low-balance
alert. Only the fields sent change; the account is re-validated as a
whole, as when it was created.

Parameters:
  - account_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Body (AccountUpdate):
  - code (string | null)
  - excluded_from_totals (boolean | null): Leave its balance out of net worth and other totals.
  - hidden (boolean | null): Hide it from lists in the app.
  - institution_id (integer | null): File it at this bank; null takes it out of its bank. Refused for an account a bank connection feeds, whose bank comes from the connection.
  - is_placeholder (boolean | null)
  - low_balance_alert (string | null): Tell the member when the balance drops below this amount, in the account's currency (`"200.00"`); null stops it. Only an open asset account (a checking or savings account, a wallet) can carry one. Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - name (string | null)
  - parent_id (integer | null): Null takes it out of its parent.
  - purpose (string | null): `business` files its transactions as business by default; `personal` or null returns it to the default. Pattern: `^(personal|business)$`.
  - type (string | null): Pattern: `^(asset|liability|equity|income|expense)$`.

Returns 200 Account.

### PUT /v1/accounts/{account_id}/balance — Set a manual account's balance (`accounts.set_balance`, write)

Say what an account nothing syncs holds today, so the ledger catches
up with the charges it cannot see. The account's opening entry is
re-derived so its balance reads exactly this from today, with every
posting it already has taken into account. For real asset and liability
accounts only, and refused for one a bank connection feeds (its bank
reports the balance).

Parameters:
  - account_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Body (BalanceSet):
  - balance (string, required): What the account holds (asset) or owes (liability) today, in its own terms: a card with $300 on it is `"300.00"`. Pattern: `^-?\d{1,12}(\.\d{1,2})?$`.

Returns 200 Account.

### POST /v1/accounts/{account_id}/close — Close an account (`accounts.close`, write, destructive)

Retire an account the way the app's "Remove account" does: a closing
entry dated today brings its balance to zero, it is deactivated (history
before today stays), and if a bank connection feeds it, the connection
stops syncing it; when it was the connection's last account, the
connection itself is removed at the bank. Closing a closed account does
nothing. Needs an admin, because it stops data collection for everyone
in the ledger. Only an asset or liability account can be closed: a
category or an equity account is refused with a 422.

Parameters:
  - account_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 200 ClosedAccount.

### GET /v1/accounts/{account_id}/ledger — List an account's journal lines (`accounts.ledger`, read)

The account's register: every journal line posted to it, oldest
first (by date, then entry, then line), each with the running balance
after it. The last line's running balance is the account's `balance`.

Parameters:
  - account_id (integer, required, path)
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - X-Organization-Id (string | null, header)

Returns 200 LedgerLineList.

### POST /v1/accounts/{account_id}/purge — Erase an account and its history (`accounts.purge`, write, destructive)

Erase an account and everything it brought in, as if it had never
been linked: its transactions, every journal entry on it (openings,
closings, valuations included), balances, holdings, its connection when
nothing else hangs off it, and rules that name it. Unlike
`accounts.close`, no history is kept. The other side of a transfer into
it is kept and categorized again, so the accounts that stay do not
move; an entry that would move one of them is a 409 naming it (see
`blockers`, or call with `dry_run=true` first). Needs an admin.

Parameters:
  - account_id (integer, required, path)
  - dry_run (boolean, query): Report what would go, change nothing.
  - X-Organization-Id (string | null, header)

Returns 200 Purge.

#### Account
  - balance (Money, required): The account's balance, positive on its normal side: what an asset holds, what a liability owes, what an expense category has spent in total.
  - code (string, required): Chart-of-accounts code, unique in the ledger (`1010`, `5190`).
  - created_at (datetime, required)
  - excluded_from_totals (boolean, required): Left out of net worth and other totals.
  - hidden (boolean, required): Hidden from lists in the app.
  - id (integer, required): The account id. Transactions and journal lines name it as `account_id`.
  - institution_id (integer | null, required): The bank or provider the account belongs to.
  - is_active (boolean, required): False once the account is closed. Its history stays in the ledger.
  - is_placeholder (boolean, required): A grouping account that holds no postings of its own.
  - low_balance_alert (LowBalanceAlert | null, required): The member is told when this account's balance drops below a threshold, and has a to-do until it is back above; null when nobody set one. Only an open asset account can carry one. Set it with `accounts.update`.
  - mask (string | null, required): The last digits of the account number, as the bank shows them.
  - name (string, required)
  - normal_balance (string, required): `debit` or `credit`: the side that increases the account.
  - overdraft_ahead (LowBalanceShortfall | null, required): With no `low_balance_alert` set: the recurring bills and paychecks expected on this open asset account over the next month, and the payments of the credit cards it pays, take its balance below zero; `needed` is what to move in to stay at or above zero. The member sees it on their to-do list and the account's page but is never notified of it unasked: setting a `low_balance_alert` is how they ask. Null when the bills ahead do not, when the balance is already below zero, and whenever an alert is set (its `shortfall` says it then).
  - parent_id (integer | null, required): The parent account, for sub-accounts and subcategories.
  - purpose (string | null, required): `business` if its transactions count as business by default; null for personal.
  - role (string | null, required): What the account is for, when known (`checking`, `credit_card`, `mortgage`, …).
  - type (string, required): `asset`, `liability`, `equity`, `income` or `expense`.
  - unshared_at (datetime | null, required): When the bank stopped sharing this account on its connection (deselected, closed or revoked at the bank) while the connection itself keeps syncing, or null while it is shared. Its `balance` is frozen at the last number the bank sent. A person fixes it in the app by sharing the account again, or closes it with `accounts.close`.

#### AccountCreate
  - code (string, required): Chart-of-accounts code, unique in the ledger. Must start with the type's prefix (see `account_types.list`): `1` asset, `2` liability, `3` equity, `4` income, `5` or `6` expense.
  - institution_id (integer | null): File it at this bank (see `institutions.list`).
  - is_placeholder (boolean): A grouping account that holds no postings of its own. Default: `false`.
  - name (string, required)
  - parent_id (integer | null): Nest it under this account: a group (`is_placeholder`) of the same type, or, for an income or expense category, a top-level category of its type.
  - type (string, required): `asset`, `liability`, `equity`, `income` or `expense`. Pattern: `^(asset|liability|equity|income|expense)$`.

#### AccountList
  - data (array of Account, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

#### AccountUpdate
  - code (string | null)
  - excluded_from_totals (boolean | null): Leave its balance out of net worth and other totals.
  - hidden (boolean | null): Hide it from lists in the app.
  - institution_id (integer | null): File it at this bank; null takes it out of its bank. Refused for an account a bank connection feeds, whose bank comes from the connection.
  - is_placeholder (boolean | null)
  - low_balance_alert (string | null): Tell the member when the balance drops below this amount, in the account's currency (`"200.00"`); null stops it. Only an open asset account (a checking or savings account, a wallet) can carry one. Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - name (string | null)
  - parent_id (integer | null): Null takes it out of its parent.
  - purpose (string | null): `business` files its transactions as business by default; `personal` or null returns it to the default. Pattern: `^(personal|business)$`.
  - type (string | null): Pattern: `^(asset|liability|equity|income|expense)$`.

#### BalanceSet
  - balance (string, required): What the account holds (asset) or owes (liability) today, in its own terms: a card with $300 on it is `"300.00"`. Pattern: `^-?\d{1,12}(\.\d{1,2})?$`.

#### ClosedAccount
  - account (Account, required)
  - connection_removed (boolean, required): It was the last account behind a bank connection, so the connection was removed too.

#### LedgerLine
  - amount (Money, required): Always positive; `direction` says which side.
  - date (date, required)
  - description (string, required)
  - direction (string, required): `debit` or `credit`.
  - id (integer, required): The journal line id.
  - running_balance (Money, required): The account's balance after this line, positive on its normal side, counting every earlier line whatever page it is on.
  - source (string, required): Where the entry came from: `plaid`, `import`, `manual`, …
  - statement_line (string | null, required): The line as the bank printed it on the statement (`SQ *BLUE BOTTLE 0412 OAKLAND CA`). Null on an entry nobody imported.
  - transaction_id (integer, required): The journal entry the line belongs to (`transactions.get` takes it).

#### LedgerLineList
  - data (array of LedgerLine, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

#### LowBalanceAlert
  - below (boolean, required): The balance is under the threshold right now.
  - below_since (datetime | null, required): When the balance was seen dropping below the threshold, which is when the member was notified; null while it is at or above it.
  - shortfall (LowBalanceShortfall | null, required): The recurring bills and paychecks expected on the account over the next month, and the payments of the credit cards it pays (bills first on a day), take the balance under the threshold; the member is notified once and has a to-do while it stands. Null when they do not, and while the balance is already below.
  - threshold (Money, required): Alert when the balance drops below this.

#### LowBalanceShortfall
  - bill_amount (Money, required): That bill's usual amount; for a card, what paying it in full takes.
  - bill_kind (string, required): `recurring`: a charge the recurring detector found. `card_payment`: a credit card this account paid last, expected to be paid in full by its due date.
  - bill_name (string, required): The expected bill that takes it under: a recurring charge's name, or the name of the credit card whose payment it is.
  - lowest (Money, required): The lowest the balance is projected to reach over the month.
  - needed (Money, required): What to move in now to stay at or above the threshold all month.
  - short_on (date, required): The first day the balance is projected under the threshold.

#### Money
  - amount (string, required): Pattern: `^-?\d+\.\d{2}$`.
  - currency (string, required): ISO 4217 code. Pattern: `^[A-Z]{3}$`.

#### Purge
  - account_ids (array of integer, required)
  - blocker_count (integer, required)
  - blockers (array of PurgeBlocker, required)
  - entries (integer, required): Journal entries deleted outright.
  - erased (boolean, required): False on a dry run, or when blockers refused it.
  - released (integer, required): Transactions on accounts that stay, unpaired from a transfer into an erased account and categorized again. Their balances do not move.
  - transactions (integer, required): Transactions of the erased accounts.

#### PurgeBlocker
  - date (string | null)
  - id (integer, required)
  - kind (string, required): `entry`: a journal entry (`id` is its transaction id) that also moves an account being kept. `child_account`: a sub-account of one being erased.
  - label (string, required)

## Alternatives

### GET /v1/alternatives — List alternative investments (`alternatives.list`, read)

Every alternative investment in the ledger, live, exited or written
off, with its gain and multiple.

Parameters:
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - X-Organization-Id (string | null, header)

Returns 200 InvestmentList.

### POST /v1/alternatives — Record an alternative investment (`alternatives.create`, write)

Record a position and post its value to the ledger's Alternative
Investments account. Send an `Idempotency-Key` so a retry cannot record
it twice.

Parameters:
  - Idempotency-Key (string | null, header): Any unique string (a UUID is ideal). A retry with the same key returns the first attempt's response instead of doing the work twice. Keys are kept for 24 hours.
  - X-Organization-Id (string | null, header)

Body (InvestmentCreate):
  - category (string, required): One of `angel`, `fund`, `private_credit`, `collectible`, `crypto`, `other`.
  - currency (string | null): ISO 4217 code of the amounts, when not the ledger's base currency. Pattern: `^[A-Z]{3}$`.
  - current_value (string | null): What it is worth now. Defaults to `invested_amount`. Posted to the ledger. Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - invested_amount (string, required): What went in, as a decimal string. Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - invested_date (date | null)
  - issuer (string | null)
  - name (string, required)
  - notes (string | null)
  - terms (object | null): Free-form deal terms.
  - vehicle (string): One of `safe`, `convertible_note`, `priced_round`, `lp_interest`, `note`, `direct`, `other`. Default: `"other"`.

Returns 201 Investment.

### DELETE /v1/alternatives/{investment_id} — Delete an alternative investment (`alternatives.delete`, write, destructive)

Delete a position and take its value off the ledger, with its
documents and their files. To keep its history, set `status` to
`exited` or `written_off` instead.

Parameters:
  - investment_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 204.

### GET /v1/alternatives/{investment_id} — Get an alternative investment (`alternatives.get`, read)

One alternative investment with the documents filed against it.

Parameters:
  - investment_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 200 Investment.

### PATCH /v1/alternatives/{investment_id} — Update an alternative investment (`alternatives.update`, write)

Revalue a position, record its exit or write-off, correct its details,
or hide it or leave it out of the totals. Only the fields sent change,
and any change in what the position is worth is posted to the ledger.

Parameters:
  - investment_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Body (InvestmentUpdate):
  - current_value (string): Revalue a live position; the change is posted to the ledger. Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - excluded_from_totals (boolean): Leave it out of net worth: its value is taken off the ledger.
  - exit_date (date): Sent with an exit. Defaults to today.
  - exit_proceeds (string): What came back, sent with an exit. Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - hidden (boolean)
  - issuer (string)
  - name (string)
  - notes (string)
  - status (string): One of `active`, `exited`, `written_off`. Exiting or writing off takes the position's value off the ledger; reactivating restores it.
  - terms (object)

Returns 200 Investment.

### POST /v1/alternatives/{investment_id}/documents — Attach a document to an alternative investment (`alternatives.add_document`, write)

File a document (the SAFE, the subscription agreement, a K-1) against
a position. Upload it first with `files.create` and send its `file_id`;
the document is kept with the position, after the upload itself
expires, and shows in the app. Send an `Idempotency-Key` so a retry
cannot file it twice.

Parameters:
  - investment_id (integer, required, path)
  - Idempotency-Key (string | null, header): Any unique string (a UUID is ideal). A retry with the same key returns the first attempt's response instead of doing the work twice. Keys are kept for 24 hours.
  - X-Organization-Id (string | null, header)

Body (InvestmentDocumentAdd):
  - file_id (integer, required): The document, uploaded with `files.create`.
  - name (string | null): What to call it. Defaults to the uploaded file's name.

Returns 201 InvestmentDocumentRef.

### DELETE /v1/alternatives/{investment_id}/documents/{investment_document_id} — Remove a document from an alternative investment (`alternatives.delete_document`, write, destructive)

Remove a document from a position, and delete its file.
`investment_document_id` is a document's `id` from the position's
`documents`.

Parameters:
  - investment_id (integer, required, path)
  - investment_document_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 204.

#### Investment
  - category (string, required): One of `angel`, `fund`, `private_credit`, `collectible`, `crypto`, `other`.
  - current_value (Money, required): What it is worth now. Zero once exited or written off.
  - documents (array of InvestmentDocumentRef, required)
  - excluded_from_totals (boolean, required): Left out of net worth: its value is not posted to the ledger.
  - exit_date (date | null, required)
  - exit_proceeds (Money | null, required): What came back on exit, when recorded.
  - gain (Money, required): `current_value` minus `invested_amount`.
  - hidden (boolean, required): Hidden from the app's list; still counted.
  - id (integer, required)
  - invested_amount (Money, required): What went in.
  - invested_date (date | null, required)
  - issuer (string | null, required): The company, fund or counterparty.
  - multiple (string | null, required): `current_value` ÷ `invested_amount` as a decimal string (`"1.25"`). Null when nothing was invested.
  - name (string, required)
  - notes (string | null, required)
  - status (string, required): One of `active`, `exited`, `written_off`.
  - terms (object | null, required): Free-form deal terms (valuation cap, discount, carry, …), as recorded.
  - valued_at (date | null, required): The date `current_value` was last set.
  - vehicle (string, required): The instrument held: one of `safe`, `convertible_note`, `priced_round`, `lp_interest`, `note`, `direct`, `other`.

#### InvestmentCreate
  - category (string, required): One of `angel`, `fund`, `private_credit`, `collectible`, `crypto`, `other`.
  - currency (string | null): ISO 4217 code of the amounts, when not the ledger's base currency. Pattern: `^[A-Z]{3}$`.
  - current_value (string | null): What it is worth now. Defaults to `invested_amount`. Posted to the ledger. Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - invested_amount (string, required): What went in, as a decimal string. Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - invested_date (date | null)
  - issuer (string | null)
  - name (string, required)
  - notes (string | null)
  - terms (object | null): Free-form deal terms.
  - vehicle (string): One of `safe`, `convertible_note`, `priced_round`, `lp_interest`, `note`, `direct`, `other`. Default: `"other"`.

#### InvestmentDocumentAdd
  - file_id (integer, required): The document, uploaded with `files.create`.
  - name (string | null): What to call it. Defaults to the uploaded file's name.

#### InvestmentDocumentRef
  - content_type (string | null, required)
  - created_at (datetime, required)
  - id (integer, required)
  - name (string, required)
  - size_bytes (integer | null, required)

#### InvestmentList
  - data (array of Investment, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

#### InvestmentUpdate
  - current_value (string): Revalue a live position; the change is posted to the ledger. Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - excluded_from_totals (boolean): Leave it out of net worth: its value is taken off the ledger.
  - exit_date (date): Sent with an exit. Defaults to today.
  - exit_proceeds (string): What came back, sent with an exit. Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - hidden (boolean)
  - issuer (string)
  - name (string)
  - notes (string)
  - status (string): One of `active`, `exited`, `written_off`. Exiting or writing off takes the position's value off the ledger; reactivating restores it.
  - terms (object)

#### Money
  - amount (string, required): Pattern: `^-?\d+\.\d{2}$`.
  - currency (string, required): ISO 4217 code. Pattern: `^[A-Z]{3}$`.

## Budgets

### GET /v1/budget-categories — List categories with their budget figures (`budget_categories.list`, read)

Every expense category in one period, as the app's budgets page lists
them: budgeted or not, with what was spent and what it usually costs.
Inactive categories with no budget and no spending are left out.

Parameters:
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - period (string | null, query): The period to measure. Defaults to the current one. A budget period key: `YYYY-MM` while the ledger budgets by calendar month, otherwise the ISO date (`YYYY-MM-DD`) of the period's first day. As input either form is accepted, and any day names the period it falls in.
  - purpose (string | null, query): Count only `personal` or only `business` spending. Both by default.
  - X-Organization-Id (string | null, header)

Returns 200 BudgetCategoryList.

### GET /v1/budget-cycle — Get the budget cycle (`budget_cycle.get`, read)

The cycle the ledger's budgets run on: the calendar month unless the
household budgets by paycheck.

Parameters:
  - X-Organization-Id (string | null, header)

Returns 200 BudgetCycle.

### PUT /v1/budget-cycle — Set the budget cycle (`budget_cycle.update`, write)

Choose the cycle every budget runs on: the calendar month, a monthly
period starting on another day, twice a month, or every one or two
weeks from a payday. Budgets keep their amounts; their start and their
per-period amounts move to the periods the new cycle defines.

Parameters:
  - X-Organization-Id (string | null, header)

Body (BudgetCycleUpdate):
  - anchor_date (date, required): See `BudgetCycle.anchor_date`.
  - cadence (string, required): See `BudgetCycle.cadence`. Pattern: `^(monthly|semi_monthly|biweekly|weekly)$`.

Returns 200 BudgetCycle.

### GET /v1/budgets — List budgets (`budgets.list`, read)

The ledger's budgets, each measured over one period (the current one
unless `period` says otherwise): allowed, carried over, spent and left
for a spending budget; planned and paid for a loan's extra payment.

Parameters:
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - period (string | null, query): The period to measure. Defaults to the current one. A budget period key: `YYYY-MM` while the ledger budgets by calendar month, otherwise the ISO date (`YYYY-MM-DD`) of the period's first day. As input either form is accepted, and any day names the period it falls in.
  - purpose (string | null, query): Count only `personal` or only `business` spending. Both by default.
  - kind (string | null, query): Only `spend` or only `paydown` budgets.
  - X-Organization-Id (string | null, header)

Returns 200 BudgetList.

### POST /v1/budgets — Create a budget (`budgets.create`, write)

Budget a category: an amount per period (or per year), optionally
rolling over. With `kind: paydown`, plan extra principal on a loan
instead. A category has at most one budget: change an existing one with
`budgets.update`.

Parameters:
  - Idempotency-Key (string | null, header): Any unique string (a UUID is ideal). A retry with the same key returns the first attempt's response instead of doing the work twice. Keys are kept for 24 hours.
  - X-Organization-Id (string | null, header)

Body (BudgetCreate):
  - account_id (integer, required): An expense category (`accounts.list?type=expense`) or, with `kind: paydown`, a confirmed loan's liability account.
  - amount (string, required): Decimal string, at most two decimal places. Per period, or per calendar year when `frequency` is `annual`. Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - frequency (string): `monthly` (per budget period) or `annual` (per calendar year; spend only). Pattern: `^(monthly|annual)$`. Default: `"monthly"`.
  - kind (string): `spend` or `paydown`. Pattern: `^(spend|paydown)$`. Default: `"spend"`.
  - rollover (boolean): Carry what is left (or owed) into the next period. Monthly only. Default: `false`.
  - start_period (string | null): The period the plan starts in; the current one if omitted. A budget period key: `YYYY-MM` while the ledger budgets by calendar month, otherwise the ISO date (`YYYY-MM-DD`) of the period's first day. As input either form is accepted, and any day names the period it falls in. Pattern: `^\d{4}-\d{2}(-\d{2})?$`.

Returns 201 BudgetOut.

### DELETE /v1/budgets/{budget_id} — Delete a budget (`budgets.delete`, write, destructive)

Remove a budget and its per-period amounts. The spending it measured
stays in the ledger.

Parameters:
  - budget_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 204.

### GET /v1/budgets/{budget_id} — Get a budget (`budgets.get`, read)

One budget by its id, measured over `period` (the current one by default).

Parameters:
  - budget_id (integer, required, path)
  - period (string | null, query): The period to measure. Defaults to the current one. A budget period key: `YYYY-MM` while the ledger budgets by calendar month, otherwise the ISO date (`YYYY-MM-DD`) of the period's first day. As input either form is accepted, and any day names the period it falls in.
  - purpose (string | null, query): Count only `personal` or only `business` spending. Both by default.
  - X-Organization-Id (string | null, header)

Returns 200 BudgetOut.

### PATCH /v1/budgets/{budget_id} — Update a budget (`budgets.update`, write)

Change a budget's amount, frequency, rollover or start. Only the
fields sent change. Making a budget `annual` drops its per-period
amounts.

Parameters:
  - budget_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Body (BudgetUpdate):
  - amount (string | null): See `BudgetCreate.amount`. Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - frequency (string | null): Pattern: `^(monthly|annual)$`.
  - rollover (boolean | null)
  - start_period (string | null): A budget period key: `YYYY-MM` while the ledger budgets by calendar month, otherwise the ISO date (`YYYY-MM-DD`) of the period's first day. As input either form is accepted, and any day names the period it falls in. Pattern: `^\d{4}-\d{2}(-\d{2})?$`.

Returns 200 BudgetOut.

### PUT /v1/budgets/{budget_id}/period-amount — Set one period's amount (`budgets.set_period_amount`, write)

Give one period of a monthly budget its own amount ("December is
600, not 200"), including a period that has not started; null returns it
to the default. Answers with the budget measured over that period.

Parameters:
  - budget_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Body (PeriodAmountUpdate):
  - amount (string | null, required): That period's own amount, or null to return it to the budget's default. Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - period (string, required): The period to plan. A budget period key: `YYYY-MM` while the ledger budgets by calendar month, otherwise the ISO date (`YYYY-MM-DD`) of the period's first day. As input either form is accepted, and any day names the period it falls in. Pattern: `^\d{4}-\d{2}(-\d{2})?$`.

Returns 200 BudgetOut.

### POST /v1/budgets/fill — Fill budgets in one step (`budgets.fill`, write)

The app's one-click fills: budget every category at what it usually
costs, or carry last period's amounts into this one. At most 100
budgets are written. Send `dry_run: true` first to see the list.

Parameters:
  - Idempotency-Key (string | null, header): Any unique string (a UUID is ideal). A retry with the same key returns the first attempt's response instead of doing the work twice. Keys are kept for 24 hours.
  - X-Organization-Id (string | null, header)

Body (BudgetFill):
  - dry_run (boolean): List what would be written and write nothing. Default: `false`.
  - only_unbudgeted (boolean): `usual` only: leave categories that already have a budget alone. Default: `true`.
  - period (string | null): The period to fill; the current one if omitted. A budget period key: `YYYY-MM` while the ledger budgets by calendar month, otherwise the ISO date (`YYYY-MM-DD`) of the period's first day. As input either form is accepted, and any day names the period it falls in. Pattern: `^\d{4}-\d{2}(-\d{2})?$`.
  - source (string): `usual`: budget every top-level category at what it usually costs (its three-period average, whole units). `last_period`: give every monthly budget this period the amount last period allowed, where they differ. Pattern: `^(usual|last_period)$`. Default: `"usual"`.

Returns 200 BudgetFillResult.

### GET /v1/budgets/summary — Summarize a budget period (`budgets.summary`, read)

The totals of one budget period: what the spending budgets allowed,
carried, spent and have left, and what was spent outside any budget.
Per budget, see `budgets.list`; per category, `budget_categories.list`.

Parameters:
  - period (string | null, query): The period to measure. Defaults to the current one. A budget period key: `YYYY-MM` while the ledger budgets by calendar month, otherwise the ISO date (`YYYY-MM-DD`) of the period's first day. As input either form is accepted, and any day names the period it falls in.
  - purpose (string | null, query): Count only `personal` or only `business` spending. Both by default.
  - X-Organization-Id (string | null, header)

Returns 200 BudgetSummary.

#### BudgetCategory
  - account_id (integer, required): The expense category's account id.
  - available (Money, required)
  - average (Money, required): Mean spend per period over the last three complete periods.
  - budget_id (integer | null, required): Its budget (see `budgets.get`), or null.
  - budgeted (Money, required): What the budget allows over the span measured; zero with no budget.
  - carried (Money | null, required): Rolled in from earlier periods; null unless the budget rolls over.
  - code (string, required)
  - name (string, required)
  - parent_account_id (integer | null, required): The category this is a subcategory of. A budget on the parent covers its subcategories.
  - period_spent (Money, required): Spent in this period alone.
  - remaining (Money, required)
  - spent (Money, required): Spent over the span `budgeted` covers, subcategories included.

#### BudgetCategoryList
  - data (array of BudgetCategory, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

#### BudgetCreate
  - account_id (integer, required): An expense category (`accounts.list?type=expense`) or, with `kind: paydown`, a confirmed loan's liability account.
  - amount (string, required): Decimal string, at most two decimal places. Per period, or per calendar year when `frequency` is `annual`. Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - frequency (string): `monthly` (per budget period) or `annual` (per calendar year; spend only). Pattern: `^(monthly|annual)$`. Default: `"monthly"`.
  - kind (string): `spend` or `paydown`. Pattern: `^(spend|paydown)$`. Default: `"spend"`.
  - rollover (boolean): Carry what is left (or owed) into the next period. Monthly only. Default: `false`.
  - start_period (string | null): The period the plan starts in; the current one if omitted. A budget period key: `YYYY-MM` while the ledger budgets by calendar month, otherwise the ISO date (`YYYY-MM-DD`) of the period's first day. As input either form is accepted, and any day names the period it falls in. Pattern: `^\d{4}-\d{2}(-\d{2})?$`.

#### BudgetCycle
  - anchor_date (date, required): For `monthly` and `semi_monthly`, the day of the month a period starts on. For `biweekly` and `weekly`, a payday the periods count from.
  - cadence (string, required): `monthly`, `semi_monthly` (twice a month, 15 days apart), `biweekly` (every other week) or `weekly`.
  - is_calendar_month (boolean, required): The plain calendar month (monthly from the 1st). Period keys are then `YYYY-MM`; otherwise they are the period's first day.

#### BudgetCycleUpdate
  - anchor_date (date, required): See `BudgetCycle.anchor_date`.
  - cadence (string, required): See `BudgetCycle.cadence`. Pattern: `^(monthly|semi_monthly|biweekly|weekly)$`.

#### BudgetFill
  - dry_run (boolean): List what would be written and write nothing. Default: `false`.
  - only_unbudgeted (boolean): `usual` only: leave categories that already have a budget alone. Default: `true`.
  - period (string | null): The period to fill; the current one if omitted. A budget period key: `YYYY-MM` while the ledger budgets by calendar month, otherwise the ISO date (`YYYY-MM-DD`) of the period's first day. As input either form is accepted, and any day names the period it falls in. Pattern: `^\d{4}-\d{2}(-\d{2})?$`.
  - source (string): `usual`: budget every top-level category at what it usually costs (its three-period average, whole units). `last_period`: give every monthly budget this period the amount last period allowed, where they differ. Pattern: `^(usual|last_period)$`. Default: `"usual"`.

#### BudgetFillItem
  - account_id (integer, required): The expense category.
  - amount (Money, required): What the fill writes for the period.
  - current (Money | null, required): What the period allowed before; null with no budget.
  - name (string, required)

#### BudgetFillResult
  - applied (boolean, required): False on a dry run.
  - items (array of BudgetFillItem, required)
  - period (string, required): The period filled.
  - source (string, required)

#### BudgetList
  - data (array of BudgetOut, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

#### BudgetOut
  - account_id (integer, required): The expense category (see `accounts.list?type=expense`) or, for a `paydown` budget, the loan's liability account.
  - amount (Money, required): The default amount per period, or per year when `annual`.
  - created_at (datetime, required)
  - frequency (string, required): `monthly`: `amount` is per budget period. `annual`: `amount` is per calendar year.
  - id (integer, required): The budget id.
  - kind (string, required): `spend` (a spending plan) or `paydown` (extra principal on a loan).
  - paydown (PaydownStatus | null, required): For a `paydown` budget whose loan is still confirmed; null otherwise.
  - period (string, required): The period `spending` or `paydown` is measured over.
  - period_amounts (array of BudgetPeriodAmount, required): Periods that have their own amount instead of the default.
  - rollover (boolean, required): What is not spent carries into the next period, and an overspend is owed by it.
  - spending (SpendingStatus | null, required): For a `spend` budget; null otherwise.
  - start_period (string, required): The period the plan starts in. A budget period key: `YYYY-MM` while the ledger budgets by calendar month, otherwise the ISO date (`YYYY-MM-DD`) of the period's first day. As input either form is accepted, and any day names the period it falls in.
  - updated_at (datetime, required)

#### BudgetPeriod
  - end (date, required): Inclusive.
  - key (string, required): A budget period key: `YYYY-MM` while the ledger budgets by calendar month, otherwise the ISO date (`YYYY-MM-DD`) of the period's first day. As input either form is accepted, and any day names the period it falls in.
  - start (date, required)

#### BudgetPeriodAmount
  - amount (Money, required)
  - period (string, required): A budget period key: `YYYY-MM` while the ledger budgets by calendar month, otherwise the ISO date (`YYYY-MM-DD`) of the period's first day. As input either form is accepted, and any day names the period it falls in.

#### BudgetSummary
  - cycle (BudgetCycle, required)
  - is_future (boolean, required): The period has not started: nothing is spent yet, and `carried` counts only the periods that have finished.
  - next_period (string, required): The next period's key.
  - period (BudgetPeriod, required)
  - previous_period (string, required): The previous period's key.
  - totals (BudgetTotals, required)
  - unbudgeted_spent (Money, required): Spent this period in categories with no budget.

#### BudgetTotals
  - available (Money, required): `budgeted` + `carried`.
  - budgeted (Money, required)
  - carried (Money, required): Rolled in from earlier periods; negative is an overspend being repaid.
  - remaining (Money, required): `available` − `spent`. Negative is over budget.
  - spent (Money, required)

#### BudgetUpdate
  - amount (string | null): See `BudgetCreate.amount`. Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - frequency (string | null): Pattern: `^(monthly|annual)$`.
  - rollover (boolean | null)
  - start_period (string | null): A budget period key: `YYYY-MM` while the ledger budgets by calendar month, otherwise the ISO date (`YYYY-MM-DD`) of the period's first day. As input either form is accepted, and any day names the period it falls in. Pattern: `^\d{4}-\d{2}(-\d{2})?$`.

#### Money
  - amount (string, required): Pattern: `^-?\d+\.\d{2}$`.
  - currency (string, required): ISO 4217 code. Pattern: `^[A-Z]{3}$`.

#### PaydownStatus
  - extra_paid (Money, required): `paid_down` beyond `scheduled_principal`, never below zero.
  - loan_id (integer, required)
  - paid_down (Money, required): Everything paid to the loan's principal in the period.
  - planned (Money, required): The extra principal planned for the period.
  - remaining (Money, required): `planned` − `extra_paid`.
  - scheduled_principal (Money, required): What the scheduled payment alone puts to principal in a period.

#### PeriodAmountUpdate
  - amount (string | null, required): That period's own amount, or null to return it to the budget's default. Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - period (string, required): The period to plan. A budget period key: `YYYY-MM` while the ledger budgets by calendar month, otherwise the ISO date (`YYYY-MM-DD`) of the period's first day. As input either form is accepted, and any day names the period it falls in. Pattern: `^\d{4}-\d{2}(-\d{2})?$`.

#### SpendingStatus
  - available (Money, required): `budgeted` + `carried`.
  - average (Money, required): Mean spend per period over the last three complete periods.
  - budgeted (Money, required): What the plan allows over the span measured: the period, or the calendar year for an `annual` budget.
  - carried (Money | null, required): Rolled in from earlier periods (negative: an overspend being repaid). Null unless the budget rolls over.
  - period_spent (Money, required): Spent in this period alone.
  - remaining (Money, required): `available` − `spent`. Negative is over budget.
  - spent (Money, required): Spent over the same span as `budgeted`, subcategories included.

## Cards

### GET /v1/cards — List credit cards with their statements and due dates (`cards.list`, read)

Every open credit card in the ledger: what it owes, its limit and
purchase APR, the latest statement and its minimum payment, and when
the next payment is due. A due date the bank has not renewed yet is
carried forward a month and says so in `due_date_source`. Closed cards
are left out.

Parameters:
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - X-Organization-Id (string | null, header)

Returns 200 CardList.

#### Card
  - available (Money | null, required): Available credit, as the bank last reported it.
  - balance (Money, required): What is owed on the card, positive; negative when the issuer owes you. From the ledger, so it includes pending charges the bank's balance leaves out.
  - bank_due_date (date | null, required): The due date the bank last gave.
  - details_frozen_since (datetime | null, required): Since when the statement fields above have not been updated (the bank's card details keep failing, or its connection stopped syncing; `plaid_items.list` says which). Null while they are current.
  - due_date_source (string | null, required): `bank` when the bank gave `next_payment_due_date`; `projected` when the bank's last date (`bank_due_date`) has passed with no newer one, so this is the same day of the month carried forward (card payments fall on the same day every month).
  - id (integer, required): The card's account id, as `accounts.list` reports it.
  - institution_name (string | null, required): The issuer, as the ledger names it.
  - is_overdue (boolean | null, required): The bank's own overdue flag.
  - last_payment (Money | null, required): The last payment the bank recorded.
  - last_payment_date (date | null)
  - limit (Money | null, required): The credit limit, when the bank reports one.
  - mask (string | null, required): The last digits of the card number (`4721`).
  - minimum_payment (Money | null, required): The minimum the latest statement asked for, due on `bank_due_date`.
  - name (string, required)
  - next_payment_due_date (date | null, required): The next day a payment is due. The bank's own date stays for a day after it passes (it is still that day somewhere), then the next one is projected. Null when the bank expects no payment.
  - purchase_apr (string | null, required): The purchase APR in percent, as a decimal string (`"23.24"`).
  - statement_balance (Money | null, required): The latest statement's balance: the cycle due on `bank_due_date`.
  - statement_issued_on (date | null, required): The day the latest statement closed.
  - statement_paid (Money | null, required): Payments and credits posted to the card since the statement closed, through its due date.
  - statement_remaining (Money | null, required): What is left of `statement_balance`: pay this by `bank_due_date` to avoid interest. Zero once it is paid in full.
  - statement_status (string | null, required): How much of the latest statement is paid, from the payments and credits the ledger holds since it closed (through `bank_due_date` and a day's grace): `paid_in_full` (no interest on it), `unpaid` (some still owed, its due date ahead) or `carried` (its due date passed with some unpaid, so the issuer charges interest). Null when there is nothing to judge: no balance owed, no closing date from the bank, or a due date more than a month gone.

#### CardList
  - data (array of Card, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

#### Money
  - amount (string, required): Pattern: `^-?\d+\.\d{2}$`.
  - currency (string, required): ISO 4217 code. Pattern: `^[A-Z]{3}$`.

## Categories

### POST /v1/categories — Create a category (`categories.create`, write)

Create an income or expense category to file transactions under. Its
code is allocated for you, and it is nested under its type's group, or
under `parent_id` as a subcategory (one level deep). Returns it as an
account, the shape every category has on this API. Send an
`Idempotency-Key` so a retry cannot create it twice.

Parameters:
  - Idempotency-Key (string | null, header): Any unique string (a UUID is ideal). A retry with the same key returns the first attempt's response instead of doing the work twice. Keys are kept for 24 hours.
  - X-Organization-Id (string | null, header)

Body (CategoryCreate):
  - name (string, required)
  - parent_id (integer | null): Make it a subcategory of this top-level category of the same type (an account id from `accounts.list?type=expense`). Omitted, it is a top-level category.
  - type (string, required): `expense` for spending, `income` for money coming in. Pattern: `^(expense|income)$`.

Returns 201 Account.

#### Account
  - balance (Money, required): The account's balance, positive on its normal side: what an asset holds, what a liability owes, what an expense category has spent in total.
  - code (string, required): Chart-of-accounts code, unique in the ledger (`1010`, `5190`).
  - created_at (datetime, required)
  - excluded_from_totals (boolean, required): Left out of net worth and other totals.
  - hidden (boolean, required): Hidden from lists in the app.
  - id (integer, required): The account id. Transactions and journal lines name it as `account_id`.
  - institution_id (integer | null, required): The bank or provider the account belongs to.
  - is_active (boolean, required): False once the account is closed. Its history stays in the ledger.
  - is_placeholder (boolean, required): A grouping account that holds no postings of its own.
  - low_balance_alert (LowBalanceAlert | null, required): The member is told when this account's balance drops below a threshold, and has a to-do until it is back above; null when nobody set one. Only an open asset account can carry one. Set it with `accounts.update`.
  - mask (string | null, required): The last digits of the account number, as the bank shows them.
  - name (string, required)
  - normal_balance (string, required): `debit` or `credit`: the side that increases the account.
  - overdraft_ahead (LowBalanceShortfall | null, required): With no `low_balance_alert` set: the recurring bills and paychecks expected on this open asset account over the next month, and the payments of the credit cards it pays, take its balance below zero; `needed` is what to move in to stay at or above zero. The member sees it on their to-do list and the account's page but is never notified of it unasked: setting a `low_balance_alert` is how they ask. Null when the bills ahead do not, when the balance is already below zero, and whenever an alert is set (its `shortfall` says it then).
  - parent_id (integer | null, required): The parent account, for sub-accounts and subcategories.
  - purpose (string | null, required): `business` if its transactions count as business by default; null for personal.
  - role (string | null, required): What the account is for, when known (`checking`, `credit_card`, `mortgage`, …).
  - type (string, required): `asset`, `liability`, `equity`, `income` or `expense`.
  - unshared_at (datetime | null, required): When the bank stopped sharing this account on its connection (deselected, closed or revoked at the bank) while the connection itself keeps syncing, or null while it is shared. Its `balance` is frozen at the last number the bank sent. A person fixes it in the app by sharing the account again, or closes it with `accounts.close`.

#### CategoryCreate
  - name (string, required)
  - parent_id (integer | null): Make it a subcategory of this top-level category of the same type (an account id from `accounts.list?type=expense`). Omitted, it is a top-level category.
  - type (string, required): `expense` for spending, `income` for money coming in. Pattern: `^(expense|income)$`.

#### LowBalanceAlert
  - below (boolean, required): The balance is under the threshold right now.
  - below_since (datetime | null, required): When the balance was seen dropping below the threshold, which is when the member was notified; null while it is at or above it.
  - shortfall (LowBalanceShortfall | null, required): The recurring bills and paychecks expected on the account over the next month, and the payments of the credit cards it pays (bills first on a day), take the balance under the threshold; the member is notified once and has a to-do while it stands. Null when they do not, and while the balance is already below.
  - threshold (Money, required): Alert when the balance drops below this.

#### LowBalanceShortfall
  - bill_amount (Money, required): That bill's usual amount; for a card, what paying it in full takes.
  - bill_kind (string, required): `recurring`: a charge the recurring detector found. `card_payment`: a credit card this account paid last, expected to be paid in full by its due date.
  - bill_name (string, required): The expected bill that takes it under: a recurring charge's name, or the name of the credit card whose payment it is.
  - lowest (Money, required): The lowest the balance is projected to reach over the month.
  - needed (Money, required): What to move in now to stay at or above the threshold all month.
  - short_on (date, required): The first day the balance is projected under the threshold.

#### Money
  - amount (string, required): Pattern: `^-?\d+\.\d{2}$`.
  - currency (string, required): ISO 4217 code. Pattern: `^[A-Z]{3}$`.

## Categorization

### POST /v1/categorization/run — Categorize waiting events (`categorization.run`, write)

Categorize the events still waiting for a category: merchant memory
and the bank's own categories first, then a model for the rest, then
your rules, which outrank every guess. Answers 202 with a Job (`kind`
`categorize`). Its `result` counts each stage: `rule`,
`merchant_memory`, `pfc`, `transfer` and `uncategorized` at the top
level, then `llm` (`llm`, `new_categories`, `skipped`), `rules`
(`matched`, `changed`), and with `drain` also `journals_generated`.
Only uncategorized events are touched.

Parameters:
  - Idempotency-Key (string | null, header): Any unique string (a UUID is ideal). A retry with the same key returns the first attempt's response instead of doing the work twice. Keys are kept for 24 hours.
  - X-Organization-Id (string | null, header)

Body (CategorizationRun):
  - drain (boolean): Work through the whole backlog of events waiting for the model, then post what was categorized. Without it, one batch is categorized and nothing is posted. Default: `false`.

Returns 202 Job.

### POST /v1/journals/generate — Post categorized events (`journals.generate`, write)

Post every categorized event that is not posted yet as a balanced
journal entry, by the posting rules (`posting_rules.list`). Answers 202
with a Job (`kind` `generate_journals`); its `result`:
`journals_generated`. Posted events are never posted twice.

Parameters:
  - Idempotency-Key (string | null, header): Any unique string (a UUID is ideal). A retry with the same key returns the first attempt's response instead of doing the work twice. Keys are kept for 24 hours.
  - X-Organization-Id (string | null, header)

Returns 202 Job.

### GET /v1/merchant-mappings — List merchant mappings (`merchant_mappings.list`, read)

The ledger's merchant memory: each merchant it recognizes, the name
it shows and the category it files the merchant under.

Parameters:
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - X-Organization-Id (string | null, header)

Returns 200 MerchantMappingList.

### POST /v1/merchant-mappings — Remember a merchant (`merchant_mappings.create`, write)

Teach the ledger a merchant: events whose description starts with
`merchant_pattern` are named `merchant_name` and filed under
`category_id` from now on. A pattern already remembered is updated in
place, so sending the same one twice leaves one mapping. Events already
categorized are left as they are.

Parameters:
  - Idempotency-Key (string | null, header): Any unique string (a UUID is ideal). A retry with the same key returns the first attempt's response instead of doing the work twice. Keys are kept for 24 hours.
  - X-Organization-Id (string | null, header)

Body (MerchantMappingInput):
  - category_id (integer | null): The income or expense account to file its events under.
  - confidence (string): How sure a match is, `0` to `1`, as a decimal string. Pattern: `^(0(\.\d{1,2})?|1(\.0{1,2})?)$`. Default: `"0.90"`.
  - merchant_name (string, required): The clean name to show.
  - merchant_pattern (string, required): The text a description starts with, e.g. `STARBUCKS`. Matched ignoring case.

Returns 201 MerchantMappingOut.

### GET /v1/posting-rules — List posting rules (`posting_rules.list`, read)

The rules that turn categorized events into balanced journal
entries. They are the platform's, the same for every ledger, and cannot
be changed through the API.

Parameters:
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - X-Organization-Id (string | null, header)

Returns 200 PostingRuleList.

#### CategorizationRun
  - drain (boolean): Work through the whole backlog of events waiting for the model, then post what was categorized. Without it, one batch is categorized and nothing is posted. Default: `false`.

#### Job
  - created_at (datetime, required)
  - error (JobError | null, required): Why it failed, once `failed`.
  - finished_at (datetime | null, required)
  - id (integer, required)
  - kind (string, required): What the job does: `import`, `categorize`, `generate_journals`, `reconcile`, `run_schedules`, `plaid_sync` or `rocket_money_import`.
  - result (object | null, required): What the work produced, once `succeeded`. Its fields depend on `kind` and are listed on the operation that started it. Counts are integers.
  - status (string, required): `queued`, `running`, `succeeded` or `failed`.

#### JobError
  - code (string, required): Stable error code, as in the error envelope.
  - message (string, required): For people, in the request's language. Never match on it.

#### MerchantMappingInput
  - category_id (integer | null): The income or expense account to file its events under.
  - confidence (string): How sure a match is, `0` to `1`, as a decimal string. Pattern: `^(0(\.\d{1,2})?|1(\.0{1,2})?)$`. Default: `"0.90"`.
  - merchant_name (string, required): The clean name to show.
  - merchant_pattern (string, required): The text a description starts with, e.g. `STARBUCKS`. Matched ignoring case.

#### MerchantMappingList
  - data (array of MerchantMappingOut, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

#### MerchantMappingOut
  - category_id (integer | null, required): The income or expense account its events are filed under.
  - confidence (string, required): How sure a match is, a decimal string from `0.00` to `1.00`.
  - created_at (datetime, required)
  - id (integer, required)
  - merchant_name (string, required): The clean name shown for it.
  - merchant_pattern (string, required): The text a description starts with, upper case.
  - source (string, required): `manual`, `learned` (from a person's correction), or `api`.

#### PostingRuleList
  - data (array of PostingRuleOut, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

#### PostingRuleOut
  - credit_account_role (string, required)
  - debit_account_role (string, required): `source`, `category` or `other`: the event's own account, its category, or the account on the other end.
  - event_type (string, required)
  - id (integer, required)
  - is_active (boolean, required)
  - priority (integer, required): Higher wins when two rules fit.
  - source_account_role (string | null, required): Only for events on accounts with this role (`checking`, `credit_card`, …); null for any.

## Changes

### GET /v1/changes — List changes (the change feed) (`changes.list`, read)

What changed in the ledger, oldest first, over the last 30 days. To
follow it, store `next_cursor` and pass it as `cursor` on the next poll:
you get exactly what happened since, in order, never twice and never
skipping one (a change appears only once everything before it has
committed, so a long import can hold the feed back a moment, never make
it jump). Without a cursor the feed starts 30 days ago.

A change carries ids, not records: fetch `transactions.get` and the like
for the current state. The same objects are delivered to webhook
endpoints (`webhook_endpoints.create`).

Parameters:
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - type (array of string | null, query): Only these change types (`transaction.created`, `transaction.updated`, `transaction.deleted`, `account.balance_updated`, `budget.exceeded`, `import.completed`, `job.completed`). Repeat to ask for several.
  - X-Organization-Id (string | null, header)

Returns 200 ChangeList.

### GET /v1/changes/{change_id} — Get a change (`changes.get`, read)

One change by its `id`, for 30 days. A receiver that would rather
not trust a delivery's body can fetch it here by the
`LumaLedger-Change-Id` header instead.

Parameters:
  - change_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 200 Change.

#### Change
  - created_at (datetime, required)
  - data (object, required): The ids the change concerns, and a few facts. By `type`: `transaction.*` → `transaction_id` (and `previous_transaction_id` when a refile re-posted it under a new id); `account.balance_updated` → `account_id`, `balance` (Money, as the bank reported it), `as_of`; `budget.exceeded` → `category_id`, `period`, `spent` and `available` (Money); `import.completed` → `account_id`, `row_count`, `new_count`, `duplicate_count`; `job.completed` → `job_id`, `kind`, `status`. Fetch the resource for the rest.
  - id (integer, required): Increases with every change. Deliveries are at least once: dedupe on it.
  - type (string, required): What happened: `transaction.created`, `transaction.updated`, `transaction.deleted`, `account.balance_updated`, `budget.exceeded`, `import.completed`, `job.completed`. More may be added.

#### ChangeList
  - data (array of Change, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

## Counterparties

### GET /v1/counterparties — List counterparties (`counterparties.list`, read)

Everyone the ledger paid or was paid by in the window, with totals
each way and how their transactions are filed. Counterparties are
derived from transactions when asked, so a page is computed from the
whole window each time.

Parameters:
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - lookback_days (integer, query): Only counterparties with transactions in this many days, and totals over them.
  - X-Organization-Id (string | null, header)

Returns 200 CounterpartyList.

### GET /v1/counterparties/{counterparty_key} — Get a counterparty (`counterparties.get`, read)

One counterparty over all time: totals, its rules, what the web
lookup found, money per month and its latest transactions.

Parameters:
  - counterparty_key (string, required, path)
  - X-Organization-Id (string | null, header)

Returns 200 CounterpartyDetails.

### PATCH /v1/counterparties/{counterparty_key} — Update a counterparty (`counterparties.update`, write)

Rename a counterparty, give it a logo or a note, or correct what its
About card says. Only the fields sent change. Its key does not change:
the name is an overlay on what the bank sends.

Parameters:
  - counterparty_key (string, required, path)
  - X-Organization-Id (string | null, header)

Body (CounterpartyUpdate):
  - about_description (string | null)
  - about_summary (string | null)
  - address (string | null)
  - display_name (string | null)
  - logo_url (string | null)
  - notes (string | null)
  - phone (string | null)
  - website (string | null)

Returns 200 CounterpartyDetails.

### GET /v1/counterparties/{counterparty_key}/about — Get who a counterparty is (`counterparties.get_about`, read)

The stored answer to "who is this?", and whether a lookup is still
running (poll this after `counterparties.identify`).

Parameters:
  - counterparty_key (string, required, path)
  - X-Organization-Id (string | null, header)

Returns 200 CounterpartyAboutResult.

### POST /v1/counterparties/{counterparty_key}/category — File a counterparty under a category or mark it a transfer (`counterparties.set_category`, write)

Give a counterparty a standing rule: file its transactions under
`category_id`, or treat them as transfers to one of the ledger's own
accounts (`transfer_account_id`: a card payment, a savings sweep). With
`backfill` its past transactions move too. Send neither to drop the
rule; transactions keep where they are.

Parameters:
  - counterparty_key (string, required, path)
  - X-Organization-Id (string | null, header)

Body (CounterpartyCategoryInput):
  - backfill (boolean): Also refile the transactions already in the ledger. Default: `true`.
  - category_id (integer | null): An income or expense account (see `accounts.list`).
  - transfer_account_id (integer | null): One of the ledger's own accounts: its transactions become transfers there.

Returns 200 CounterpartyCategoryResult.

### POST /v1/counterparties/{counterparty_key}/identify — Look up who a counterparty is (`counterparties.identify`, write)

Search the web for who a counterparty is, and keep the answer. A
counterparty already identified returns what is stored without searching
again, so this is safe to call on every read. When the search runs in
the background `pending` is true: poll `counterparties.get_about`.
Answers 503 when lookups are not configured on this server.

Parameters:
  - counterparty_key (string, required, path)
  - X-Organization-Id (string | null, header)

Returns 200 CounterpartyAboutResult.

### DELETE /v1/counterparties/{counterparty_key}/merge — Separate a merged counterparty (`counterparties.unmerge`, write)

Undo `counterparties.merge` for this key (one of the target's
`merged_from`): its transactions group on their own again.

Parameters:
  - counterparty_key (string, required, path)
  - X-Organization-Id (string | null, header)

Returns 204.

### POST /v1/counterparties/{counterparty_key}/merge — Merge a counterparty into another (`counterparties.merge`, write)

Treat this counterparty as part of `target_key`: its transactions,
past and future, group under the target, and its own key stops
resolving. Nothing is refiled — who a transaction is with changes, not
what it was for. Returns the target as it now stands; undo with
`counterparties.unmerge`.

Parameters:
  - counterparty_key (string, required, path)
  - X-Organization-Id (string | null, header)

Body (CounterpartyMergeInput):
  - target_key (string, required): The counterparty to fold this one into — a `key` from `counterparties.list`.

Returns 200 CounterpartyDetails.

### POST /v1/counterparties/{counterparty_key}/rules — Add a rule for a counterparty (`counterparties.create_rule`, write)

Add a rule narrower than the counterparty's default: "over 200 from
this store is Equipment, the rest stays Supplies". With `backfill`, its
matching past transactions are refiled now.

Parameters:
  - counterparty_key (string, required, path)
  - Idempotency-Key (string | null, header): Any unique string (a UUID is ideal). A retry with the same key returns the first attempt's response instead of doing the work twice. Keys are kept for 24 hours.
  - X-Organization-Id (string | null, header)

Body (CounterpartyRuleInput):
  - amount_max (string | null): Positive decimal string. Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - amount_min (string | null): Positive decimal string. Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - backfill (boolean): Also refile matching transactions already in the ledger. Default: `true`.
  - category_id (integer, required): The income or expense account to file matching transactions under.
  - match_entity (boolean): Also match on the provider's merchant id. Default: `true`.
  - pattern (string | null): Description text to match; defaults to the counterparty's own pattern.

Returns 201 CounterpartyRuleResult.

### DELETE /v1/counterparties/{counterparty_key}/rules/{counterparty_rule_id} — Delete a counterparty rule (`counterparties.delete_rule`, write, destructive)

Delete one of a counterparty's rules (ids from `counterparties.get`)
and refile its transactions under whatever governs them next.

Parameters:
  - counterparty_key (string, required, path)
  - counterparty_rule_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 204.

#### AboutSource
  - title (string, required)
  - url (string, required)

#### Counterparty
  - about_summary (string | null, required): One line on who it is, when it has been identified.
  - average_amount (Money, required): The average transaction, as a positive amount.
  - category_id (integer | null, required): The category its transactions are filed under, when there is one.
  - confidence (string, required): How sure the ledger is about where its transactions are filed, 0 to 1, as a decimal string. Pattern: `^[01]\.\d{4}$`.
  - confidence_band (string, required): `sure`, `unsure` or `unfiled`.
  - counterparty_type (string | null, required): `merchant`, `financial_institution`, `income_source`, `payment_app`, `marketplace`, … Null when unknown.
  - customized (boolean, required): The user renamed it or gave it a logo.
  - direction (string, required): `out` (you pay it), `in` (it pays you) or `both`.
  - first_date (date, required)
  - has_rule (boolean, required): A standing rule, not a guess, decides where its transactions go.
  - key (string, required): The counterparty key: its id. See the resource description.
  - last_date (date, required)
  - logo_url (string | null, required)
  - merchant_entity_id (string | null, required): The bank data provider's id for the merchant, when it has one.
  - mixed (boolean, required): Its past transactions sit in more than one category.
  - name (string, required): What to call it: the user's name for it, else the one derived from the bank's descriptions.
  - pattern (string, required): The description text a rule for it matches on.
  - rule_count (integer, required)
  - seller_unknown (boolean, required): Every transaction names only an intermediary (a marketplace, a wallet).
  - suggests_transfer (boolean, required): Looks like money between the user's own accounts, and nothing is decided yet.
  - total_paid (Money, required): Paid to it, as a positive amount.
  - total_received (Money, required): Received from it, as a positive amount.
  - transaction_count (integer, required)
  - transfer_account_id (integer | null, required): Set instead of `category_id` when its transactions are transfers to one of the ledger's own accounts (a card payment, a savings sweep).

#### CounterpartyAbout
  - address (string | null, required)
  - description (string | null, required)
  - looked_up_at (datetime, required)
  - phone (string | null, required)
  - sources (array of AboutSource, required)
  - status (string, required): `found`, or `unknown` when the lookup could not tell.
  - summary (string | null, required)
  - website (string | null, required)

#### CounterpartyAboutEdits
  - address (string | null, required)
  - description (string | null, required)
  - phone (string | null, required)
  - summary (string | null, required)
  - website (string | null, required)

#### CounterpartyAboutResult
  - about (CounterpartyAbout | null, required)
  - pending (boolean, required): A lookup is running; poll `counterparties.get_about` until this is false.

#### CounterpartyCategoryInput
  - backfill (boolean): Also refile the transactions already in the ledger. Default: `true`.
  - category_id (integer | null): An income or expense account (see `accounts.list`).
  - transfer_account_id (integer | null): One of the ledger's own accounts: its transactions become transfers there.

#### CounterpartyCategoryResult
  - cleared (boolean, required): A standing rule was dropped (neither target was sent).
  - moved (integer, required): Past transactions refiled.

#### CounterpartyDetails
  - about (CounterpartyAbout | null, required): The lookup's answer; null when never looked up.
  - about_edits (CounterpartyAboutEdits | null, required)
  - about_summary (string | null, required): One line on who it is, when it has been identified.
  - average_amount (Money, required): The average transaction, as a positive amount.
  - category_id (integer | null, required): The category its transactions are filed under, when there is one.
  - confidence (string, required): How sure the ledger is about where its transactions are filed, 0 to 1, as a decimal string. Pattern: `^[01]\.\d{4}$`.
  - confidence_band (string, required): `sure`, `unsure` or `unfiled`.
  - counterparty_type (string | null, required): `merchant`, `financial_institution`, `income_source`, `payment_app`, `marketplace`, … Null when unknown.
  - customized (boolean, required): The user renamed it or gave it a logo.
  - derived_name (string, required): The name before any rename.
  - descriptions (array of string, required): Distinct descriptions the bank has used for it (at most 12).
  - direction (string, required): `out` (you pay it), `in` (it pays you) or `both`.
  - first_date (date, required)
  - has_rule (boolean, required): A standing rule, not a guess, decides where its transactions go.
  - key (string, required): The counterparty key: its id. See the resource description.
  - last_date (date, required)
  - logo_url (string | null, required)
  - merchant_entity_id (string | null, required): The bank data provider's id for the merchant, when it has one.
  - merged_from (array of MergedCounterparty, required): Counterparties merged into this one (`counterparties.merge`), oldest merge first.
  - mixed (boolean, required): Its past transactions sit in more than one category.
  - monthly (array of CounterpartyMonth, required): Paid and received per month, oldest first.
  - name (string, required): What to call it: the user's name for it, else the one derived from the bank's descriptions.
  - notes (string | null, required)
  - pattern (string, required): The description text a rule for it matches on.
  - recent_transactions (array of CounterpartyTransaction, required): Its latest 50 transactions, newest first.
  - rule_count (integer, required)
  - rules (array of CounterpartyRule, required)
  - seller_unknown (boolean, required): Every transaction names only an intermediary (a marketplace, a wallet).
  - suggested_pattern (string, required): The pattern a new rule would match on by default.
  - suggests_transfer (boolean, required): Looks like money between the user's own accounts, and nothing is decided yet.
  - total_moved (Money, required): Moved to or from the ledger's own accounts in its transactions filed as transfers (`recent_transactions[].transfer_account_id`), as a positive amount. Never part of `total_paid` or `total_received`.
  - total_paid (Money, required): Paid to it, as a positive amount.
  - total_received (Money, required): Received from it, as a positive amount.
  - transaction_count (integer, required)
  - transfer_account_id (integer | null, required): Set instead of `category_id` when its transactions are transfers to one of the ledger's own accounts (a card payment, a savings sweep).
  - website (string | null, required)

#### CounterpartyList
  - data (array of Counterparty, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

#### CounterpartyMergeInput
  - target_key (string, required): The counterparty to fold this one into — a `key` from `counterparties.list`.

#### CounterpartyMonth
  - month (string, required): `YYYY-MM`.
  - paid (Money, required)
  - received (Money, required)

#### CounterpartyRule
  - amount_max (Money | null, required): Only transactions of at most this much.
  - amount_min (Money | null, required): Only transactions of at least this much.
  - category_id (integer | null, required)
  - id (integer, required): The rule id (`counterparty_rule_id`).
  - matches_entity (boolean, required): Also matches on the provider's merchant id, exactly.
  - pattern (string, required): Description text the rule matches (uppercase).
  - source (string, required): `manual` (someone wrote it) or `learned`.
  - transfer_account_id (integer | null, required)

#### CounterpartyRuleInput
  - amount_max (string | null): Positive decimal string. Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - amount_min (string | null): Positive decimal string. Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - backfill (boolean): Also refile matching transactions already in the ledger. Default: `true`.
  - category_id (integer, required): The income or expense account to file matching transactions under.
  - match_entity (boolean): Also match on the provider's merchant id. Default: `true`.
  - pattern (string | null): Description text to match; defaults to the counterparty's own pattern.

#### CounterpartyRuleResult
  - moved (integer, required): Past transactions refiled.
  - rule (CounterpartyRule, required)

#### CounterpartyTransaction
  - amount (Money, required): Always positive; `direction` says which way it moved.
  - category_id (integer | null, required)
  - date (date, required)
  - description (string, required)
  - direction (string, required): `out` or `in`.
  - transaction_id (integer | null, required): The transaction id for `transactions.get`; null while it has not posted to the ledger.
  - transfer_account_id (integer | null, required): Set instead of `category_id` when the transaction was filed, by hand or by a rule, as a transfer to or from one of the ledger's own accounts.

#### CounterpartyUpdate
  - about_description (string | null)
  - about_summary (string | null)
  - address (string | null)
  - display_name (string | null)
  - logo_url (string | null)
  - notes (string | null)
  - phone (string | null)
  - website (string | null)

#### MergedCounterparty
  - key (string, required): The key it had on its own; pass it to `counterparties.unmerge`.
  - name (string, required): What it was called when it was merged.

#### Money
  - amount (string, required): Pattern: `^-?\d+\.\d{2}$`.
  - currency (string, required): ISO 4217 code. Pattern: `^[A-Z]{3}$`.

## Documents

### GET /v1/documents — List documents (`documents.list`, read)

The vault's documents: bank statements, latest period first, then
uploads without a period, newest first.

Parameters:
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - doc_type (string | null, query): Only documents of this kind: `other`, `statement`, `tax`.
  - X-Organization-Id (string | null, header)

Returns 200 DocumentList.

### POST /v1/documents — Upload a document (`documents.create`, write)

Upload a file to the vault as `multipart/form-data`.

Parameters:
  - Idempotency-Key (string | null, header): Any unique string (a UUID is ideal). A retry with the same key returns the first attempt's response instead of doing the work twice. Keys are kept for 24 hours.
  - X-Organization-Id (string | null, header)

Body (multipart/form-data):
  - doc_type (string): What it is: `other`, `statement`, `tax`. Pattern: `^(other|statement|tax)$`. Default: `"other"`.
  - file (file, required): A PDF, PNG, JPEG, WebP or HEIC file, at most 15 MB. Its content type must match its bytes.
  - name (string | null): What to call it. Defaults to the file's name.
  - tax_year (integer | null): The tax year a tax document is for.

Returns 201 Document.

### DELETE /v1/documents/{document_id} — Delete a document (`documents.delete`, write, destructive)

Delete a document and its file. A statement pulled from a bank comes
back with the next `documents.sync_statements` while the bank still
offers it.

Parameters:
  - document_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 204.

### GET /v1/documents/{document_id} — Get a document (`documents.get`, read)

One document's details. Its file is `documents.download`.

Parameters:
  - document_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 200 Document.

### GET /v1/documents/{document_id}/download — Download a document (`documents.download`, read)

A short-lived URL for the document's file, named so a browser saves
it under the document's name.

Parameters:
  - document_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 200 Download.

### POST /v1/documents/statements/sync — Pull new bank statements (`documents.sync_statements`, write)

Pull any new statements from every linked bank into the vault. A bank
that does not offer statements reports an `error` without failing the
rest, and statements already stored are skipped, so calling it again is
harmless. Premium only: on the free plan this is a 402
`premium_required`.

Parameters:
  - X-Organization-Id (string | null, header)

Returns 200 StatementSync.

#### Document
  - account_id (integer | null, required): The account a statement is for (see `accounts.get`).
  - created_at (datetime, required)
  - doc_type (string, required): One of `other`, `statement`, `tax`.
  - id (integer, required)
  - institution_name (string | null, required): The bank a statement came from.
  - mime_type (string, required)
  - name (string, required)
  - period_end (date | null, required): A statement's last day.
  - period_start (date | null, required): A statement's first day.
  - size_bytes (integer, required)
  - source (string, required): `plaid` (a statement pulled from the bank) or `upload`.
  - tax_year (integer | null, required)

#### DocumentList
  - data (array of Document, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

#### Download
  - expires_at (datetime | null, required): When a signed `url` stops working. Null for a `data:` URL.
  - url (string, required): A signed HTTPS URL for the file, or a `data:` URL carrying the file itself when it is stored inline. Needs no credential.

#### StatementSync
  - downloaded (integer, required): New statements stored, across every bank.
  - items (array of StatementSyncItem, required): One per linked bank.

#### StatementSyncItem
  - downloaded (integer | null, required): New statements stored from this bank.
  - error (string | null, required): Why this bank's statements could not be pulled.
  - institution (string, required)
  - outside_window (integer | null, required): Statements older than the pull reaches back.
  - skipped (integer | null, required): Statements already in the vault.

## Equity Grants

### GET /v1/equity-grants — List equity grants (`equity_grants.list`, read)

The ledger's equity grants, each with what has vested and its
schedule. Values are in the market's currency (USD).

Parameters:
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - account_id (integer | null, query): Only grants whose shares land in this account.
  - X-Organization-Id (string | null, header)

Returns 200 EquityGrantList.

### POST /v1/equity-grants — Create an equity grant (`equity_grants.create`, write)

Record a grant and its vesting schedule. Every tranche already
vested posts as wages at its vest-day close, with the withheld shares as
tax; later tranches post on their dates.

Parameters:
  - Idempotency-Key (string | null, header): Any unique string (a UUID is ideal). A retry with the same key returns the first attempt's response instead of doing the work twice. Keys are kept for 24 hours.
  - X-Organization-Id (string | null, header)

Body (EquityGrantCreate):
  - account_id (integer, required): The investment account vested shares land in.
  - cliff_months (integer): 0–240. Default: `0`.
  - duration_months (integer, required): 1–240.
  - frequency_months (integer, required): 1–120.
  - grant_date (date, required)
  - grant_type (string): Pattern: `^rsu$`. Default: `"rsu"`.
  - label (string | null)
  - symbol (string, required)
  - total_shares (string, required): Pattern: `^\d+(\.\d+)?$`.
  - vest_start (date | null): Omitted: the grant date.
  - withholding_rate (string | null): Share of each vest withheld for tax, 0–1. Omitted: 0.22. Pattern: `^(0(\.\d+)?|1(\.0+)?)$`.

Returns 201 EquityGrant.

### DELETE /v1/equity-grants/{equity_grant_id} — Delete an equity grant (`equity_grants.delete`, write, destructive)

Remove a grant and everything its vests posted.

Parameters:
  - equity_grant_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 204.

### GET /v1/equity-grants/{equity_grant_id} — Get an equity grant (`equity_grants.get`, read)

One equity grant by its id.

Parameters:
  - equity_grant_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 200 EquityGrant.

#### EquityGrant
  - account_id (integer, required): The investment account vested shares land in (see `accounts.list`).
  - account_name (string, required)
  - cliff_months (integer, required)
  - duration_months (integer, required)
  - frequency_months (integer, required)
  - grant_date (date, required)
  - grant_type (string, required): `rsu`.
  - id (integer, required)
  - label (string | null, required)
  - next_vest (Vest | null, required)
  - schedule (array of Vest, required)
  - symbol (string, required)
  - total_shares (string, required): Pattern: `^-?\d+(\.\d+)?$`.
  - unvested_shares (string, required): Not the holder's yet: never part of net worth. Pattern: `^-?\d+(\.\d+)?$`.
  - unvested_value (Money | null, required): `unvested_shares` at the latest close.
  - vest_start (date, required)
  - vested_income (Money, required): Wages the vested tranches paid, at their vest-day closes.
  - vested_shares (string, required): Pattern: `^-?\d+(\.\d+)?$`.
  - withholding_rate (string, required): Share of each vest withheld for tax, 0–1. Pattern: `^-?\d+(\.\d+)?$`.

#### EquityGrantCreate
  - account_id (integer, required): The investment account vested shares land in.
  - cliff_months (integer): 0–240. Default: `0`.
  - duration_months (integer, required): 1–240.
  - frequency_months (integer, required): 1–120.
  - grant_date (date, required)
  - grant_type (string): Pattern: `^rsu$`. Default: `"rsu"`.
  - label (string | null)
  - symbol (string, required)
  - total_shares (string, required): Pattern: `^\d+(\.\d+)?$`.
  - vest_start (date | null): Omitted: the grant date.
  - withholding_rate (string | null): Share of each vest withheld for tax, 0–1. Omitted: 0.22. Pattern: `^(0(\.\d+)?|1(\.0+)?)$`.

#### EquityGrantList
  - data (array of EquityGrant, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

#### Money
  - amount (string, required): Pattern: `^-?\d+\.\d{2}$`.
  - currency (string, required): ISO 4217 code. Pattern: `^[A-Z]{3}$`.

#### UnitPrice
  - amount (string, required): Pattern: `^-?\d+(\.\d+)?$`.
  - currency (string, required): ISO 4217 code. Pattern: `^[A-Z]{3}$`.

#### Vest
  - date (date, required)
  - price (UnitPrice | null, required): The close it vested at, or today's for a tranche still ahead.
  - shares (string, required): Pattern: `^-?\d+(\.\d+)?$`.
  - shares_withheld (string | null, required): Kept back to cover tax; vested tranches only. Pattern: `^-?\d+(\.\d+)?$`.
  - value (Money | null, required): `shares` × `price`: wages on a vested tranche, an estimate on a future one.
  - vested (boolean, required): True once the date has passed: the shares are the holder's.

## Events

### GET /v1/events — List financial events (`events.list`, read)

The ledger's financial events, newest first, with how each was
categorized. Unposted ones are waiting for a category or a match;
`categorization.run` and `journals.generate` move them along.

Parameters:
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - account_id (integer | null, query): Only events on this account.
  - event_type (string | null, query): Only events of this `event_type`.
  - is_posted (boolean | null, query): Only posted (true) or unposted (false) events.
  - X-Organization-Id (string | null, header)

Returns 200 EventList.

### GET /v1/events/{event_id} — Get a financial event (`events.get`, read)

One financial event by its id.

Parameters:
  - event_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 200 Event.

### POST /v1/events/{event_id}/categorize — Categorize a financial event (`events.categorize`, write)

File one event under a category by hand, as confirmed (`reviewed`).
Its transaction is re-posted at once, so the ledger shows the new
category. With `learn_merchant`, the merchant is remembered and its
next events are filed the same way.

Parameters:
  - event_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Body (EventCategorize):
  - category_id (integer, required): The income or expense account to file it under (from `accounts.list`), on the event's side: an income account for money in, an expense account for money out.
  - learn_merchant (boolean): Also file this merchant's future events here. Default: `true`.

Returns 200 Event.

### DELETE /v1/events/{event_id}/splits — Remove an event's splits (`splits.delete`, write, destructive)

Remove every split, filing the event under its own single category
again. Its transaction is re-posted at once.

Parameters:
  - event_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 204.

### GET /v1/events/{event_id}/splits — List an event's splits (`splits.list`, read)

How one event is split across categories. Empty when it is filed
under a single category (`category_id` on the event).

Parameters:
  - event_id (integer, required, path)
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - X-Organization-Id (string | null, header)

Returns 200 SplitList.

### PUT /v1/events/{event_id}/splits — Split an event across categories (`splits.replace`, write)

Replace an event's splits with these. The amounts must add up to the
event's amount, and every part is filed on the event's own side: an
expense account for spending, fees and refunds, an income account for
income and interest. Its transaction is re-posted at once with one line
per split. Answers with every split (up to 200, so one page).

Parameters:
  - event_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Body (SplitsReplace):
  - splits (array of SplitInput, required): Every split the event is to have. Their amounts must add up to the event's amount. To go back to one category, use `splits.delete`.

Returns 200 SplitList.

#### Event
  - account_id (integer, required): The bank, card or loan account it happened on.
  - amount (Money, required): Always positive; `event_type` says which way it went. In the account's currency.
  - categorization_source (string | null, required): Who filed it: `manual`, `merchant_memory`, `pfc` (the bank's own category), `llm`, `transfer`, `user_rule:…`, `uncategorized`, …
  - category_id (integer | null, required): The income or expense account it is filed under.
  - confidence (string | null, required): How sure the categorizer was, a decimal string from `0.00` to `1.00`.
  - created_at (datetime, required)
  - date (date, required)
  - description (string, required): The source's own text for it.
  - event_type (string, required): What kind of movement: `expense`, `income`, `transfer`, `cc_payment`, `refund`, `fee` or `interest`.
  - id (integer, required)
  - is_posted (boolean, required)
  - is_superseded (boolean, required): Replaced by a later version of itself (a pending charge that posted).
  - merchant_name (string | null, required): The merchant recognized in the description.
  - other_account_id (integer | null, required): For a transfer, the ledger's own account on the other end.
  - reviewed (boolean, required): A person has confirmed or set the category.
  - transaction_id (integer | null, required): The transaction (journal entry) it is posted as, once posted. See `transactions.get`.

#### EventCategorize
  - category_id (integer, required): The income or expense account to file it under (from `accounts.list`), on the event's side: an income account for money in, an expense account for money out.
  - learn_merchant (boolean): Also file this merchant's future events here. Default: `true`.

#### EventList
  - data (array of Event, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

#### Money
  - amount (string, required): Pattern: `^-?\d+\.\d{2}$`.
  - currency (string, required): ISO 4217 code. Pattern: `^[A-Z]{3}$`.

#### Split
  - amount (Money, required): Positive, in the event's account's currency.
  - category_id (integer, required): The income or expense account this part is filed under.
  - external_ref (string | null, required): The line's id or SKU in its source.
  - id (integer, required)
  - memo (string | null, required)
  - product_name (string | null, required)
  - quantity (string | null, required): A decimal string, up to four places.
  - source (string, required): Who split it: `manual`, or the integration that did.
  - unit_price (Money | null, required)

#### SplitInput
  - amount (string, required): Positive decimal string, at most two decimal places: `"42.50"`. Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - category_id (integer, required): An income or expense account in this ledger.
  - external_ref (string | null): The line's id or SKU in your system.
  - memo (string | null)
  - product_name (string | null)
  - quantity (string | null): Decimal string. Pattern: `^\d{1,8}(\.\d{1,4})?$`.
  - unit_price (string | null): Decimal string. Pattern: `^\d{1,12}(\.\d{1,2})?$`.

#### SplitList
  - data (array of Split, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

#### SplitsReplace
  - splits (array of SplitInput, required): Every split the event is to have. Their amounts must add up to the event's amount. To go back to one category, use `splits.delete`.

## Files

### POST /v1/files — Upload a file (`files.create`, write)

Upload a file as `multipart/form-data` and get back its `id`, to send
as `file_id` to an operation that needs a file: a document for
`alternatives.add_document`, a bank's export for `imports.create`, a
Rocket Money export for `imports.create_rocket_money`. The file is kept
24 hours (`expires_at`). Send an `Idempotency-Key` so a retry cannot
upload it twice.

Parameters:
  - Idempotency-Key (string | null, header): Any unique string (a UUID is ideal). A retry with the same key returns the first attempt's response instead of doing the work twice. Keys are kept for 24 hours.
  - X-Organization-Id (string | null, header)

Body (multipart/form-data):
  - file (file, required): Any file, at most 15 MB.

Returns 201 UploadedFile.

### DELETE /v1/files/{file_id} — Delete a file (`files.delete`, write, destructive)

Delete a file before it expires. What an operation already made from
it (an attached document, imported rows) stays.

Parameters:
  - file_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 204.

### GET /v1/files/{file_id} — Get a file (`files.get`, read)

A file's details, while it lasts. An expired file is not found.

Parameters:
  - file_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 200 UploadedFile.

#### UploadedFile
  - content_type (string, required): As the upload declared it.
  - created_at (datetime, required)
  - expires_at (datetime, required): When the file is deleted. Use it before then; upload it again after.
  - filename (string, required)
  - id (integer, required): Send it as `file_id` to the operation that needs the file.
  - sha256 (string, required): Hex SHA-256 of the bytes, to check what arrived.
  - size_bytes (integer, required)

## Holdings

### GET /v1/holdings — List holdings (`holdings.list`, read)

Every holding row: how many shares of which security each investment
account holds, what the institution says it is worth and what it cost.
Amounts are in the holding's own currency.

Parameters:
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - account_id (integer | null, query): Only this account's holdings. Ids from `accounts.list`.
  - X-Organization-Id (string | null, header)

Returns 200 HoldingList.

#### Holding
  - account_id (integer, required): The investment account holding it.
  - cost_basis (Money | null, required)
  - gain_percent (string | null, required): Unrealized gain since purchase, in percent; null without both a market value and a cost basis. Pattern: `^-?\d+(\.\d+)?$`.
  - id (integer, required)
  - is_estimate (boolean, required): True when `value` is the cost basis.
  - market_value (Money | null, required): The institution's value; null for manual rows.
  - name (string, required)
  - price (UnitPrice | null, required): The institution's last price per share.
  - price_as_of (date | null, required): When the institution priced it.
  - quantity (string, required): Shares or units held. Pattern: `^-?\d+(\.\d+)?$`.
  - source (string, required): `plaid`, `manual` or `seed`.
  - symbol (string, required): The ticker; `positions.get` takes it.
  - updated_at (datetime, required)
  - value (Money, required): `market_value`, or `cost_basis` standing in when there is none — for the owned (vested) units only, so unvested award units never count.
  - vested_quantity (string | null, required): For an equity award (a stock-plan account): the units that have vested and are the holder's. Null when the institution reports no vesting split, meaning every unit is owned. Pattern: `^-?\d+(\.\d+)?$`.

#### HoldingList
  - data (array of Holding, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

#### Money
  - amount (string, required): Pattern: `^-?\d+\.\d{2}$`.
  - currency (string, required): ISO 4217 code. Pattern: `^[A-Z]{3}$`.

#### UnitPrice
  - amount (string, required): Pattern: `^-?\d+(\.\d+)?$`.
  - currency (string, required): ISO 4217 code. Pattern: `^[A-Z]{3}$`.

## Imports

### GET /v1/imports — List imports (`imports.list`, read)

The files imported into this ledger, newest first: bank exports,
OFX statements, Rocket Money exports and the app's Chase importer.

Parameters:
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - account_id (integer | null, query): Only imports into this account.
  - X-Organization-Id (string | null, header)

Returns 200 ImportList.

### POST /v1/imports — Import a file (`imports.create`, write)

Import a bank's export into an account and settle the ledger:
transfers are matched, transactions categorized, your rules applied and
everything posted. Answers 202 with a Job (`kind` `import`); poll
`jobs.get`. Its `result`: `import_id` (see `imports.list`),
`row_count`, `new_count`, `duplicate_count` (rows already in the
ledger, skipped) and `journals_posted`. Importing the same file twice is
harmless: its rows come back as duplicates. Send the file as `content`,
or upload it with `files.create` and send its `file_id`. Send an
`Idempotency-Key` so a retry cannot start a second job.

Parameters:
  - Idempotency-Key (string | null, header): Any unique string (a UUID is ideal). A retry with the same key returns the first attempt's response instead of doing the work twice. Keys are kept for 24 hours.
  - X-Organization-Id (string | null, header)

Body (ImportCreate):
  - account_id (integer, required): The bank, card or loan account the rows belong to (an asset or liability from `accounts.list`).
  - content (string | null): The file's text: a CSV, or an OFX / QFX statement. At most 5 MB. Send this or `file_id`.
  - file_id (integer | null): A file uploaded with `files.create`, instead of `content`. At most 5 MB.
  - filename (string | null): Shown in the import history. Defaults to the uploaded file's name.
  - mapping (ImportMapping | null): How to read a CSV (from `imports.preview`). Required for a CSV, ignored for an OFX / QFX statement.

Returns 202 Job.

### POST /v1/imports/preview — Preview a file before importing it (`imports.preview`, read)

Read a bank's export and say how it would be imported, writing
nothing: its columns, its first rows, a guessed `mapping`, and the
`saved_mapping` the account used last time for the same header. Check
or correct the mapping, then send it to `imports.create`. An OFX / QFX
statement needs no mapping (`format` is `ofx`). Send the file as
`content`, or upload it with `files.create` and send its `file_id`.

Parameters:
  - X-Organization-Id (string | null, header)

Body (ImportPreviewRequest):
  - account_id (integer | null): The account the file is for. Given, the preview includes the mapping this account last imported a file with the same header with.
  - content (string | null): The file's text: a CSV, or an OFX / QFX statement. At most 5 MB. Send this or `file_id`.
  - file_id (integer | null): A file uploaded with `files.create`, instead of `content`. At most 5 MB.

Returns 200 ImportPreview.

### POST /v1/imports/rocket-money — Import a Rocket Money export (`imports.create_rocket_money`, write)

Switch from Rocket Money: create the accounts its export names that
the ledger does not have, import each account's transactions, and settle
the ledger. Answers 202 with a Job (`kind` `rocket_money_import`); poll
`jobs.get`. Its `result`: `accounts_created`, `accounts_used`,
`row_count`, `new_count`, `duplicate_count` and `journals_posted`.
Importing the same export twice is harmless: the second time finds the
accounts it made and every row a duplicate. Send an `Idempotency-Key` so
a retry cannot start a second job.

Parameters:
  - Idempotency-Key (string | null, header): Any unique string (a UUID is ideal). A retry with the same key returns the first attempt's response instead of doing the work twice. Keys are kept for 24 hours.
  - X-Organization-Id (string | null, header)

Body (RocketMoneyImportCreate):
  - content (string | null): The export's text (Rocket Money's "Export transactions" CSV). At most 5 MB. Send this or `file_id`.
  - file_id (integer | null): A file uploaded with `files.create`, instead of `content`. At most 5 MB.
  - filename (string | null): Shown in the import history. Defaults to the uploaded file's name.
  - plan (array of RocketMoneyPlanStep): Changes to the preview, per account. Empty: import as previewed.

Returns 202 Job.

### POST /v1/imports/rocket-money/preview — Preview a Rocket Money export (`imports.preview_rocket_money`, read)

Read a Rocket Money export (one CSV for every account) and say what
importing it would do, writing nothing: each account it names, how many
transactions, and the ledger account its rows would go into, or that a
new one would be created. Adjust with a `plan`, then send it to
`imports.create_rocket_money`.

Parameters:
  - X-Organization-Id (string | null, header)

Body (RocketMoneyPreviewRequest):
  - content (string | null): The export's text (Rocket Money's "Export transactions" CSV). At most 5 MB. Send this or `file_id`.
  - file_id (integer | null): A file uploaded with `files.create`, instead of `content`. At most 5 MB.

Returns 200 RocketMoneyPreview.

#### Import
  - account_id (integer, required): The account its rows were written into.
  - created_at (datetime, required)
  - duplicate_count (integer, required): Rows skipped because they were already in the ledger.
  - filename (string, required)
  - id (integer, required)
  - new_count (integer, required): Rows written as new transactions.
  - row_count (integer, required): Rows in the file.
  - source (string, required): What read it: `csv`, `ofx`, `chase_checking`, `rocket_money`, …
  - status (string, required)

#### ImportCreate
  - account_id (integer, required): The bank, card or loan account the rows belong to (an asset or liability from `accounts.list`).
  - content (string | null): The file's text: a CSV, or an OFX / QFX statement. At most 5 MB. Send this or `file_id`.
  - file_id (integer | null): A file uploaded with `files.create`, instead of `content`. At most 5 MB.
  - filename (string | null): Shown in the import history. Defaults to the uploaded file's name.
  - mapping (ImportMapping | null): How to read a CSV (from `imports.preview`). Required for a CSV, ignored for an OFX / QFX statement.

#### ImportList
  - data (array of Import, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

#### ImportMapping
  - amount_column (string | null)
  - balance_column (string | null): The running balance after each row.
  - category_column (string | null)
  - credit_column (string | null): Money in, when the file splits the two.
  - date_column (string, required)
  - date_format (string): A strptime format, one of `date_formats` in the preview. Default: `"%Y-%m-%d"`.
  - debit_column (string | null): Money out, when the file splits the two.
  - decimal_comma (boolean): Amounts are written `1.234,56`. Default: `false`.
  - description_column (string, required)
  - outflow_is_positive (boolean): With one `amount_column`: money out is written as a positive number. Default: `false`.

#### ImportPreview
  - columns (array of string, required): The file's column names, or the OFX fields a transaction has.
  - date_formats (array of string, required): The formats `mapping.date_format` may take.
  - format (string, required): `csv` or `ofx`. An OFX file needs no mapping.
  - mapping (ImportMapping | null, required): Our reading of the columns; null when nothing plausible fits.
  - ofx (OfxStatementInfo | null, required)
  - rows (array of array of string, required): The first rows, cell for cell, in `columns` order.
  - saved_at (datetime | null, required)
  - saved_mapping (ImportMapping | null, required): The mapping this account last used for a file with this header, when `account_id` was sent and there is one. Import with it as it is.
  - total_rows (integer, required)

#### ImportPreviewRequest
  - account_id (integer | null): The account the file is for. Given, the preview includes the mapping this account last imported a file with the same header with.
  - content (string | null): The file's text: a CSV, or an OFX / QFX statement. At most 5 MB. Send this or `file_id`.
  - file_id (integer | null): A file uploaded with `files.create`, instead of `content`. At most 5 MB.

#### Job
  - created_at (datetime, required)
  - error (JobError | null, required): Why it failed, once `failed`.
  - finished_at (datetime | null, required)
  - id (integer, required)
  - kind (string, required): What the job does: `import`, `categorize`, `generate_journals`, `reconcile`, `run_schedules`, `plaid_sync` or `rocket_money_import`.
  - result (object | null, required): What the work produced, once `succeeded`. Its fields depend on `kind` and are listed on the operation that started it. Counts are integers.
  - status (string, required): `queued`, `running`, `succeeded` or `failed`.

#### JobError
  - code (string, required): Stable error code, as in the error envelope.
  - message (string, required): For people, in the request's language. Never match on it.

#### Money
  - amount (string, required): Pattern: `^-?\d+\.\d{2}$`.
  - currency (string, required): ISO 4217 code. Pattern: `^[A-Z]{3}$`.

#### OfxStatementInfo
  - account_last4 (string | null, required)
  - account_type (string | null, required): The file's own word: `CHECKING`, `SAVINGS`, `CREDITLINE`, …
  - balance (Money | null, required): The statement's closing balance, in the statement's currency.
  - balance_date (date | null, required)
  - currency (string | null, required)
  - from_date (date | null, required)
  - to_date (date | null, required)

#### RocketMoneyAccount
  - account_type (string | null, required): The file's own words for the account (`Credit Card`).
  - existing_account_id (integer | null, required): An account already in the ledger that looks like this one (same name, same side of the balance sheet, same bank when it says one). Its rows go there unless `plan` says otherwise. Null: a new account is created.
  - existing_account_name (string | null, required)
  - from_date (date, required)
  - institution (string, required)
  - key (string, required): Names this account in `plan` when importing. Opaque.
  - kind (string, required): What a new account would be made as, read off `account_type`: one of `kinds`. Overrule it in `plan`.
  - mask (string | null, required): The account number's last digits, when the file has them.
  - name (string, required)
  - row_count (integer, required): Transactions in the file for this account.
  - to_date (date, required)

#### RocketMoneyImportCreate
  - content (string | null): The export's text (Rocket Money's "Export transactions" CSV). At most 5 MB. Send this or `file_id`.
  - file_id (integer | null): A file uploaded with `files.create`, instead of `content`. At most 5 MB.
  - filename (string | null): Shown in the import history. Defaults to the uploaded file's name.
  - plan (array of RocketMoneyPlanStep): Changes to the preview, per account. Empty: import as previewed.

#### RocketMoneyPlanStep
  - account_id (integer | null): Import its rows into this ledger account (an asset or liability from `accounts.list`) instead of the previewed one.
  - key (string, required): The account's `key` from `imports.preview_rocket_money`.
  - kind (string | null): When a new account is created: what it is, one of the preview's `kinds`.
  - skip (boolean): Leave this account's rows out. Default: `false`.

#### RocketMoneyPreview
  - accounts (array of RocketMoneyAccount, required)
  - kinds (array of string, required): The kinds a new account may be given in `plan`.
  - outflow_is_positive (boolean, required): The file writes money out as positive amounts (Rocket Money's way), read off its income rows. Informational: the import already accounts for it.
  - total_rows (integer, required)

#### RocketMoneyPreviewRequest
  - content (string | null): The export's text (Rocket Money's "Export transactions" CSV). At most 5 MB. Send this or `file_id`.
  - file_id (integer | null): A file uploaded with `files.create`, instead of `content`. At most 5 MB.

## Insights

### GET /v1/insights — List insights (`insights.list`, read)

What is worth knowing about the ledger today, most urgent first and
only a handful: budgets over or nearly used, a subscription that
changed price, savings-goal milestones, a budget streak, last month's
recap, and categories that moved well away from the usual. Every figure
comes from the ledger; nothing is stored.

Parameters:
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - X-Organization-Id (string | null, header)

Returns 200 InsightList.

#### Insight
  - data (object, required): The facts behind it, per kind (ids such as `account_id` or `goal_id`, names, dates). Amounts are decimal strings in `data.currency`; `change`, `milestone`, `share` are ratios from 0 to 1, also as decimal strings. Keys are snake_case.
  - id (string, required): Stable while the observation holds: its kind and its subject (`price_change:netflix`).
  - kind (string, required): One of `budget_over`, `budget_near`, `spending_down`, `spending_up`, `price_change`, `goal_reached`, `goal_progress`, `budget_streak`, `month_recap`; new kinds may be added.
  - tone (string, required): One of `positive`, `attention`, `neutral`: a win, something to look at, or just a fact.

#### InsightList
  - data (array of Insight, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

## Institutions

### GET /v1/institutions — List institutions (`institutions.list`, read)

Every bank in the ledger, connected or typed in, by the name the app
shows. `accounts.list?institution_id=` gives the accounts at one.

Parameters:
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - X-Organization-Id (string | null, header)

Returns 200 InstitutionList.

### POST /v1/institutions — Create an institution (`institutions.create`, write)

Add a bank by hand, for one no connection reaches (a credit union, a
broker abroad). It starts empty; add accounts to it with
`institutions.create_account`. A bank with the same name already in the
ledger is a 409: use that one. Send an `Idempotency-Key` so a retry
cannot create it twice.

Parameters:
  - Idempotency-Key (string | null, header): Any unique string (a UUID is ideal). A retry with the same key returns the first attempt's response instead of doing the work twice. Keys are kept for 24 hours.
  - X-Organization-Id (string | null, header)

Body (InstitutionCreate):
  - name (string, required)
  - url (string | null): The bank's website.

Returns 201 Institution.

### DELETE /v1/institutions/{institution_id} — Delete an institution (`institutions.delete`, write, destructive)

Delete a bank nothing points at: a typo, a duplicate, one whose
accounts are all gone. A bank that still has an account (closed ones
included, whose only record of where they were held is this) or a
connection is a 409.

Parameters:
  - institution_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 204.

### GET /v1/institutions/{institution_id} — Get an institution (`institutions.get`, read)

One bank, with the connections that feed it, whether it could be
deleted, and who it is (the About card: what the web lookup found and
your corrections to it).

Parameters:
  - institution_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 200 InstitutionDetail.

### PATCH /v1/institutions/{institution_id} — Update an institution (`institutions.update`, write)

Rename a bank, keep notes about it, or correct what its About card
says. A rename applies everywhere the ledger names the bank, and
survives the connection refreshing the bank's own name. Only the fields
sent change.

Parameters:
  - institution_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Body (InstitutionUpdate):
  - about_address (string | null)
  - about_description (string | null)
  - about_phone (string | null)
  - about_summary (string | null): Correct the About card's one line. Null shows what the lookup found; an empty string shows nothing.
  - about_website (string | null): http(s) only.
  - name (string | null): Rename it everywhere the ledger shows it. Null puts the bank's own name back.
  - notes (string | null): Null clears them.

Returns 200 Institution.

### GET /v1/institutions/{institution_id}/about — Get who an institution is (`institutions.get_about`, read)

The stored answer to "who is this bank?", and whether a lookup is
still running (poll this after `institutions.identify`).

Parameters:
  - institution_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 200 InstitutionAboutResult.

### POST /v1/institutions/{institution_id}/accounts — Create an account at an institution (`institutions.create_account`, write)

Open an account at this bank by hand: one no connection returns (a
second card, a CD, an account at a bank that cannot be connected). Its
type, role and code follow from `kind`, and with a `balance` it reads
that amount from today, with transfers to it counted on top. Once it
exists, transfers to it can be filed as transfers instead of spending.
Send an `Idempotency-Key` so a retry cannot create it twice.

Parameters:
  - institution_id (integer, required, path)
  - Idempotency-Key (string | null, header): Any unique string (a UUID is ideal). A retry with the same key returns the first attempt's response instead of doing the work twice. Keys are kept for 24 hours.
  - X-Organization-Id (string | null, header)

Body (ManualAccountCreate):
  - balance (string | null): What it holds (asset) or owes (liability) today, as a decimal string. Omitted, it starts at zero and holds only what transfers put there. Pattern: `^-?\d{1,12}(\.\d{1,2})?$`.
  - kind (string, required): What it is, which decides its type and role: `checking`, `savings`, `cash`, `brokerage`, `retirement`, `other_asset`, `credit_card`, `loan`, `mortgage`, `student_loan`, `other_liability`. Pattern: `^(checking|savings|cash|brokerage|retirement|other_asset|credit_card|loan|mortgage|student_loan|other_liability)$`.
  - mask (string | null): The last digits the bank prints.
  - name (string, required)

Returns 201 Account.

### POST /v1/institutions/{institution_id}/identify — Look up who an institution is (`institutions.identify`, write)

Search the web for who a bank, broker or card issuer is, and keep the
answer. A bank already identified returns what is stored without
searching again, so this is safe to call on every read. When the search
runs in the background `pending` is true: poll `institutions.get_about`.
Answers 503 when lookups are not configured on this server.

Parameters:
  - institution_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 200 InstitutionAboutResult.

### POST /v1/institutions/{institution_id}/purge — Erase an institution and everything it brought in (`institutions.purge`, write, destructive)

Put the ledger back the way it was before this bank was linked:
every account at it, open or closed, is erased with its history (see
`accounts.purge`), every connection to it is revoked, and the bank
itself is deleted. The other side of a transfer into one of its
accounts is kept and categorized again. An entry that would move an
account being kept is a 409 naming it. Needs an admin.

Parameters:
  - institution_id (integer, required, path)
  - dry_run (boolean, query): Report what would go, change nothing.
  - X-Organization-Id (string | null, header)

Returns 200 Purge.

#### AboutSource
  - title (string, required)
  - url (string, required)

#### Account
  - balance (Money, required): The account's balance, positive on its normal side: what an asset holds, what a liability owes, what an expense category has spent in total.
  - code (string, required): Chart-of-accounts code, unique in the ledger (`1010`, `5190`).
  - created_at (datetime, required)
  - excluded_from_totals (boolean, required): Left out of net worth and other totals.
  - hidden (boolean, required): Hidden from lists in the app.
  - id (integer, required): The account id. Transactions and journal lines name it as `account_id`.
  - institution_id (integer | null, required): The bank or provider the account belongs to.
  - is_active (boolean, required): False once the account is closed. Its history stays in the ledger.
  - is_placeholder (boolean, required): A grouping account that holds no postings of its own.
  - low_balance_alert (LowBalanceAlert | null, required): The member is told when this account's balance drops below a threshold, and has a to-do until it is back above; null when nobody set one. Only an open asset account can carry one. Set it with `accounts.update`.
  - mask (string | null, required): The last digits of the account number, as the bank shows them.
  - name (string, required)
  - normal_balance (string, required): `debit` or `credit`: the side that increases the account.
  - overdraft_ahead (LowBalanceShortfall | null, required): With no `low_balance_alert` set: the recurring bills and paychecks expected on this open asset account over the next month, and the payments of the credit cards it pays, take its balance below zero; `needed` is what to move in to stay at or above zero. The member sees it on their to-do list and the account's page but is never notified of it unasked: setting a `low_balance_alert` is how they ask. Null when the bills ahead do not, when the balance is already below zero, and whenever an alert is set (its `shortfall` says it then).
  - parent_id (integer | null, required): The parent account, for sub-accounts and subcategories.
  - purpose (string | null, required): `business` if its transactions count as business by default; null for personal.
  - role (string | null, required): What the account is for, when known (`checking`, `credit_card`, `mortgage`, …).
  - type (string, required): `asset`, `liability`, `equity`, `income` or `expense`.
  - unshared_at (datetime | null, required): When the bank stopped sharing this account on its connection (deselected, closed or revoked at the bank) while the connection itself keeps syncing, or null while it is shared. Its `balance` is frozen at the last number the bank sent. A person fixes it in the app by sharing the account again, or closes it with `accounts.close`.

#### Connection
  - bank_refreshed_at (datetime | null, required): When the data provider last reached the bank itself, new data or not (transactions and holdings alike). Same meaning as on `plaid_items.list`.
  - created_at (datetime, required)
  - error_code (string | null, required): Why it is not syncing, as a stable code.
  - item_id (string, required): The connection's id.
  - last_synced_at (datetime | null, required)
  - new_accounts_available_at (datetime | null, required): When the bank said it has accounts this connection does not share yet, or null. Same meaning as on `plaid_items.list`.
  - stale (boolean, required): True when it should be syncing but has delivered no fresh data for over two days.
  - stale_reason (string | null, required): `sync_failing` (our syncs fail; sync again), `holdings_failing` (syncs succeed but the investment positions could not be fetched; sync again), `investment_transactions_failing` (the same for the investment activity), `liabilities_failing` (the same for cards' and loans' due dates and rates) or `bank_silent` (syncs succeed but the bank has not answered since `bank_refreshed_at`); null when not stale. Same meaning as on `plaid_items.list`.
  - status (string, required): `active`, `error`, `login_required` or `revoked`.

#### Institution
  - id (integer, required): The institution id. Accounts name it as `institution_id`.
  - name (string, required): The name to show: your rename if you gave one, else the bank's own.
  - notes (string | null, required): Free text kept about the bank.
  - official_name (string | null, required): The bank's own name, present only when a rename is hiding it.
  - slug (string, required): Stable key of the bank's name (`chase`), unique in the ledger.
  - source (string, required): `plaid` when a connection brought it, `manual` when typed in.
  - url (string | null, required)

#### InstitutionAbout
  - address (string | null, required)
  - description (string | null, required)
  - looked_up_at (datetime, required)
  - phone (string | null, required)
  - sources (array of AboutSource, required)
  - status (string, required): `found`, or `unknown` when the lookup could not tell.
  - summary (string | null, required)
  - website (string | null, required)

#### InstitutionAboutEdits
  - address (string | null, required)
  - description (string | null, required)
  - phone (string | null, required)
  - summary (string | null, required)
  - website (string | null, required)

#### InstitutionAboutResult
  - about (InstitutionAbout | null, required)
  - pending (boolean, required): A lookup is running; poll `institutions.get_about` until this is false.

#### InstitutionCreate
  - name (string, required)
  - url (string | null): The bank's website.

#### InstitutionDetail
  - about (InstitutionAbout | null, required): The web lookup's answer; null when never looked up (`institutions.identify`).
  - about_edits (InstitutionAboutEdits | null, required): Your corrections to that answer; null when there are none.
  - connections (array of Connection, required)
  - deletable (boolean, required): True when no account (closed ones included) and no connection points at it, so `institutions.delete` would succeed.
  - id (integer, required): The institution id. Accounts name it as `institution_id`.
  - name (string, required): The name to show: your rename if you gave one, else the bank's own.
  - notes (string | null, required): Free text kept about the bank.
  - official_name (string | null, required): The bank's own name, present only when a rename is hiding it.
  - slug (string, required): Stable key of the bank's name (`chase`), unique in the ledger.
  - source (string, required): `plaid` when a connection brought it, `manual` when typed in.
  - url (string | null, required)

#### InstitutionList
  - data (array of Institution, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

#### InstitutionUpdate
  - about_address (string | null)
  - about_description (string | null)
  - about_phone (string | null)
  - about_summary (string | null): Correct the About card's one line. Null shows what the lookup found; an empty string shows nothing.
  - about_website (string | null): http(s) only.
  - name (string | null): Rename it everywhere the ledger shows it. Null puts the bank's own name back.
  - notes (string | null): Null clears them.

#### LowBalanceAlert
  - below (boolean, required): The balance is under the threshold right now.
  - below_since (datetime | null, required): When the balance was seen dropping below the threshold, which is when the member was notified; null while it is at or above it.
  - shortfall (LowBalanceShortfall | null, required): The recurring bills and paychecks expected on the account over the next month, and the payments of the credit cards it pays (bills first on a day), take the balance under the threshold; the member is notified once and has a to-do while it stands. Null when they do not, and while the balance is already below.
  - threshold (Money, required): Alert when the balance drops below this.

#### LowBalanceShortfall
  - bill_amount (Money, required): That bill's usual amount; for a card, what paying it in full takes.
  - bill_kind (string, required): `recurring`: a charge the recurring detector found. `card_payment`: a credit card this account paid last, expected to be paid in full by its due date.
  - bill_name (string, required): The expected bill that takes it under: a recurring charge's name, or the name of the credit card whose payment it is.
  - lowest (Money, required): The lowest the balance is projected to reach over the month.
  - needed (Money, required): What to move in now to stay at or above the threshold all month.
  - short_on (date, required): The first day the balance is projected under the threshold.

#### ManualAccountCreate
  - balance (string | null): What it holds (asset) or owes (liability) today, as a decimal string. Omitted, it starts at zero and holds only what transfers put there. Pattern: `^-?\d{1,12}(\.\d{1,2})?$`.
  - kind (string, required): What it is, which decides its type and role: `checking`, `savings`, `cash`, `brokerage`, `retirement`, `other_asset`, `credit_card`, `loan`, `mortgage`, `student_loan`, `other_liability`. Pattern: `^(checking|savings|cash|brokerage|retirement|other_asset|credit_card|loan|mortgage|student_loan|other_liability)$`.
  - mask (string | null): The last digits the bank prints.
  - name (string, required)

#### Money
  - amount (string, required): Pattern: `^-?\d+\.\d{2}$`.
  - currency (string, required): ISO 4217 code. Pattern: `^[A-Z]{3}$`.

#### Purge
  - account_ids (array of integer, required)
  - blocker_count (integer, required)
  - blockers (array of PurgeBlocker, required)
  - entries (integer, required): Journal entries deleted outright.
  - erased (boolean, required): False on a dry run, or when blockers refused it.
  - released (integer, required): Transactions on accounts that stay, unpaired from a transfer into an erased account and categorized again. Their balances do not move.
  - transactions (integer, required): Transactions of the erased accounts.

#### PurgeBlocker
  - date (string | null)
  - id (integer, required)
  - kind (string, required): `entry`: a journal entry (`id` is its transaction id) that also moves an account being kept. `child_account`: a sub-account of one being erased.
  - label (string, required)

## Jobs

### GET /v1/jobs/{job_id} — Get a job (`jobs.get`, read)

A job started by a long operation (`imports.create`,
`plaid_items.sync`, `categorization.run`, …), by the `id` its 202
answered with. Poll until `status` is `succeeded` or `failed`; a second
or two between polls is plenty. Jobs are kept for 30 days.

Parameters:
  - job_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 200 Job.

#### Job
  - created_at (datetime, required)
  - error (JobError | null, required): Why it failed, once `failed`.
  - finished_at (datetime | null, required)
  - id (integer, required)
  - kind (string, required): What the job does: `import`, `categorize`, `generate_journals`, `reconcile`, `run_schedules`, `plaid_sync` or `rocket_money_import`.
  - result (object | null, required): What the work produced, once `succeeded`. Its fields depend on `kind` and are listed on the operation that started it. Counts are integers.
  - status (string, required): `queued`, `running`, `succeeded` or `failed`.

#### JobError
  - code (string, required): Stable error code, as in the error envelope.
  - message (string, required): For people, in the request's language. Never match on it.

## Loans

### GET /v1/loans — List loans (`loans.list`, read)

Every loan in the ledger except dismissed ones, with today's balance
and, where the terms allow, when it will be paid off. Suggested loans
(found from the bank, not yet confirmed) are listed too; they post
nothing until confirmed with `loans.set_status`.

Parameters:
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - X-Organization-Id (string | null, header)

Returns 200 LoanList.

### POST /v1/loans — Create a loan (`loans.create`, write)

Record a loan's terms against its liability account. A confirmed
amortizing or accrue-only loan gets a standing order that posts its
interest (and its payments) every period from then on. Send an
`Idempotency-Key` so a retry cannot create it twice.

Parameters:
  - Idempotency-Key (string | null, header): Any unique string (a UUID is ideal). A retry with the same key returns the first attempt's response instead of doing the work twice. Keys are kept for 24 hours.
  - X-Organization-Id (string | null, header)

Body (LoanCreate):
  - accrual_mode (string, required): One of `accrue_only`, `amortizing`, `statement`.
  - annual_rate (string | null): Annual rate in percent, as a decimal string: `"6.125"`. 0–200. Pattern: `^\d{1,3}(\.\d{1,4})?$`.
  - day_count (string): One of `act365`, `act360`, `thirty360`. Default: `"act365"`.
  - escrow_account_id (integer | null): Where the escrow share goes.
  - escrow_amount (string | null): Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - interest_account_id (integer | null): Expense account for interest.
  - kind (string, required): One of `mortgage`, `auto`, `student`, `personal`, `heloc`, `credit_card`, `other`.
  - liability_account_id (integer, required): The liability account that holds the balance (see `accounts.list`). One loan per account.
  - maturity_date (date | null)
  - name (string, required)
  - original_principal (string | null): Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - origination_date (date | null)
  - payable_account_id (integer | null): Liability for accrued interest (`accrue_only`). Must not be the loan's own account.
  - payment_account_id (integer | null): The account payments leave from.
  - payment_amount (string | null): The regular payment, escrow included. Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - payment_cadence (string): One of `weekly`, `monthly`, `quarterly`, `annually`. Default: `"monthly"`.
  - payment_day (integer | null): 1–28, or -1 for the month's last day.
  - rate_type (string): `fixed` or `variable`. Default: `"fixed"`.
  - term_months (integer | null): 1–1200.

Returns 201 LoanDetail.

### DELETE /v1/loans/{loan_id} — Delete a loan (`loans.delete`, write, destructive)

Delete a loan's terms and its standing order. The liability account
and everything already posted to it stay in the ledger.

Parameters:
  - loan_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 204.

### GET /v1/loans/{loan_id} — Get a loan (`loans.get`, read)

One loan with its amortization table, recomputed from today's
balance: every remaining period's interest, principal and escrow until
the balance reaches zero.

Parameters:
  - loan_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 200 LoanDetail.

### PATCH /v1/loans/{loan_id} — Update a loan (`loans.update`, write)

Change a loan's terms. Only the fields sent change, and the standing
order is rewritten from the result, so the next posting follows the new
terms.

Parameters:
  - loan_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Body (LoanUpdate):
  - accrual_mode (string): One of `accrue_only`, `amortizing`, `statement`.
  - annual_rate (string | null): Pattern: `^\d{1,3}(\.\d{1,4})?$`.
  - day_count (string): One of `act365`, `act360`, `thirty360`.
  - escrow_account_id (integer | null)
  - escrow_amount (string | null): Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - interest_account_id (integer | null)
  - kind (string): One of `mortgage`, `auto`, `student`, `personal`, `heloc`, `credit_card`, `other`.
  - liability_account_id (integer)
  - maturity_date (date | null)
  - name (string)
  - original_principal (string | null): Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - origination_date (date | null)
  - payable_account_id (integer | null)
  - payment_account_id (integer | null)
  - payment_amount (string | null): Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - payment_cadence (string): One of `weekly`, `monthly`, `quarterly`, `annually`.
  - payment_day (integer | null)
  - rate_type (string): `fixed` or `variable`.
  - term_months (integer | null): 1–1200.

Returns 200 LoanDetail.

### POST /v1/loans/{loan_id}/status — Confirm, close or dismiss a loan (`loans.set_status`, write)

Confirm a loan found from the bank (which starts its standing order),
close one that is paid off, or dismiss one that is not yours. A
dismissed loan posts nothing and is no longer listed; it is kept so the
next sync does not suggest it again.

Parameters:
  - loan_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Body (LoanStatusChange):
  - escrow_account_id (integer | null)
  - escrow_amount (string | null): Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - payment_account_id (integer | null)
  - payment_amount (string | null): Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - status (string, required): One of `confirmed`, `dismissed`, `closed`.

Returns 200 LoanDetail.

### GET /v1/loans/payoff-plan — Plan paying off every debt (`loans.payoff_plan`, read)

Simulate paying off every confirmed debt month by month, two ways:
avalanche (the highest rate first, cheapest in interest) and snowball
(the smallest balance first). Both roll a finished debt's payment into
the next one, and both are compared with the baseline of paying each
debt's regular payment and nothing more. The arithmetic is monthly, so
dates are to the month.

Parameters:
  - extra (string, query): Extra put towards debt each month on top of the regular payments, as a decimal string (`"250.00"`). At most 1000000.
  - X-Organization-Id (string | null, header)

Returns 200 PayoffPlan.

#### Loan
  - accrual_mode (string, required): `amortizing`: a level payment posts each period and splits into interest and principal. `accrue_only`: interest accrues to a payable, no payments are modelled. `statement`: the servicer reports the interest and the ledger never computes it.
  - annual_rate (string | null, required): Annual interest rate in percent, as a decimal string: `"6.125"`.
  - balance (Money, required): What is owed today: the liability account's balance, positive while owed.
  - day_count (string, required): How a period's interest counts days: `act365`, `act360`, `thirty360`.
  - escrow_account_id (integer | null, required): The account the escrow share of a payment goes to.
  - escrow_amount (Money | null, required): The escrow share of each payment.
  - id (integer, required)
  - interest_account_id (integer | null, required): The expense account interest posts to.
  - kind (string, required): One of `mortgage`, `auto`, `student`, `personal`, `heloc`, `credit_card`, `other`.
  - liability_account_id (integer, required): The liability account that holds the balance.
  - maturity_date (date | null, required)
  - name (string, required)
  - original_principal (Money | null, required)
  - origination_date (date | null, required)
  - payable_account_id (integer | null, required): The liability accrued interest posts to (`accrue_only` loans).
  - payment_account_id (integer | null, required): The account payments leave from.
  - payment_amount (Money | null, required): The regular payment, escrow included.
  - payment_cadence (string, required): One of `weekly`, `monthly`, `quarterly`, `annually`.
  - payment_day (integer | null, required): Day of the month payments fall on, 1–28, or -1 for the month's last day.
  - projection (LoanProjection | null, required): Null when the terms cannot be projected (not amortizing, or no payment or rate).
  - rate_type (string, required): `fixed` or `variable`.
  - scheduled_entry_id (integer | null, required): The standing order that posts the loan's interest and payments, when it has one.
  - source (string, required): `manual`, `inferred` (from the bank) or `assistant`.
  - status (string, required): `suggested` (inferred from the bank, awaiting a verdict), `confirmed`, `closed` or `dismissed`. Only confirmed loans post and count in plans.
  - term_months (integer | null, required)

#### LoanCreate
  - accrual_mode (string, required): One of `accrue_only`, `amortizing`, `statement`.
  - annual_rate (string | null): Annual rate in percent, as a decimal string: `"6.125"`. 0–200. Pattern: `^\d{1,3}(\.\d{1,4})?$`.
  - day_count (string): One of `act365`, `act360`, `thirty360`. Default: `"act365"`.
  - escrow_account_id (integer | null): Where the escrow share goes.
  - escrow_amount (string | null): Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - interest_account_id (integer | null): Expense account for interest.
  - kind (string, required): One of `mortgage`, `auto`, `student`, `personal`, `heloc`, `credit_card`, `other`.
  - liability_account_id (integer, required): The liability account that holds the balance (see `accounts.list`). One loan per account.
  - maturity_date (date | null)
  - name (string, required)
  - original_principal (string | null): Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - origination_date (date | null)
  - payable_account_id (integer | null): Liability for accrued interest (`accrue_only`). Must not be the loan's own account.
  - payment_account_id (integer | null): The account payments leave from.
  - payment_amount (string | null): The regular payment, escrow included. Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - payment_cadence (string): One of `weekly`, `monthly`, `quarterly`, `annually`. Default: `"monthly"`.
  - payment_day (integer | null): 1–28, or -1 for the month's last day.
  - rate_type (string): `fixed` or `variable`. Default: `"fixed"`.
  - term_months (integer | null): 1–1200.

#### LoanDetail
  - accrual_mode (string, required): `amortizing`: a level payment posts each period and splits into interest and principal. `accrue_only`: interest accrues to a payable, no payments are modelled. `statement`: the servicer reports the interest and the ledger never computes it.
  - annual_rate (string | null, required): Annual interest rate in percent, as a decimal string: `"6.125"`.
  - balance (Money, required): What is owed today: the liability account's balance, positive while owed.
  - day_count (string, required): How a period's interest counts days: `act365`, `act360`, `thirty360`.
  - escrow_account_id (integer | null, required): The account the escrow share of a payment goes to.
  - escrow_amount (Money | null, required): The escrow share of each payment.
  - id (integer, required)
  - interest_account_id (integer | null, required): The expense account interest posts to.
  - kind (string, required): One of `mortgage`, `auto`, `student`, `personal`, `heloc`, `credit_card`, `other`.
  - liability_account_id (integer, required): The liability account that holds the balance.
  - maturity_date (date | null, required)
  - name (string, required)
  - original_principal (Money | null, required)
  - origination_date (date | null, required)
  - payable_account_id (integer | null, required): The liability accrued interest posts to (`accrue_only` loans).
  - payment_account_id (integer | null, required): The account payments leave from.
  - payment_amount (Money | null, required): The regular payment, escrow included.
  - payment_cadence (string, required): One of `weekly`, `monthly`, `quarterly`, `annually`.
  - payment_day (integer | null, required): Day of the month payments fall on, 1–28, or -1 for the month's last day.
  - projection (LoanProjection | null, required): Null when the terms cannot be projected (not amortizing, or no payment or rate).
  - rate_type (string, required): `fixed` or `variable`.
  - schedule (array of LoanPeriod, required): Every remaining period until payoff, capped at 600. Empty when there is no projection.
  - scheduled_entry_id (integer | null, required): The standing order that posts the loan's interest and payments, when it has one.
  - source (string, required): `manual`, `inferred` (from the bank) or `assistant`.
  - status (string, required): `suggested` (inferred from the bank, awaiting a verdict), `confirmed`, `closed` or `dismissed`. Only confirmed loans post and count in plans.
  - term_months (integer | null, required)

#### LoanList
  - data (array of Loan, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

#### LoanPeriod
  - balance_after (Money, required)
  - escrow (Money, required)
  - interest (Money, required)
  - period_end (date, required)
  - principal (Money, required)

#### LoanProjection
  - never_pays_off (boolean, required): The payment does not cover the interest, so the balance would grow forever.
  - payoff_date (date | null, required): The period the balance reaches zero. Null when it never does.
  - periods_remaining (integer, required)
  - total_interest (Money, required): Interest still to pay from today until payoff.

#### LoanStatusChange
  - escrow_account_id (integer | null)
  - escrow_amount (string | null): Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - payment_account_id (integer | null)
  - payment_amount (string | null): Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - status (string, required): One of `confirmed`, `dismissed`, `closed`.

#### LoanUpdate
  - accrual_mode (string): One of `accrue_only`, `amortizing`, `statement`.
  - annual_rate (string | null): Pattern: `^\d{1,3}(\.\d{1,4})?$`.
  - day_count (string): One of `act365`, `act360`, `thirty360`.
  - escrow_account_id (integer | null)
  - escrow_amount (string | null): Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - interest_account_id (integer | null)
  - kind (string): One of `mortgage`, `auto`, `student`, `personal`, `heloc`, `credit_card`, `other`.
  - liability_account_id (integer)
  - maturity_date (date | null)
  - name (string)
  - original_principal (string | null): Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - origination_date (date | null)
  - payable_account_id (integer | null)
  - payment_account_id (integer | null)
  - payment_amount (string | null): Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - payment_cadence (string): One of `weekly`, `monthly`, `quarterly`, `annually`.
  - payment_day (integer | null)
  - rate_type (string): `fixed` or `variable`.
  - term_months (integer | null): 1–1200.

#### Money
  - amount (string, required): Pattern: `^-?\d+\.\d{2}$`.
  - currency (string, required): ISO 4217 code. Pattern: `^[A-Z]{3}$`.

#### PayoffBaseline
  - debt_free_date (date | null, required)
  - months (integer | null, required)
  - total_interest (Money, required)

#### PayoffDebt
  - annual_rate (string, required): Annual interest rate in percent, as a decimal string.
  - balance (Money, required)
  - kind (string, required)
  - loan_id (integer, required)
  - monthly_payment (Money, required): What goes to this debt each month today, escrow excluded.
  - name (string, required)
  - payment_estimated (boolean, required): No payment is on file, so the usual card minimum was assumed.

#### PayoffPlan
  - baseline (PayoffBaseline, required)
  - debts (array of PayoffDebt, required)
  - extra_monthly (Money, required): The extra put towards debt each month, as asked.
  - skipped (array of PayoffSkipped, required)
  - strategies (array of PayoffStrategy, required)
  - total_balance (Money, required)
  - total_monthly_payment (Money, required)

#### PayoffSkipped
  - loan_id (integer, required)
  - name (string, required)
  - reason (string, required): Why it was left out: `no_rate` (no interest rate on file).

#### PayoffStep
  - interest (Money, required): Interest paid on this debt until then.
  - loan_id (integer, required)
  - months (integer, required)
  - name (string, required)
  - payoff_date (date, required): First of the month this debt reaches zero.

#### PayoffStrategy
  - debt_free_date (date | null, required): Null when the payments never clear the interest.
  - interest_saved (Money, required): Against the baseline. Never negative.
  - months (integer | null, required)
  - months_saved (integer, required)
  - steps (array of PayoffStep, required): The debts in the order they are paid off.
  - strategy (string, required): `avalanche` (highest rate first) or `snowball` (smallest balance first).
  - total_interest (Money, required)

## Me

### GET /v1/me — Get the caller (`me.get`, read)

Who is calling: the user, the ledger the request acts in, their role
there, and whether this credential may write. Useful as a first call to
check a token works and what it can do.

Parameters:
  - X-Organization-Id (string | null, header)

Returns 200 Me.

#### Me
  - can_write (boolean, required): Whether write operations will be allowed: a write-scoped credential and an `editor` role or above.
  - credential (string, required): `token` for an API token, `oauth` for an app the user connected with OAuth (an MCP client), `session` for a signed-in user.
  - organization_id (string, required): The ledger this request acts in (a UUID): a token's own ledger, else the `X-Organization-Id` header, else the user's personal ledger.
  - role (string, required): The user's role in that ledger: `owner`, `admin`, `editor` or `viewer`.
  - token_scope (string | null, required): `read` or `write` for a token; `read` or `propose` for an OAuth app (which never writes: with `propose` it proposes changes over MCP for the user to approve); null for a session.
  - user_id (string, required): The user's id (a UUID). A token acts as the user who made it.

## Net Worth

### GET /v1/net-worth — Get net worth (`net_worth.get`, read)

The ledger's net worth today, its assets and liabilities, and how it
moved over the past 30 days, in the ledger's base currency.

Parameters:
  - X-Organization-Id (string | null, header)

Returns 200 NetWorth.

### GET /v1/net-worth/breakdown — Net worth breakdown (`net_worth.breakdown`, read)

What the assets and liabilities are made of today: classes (cash,
investments, retirement, real estate, credit cards, loans, …) and the
accounts in each, with every account's balance in the base currency and
its own.

Parameters:
  - X-Organization-Id (string | null, header)

Returns 200 NetWorthBreakdown.

### GET /v1/net-worth/cash-real-value — Real value of cash after inflation (`net_worth.cash_real_value`, read)

What holding each checking and savings account was really worth over
the window: the interest it earned against the consumer price index of
its currency. The index line strips out deposits and withdrawals, so
two accounts are comparable; `purchasing_power_change` is the money
actually held against the inflation it sat through.

Parameters:
  - range (string, query): The window: `3M`, `6M`, `1Y` or `ALL`.
  - X-Organization-Id (string | null, header)

Returns 200 CashRealValue.

### GET /v1/net-worth/history — Net worth over time (`net_worth.history`, read)

Net worth, assets and liabilities at the end of every day that moved
them between `since` and `until`. The first point sits on `since` and
carries in the balance from before it, so a chart starts at the right
height. Narrow the series to some accounts with `account_id` or `role`
(the investments or one loan's page do).

Parameters:
  - since (date | null, query): First day of the window. Defaults to a year ago.
  - until (date | null, query): Last day of the window. Defaults to today.
  - account_id (array of integer | null, query): Only these accounts (repeat the parameter). Ids from `accounts.list`.
  - role (array of string | null, query): Only accounts with these roles (`brokerage`, `ira`, `mortgage`, …).
  - X-Organization-Id (string | null, header)

Returns 200 NetWorthHistory.

#### BreakdownAccount
  - account_id (integer, required)
  - balance (Money, required): In the ledger's base currency, positive on the account's normal side.
  - linked_asset (LinkedAsset | null, required): For a mortgage or auto loan: the property or vehicle it finances.
  - mask (string | null, required)
  - name (string, required)
  - native_balance (Money, required): In the account's own currency.
  - role (string | null, required)
  - share (string, required): Share of its side (assets or liabilities), 0–1. Pattern: `^-?\d+(\.\d+)?$`.

#### BreakdownGroup
  - accounts (array of BreakdownAccount, required): Largest first.
  - key (string, required): The class: `cash`, `investments`, `retirement`, `tax_advantaged`, `alternatives`, `real_estate`, `vehicles`, `other_assets`; or `credit_cards`, `loans`, `other_liabilities`.
  - share (string, required): Share of its side, 0–1. Pattern: `^-?\d+(\.\d+)?$`.
  - total (Money, required)

#### BreakdownSide
  - groups (array of BreakdownGroup, required): Largest first.
  - total (Money, required)

#### CashRealValue
  - accounts (array of CashRealValueAccount, required)
  - range (string, required)

#### CashRealValueAccount
  - account_id (integer, required)
  - currency (string, required)
  - end (date, required)
  - index (array of string, required): A time-weighted real-return index based at 100 on `start`, thinned to at most 60 evenly spaced points ending at `end`. Below 100 means inflation outran the interest the money earned.
  - index_code (string, required): The consumer price index it is measured against.
  - inflation_percent (string, required): Pattern: `^-?\d+(\.\d+)?$`.
  - interest_earned (Money, required)
  - provisional (boolean, required): The tail is held flat: no published index covers it yet.
  - purchasing_power_change (Money, required): What the money actually held gained or lost in purchasing power.
  - real_return_percent (string, required): Pattern: `^-?\d+(\.\d+)?$`.
  - start (date, required)

#### LinkedAsset
  - kind (string, required): `property` or `vehicle`: what this loan finances.
  - label (string, required)

#### Money
  - amount (string, required): Pattern: `^-?\d+\.\d{2}$`.
  - currency (string, required): ISO 4217 code. Pattern: `^[A-Z]{3}$`.

#### NetWorth
  - as_of (date, required)
  - assets (Money, required): Everything the ledger owns, in its base currency.
  - change_30d (Money, required): How much net worth moved over the past 30 days.
  - change_30d_percent (string, required): `change_30d` as a percentage of net worth 30 days ago (`"4.25"` is 4.25%). `"0"` when there is nothing to compare with. Pattern: `^-?\d+(\.\d+)?$`.
  - liabilities (Money, required): Everything it owes, positive.
  - net_worth (Money, required): `assets` − `liabilities`.

#### NetWorthBreakdown
  - as_of (date, required)
  - assets (BreakdownSide, required)
  - currency (string, required): The ledger's base currency.
  - liabilities (BreakdownSide, required)

#### NetWorthHistory
  - currency (string, required): The ledger's base currency; every amount is in it.
  - points (array of NetWorthPoint, required): Oldest first.

#### NetWorthPoint
  - assets (Money, required)
  - date (date, required)
  - liabilities (Money, required): Positive: what is owed.
  - value (Money, required): Net worth at the end of the day.

## Notifications

### GET /v1/notifications — List notifications (`notifications.list`, read)

The caller's notifications in this ledger, newest first: bills due,
renewals, large transactions, budgets crossing their line, receipts
matched, a bank that needs signing in again.

Parameters:
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - unread_only (boolean, query): Only notifications not yet read.
  - X-Organization-Id (string | null, header)

Returns 200 NotificationList.

### GET /v1/notifications/preferences — Get notification preferences (`notification_preferences.get`, read)

The categories of notification the caller has muted, and whether the
weekly summary email and alert emails are on.

Parameters:
  - X-Organization-Id (string | null, header)

Returns 200 NotificationPreferences.

### PATCH /v1/notifications/preferences — Update notification preferences (`notification_preferences.update`, write)

Mute or unmute categories of notification, and switch the weekly
summary email and alert emails on or off. Muting stops new notifications of that kind;
nothing is replayed when it is turned back on.

Parameters:
  - X-Organization-Id (string | null, header)

Body (NotificationPreferencesUpdate):
  - alert_emails (boolean | null): Switch alert emails (a low balance, a shortfall ahead, a payment due) on or off. Muting a category stops its emails too.
  - categories (array of CategoryMute | null): Categories to mute or unmute. Categories not listed keep their setting.
  - timezone (string | null): With `weekly_digest`: the IANA zone whose Monday morning it is sent on (`Europe/Madrid`). UTC when unknown.
  - weekly_digest (boolean | null): Switch the Monday summary email on or off. On needs the caller's login to have an email address.

Returns 200 NotificationPreferences.

### POST /v1/notifications/read — Mark notifications read (`notifications.mark_read`, write)

Mark some of the caller's notifications read, or all of them with
`all`. Ids that are not the caller's, or already read, are skipped.

Parameters:
  - X-Organization-Id (string | null, header)

Body (MarkRead):
  - all (boolean): Mark every unread notification read instead. Default: `false`.
  - notification_ids (array of integer | null): The notifications to mark read, from `notifications.list`.

Returns 200 MarkReadResult.

#### CategoryMute
  - key (string, required): One of `bills`, `subscriptions`, `budgets`, `goals`, `large_transactions`, `accounts`, `investments`, `discoveries`, `receipts`, `automations`. Pattern: `^(bills|subscriptions|budgets|goals|large_transactions|accounts|investments|discoveries|receipts|automations)$`.
  - muted (boolean, required)

#### MarkRead
  - all (boolean): Mark every unread notification read instead. Default: `false`.
  - notification_ids (array of integer | null): The notifications to mark read, from `notifications.list`.

#### MarkReadResult
  - marked (integer, required): How many were unread and are now read.

#### Notification
  - body (string | null, required)
  - created_at (datetime, required)
  - data (object, required): Facts behind it, per type (ids, names, dates); amounts are decimal strings. Keys are snake_case.
  - id (integer, required)
  - read (boolean, required)
  - severity (string, required): `info` or `warning`.
  - title (string, required): Written in the language the ledger member reads.
  - type (string, required): What happened, e.g. `subscription_renewal`, `large_transaction`, `budget_threshold`. New types are added over time.

#### NotificationCategory
  - key (string, required): A category of notifications that can be muted.
  - muted (boolean, required)

#### NotificationList
  - data (array of Notification, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

#### NotificationPreferences
  - alert_emails (boolean, required): A low balance, a balance projected to run short and a card payment due also arrive by email, once each. On unless switched off.
  - categories (array of NotificationCategory, required): Every category, in the app's order. Security notices belong to none and cannot be muted.
  - weekly_digest (boolean, required): The Monday summary email is on.

#### NotificationPreferencesUpdate
  - alert_emails (boolean | null): Switch alert emails (a low balance, a shortfall ahead, a payment due) on or off. Muting a category stops its emails too.
  - categories (array of CategoryMute | null): Categories to mute or unmute. Categories not listed keep their setting.
  - timezone (string | null): With `weekly_digest`: the IANA zone whose Monday morning it is sent on (`Europe/Madrid`). UTC when unknown.
  - weekly_digest (boolean | null): Switch the Monday summary email on or off. On needs the caller's login to have an email address.

## Paystubs

### GET /v1/paystubs — List paystubs (`paystubs.list`, read)

The ledger's paystubs with every row read from them, newest pay date
first. Stubs that could not be read (no pay date) come first.

Parameters:
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - status (string | null, query): Only paystubs in this state: `pending`, `parsed`, `failed`.
  - year (integer | null, query): Only paychecks paid in this year.
  - X-Organization-Id (string | null, header)

Returns 200 PaystubList.

### POST /v1/paystubs — Upload a paystub (`paystubs.create`, write)

Upload a paystub as `multipart/form-data`. One stub is read before
the answer (a few seconds). A payroll export holding many paychecks is
split into one paystub each and read in the background: they come back
`pending`, and `reading` says how many to wait for. A paycheck already
on file is not added twice (`duplicate`).

Parameters:
  - Idempotency-Key (string | null, header): Any unique string (a UUID is ideal). A retry with the same key returns the first attempt's response instead of doing the work twice. Keys are kept for 24 hours.
  - X-Organization-Id (string | null, header)

Body (multipart/form-data):
  - file (file, required): A paystub, or a payroll export of many, as PDF or image; at most 10 MB.

Returns 201 PaystubUpload.

### DELETE /v1/paystubs/{paystub_id} — Delete a paystub (`paystubs.delete`, write, destructive)

Delete a paystub, its rows and its file.

Parameters:
  - paystub_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 204.

### GET /v1/paystubs/{paystub_id} — Get a paystub (`paystubs.get`, read)

One paystub with every row read from it.

Parameters:
  - paystub_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 200 Paystub.

### GET /v1/paystubs/{paystub_id}/download — Download a paystub's file (`paystubs.download`, read)

A short-lived URL for the paystub's PDF or image. A stub cut out of a
payroll export is its own file, just its pages.

Parameters:
  - paystub_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 200 Download.

### POST /v1/paystubs/{paystub_id}/reprocess — Read a paystub again (`paystubs.reprocess`, write)

Read the file again, for a stub that failed or whose numbers did not
add up. If the new reading is a paycheck already on file, this copy is
dropped and the one on file is returned with `duplicate`.

Parameters:
  - paystub_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 200 PaystubResult.

### GET /v1/paystubs/breakdown — Break down a year of pay (`paystubs.breakdown`, read)

Where a year's gross pay went (taxes, retirement, health, other
deductions, net), by band, by employer and by month, from the stubs on
file. Only what was uploaded is counted; `reported_ytd_gross` is the
employer's own figure for the whole year so far.

Parameters:
  - year (integer, required, query): The calendar year of the pay dates.
  - X-Organization-Id (string | null, header)

Returns 200 PayBreakdown.

#### BreakdownBand
  - amount (Money, required)
  - group (string, required)
  - rows (array of BreakdownRow, required)

#### BreakdownMonth
  - deductions (Money, required)
  - gross (Money, required)
  - month (integer, required): 1–12.
  - net (Money, required)
  - taxes (Money, required)

#### BreakdownRow
  - amount (Money, required)
  - key (string, required): The concept key, or `?<printed wording>` for a row that was not recognized.
  - label (string | null, required): The employer's wording, when there is no concept key.

#### Download
  - expires_at (datetime | null, required): When a signed `url` stops working. Null for a `data:` URL.
  - url (string, required): A signed HTTPS URL for the file, or a `data:` URL carrying the file itself when it is stored inline. Needs no credential.

#### Money
  - amount (string, required): Pattern: `^-?\d+\.\d{2}$`.
  - currency (string, required): ISO 4217 code. Pattern: `^[A-Z]{3}$`.

#### PayBreakdown
  - bands (array of BreakdownBand, required)
  - employer_cost (Money, required)
  - employers (array of BreakdownRow, required)
  - gross (Money, required)
  - imputed (Money, required)
  - latest_pay_date (date | null, required)
  - mixed_currency (boolean, required): True when the year's stubs are in more than one currency. Nothing is converted: the totals are plain sums.
  - months (array of BreakdownMonth, required)
  - net (Money, required)
  - post_tax (Money, required)
  - pre_tax (Money, required)
  - reimbursements (Money, required)
  - reported_ytd_gross (Money | null, required): The employer's own year-to-date gross from the latest stub. It counts every pay period, including ones not uploaded, so the gap to `gross` is what is missing.
  - reported_ytd_net (Money | null, required)
  - stub_count (integer, required)
  - taxes (Money, required)
  - year (integer, required)

#### Paystub
  - created_at (datetime, required)
  - currency (string, required)
  - employee_name (string | null, required)
  - employer_cost (Money | null, required): What the employer paid on top (its payroll taxes and contributions), never deducted from the employee.
  - employer_name (string | null, required)
  - gross_pay (Money | null, required)
  - hours (string | null, required): Hours paid, as a decimal string.
  - id (integer, required)
  - imputed_pay (Money | null, required): Taxable benefits added to gross that were never cash.
  - lines (array of PaystubLine, required)
  - mime_type (string, required)
  - net_pay (Money | null, required)
  - original_name (string | null, required)
  - parse_error (string | null, required): Why the file could not be read at all.
  - parsed_at (datetime | null, required)
  - pay_date (date | null, required)
  - period_end (date | null, required)
  - period_start (date | null, required)
  - post_tax_deductions (Money | null, required)
  - pre_tax_deductions (Money | null, required)
  - reconcile_error (string | null, required): Set when the stub was read but its own totals do not add up.
  - reimbursements (Money | null, required)
  - size_bytes (integer, required)
  - source (string, required)
  - status (string, required): `pending` (being read), `parsed` or `failed` (see `parse_error`).
  - taxes (Money | null, required)
  - ytd_gross_pay (Money | null, required)
  - ytd_net_pay (Money | null, required)

#### PaystubLine
  - account_mask (string | null, required): Deposit rows: the last digits of the account paid into.
  - amount (Money, required)
  - concept (string | null, required): What the row is, as a stable key (`federal_income_tax`, `regular`). Null when the wording was not recognized.
  - description (string, required): Exactly as the employer printed it.
  - group (string, required): The band of the gross-to-net breakdown it belongs to.
  - hours (string | null, required): Decimal string.
  - id (integer, required)
  - kind (string, required): Which block of the stub it came from: `earning`, `tax`, `deduction`, `deposit`, …
  - line_no (integer, required)
  - rate (string | null, required): Pay rate, as a decimal string (up to four places).
  - ytd_amount (Money | null, required)

#### PaystubList
  - data (array of Paystub, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

#### PaystubResult
  - created_at (datetime, required)
  - currency (string, required)
  - duplicate (boolean, required): True when this turned out to be a paycheck already on file: the one already held is returned and the new copy was dropped.
  - employee_name (string | null, required)
  - employer_cost (Money | null, required): What the employer paid on top (its payroll taxes and contributions), never deducted from the employee.
  - employer_name (string | null, required)
  - gross_pay (Money | null, required)
  - hours (string | null, required): Hours paid, as a decimal string.
  - id (integer, required)
  - imputed_pay (Money | null, required): Taxable benefits added to gross that were never cash.
  - lines (array of PaystubLine, required)
  - mime_type (string, required)
  - net_pay (Money | null, required)
  - original_name (string | null, required)
  - parse_error (string | null, required): Why the file could not be read at all.
  - parsed_at (datetime | null, required)
  - pay_date (date | null, required)
  - period_end (date | null, required)
  - period_start (date | null, required)
  - post_tax_deductions (Money | null, required)
  - pre_tax_deductions (Money | null, required)
  - reconcile_error (string | null, required): Set when the stub was read but its own totals do not add up.
  - reimbursements (Money | null, required)
  - size_bytes (integer, required)
  - source (string, required)
  - status (string, required): `pending` (being read), `parsed` or `failed` (see `parse_error`).
  - taxes (Money | null, required)
  - ytd_gross_pay (Money | null, required)
  - ytd_net_pay (Money | null, required)

#### PaystubUpload
  - paystubs (array of PaystubResult, required): Newest pay date first.
  - reading (integer, required): How many are still `pending`, being read in the background. Poll `paystubs.get` until it is 0.
  - split (boolean, required): True when the file held more than one paycheck.

## Plaid Items

### GET /v1/plaid-items — List bank connections (`plaid_items.list`, read)

The ledger's bank connections, with when each last synced and
whether it needs a person to sign in again.

Parameters:
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - X-Organization-Id (string | null, header)

Returns 200 PlaidItemList.

### POST /v1/plaid-items/sync — Sync bank connections (`plaid_items.sync`, write)

Pull new transactions and balances from the bank, then settle the
ledger: match transfers, categorize, apply your rules and post.
Connections already sync by themselves when the bank says there is
something new; this is the "refresh" button. Answers 202 with a Job
(`kind` `plaid_sync`). Its `result`: `items_synced`, `results` (per
connection, its counts or an `error`), `matched`, `categorized`,
`llm_categorized`, `rules_applied`, `journals_generated` and
`notifications`. A connection that fails is reported in `results` and
does not fail the job.

Parameters:
  - Idempotency-Key (string | null, header): Any unique string (a UUID is ideal). A retry with the same key returns the first attempt's response instead of doing the work twice. Keys are kept for 24 hours.
  - X-Organization-Id (string | null, header)

Body (PlaidSync):
  - full (boolean): Pull the whole history again instead of only what is new. Rows already in the ledger are not duplicated. Default: `false`.
  - plaid_item_id (string | null): Sync only this connection (from `plaid_items.list`). Omitted, every one is synced.

Returns 202 Job.

#### Job
  - created_at (datetime, required)
  - error (JobError | null, required): Why it failed, once `failed`.
  - finished_at (datetime | null, required)
  - id (integer, required)
  - kind (string, required): What the job does: `import`, `categorize`, `generate_journals`, `reconcile`, `run_schedules`, `plaid_sync` or `rocket_money_import`.
  - result (object | null, required): What the work produced, once `succeeded`. Its fields depend on `kind` and are listed on the operation that started it. Counts are integers.
  - status (string, required): `queued`, `running`, `succeeded` or `failed`.

#### JobError
  - code (string, required): Stable error code, as in the error envelope.
  - message (string, required): For people, in the request's language. Never match on it.

#### PlaidItemList
  - data (array of PlaidItemOut, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

#### PlaidItemOut
  - bank_refreshed_at (datetime | null, required): When the data provider last reached the bank itself for this connection, new data or not; for a connection with investment accounts, the older of its transactions and its holdings. A sync can succeed while this stays old: the provider then hands back what it last fetched.
  - created_at (datetime, required)
  - error_code (string | null, required): Why the last sync failed, a stable code.
  - holdings_error_code (string | null, required): Why fetching the investment positions failed, a stable code; set exactly when `holdings_failing_since` is.
  - holdings_failing_since (datetime | null, required): When fetching this connection's investment positions started failing while its syncs still succeed, or null while it works (or the connection has no investment account). Its positions are as of then.
  - institution_id (integer | null, required): The ledger's institution (the bank) it belongs to.
  - institution_name (string | null, required)
  - investment_transactions_error_code (string | null, required): Why fetching the investment activity failed, a stable code; set exactly when `investment_transactions_failing_since` is.
  - investment_transactions_failing_since (datetime | null, required): When fetching this connection's investment activity (trades, dividends, deposits, fees) started failing while its syncs still succeed, or null while it works (or the connection has no investment account). Activity after then may be missing from the ledger.
  - last_synced_at (datetime | null, required): When our last sync succeeded.
  - liabilities_error_code (string | null, required): Why fetching the card and loan details failed, a stable code; set exactly when `liabilities_failing_since` is.
  - liabilities_failing_since (datetime | null, required): When fetching this connection's card and loan details (due date, minimum payment, statement balance, rates) started failing while its syncs still succeed, or null while it works (or the connection has no card or loan). Those details are as of then.
  - new_accounts_available_at (datetime | null, required): When the bank said it has accounts this connection does not share yet (usually a card opened after it was linked), or null when none are waiting. Their activity is not in the ledger until a person adds them in the app, which only they can do: it happens in the bank's own sign-in window.
  - plaid_item_id (string, required): Plaid's id for the connection. Pass it to `plaid_items.sync`.
  - stale (boolean, required): True when the connection should be syncing (`active` or `error`) but has delivered no fresh data for over two days: its balances and transactions are out of date. `stale_reason` says which fix applies.
  - stale_reason (string | null, required): Why it is stale, or null when it is not. `sync_failing`: our syncs are not getting through; try `plaid_items.sync`, and if it keeps failing `error_code` says why. `holdings_failing`: syncs bring the activity in but fetching the investment positions has failed since `holdings_failing_since`, so their values are frozen; try `plaid_items.sync`, and `holdings_error_code` says why. `investment_transactions_failing` and `liabilities_failing` are the same for the investment activity (trades, dividends, deposits; since `investment_transactions_failing_since`) and for cards' and loans' due dates, minimum payments and rates (since `liabilities_failing_since`). `bank_silent`: syncs succeed but the bank has not answered since `bank_refreshed_at`, so another sync will not help; it usually clears by itself, and a person can reconnect the bank in the app.
  - status (string, required): `active`, `error` (the last sync failed; the next one may work), `login_required` (a person must sign in again in the app) or `revoked`.

#### PlaidSync
  - full (boolean): Pull the whole history again instead of only what is new. Rows already in the ledger are not duplicated. Default: `false`.
  - plaid_item_id (string | null): Sync only this connection (from `plaid_items.list`). Omitted, every one is synced.

## Plan

### GET /v1/plan — Get the plan (`plan.get`, read)

The plan's settings (retirement age, return and inflation
assumptions, overrides) and where the household stands today. A ledger
that never set a plan answers with the defaults.

Parameters:
  - X-Organization-Id (string | null, header)

Returns 200 PlanOut.

### PATCH /v1/plan — Update the plan (`plan.update`, write)

Change the plan's settings: birth year, retirement age, how long to
plan for, income or spending overrides, and the return, volatility,
inflation and growth assumptions. Only the fields sent change.

Parameters:
  - X-Organization-Id (string | null, header)

Body (PlanUpdate):
  - birth_year (integer | null)
  - expected_return_pct (string | null): Pattern: `^-?\d{1,3}(\.\d{1,3})?$`.
  - income_growth_rate_pct (string | null): Pattern: `^-?\d{1,3}(\.\d{1,3})?$`.
  - inflation_rate_pct (string | null): Pattern: `^-?\d{1,3}(\.\d{1,3})?$`.
  - monthly_income_override (string | null): Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - monthly_spending_override (string | null): Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - plan_to_age (integer | null): After `retirement_age`.
  - property_growth_rate_pct (string | null): Pattern: `^-?\d{1,3}(\.\d{1,3})?$`.
  - retirement_age (integer | null)
  - retirement_income_monthly (string | null): Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - retirement_spending_pct (string | null): Pattern: `^-?\d{1,3}(\.\d{1,3})?$`.
  - return_volatility_pct (string | null): Pattern: `^-?\d{1,3}(\.\d{1,3})?$`.

Returns 200 PlanOut.

### GET /v1/plan/milestones — List plan milestones (`plan_milestones.list`, read)

The milestones on the plan: a home, a car, a big purchase, a
windfall, a change in income, a new recurring cost.

Parameters:
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - X-Organization-Id (string | null, header)

Returns 200 PlanMilestoneList.

### POST /v1/plan/milestones — Add a plan milestone (`plan_milestones.create`, write)

Put something the household can see coming on the plan: "a $650k
house in June 2028, 20% down", a windfall, a raise, childcare from
next year. The projection accounts for it from then on.

Parameters:
  - Idempotency-Key (string | null, header): Any unique string (a UUID is ideal). A retry with the same key returns the first attempt's response instead of doing the work twice. Keys are kept for 24 hours.
  - X-Organization-Id (string | null, header)

Body (PlanMilestoneCreate):
  - amount (string, required): Decimal string. Positive, except an `income_change`, which may be negative (a cut) but not zero. Pattern: `^-?\d{1,12}(\.\d{1,2})?$`.
  - ends_on (date | null): For `income_change` and `recurring_expense` only.
  - is_enabled (boolean): Default: `true`.
  - kind (string, required): One of: `home_purchase`, `vehicle_purchase`, `big_purchase`, `windfall`, `income_change`, `recurring_expense`, `time_off`.
  - label (string, required)
  - starts_on (date, required): This month or later.
  - terms (object): The kind's own terms; every one is optional and has a default. `home_purchase`: `down_payment_pct`, `mortgage_rate_pct`, `term_years`, `maintenance_pct`, `hoa_monthly`, `replaces_housing` (bool), `appreciation_pct` (null: the plan's property growth). `vehicle_purchase`: `down_payment_pct`, `loan_rate_pct`, `term_years`, `depreciation_pct`. Other kinds take none. Numbers other than `term_years` are decimal strings.

Returns 201 PlanMilestone.

### DELETE /v1/plan/milestones/{milestone_id} — Delete a plan milestone (`plan_milestones.delete`, write, destructive)

Take a milestone off the plan.

Parameters:
  - milestone_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 204.

### GET /v1/plan/milestones/{milestone_id} — Get a plan milestone (`plan_milestones.get`, read)

One milestone by its id.

Parameters:
  - milestone_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 200 PlanMilestone.

### PATCH /v1/plan/milestones/{milestone_id} — Update a plan milestone (`plan_milestones.update`, write)

Change a milestone, or switch it off and on (`is_enabled`) to see
the projection with and without it. Only the fields sent change.

Parameters:
  - milestone_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Body (PlanMilestoneUpdate):
  - amount (string | null): Pattern: `^-?\d{1,12}(\.\d{1,2})?$`.
  - ends_on (date | null)
  - is_enabled (boolean | null)
  - kind (string | null)
  - label (string | null)
  - starts_on (date | null)
  - terms (object | null): The kind's own terms; every one is optional and has a default. `home_purchase`: `down_payment_pct`, `mortgage_rate_pct`, `term_years`, `maintenance_pct`, `hoa_monthly`, `replaces_housing` (bool), `appreciation_pct` (null: the plan's property growth). `vehicle_purchase`: `down_payment_pct`, `loan_rate_pct`, `term_years`, `depreciation_pct`. Other kinds take none. Numbers other than `term_years` are decimal strings.

Returns 200 PlanMilestone.

### GET /v1/plan/projection — Project net worth (`plan.projection`, read)

Run the forecast: net worth month by month to the end of the plan,
as a median and ranges over many market paths, with the odds of each
milestone and of the money lasting through retirement. Slower than the
other reads; call it when the answer is needed, not on every change.

Parameters:
  - X-Organization-Id (string | null, header)

Returns 200 Projection.

#### MilestoneOutcome
  - affordability (string | null, required): A home's mortgage-calculator verdict, as a rating.
  - age (integer | null, required)
  - amount (Money, required)
  - chance (string | null, required): Share of market paths, `0.0000`–`1.0000`, in which it is paid for without the money running out. Null for money arriving and for a milestone switched off.
  - date (date, required)
  - is_enabled (boolean, required)
  - kind (string, required)
  - label (string, required)
  - milestone_id (integer, required)
  - month (integer, required): Index into `points`: where on the projection it lands.
  - monthly (Money, required): What it costs (or brings in) each month after.
  - net_worth (Money, required): Median net worth that month.
  - rating (string | null, required): `on_track`, `stretch` or `out_of_reach`.
  - upfront (Money, required): What changes hands on the day.

#### Money
  - amount (string, required): Pattern: `^-?\d+\.\d{2}$`.
  - currency (string, required): ISO 4217 code. Pattern: `^[A-Z]{3}$`.

#### PlanDebt
  - account_id (integer, required): The liability account.
  - annual_rate_pct (string, required)
  - balance (Money, required)
  - monthly_payment (Money, required)
  - name (string, required)

#### PlanMilestone
  - amount (Money, required): A price or a windfall in that day's money; a monthly cost or change in earnings in today's money. Negative only for an income cut.
  - ends_on (date | null, required): For `income_change` and `recurring_expense` only.
  - id (integer, required)
  - is_enabled (boolean, required): Switched off: kept, but left out of the projection.
  - kind (string, required): One of: `home_purchase`, `vehicle_purchase`, `big_purchase`, `windfall`, `income_change`, `recurring_expense`, `time_off`.
  - label (string, required)
  - starts_on (date, required)
  - terms (object, required): The kind's own terms; every one is optional and has a default. `home_purchase`: `down_payment_pct`, `mortgage_rate_pct`, `term_years`, `maintenance_pct`, `hoa_monthly`, `replaces_housing` (bool), `appreciation_pct` (null: the plan's property growth). `vehicle_purchase`: `down_payment_pct`, `loan_rate_pct`, `term_years`, `depreciation_pct`. Other kinds take none. Numbers other than `term_years` are decimal strings.

#### PlanMilestoneCreate
  - amount (string, required): Decimal string. Positive, except an `income_change`, which may be negative (a cut) but not zero. Pattern: `^-?\d{1,12}(\.\d{1,2})?$`.
  - ends_on (date | null): For `income_change` and `recurring_expense` only.
  - is_enabled (boolean): Default: `true`.
  - kind (string, required): One of: `home_purchase`, `vehicle_purchase`, `big_purchase`, `windfall`, `income_change`, `recurring_expense`, `time_off`.
  - label (string, required)
  - starts_on (date, required): This month or later.
  - terms (object): The kind's own terms; every one is optional and has a default. `home_purchase`: `down_payment_pct`, `mortgage_rate_pct`, `term_years`, `maintenance_pct`, `hoa_monthly`, `replaces_housing` (bool), `appreciation_pct` (null: the plan's property growth). `vehicle_purchase`: `down_payment_pct`, `loan_rate_pct`, `term_years`, `depreciation_pct`. Other kinds take none. Numbers other than `term_years` are decimal strings.

#### PlanMilestoneList
  - data (array of PlanMilestone, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

#### PlanMilestoneUpdate
  - amount (string | null): Pattern: `^-?\d{1,12}(\.\d{1,2})?$`.
  - ends_on (date | null)
  - is_enabled (boolean | null)
  - kind (string | null)
  - label (string | null)
  - starts_on (date | null)
  - terms (object | null): The kind's own terms; every one is optional and has a default. `home_purchase`: `down_payment_pct`, `mortgage_rate_pct`, `term_years`, `maintenance_pct`, `hoa_monthly`, `replaces_housing` (bool), `appreciation_pct` (null: the plan's property growth). `vehicle_purchase`: `down_payment_pct`, `loan_rate_pct`, `term_years`, `depreciation_pct`. Other kinds take none. Numbers other than `term_years` are decimal strings.

#### PlanOut
  - settings (PlanSettings, required)
  - starting_point (StartingPoint, required)

#### PlanSettings
  - birth_year (integer | null, required): Ages on the projection count from it. Null until set.
  - expected_return_pct (string, required): Mean yearly return on investments.
  - income_growth_rate_pct (string, required)
  - inflation_rate_pct (string, required)
  - monthly_income_override (Money | null, required): After-tax income per month to plan with instead of the ledger's average.
  - monthly_spending_override (Money | null, required): Spending per month to plan with instead of the ledger's average.
  - plan_to_age (integer, required): How far the projection runs.
  - property_growth_rate_pct (string, required)
  - retirement_age (integer, required)
  - retirement_income_monthly (Money, required): Pension or social security per month, in today's money.
  - retirement_spending_pct (string, required): Spending in retirement, as a percentage of today's.
  - return_volatility_pct (string, required): Yearly standard deviation of that return.

#### PlanUpdate
  - birth_year (integer | null)
  - expected_return_pct (string | null): Pattern: `^-?\d{1,3}(\.\d{1,3})?$`.
  - income_growth_rate_pct (string | null): Pattern: `^-?\d{1,3}(\.\d{1,3})?$`.
  - inflation_rate_pct (string | null): Pattern: `^-?\d{1,3}(\.\d{1,3})?$`.
  - monthly_income_override (string | null): Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - monthly_spending_override (string | null): Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - plan_to_age (integer | null): After `retirement_age`.
  - property_growth_rate_pct (string | null): Pattern: `^-?\d{1,3}(\.\d{1,3})?$`.
  - retirement_age (integer | null)
  - retirement_income_monthly (string | null): Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - retirement_spending_pct (string | null): Pattern: `^-?\d{1,3}(\.\d{1,3})?$`.
  - return_volatility_pct (string | null): Pattern: `^-?\d{1,3}(\.\d{1,3})?$`.

#### Projection
  - birth_year (integer | null, required)
  - headline_age (integer | null, required)
  - headline_date (date, required)
  - headline_net_worth (Money, required): Median net worth at retirement, or at the end of the plan with no retirement age to stand on.
  - milestones (array of MilestoneOutcome, required)
  - monthly_housing (Money, required)
  - monthly_income (Money, required): What the projection ran on, after the plan's overrides.
  - monthly_spending (Money, required)
  - paths (integer, required): How many market paths were simulated.
  - points (array of ProjectionPoint, required): One per month, from this month to the plan's end.
  - retirement (RetirementOutcome | null, required)
  - runs_out_age (integer | null, required)
  - runs_out_month (integer | null, required): Index into `points` where the median path runs out of money.
  - underwater (array of string, required): Debts whose payment never covers their interest, carried at their balance.

#### ProjectionPoint
  - age (integer | null, required)
  - assets (Money, required): Owned outside cash and investments (the same on every path).
  - date (date, required)
  - debts (Money, required): Owed (the same on every path).
  - high_50 (Money, required): The 75th percentile.
  - high_80 (Money, required): The 90th percentile.
  - high_95 (Money, required): The 97.5th percentile.
  - inflation_factor (string, required): Divide an amount by this for today's money.
  - liquid (Money, required): Median of cash plus investments.
  - low_50 (Money, required): The 25th percentile.
  - low_80 (Money, required): The 10th percentile.
  - low_95 (Money, required): The 2.5th percentile.
  - median (Money, required)

#### RetirementOutcome
  - age (integer, required)
  - chance (string, required): Share of paths, `0.0000`–`1.0000`, in which the money lasts.
  - date (date, required)
  - month (integer, required): Index into `points`.
  - net_worth (Money, required)
  - rating (string, required): `on_track`, `stretch` or `out_of_reach`.
  - runs_out_at_age (integer | null, required)

#### StartingPoint
  - cash (Money, required)
  - debts (array of PlanDebt, required)
  - income_months (integer, required): Months the averages cover; 0 means nothing to average.
  - invested (Money, required)
  - monthly_housing (Money, required): Rent, or the running costs of a home.
  - monthly_income (Money, required): After tax, averaged over the trailing months.
  - monthly_spending (Money, required): Everything but housing and debt service.
  - net_worth (Money, required)
  - other_assets (Money, required)
  - real_estate (Money, required)
  - vehicles (Money, required)

## Portfolio

### GET /v1/portfolio — Get the portfolio (`portfolio.get`, read)

The investment portfolio's value, cost and gain, today's move, and
how it splits: by asset class, security type, account and tax
treatment, with its concentration. Accounts left out of the ledger's
totals are left out here too.

Parameters:
  - X-Organization-Id (string | null, header)

Returns 200 Portfolio.

### GET /v1/portfolio/accounts/{account_id} — Get an account's holdings (`portfolio.account_holdings`, read)

What one account holds, position by position, valued in the account's
own currency and weighted within it. An account that holds nothing (a
checking account, most of the time) answers with no positions, not a
404. Ids come from `accounts.list`.

Parameters:
  - account_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 200 AccountHoldings.

### GET /v1/portfolio/activity — List investment activity (`investment_activity.list`, read)

The buys, sells, dividends, contributions, fees and transfers inside
the investment accounts, newest first: the history behind the holdings.
Cancelled trades are left out.

Parameters:
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - account_id (integer | null, query): Only this investment account. Ids from `accounts.list`.
  - since (date | null, query): On or after this date.
  - until (date | null, query): On or before this date.
  - X-Organization-Id (string | null, header)

Returns 200 InvestmentActivityList.

### GET /v1/portfolio/positions — List positions (`positions.list`, read)

Every security the portfolio holds, summed across accounts and valued
in the ledger's base currency, with its weight in the whole.

Parameters:
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - X-Organization-Id (string | null, header)

Returns 200 PositionList.

### GET /v1/portfolio/positions/{symbol} — Get a position (`positions.get`, read)

One position by ticker: how much the ledger holds, in which accounts,
its cost and gain, the latest quote and a year of daily closes. Only a
ticker the ledger holds is found; this is not a market-data lookup.

Parameters:
  - symbol (string, required, path)
  - X-Organization-Id (string | null, header)

Returns 200 PositionDetail.

#### AccountHoldings
  - account_id (integer, required)
  - as_of (date | null, required): The latest date the institution priced a holding.
  - currency (string, required): The account's currency; every amount is in it.
  - day_return (Money | null, required)
  - positions (array of Position, required): Largest first.
  - total_cost_basis (Money | null, required): Null when no position carries a cost basis.
  - total_value (Money, required)
  - unrealized_gain (Money | null, required)

#### Diversification
  - effective_holdings (string, required): 1 / `hhi`: how many equal positions it behaves like. Pattern: `^-?\d+(\.\d+)?$`.
  - hhi (string, required): Herfindahl–Hirschman index of the position weights, 0–1: higher is more concentrated. Pattern: `^-?\d+(\.\d+)?$`.
  - positions (integer, required)
  - top5_share (string, required): The five largest positions' weight, 0–1. Pattern: `^-?\d+(\.\d+)?$`.
  - top_share (string, required): The largest position's weight, 0–1. Pattern: `^-?\d+(\.\d+)?$`.

#### InvestmentActivity
  - account_id (integer, required)
  - cash (Money, required): Cash into the account (positive) or out of it (negative).
  - date (date, required)
  - fees (Money | null, required)
  - id (integer, required)
  - name (string, required)
  - price (UnitPrice | null, required)
  - quantity (string, required): Shares or units; 0 for a cash event. Pattern: `^-?\d+(\.\d+)?$`.
  - subtype (string | null, required): As the institution refines it (`dividend`, `contribution`, …).
  - symbol (string | null, required)
  - type (string, required): `buy`, `sell`, `cash`, `fee` or `transfer`.

#### InvestmentActivityList
  - data (array of InvestmentActivity, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

#### Money
  - amount (string, required): Pattern: `^-?\d+\.\d{2}$`.
  - currency (string, required): ISO 4217 code. Pattern: `^[A-Z]{3}$`.

#### Portfolio
  - accounts (array of PortfolioAccount, required): The accounts holding it, largest first.
  - allocation (array of Slice, required): By security type (`equity`, `etf`, `mutual fund`, …); largest first.
  - alternatives_value (Money, required): Documented alternative investments (private equity, collectibles, …).
  - as_of (date | null, required): The latest date an institution priced any holding.
  - asset_classes (array of Slice, required): `stocks`, `fixed_income`, `cash`, `crypto`, `alternatives`; largest first.
  - combined_value (Money, required): `total_value` + `alternatives_value`.
  - currency (string, required): The ledger's base currency; every amount is in it.
  - day_return (Money | null, required): Today's move, summed over the positions the market feed prices. Null until the feed has a previous close for one of them.
  - diversification (Diversification, required)
  - tax_treatments (array of Slice, required): By tax treatment of the account; largest first.
  - total_cost_basis (Money, required)
  - total_value (Money, required): What the securities are worth.
  - unrealized_gain (Money, required): Value less cost, over the positions that carry a cost basis.
  - unrealized_gain_percent (string, required): Pattern: `^-?\d+(\.\d+)?$`.

#### PortfolioAccount
  - account_id (integer, required)
  - name (string, required)
  - role (string | null, required): `brokerage`, `ira`, `roth_ira`, `401k`, `hsa`, …
  - share (string, required): Share of the holdings' value, 0–1. Pattern: `^-?\d+(\.\d+)?$`.
  - tax_treatment (string, required): `taxable`, `tax_deferred` or `tax_free`.
  - value (Money, required)

#### Position
  - cost_basis (Money | null, required): Null when an institution did not report one.
  - day_change_percent (string | null, required): Pattern: `^-?\d+(\.\d+)?$`.
  - day_return (Money | null, required): What today's move did to the position.
  - gain (Money | null, required): `value` − `cost_basis`.
  - gain_percent (string | null, required): Pattern: `^-?\d+(\.\d+)?$`.
  - is_estimate (boolean, required): The value is the cost basis standing in for a market value.
  - last_price (UnitPrice | null, required): The feed's latest daily close.
  - last_price_date (date | null, required)
  - name (string, required)
  - previous_close (UnitPrice | null, required)
  - quantity (string, required): Shares or units held. Pattern: `^-?\d+(\.\d+)?$`.
  - security_type (string, required): `equity`, `etf`, `mutual fund`, `fixed income`, `cash`, `cryptocurrency`, …
  - symbol (string, required): The ticker, as `positions.get` takes it.
  - value (Money, required)
  - weight (string, required): Share of the list's total value, 0–1. Pattern: `^-?\d+(\.\d+)?$`.

#### PositionAccount
  - account_id (integer, required)
  - mask (string | null, required)
  - name (string, required)
  - quantity (string, required): Pattern: `^-?\d+(\.\d+)?$`.
  - share (string, required): This account's share of the position, 0–1. Pattern: `^-?\d+(\.\d+)?$`.
  - value (Money, required)

#### PositionDetail
  - accounts (array of PositionAccount, required): Where it is held, largest first.
  - average_cost (UnitPrice | null, required): Cost basis per share.
  - cost_basis (Money | null, required)
  - currency (string, required): The ledger's base currency; the position's amounts are in it.
  - day_return (Money | null, required)
  - gain (Money | null, required)
  - gain_percent (string | null, required): Pattern: `^-?\d+(\.\d+)?$`.
  - history (array of PricePoint, required): A year of daily closes, oldest first.
  - is_estimate (boolean, required)
  - name (string, required)
  - quantity (string, required): Pattern: `^-?\d+(\.\d+)?$`.
  - quote (Quote, required)
  - security_type (string, required)
  - stats (Stats, required)
  - symbol (string, required)
  - value (Money, required)
  - weight (string, required): Share of the whole portfolio, 0–1. Pattern: `^-?\d+(\.\d+)?$`.

#### PositionList
  - data (array of Position, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

#### PricePoint
  - date (date, required)
  - value (UnitPrice, required): The day's close.

#### Quote
  - day_change (UnitPrice | null, required): Per share.
  - day_change_percent (string | null, required): Pattern: `^-?\d+(\.\d+)?$`.
  - last_price (UnitPrice | null, required)
  - last_price_date (date | null, required)
  - previous_close (UnitPrice | null, required)

#### Slice
  - key (string, required)
  - share (string, required): Share of the whole, 0–1. Pattern: `^-?\d+(\.\d+)?$`.
  - value (Money, required)

#### Stats
  - average_volume (integer | null, required)
  - fifty_two_week_high (UnitPrice | null, required)
  - fifty_two_week_low (UnitPrice | null, required)
  - high (UnitPrice | null, required)
  - low (UnitPrice | null, required)
  - open (UnitPrice | null, required)
  - volume (integer | null, required)

#### UnitPrice
  - amount (string, required): Pattern: `^-?\d+(\.\d+)?$`.
  - currency (string, required): ISO 4217 code. Pattern: `^[A-Z]{3}$`.

## Project Budgets

### GET /v1/project-budgets — List project budgets (`project_budgets.list`, read)

The ledger's project budgets, each with what has been spent on it so far.

Parameters:
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - X-Organization-Id (string | null, header)

Returns 200 ProjectBudgetList.

### POST /v1/project-budgets — Create a project budget (`project_budgets.create`, write)

Budget a project: an amount for everything tagged with its tag
between two dates. Tag its transactions (by hand or with a rule) and
they count towards it.

Parameters:
  - Idempotency-Key (string | null, header): Any unique string (a UUID is ideal). A retry with the same key returns the first attempt's response instead of doing the work twice. Keys are kept for 24 hours.
  - X-Organization-Id (string | null, header)

Body (ProjectBudgetCreate):
  - amount (string, required): Decimal string, at most two decimal places. Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - end_date (date | null): Omit for an open-ended project.
  - name (string, required)
  - start_date (date, required)
  - tag (string | null): The name of the tag that marks the project's transactions. Created if the ledger has no tag by that name (case-insensitively). Defaults to `name`. A tag budgets one project at most.

Returns 201 ProjectBudget.

### DELETE /v1/project-budgets/{project_budget_id} — Delete a project budget (`project_budgets.delete`, write, destructive)

Remove a project budget. Its tag, and the transactions carrying it, stay.

Parameters:
  - project_budget_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 204.

### GET /v1/project-budgets/{project_budget_id} — Get a project budget (`project_budgets.get`, read)

One project budget by its id.

Parameters:
  - project_budget_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 200 ProjectBudget.

### PATCH /v1/project-budgets/{project_budget_id} — Update a project budget (`project_budgets.update`, write)

Rename a project budget, change its amount or dates, or move it to
another tag. Only the fields sent change.

Parameters:
  - project_budget_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Body (ProjectBudgetUpdate):
  - amount (string | null): Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - end_date (date | null)
  - name (string | null)
  - start_date (date | null)
  - tag (string | null): See `ProjectBudgetCreate.tag`.

Returns 200 ProjectBudget.

#### Money
  - amount (string, required): Pattern: `^-?\d+\.\d{2}$`.
  - currency (string, required): ISO 4217 code. Pattern: `^[A-Z]{3}$`.

#### ProjectBudget
  - amount (Money, required): The budget for the whole project.
  - end_date (date | null, required): Null: open-ended.
  - id (integer, required)
  - is_current (boolean, required): Today is inside the window.
  - name (string, required)
  - remaining (Money, required): `amount` − `spent`. Negative is over budget.
  - spent (Money, required): Tagged expenses inside the window, refunds netted off.
  - start_date (date, required)
  - tag_color (string | null, required)
  - tag_id (integer, required): The tag that marks this project's transactions.
  - tag_name (string, required)
  - transaction_count (integer, required): How many tagged transactions count towards it.

#### ProjectBudgetCreate
  - amount (string, required): Decimal string, at most two decimal places. Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - end_date (date | null): Omit for an open-ended project.
  - name (string, required)
  - start_date (date, required)
  - tag (string | null): The name of the tag that marks the project's transactions. Created if the ledger has no tag by that name (case-insensitively). Defaults to `name`. A tag budgets one project at most.

#### ProjectBudgetList
  - data (array of ProjectBudget, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

#### ProjectBudgetUpdate
  - amount (string | null): Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - end_date (date | null)
  - name (string | null)
  - start_date (date | null)
  - tag (string | null): See `ProjectBudgetCreate.tag`.

## Properties

### GET /v1/properties — List properties (`properties.list`, read)

Every property in the ledger, including suggested and dismissed
ones (see `status`), each with its mortgage and equity. Values are as
last refreshed; the app refreshes Zestimates when its page is opened.

Parameters:
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - X-Organization-Id (string | null, header)

Returns 200 PropertyList.

### POST /v1/properties — Add a property (`properties.create`, write)

Add a property the household owns. Its value is posted to the
ledger, so net worth moves with it. Send an `Idempotency-Key` so a
retry cannot add it twice.

Parameters:
  - Idempotency-Key (string | null, header): Any unique string (a UUID is ideal). A retry with the same key returns the first attempt's response instead of doing the work twice. Keys are kept for 24 hours.
  - X-Organization-Id (string | null, header)

Body (PropertyCreate):
  - address_line (string | null)
  - city (string | null)
  - country (string | null)
  - estimated_value (string | null): What it is worth, as a decimal string. Posted to the ledger. Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - label (string, required): What to call it: `Lake house`.
  - lat (string | null): Pattern: `^-?\d{1,3}(\.\d{1,6})?$`.
  - lng (string | null): Pattern: `^-?\d{1,3}(\.\d{1,6})?$`.
  - postal_code (string | null)
  - region (string | null)
  - valued_at (date | null): The date the value is from. Defaults to today when a value is sent.

Returns 201 Property.

### DELETE /v1/properties/{property_id} — Delete a property (`properties.delete`, write, destructive)

Delete a property that was added by hand and take its value off the
ledger. One found from a mortgage cannot be deleted (the next sync
would find it again): set its `status` to `dismissed` instead.

Parameters:
  - property_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 204.

### GET /v1/properties/{property_id} — Get a property (`properties.get`, read)

One property with its mortgage and equity.

Parameters:
  - property_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 200 Property.

### PATCH /v1/properties/{property_id} — Update a property (`properties.update`, write)

Correct a property's address, revalue it, confirm or dismiss one
found from a mortgage, or hide it or leave it out of the totals. Only
the fields sent change. A new value, a status change or the exclusion
switch re-posts the property's value to the ledger.

Parameters:
  - property_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Body (PropertyUpdate):
  - address_line (string)
  - city (string)
  - country (string)
  - estimated_value (string): A new value. Dated today unless `valued_at` is sent with it. Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - excluded_from_totals (boolean): Leave it, and its mortgage, out of net worth.
  - hidden (boolean)
  - label (string)
  - postal_code (string)
  - region (string)
  - status (string): One of `confirmed`, `dismissed`, `suggested`.
  - valued_at (date): The date the value is from; sent alone, it re-dates the current one.

Returns 200 Property.

### GET /v1/properties/affordability — What home the household can afford (`properties.affordability`, read)

The inputs to a home-affordability calculation, from the ledger:
income, real spending, existing debt payments and cash, plus the
defaults and underwriting limits the app's calculator starts from. The
calculation itself (payment, ratios, verdict) is left to the client, so
it can be redrawn for any price without another call.

Parameters:
  - X-Organization-Id (string | null, header)

Returns 200 Affordability.

#### Affordability
  - cash_available (Money, required): Cash and savings, towards a down payment.
  - current_housing_cost (Money, required): Rent, or the running cost of the current home.
  - defaults (AffordabilityDefaults, required)
  - free_cash_flow (Money, required): Income minus spending minus debt payments.
  - guidelines (AffordabilityGuidelines, required)
  - income_months (integer, required): How many months `monthly_income` averages.
  - monthly_debt_payments (Money, required): Existing loan payments, per month.
  - monthly_income (Money, required): Averaged over the completed months of the lookback; zero with no income yet.
  - monthly_spending (Money, required): Real monthly spending, excluding the housing a purchase would replace and debt interest (carried in `monthly_debt_payments`).

#### AffordabilityDefaults
  - annual_rate_pct (string, required)
  - down_payment_pct (string, required)
  - insurance_rate_pct (string, required): Annual home insurance, percent of the price.
  - property_tax_rate_pct (string, required): Annual property tax, percent of the price.
  - term_year_options (array of integer, required)
  - term_years (integer, required)

#### AffordabilityGuidelines
  - back_end_limit_pct (string, required): All debt payments as a share of gross income.
  - back_end_max_pct (string, required): The most a lender will usually go to.
  - cash_flow_limit_pct (string, required): Housing as a share of what is genuinely spare each month.
  - cash_flow_max_pct (string, required)
  - closing_cost_pct (string, required)
  - front_end_limit_pct (string, required): Housing payment as a share of gross income.
  - pmi_annual_rate_pct (string, required)
  - pmi_ltv_threshold_pct (string, required): Loan-to-value above which mortgage insurance applies.
  - reserve_months_target (integer, required): Months of payments to keep in cash after closing.

#### Money
  - amount (string, required): Pattern: `^-?\d+\.\d{2}$`.
  - currency (string, required): ISO 4217 code. Pattern: `^[A-Z]{3}$`.

#### Property
  - address (string | null, required): The address on one line.
  - address_line (string | null, required)
  - city (string | null, required)
  - country (string | null, required)
  - equity (Money | null, required): Value minus the mortgage. Null unless both are known.
  - estimated_value (Money | null, required): What it is worth. Null when nobody has said.
  - excluded_from_totals (boolean, required): Left out of net worth, with its mortgage. Its value is not posted to the ledger.
  - hidden (boolean, required): Hidden from the app's lists and map; still counted.
  - id (integer, required)
  - label (string, required)
  - lat (string | null, required): Latitude in degrees, as a decimal string.
  - lng (string | null, required): Longitude in degrees, as a decimal string.
  - mortgage (PropertyMortgage | null, required)
  - postal_code (string | null, required)
  - region (string | null, required): State, province or region.
  - source (string, required): `manual` or `inferred` (found from a mortgage).
  - status (string, required): `suggested` (found from a mortgage, not yet reviewed), `confirmed` or `dismissed`.
  - value_age_days (integer | null, required): Days since `valued_at`.
  - value_is_stale (boolean | null, required): The value is over a year old. Null when it was never dated: not fresh, unknown.
  - value_source (string, required): Where the value came from: `user`, `zillow` (a Zestimate), `mortgage_origination` (the loan's original principal, a stand-in) or `none`.
  - valued_at (date | null, required): The date the value is true for.
  - zillow_url (string | null, required): Zillow's page for the property. Show it beside a Zestimate, as Zillow requires.

#### PropertyCreate
  - address_line (string | null)
  - city (string | null)
  - country (string | null)
  - estimated_value (string | null): What it is worth, as a decimal string. Posted to the ledger. Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - label (string, required): What to call it: `Lake house`.
  - lat (string | null): Pattern: `^-?\d{1,3}(\.\d{1,6})?$`.
  - lng (string | null): Pattern: `^-?\d{1,3}(\.\d{1,6})?$`.
  - postal_code (string | null)
  - region (string | null)
  - valued_at (date | null): The date the value is from. Defaults to today when a value is sent.

#### PropertyList
  - data (array of Property, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

#### PropertyMortgage
  - account_id (integer, required): The mortgage's liability account (see `accounts.list`).
  - balance (Money, required): What is owed on it today, positive while owed.
  - name (string, required)

#### PropertyUpdate
  - address_line (string)
  - city (string)
  - country (string)
  - estimated_value (string): A new value. Dated today unless `valued_at` is sent with it. Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - excluded_from_totals (boolean): Leave it, and its mortgage, out of net worth.
  - hidden (boolean)
  - label (string)
  - postal_code (string)
  - region (string)
  - status (string): One of `confirmed`, `dismissed`, `suggested`.
  - valued_at (date): The date the value is from; sent alone, it re-dates the current one.

## Pulse

### GET /v1/pulse — Get the money pulse (`pulse.get`, read)

How much is safe to spend today, with its working: the pay cycle's
income, what goes to savings, what was spent, the bills still due and
the cash actually on hand. Also what came in and went out since
yesterday.

Parameters:
  - X-Organization-Id (string | null, header)

Returns 200 Pulse.

#### Money
  - amount (string, required): Pattern: `^-?\d+\.\d{2}$`.
  - currency (string, required): ISO 4217 code. Pattern: `^[A-Z]{3}$`.

#### Pulse
  - bill_count (integer, required)
  - bills (array of PulseBill, required): The soonest few of those bills, soonest first.
  - card_balance (Money, required): Owed on personal credit cards.
  - cash_left (Money, required): `checking` − `upcoming_bills` − `card_balance`.
  - checking (Money, required): Cash in personal checking accounts counted in totals.
  - cycle_left (Money, required): `income` − `set_aside` − `spent_this_cycle` − `upcoming_bills`.
  - cycle_start (date, required): The last payday, or the period's first day.
  - days_left (integer, required): Days the money has to last, today included; at least 1.
  - has_cash (boolean, required): False when there is no personal checking account to measure; the other figures then mean nothing.
  - has_income (boolean, required): False when there is no income to plan around; `safe_to_spend` is then not a budget and should not be shown as one.
  - horizon (string, required): `payday` or `period_end`: what the cycle runs to.
  - horizon_date (date, required): The next payday (exclusive), or the budget period's last day (inclusive).
  - income (Money, required): Income expected over the whole cycle.
  - income_source (string, required): `paychecks` or `average`: detected paychecks, or the typical month of the last three.
  - limited_by (string, required): `cash_flow` or `cash`: which of the two limits it stopped at.
  - received_since (Money, required)
  - safe_to_spend (Money, required): The smaller of `cycle_left` and `cash_left`. Negative when the cycle is already over budget, or bills and cards claim more than is in checking.
  - safe_today (Money, required): `safe_to_spend` over `days_left`, never below zero.
  - save_share (string, required): The default share of income set aside, from 0 to 1, as a decimal string. Pattern: `^\d+(\.\d+)?$`.
  - set_aside (Money, required): Put aside for savings this cycle.
  - set_aside_source (string, required): `share` or `goals`: a share of income, or what the savings goals need, whichever is more.
  - since (date, required): Yesterday: where `spent_since` and `received_since` start.
  - spent_since (Money, required)
  - spent_this_cycle (Money, required): Personal spending since the cycle began.
  - upcoming_bills (Money, required): Every bill expected before the horizon.

#### PulseBill
  - amount (Money, required): The usual amount, which is what is expected next.
  - date (date, required): When it is expected.
  - late (boolean, required): The date has passed and it has not arrived; still counted.
  - merchant_key (string, required): The recurring charge (see `recurring.list`).
  - name (string, required)

## Receipts

### GET /v1/receipts — List receipts (`receipts.list`, read)

The ledger's receipts, newest first: the app's receipt inbox. Filter
by `status=parsed` for the ones read but not yet on a transaction.

Parameters:
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - status (string | null, query): Only receipts in this state: `pending`, `parsed`, `attached`, `failed`.
  - transaction_id (integer | null, query): Only receipts attached to this transaction.
  - X-Organization-Id (string | null, header)

Returns 200 ReceiptList.

### POST /v1/receipts — Upload a receipt (`receipts.create`, write)

Upload a receipt as `multipart/form-data` and have it read. The
answer is the receipt as read (`status` `attached`, `parsed` or
`failed`), which takes a few seconds. Given a `transaction_id` it is put
on that transaction; otherwise it is attached only when one charge
clearly matches. The same file uploaded twice is one receipt
(`already_here`).

Parameters:
  - Idempotency-Key (string | null, header): Any unique string (a UUID is ideal). A retry with the same key returns the first attempt's response instead of doing the work twice. Keys are kept for 24 hours.
  - X-Organization-Id (string | null, header)

Body (multipart/form-data):
  - file (file, required): The receipt: a JPEG, PNG, WebP, GIF, HEIC or PDF, at most 10 MB.
  - transaction_id (integer | null): Attach it to this transaction instead of letting the matcher find one.

Returns 201 ReceiptUpload.

### DELETE /v1/receipts/{receipt_id} — Delete a receipt (`receipts.delete`, write, destructive)

Delete a receipt and its file. If it was on a transaction, the split
it made comes off too.

Parameters:
  - receipt_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 204.

### GET /v1/receipts/{receipt_id} — Get a receipt (`receipts.get`, read)

One receipt with its line items and the transaction it is on.

Parameters:
  - receipt_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 200 Receipt.

### POST /v1/receipts/{receipt_id}/attach — Attach a receipt to a transaction (`receipts.attach`, write)

Put a receipt on the transaction it paid for, replacing any
transaction it was on. Its line items become the transaction's split,
scaled to what the card was actually charged.

Parameters:
  - receipt_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Body (AttachRequest):
  - transaction_id (integer, required): The transaction the receipt paid for (see `transactions.list`). It must be an imported one: a bank or card charge.

Returns 200 Receipt.

### POST /v1/receipts/{receipt_id}/detach — Detach a receipt from its transaction (`receipts.detach`, write)

Take a receipt off its transaction, and its split with it. The
receipt stays, `parsed`, ready to attach elsewhere.

Parameters:
  - receipt_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 200 Receipt.

### GET /v1/receipts/{receipt_id}/download — Download a receipt's file (`receipts.download`, read)

A short-lived URL for the receipt's image or PDF. A forwarded email
that carried no receipt has no file, and says so with a 422.

Parameters:
  - receipt_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 200 Download.

### POST /v1/receipts/{receipt_id}/not-duplicate — Mark a receipt as not a duplicate (`receipts.mark_not_duplicate`, write)

Two purchases, not one receipt sent twice: clear `duplicate_of_id`
and let the receipt find its own transaction.

Parameters:
  - receipt_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 200 Receipt.

### POST /v1/receipts/{receipt_id}/reprocess — Read a receipt again (`receipts.reprocess`, write)

Read the file again from scratch, for a receipt that failed or was
misread. It comes off its transaction first, and is placed again from
the new reading.

Parameters:
  - receipt_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 200 Receipt.

#### AttachRequest
  - transaction_id (integer, required): The transaction the receipt paid for (see `transactions.list`). It must be an imported one: a bank or card charge.

#### Download
  - expires_at (datetime | null, required): When a signed `url` stops working. Null for a `data:` URL.
  - url (string, required): A signed HTTPS URL for the file, or a `data:` URL carrying the file itself when it is stored inline. Needs no credential.

#### Money
  - amount (string, required): Pattern: `^-?\d+\.\d{2}$`.
  - currency (string, required): ISO 4217 code. Pattern: `^[A-Z]{3}$`.

#### Receipt
  - amount_before_tip (Money | null, required): What the card terminal authorized before a tip was written in.
  - attached_at (datetime | null, required)
  - card_last4 (string | null, required)
  - category_id (integer | null, required): The expense account the reader suggested.
  - created_at (datetime, required)
  - currency (string, required): ISO 4217. The ledger's base currency until the receipt is read.
  - duplicate_of_id (integer | null, required): An older receipt this one appears to repeat. Clear it with `receipts.mark_not_duplicate`.
  - email (ReceiptEmail | null, required): Set when the row stands for a forwarded email, not a file.
  - has_file (boolean, required): False for a forwarded email that carried no receipt: there is nothing to download or re-read.
  - id (integer, required)
  - items (array of ReceiptItem, required)
  - match_confidence (string | null, required): How sure the matcher was of the attachment, `0.00`–`1.00`, as a decimal string.
  - merchant_name (string | null, required)
  - mime_type (string, required)
  - notes (string | null, required)
  - original_name (string | null, required)
  - parse_error (string | null, required)
  - parsed_at (datetime | null, required)
  - payment_method (string | null, required)
  - receipt_date (date | null, required)
  - sender (string | null, required): Who sent it in (an email address or phone number).
  - size_bytes (integer, required)
  - source (string, required): How it arrived: `upload`, `email`, `whatsapp` or `gmail`.
  - stated_total (Money | null, required): The total as printed, when `total` was worked out another way.
  - status (string, required): `pending` (not read yet), `parsed` (read, not on a transaction), `attached` (on a transaction) or `failed` (could not be read; see `parse_error`).
  - subtotal (Money | null, required)
  - tax (Money | null, required)
  - tip (Money | null, required)
  - total (Money | null, required)
  - total_source (string | null, required): Where `total` came from.
  - transaction_id (integer | null, required): The transaction it is attached to (see `transactions.get`). Null until it is attached, or while that transaction is not yet posted.

#### ReceiptEmail
  - reason (string | null, required): Machine value, e.g. `no_attachment`.
  - snippet (string | null, required)
  - subject (string | null, required)

#### ReceiptItem
  - amount (Money, required)
  - category_id (integer | null, required): The expense account this line is filed under once the receipt is attached (see `accounts.list`).
  - description (string, required): As printed.
  - id (integer, required)
  - item_category (string | null, required): A short label the reader gave the item, e.g. `Produce`.
  - line_no (integer, required)
  - quantity (string | null, required): Decimal string, when printed.
  - unit_price (Money | null, required)

#### ReceiptList
  - data (array of Receipt, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

#### ReceiptUpload
  - already_here (boolean, required): True when these exact bytes were already a receipt in this ledger: that receipt is returned and nothing new was made.
  - amount_before_tip (Money | null, required): What the card terminal authorized before a tip was written in.
  - attached_at (datetime | null, required)
  - card_last4 (string | null, required)
  - category_id (integer | null, required): The expense account the reader suggested.
  - created_at (datetime, required)
  - currency (string, required): ISO 4217. The ledger's base currency until the receipt is read.
  - duplicate_of_id (integer | null, required): An older receipt this one appears to repeat. Clear it with `receipts.mark_not_duplicate`.
  - email (ReceiptEmail | null, required): Set when the row stands for a forwarded email, not a file.
  - has_file (boolean, required): False for a forwarded email that carried no receipt: there is nothing to download or re-read.
  - id (integer, required)
  - items (array of ReceiptItem, required)
  - match_confidence (string | null, required): How sure the matcher was of the attachment, `0.00`–`1.00`, as a decimal string.
  - merchant_name (string | null, required)
  - mime_type (string, required)
  - notes (string | null, required)
  - original_name (string | null, required)
  - parse_error (string | null, required)
  - parsed_at (datetime | null, required)
  - payment_method (string | null, required)
  - receipt_date (date | null, required)
  - sender (string | null, required): Who sent it in (an email address or phone number).
  - size_bytes (integer, required)
  - source (string, required): How it arrived: `upload`, `email`, `whatsapp` or `gmail`.
  - stated_total (Money | null, required): The total as printed, when `total` was worked out another way.
  - status (string, required): `pending` (not read yet), `parsed` (read, not on a transaction), `attached` (on a transaction) or `failed` (could not be read; see `parse_error`).
  - subtotal (Money | null, required)
  - tax (Money | null, required)
  - tip (Money | null, required)
  - total (Money | null, required)
  - total_source (string | null, required): Where `total` came from.
  - transaction_id (integer | null, required): The transaction it is attached to (see `transactions.get`). Null until it is attached, or while that transaction is not yet posted.

## Reconciliation

### GET /v1/reconciliation/exceptions — List reconciliation exceptions (`reconciliation_exceptions.list`, read)

What is waiting for review, newest first: events with no category,
suspected duplicates, and balances that do not match a statement.

Parameters:
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - is_resolved (boolean, query): Resolved ones instead of open ones.
  - exception_type (string | null, query): Only this `exception_type`.
  - X-Organization-Id (string | null, header)

Returns 200 ReconciliationExceptionList.

### POST /v1/reconciliation/exceptions/{exception_id}/resolve — Resolve a reconciliation exception (`reconciliation_exceptions.resolve`, write)

Mark an exception settled, with a note saying how. Resolving one
changes nothing else in the ledger: fix the cause first (categorize the
event, delete the duplicate), then resolve it.

Parameters:
  - exception_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Body (ExceptionResolve):
  - action (string): What was done, a short word kept with the note: `resolved`, `ignored`, `merged`, … Pattern: `^[a-z_]{1,40}$`. Default: `"resolved"`.
  - note (string, required): Why it is settled.

Returns 200 ReconciliationExceptionOut.

### GET /v1/reconciliation/review-queue — Count what is waiting for review (`review_queue.get`, read)

The size of the review queue: open exceptions by type, and events
still unposted.

Parameters:
  - X-Organization-Id (string | null, header)

Returns 200 ReviewQueue.

### POST /v1/reconciliation/run — Run the matchers (`reconciliation.run`, write)

Pair transfers, card payments and pending/posted twins, and flag
suspected duplicates, across the ledger. Imports and syncs already do
this; run it after correcting things by hand. Answers 202 with a Job
(`kind` `reconcile`); its `result`: `transfers_matched`,
`card_payments_matched`, `pending_posted_matched`,
`duplicates_flagged`. Only rows not matched yet are touched, so running
it again is harmless.

Parameters:
  - Idempotency-Key (string | null, header): Any unique string (a UUID is ideal). A retry with the same key returns the first attempt's response instead of doing the work twice. Keys are kept for 24 hours.
  - X-Organization-Id (string | null, header)

Returns 202 Job.

### POST /v1/reconciliation/statements — Reconcile an account against a statement (`statement_reconciliations.create`, write)

Check the ledger's balance for an account against the balance a bank
statement shows on its closing date. A statement that agrees is
recorded as the bank's balance on that date, once per account and date
(a second time is a 409 `conflict`); a difference opens a
`balance_mismatch` exception.

Parameters:
  - Idempotency-Key (string | null, header): Any unique string (a UUID is ideal). A retry with the same key returns the first attempt's response instead of doing the work twice. Keys are kept for 24 hours.
  - X-Organization-Id (string | null, header)

Body (StatementReconcile):
  - account_id (integer, required): The bank, card or loan account the statement is for.
  - statement_balance (string, required): The closing balance the statement shows, as a decimal string, in the account's currency: `"1520.34"`. Pattern: `^-?\d{1,12}(\.\d{1,2})?$`.
  - statement_date (date, required): The statement's closing date.

Returns 201 StatementReconciliation.

#### ExceptionResolve
  - action (string): What was done, a short word kept with the note: `resolved`, `ignored`, `merged`, … Pattern: `^[a-z_]{1,40}$`. Default: `"resolved"`.
  - note (string, required): Why it is settled.

#### Job
  - created_at (datetime, required)
  - error (JobError | null, required): Why it failed, once `failed`.
  - finished_at (datetime | null, required)
  - id (integer, required)
  - kind (string, required): What the job does: `import`, `categorize`, `generate_journals`, `reconcile`, `run_schedules`, `plaid_sync` or `rocket_money_import`.
  - result (object | null, required): What the work produced, once `succeeded`. Its fields depend on `kind` and are listed on the operation that started it. Counts are integers.
  - status (string, required): `queued`, `running`, `succeeded` or `failed`.

#### JobError
  - code (string, required): Stable error code, as in the error envelope.
  - message (string, required): For people, in the request's language. Never match on it.

#### Money
  - amount (string, required): Pattern: `^-?\d+\.\d{2}$`.
  - currency (string, required): ISO 4217 code. Pattern: `^[A-Z]{3}$`.

#### ReconciliationExceptionList
  - data (array of ReconciliationExceptionOut, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

#### ReconciliationExceptionOut
  - created_at (datetime, required)
  - event_id (integer | null, required): The financial event it is about (`events.get`), if one.
  - exception_type (string, required): `category_missing`, `suspected_duplicate` or `balance_mismatch`.
  - group_id (integer | null, required): The reconciliation group it is about, if one.
  - id (integer, required)
  - is_resolved (boolean, required)
  - resolution_note (string | null, required): `[action] note`, as it was resolved.
  - severity (string, required): `info`, `warning` or `error`.

#### ReviewQueue
  - balance_mismatches (integer, required): Open `balance_mismatch` exceptions.
  - duplicates (integer, required): Open `suspected_duplicate` exceptions.
  - total_exceptions (integer, required)
  - uncategorized (integer, required): Open `category_missing` exceptions.
  - unposted (integer, required): Events not yet posted to the ledger.

#### StatementReconcile
  - account_id (integer, required): The bank, card or loan account the statement is for.
  - statement_balance (string, required): The closing balance the statement shows, as a decimal string, in the account's currency: `"1520.34"`. Pattern: `^-?\d{1,12}(\.\d{1,2})?$`.
  - statement_date (date, required): The statement's closing date.

#### StatementReconciliation
  - account_id (integer, required)
  - computed_balance (Money, required): The ledger's balance for the account on that date.
  - difference (Money, required): `statement_balance` minus `computed_balance`.
  - group_id (integer, required): The reconciliation group this check is recorded as.
  - is_reconciled (boolean, required): They agree to the cent. When not, a `balance_mismatch` exception is opened.
  - statement_balance (Money, required)
  - statement_date (date, required)

## Recurring

### GET /v1/recurring — List recurring charges (`recurring.list`, read)

Every subscription, bill and recurring income the detector finds in
the last two years or so of transactions, including the ones the
household dismissed or cancelled (see `status` and `activity`). The
monthly total the app shows is the sum of `monthly_cost` over the
expenses that are neither `dismissed` nor `ended`.

Parameters:
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - direction (string | null, query): Only bills and subscriptions (`expense`) or only income (`income`).
  - X-Organization-Id (string | null, header)

Returns 200 RecurringChargeList.

### GET /v1/recurring/calendar — Get the bill calendar (`recurring.calendar`, read)

Every transaction between `start` and `end`, marked when it belongs
to a recurring charge, plus every recurring charge and income projected
onto the days it is expected. Dismissed and cancelled charges are not
projected.

Parameters:
  - start (date, required, query): The first day.
  - end (date, required, query): The last day. At most 93 days after `start`.
  - X-Organization-Id (string | null, header)

Returns 200 RecurringCalendarOut.

### POST /v1/recurring/declare — Declare a recurring charge (`recurring.declare`, write)

Say that a merchant bills (or pays) on a cadence: for a yearly renewal
with one charge behind it, which the detector cannot see yet, or to
correct a cadence it measured wrong. Declaring also confirms the charge.
The merchant must have at least one transaction in the ledger.

Parameters:
  - X-Organization-Id (string | null, header)

Body (RecurringDeclaration):
  - cadence (string, required): One of `weekly`, `biweekly`, `four_weekly`, `monthly`, `quarterly`, `yearly`. Pattern: `^(weekly|biweekly|four_weekly|monthly|quarterly|yearly)$`.
  - merchant (string, required): Who is paid (or pays), as a transaction's description names them, or a `merchant_key`. A unique part of the name is enough.

Returns 200 RecurringCharge.

### GET /v1/recurring/forecast — Forecast recurring bills (`recurring.forecast`, read)

What the recurring charges and income will come to month by month,
from today to the end of the horizon: the yearly bills a monthly view
never shows, before they land.

Parameters:
  - horizon (string, query): `quarter` (3 months) or `year` (12).
  - X-Organization-Id (string | null, header)

Returns 200 BillForecast.

### POST /v1/recurring/status — Set a recurring charge's status (`recurring.set_status`, write)

Give the household's verdict on a recurring charge: confirm it,
dismiss it as not recurring, or mark it cancelled (from `ended_on`).
`detected` forgets the verdict but keeps a declared cadence. Returns the
charge as it now reads.

Parameters:
  - X-Organization-Id (string | null, header)

Body (RecurringStatusUpdate):
  - ended_on (date | null): With `cancelled`: the day it ended. Defaults to today.
  - merchant_key (string, required): The charge, from `recurring.list`.
  - status (string, required): `confirmed`, `dismissed` (not a recurring charge), `cancelled`, or `detected` to forget the verdict. Pattern: `^(confirmed|dismissed|cancelled|detected)$`.

Returns 200 RecurringCharge.

#### BillForecast
  - biggest (array of ForecastOccurrence, required): The largest expected bills, each once, on the day it lands next.
  - charges (array of ForecastCharge, required): Bills first, costliest first, then income.
  - end (date, required)
  - expected_in (Money, required)
  - expected_out (Money, required)
  - horizon (string, required): `quarter` or `year`.
  - monthly_out (Money, required): `expected_out` per month over the horizon.
  - months (array of ForecastMonth, required)
  - net (Money, required)
  - start (date, required)
  - today (date, required)

#### CalendarCharge
  - cadence (string, required)
  - expected_amount (Money, required)
  - last_date (date, required)
  - merchant_key (string, required)
  - occurrences (integer, required)
  - status (string, required): The household's verdict, as in `recurring.list`.

#### CalendarItem
  - account_id (integer | null, required)
  - account_name (string | null, required)
  - amount (Money, required): Always positive; `flow` says which way it moved.
  - date (date, required)
  - flow (string, required): `in` or `out`.
  - name (string, required)
  - overdue (boolean, required): Expected on a day already gone, and not yet arrived.
  - projected (boolean, required): Expected on this day rather than seen.
  - recurring (CalendarCharge | null, required): The recurring charge it belongs to. Null for a one-off transaction.
  - transaction_id (integer | null, required): The transaction behind a seen item (see `transactions.get`). Null when projected.

#### CalendarTotals
  - actual_in (Money, required)
  - actual_out (Money, required)
  - projected_in (Money, required)
  - projected_out (Money, required)

#### CancelGuide
  - id (string, required)
  - method (string, required): How it is cancelled: `web`, `phone`, `store_apple`, `store_google`, `store_amazon`, `in_person`.
  - name (string, required)
  - phone (string | null, required)
  - steps (array of string, required): Step keys, in order. They name steps; they are not sentences.
  - url (string | null, required)

#### ForecastCharge
  - amount (Money, required): One occurrence.
  - cadence (string, required)
  - flow (string, required): `in` or `out`.
  - merchant_key (string, required)
  - name (string, required)
  - next_date (date, required)
  - occurrences (integer, required): How many times it is expected over the horizon.
  - status (string, required): `detected` or `confirmed`; dismissed and cancelled charges are left out.
  - total (Money, required): `amount` times `occurrences`.

#### ForecastMonth
  - expected_in (Money, required)
  - expected_out (Money, required)
  - month (string, required): `YYYY-MM`.
  - net (Money, required)
  - occurrences (array of ForecastOccurrence, required)

#### ForecastOccurrence
  - amount (Money, required)
  - cadence (string, required)
  - date (date, required)
  - flow (string, required): `in` or `out`.
  - merchant_key (string, required)
  - name (string, required)

#### Money
  - amount (string, required): Pattern: `^-?\d+\.\d{2}$`.
  - currency (string, required): ISO 4217 code. Pattern: `^[A-Z]{3}$`.

#### RecurringCalendarOut
  - end (date, required)
  - items (array of CalendarItem, required): By day; within a day, seen before expected and income first.
  - start (date, required)
  - today (date, required): The server's today, which `overdue` and `projected` are read against.
  - totals (CalendarTotals, required)

#### RecurringCharge
  - activity (string, required): What the charges say: `active`, `missed` (late) or `ended`. A cancelled charge is always `ended`.
  - amount (Money, required): The usual amount of one charge. Always positive.
  - cadence (string, required): One of `weekly`, `biweekly`, `four_weekly`, `monthly`, `quarterly`, `yearly`.
  - cancel_guide (CancelGuide | null, required): How to cancel it, when LumaLedger knows the merchant. Expenses only.
  - confidence (string, required): `high` (seen enough times to be sure) or `likely` (a regular pattern seen fewer times).
  - declared_cadence (boolean, required): The cadence was declared by the household, not measured.
  - direction (string, required): `expense` (a bill, a subscription) or `income` (a paycheck).
  - ended_on (date | null, required): When it stopped: the cancellation date given, else the last charge. Null while it is running.
  - first_date (date, required)
  - last_amount (Money, required)
  - last_date (date, required)
  - merchant_key (string, required): The charge's stable key: the merchant or payer it groups by. Pass it to `recurring.set_status`.
  - monthly_cost (Money, required): What it comes to per month at its cadence.
  - name (string, required): The name the charge appears under.
  - next_expected (date, required)
  - occurrences (integer, required): How many charges were seen.
  - previous_amount (Money, required): The usual amount before the latest charge.
  - price_changed (boolean, required): The latest charge differs from the usual amount.
  - status (string, required): The household's verdict: `detected` (none given), `confirmed`, `dismissed` (not a recurring charge) or `cancelled`.

#### RecurringChargeList
  - data (array of RecurringCharge, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

#### RecurringDeclaration
  - cadence (string, required): One of `weekly`, `biweekly`, `four_weekly`, `monthly`, `quarterly`, `yearly`. Pattern: `^(weekly|biweekly|four_weekly|monthly|quarterly|yearly)$`.
  - merchant (string, required): Who is paid (or pays), as a transaction's description names them, or a `merchant_key`. A unique part of the name is enough.

#### RecurringStatusUpdate
  - ended_on (date | null): With `cancelled`: the day it ended. Defaults to today.
  - merchant_key (string, required): The charge, from `recurring.list`.
  - status (string, required): `confirmed`, `dismissed` (not a recurring charge), `cancelled`, or `detected` to forget the verdict. Pattern: `^(confirmed|dismissed|cancelled|detected)$`.

## Reports

### GET /v1/reports/annual — Annual review (`reports.annual`, read)

The year on one page: income, spending and net for each month; the
categories it went to beside the year before; net worth from January to
now; the biggest expenses; the goals; and the year's highlights.

Parameters:
  - year (integer | null, query): Defaults to this year.
  - purpose (string | null, query): Only the `personal` or the `business` side of the ledger. Both when omitted.
  - X-Organization-Id (string | null, header)

Returns 200 AnnualReview.

### GET /v1/reports/monthly — Monthly report (`reports.monthly`, read)

The month on one page: what came in and went out, category by
category beside last month and the budget; how the budget held; the
recurring charges that landed; the biggest expenses; what net worth did;
and the savings goals. While the month is running the figures are
month-to-date.

Parameters:
  - month (string | null, query): `YYYY-MM`. Defaults to this month.
  - purpose (string | null, query): Only the `personal` or the `business` side of the ledger. Both when omitted.
  - X-Organization-Id (string | null, header)

Returns 200 MonthlyReport.

### GET /v1/reports/recap — Monthly recap (`reports.recap`, read)

A month's highlights, read off its monthly report: spending against
the month before, the savings rate, budgets kept, net worth growth, the
biggest drop in a category, goals reached. Wins lead; nothing is
invented.

Parameters:
  - month (string | null, query): `YYYY-MM`. Defaults to the last complete month.
  - X-Organization-Id (string | null, header)

Returns 200 Recap.

#### AnnualReview
  - categories (array of YearCategory, required): Biggest first.
  - currency (string, required): The ledger's base currency; every amount is in it.
  - expenses (Money, required)
  - goals (array of GoalProgress, required)
  - highlights (YearHighlights, required)
  - income (Money, required)
  - income_sources (array of IncomeSource, required)
  - is_current (boolean, required)
  - largest_expenses (array of LargestExpense, required)
  - months (array of YearMonth, required): Twelve, January to December.
  - months_elapsed (integer, required): How many of the year's months have begun.
  - net (Money, required)
  - net_worth (NetWorthChange, required)
  - previous_expenses (Money, required)
  - previous_income (Money, required)
  - previous_net (Money, required)
  - previous_year (integer, required)
  - savings_rate (string | null, required): `net` / `income`. Pattern: `^-?\d+(\.\d+)?$`.
  - year (integer, required)

#### GoalProgress
  - name (string, required)
  - progress (string, required): 0–1. Pattern: `^-?\d+(\.\d+)?$`.
  - reached (boolean, required)
  - saved (Money, required)
  - savings_goal_id (integer, required)
  - target_amount (Money, required)

#### IncomeSource
  - amount (Money, required)
  - category_id (integer, required): The income account (`accounts.list?type=income`).
  - code (string, required)
  - name (string, required)
  - previous_amount (Money, required): The same window of the period before.

#### LargestExpense
  - amount (Money, required)
  - category (string | null, required): The category it was filed under (the largest line, when split).
  - date (date, required)
  - description (string, required)
  - transaction_id (integer, required): What `transactions.get` takes.

#### Money
  - amount (string, required): Pattern: `^-?\d+\.\d{2}$`.
  - currency (string, required): ISO 4217 code. Pattern: `^[A-Z]{3}$`.

#### MonthlyReport
  - budgets (ReportBudgetSummary, required)
  - categories (array of ReportCategory, required): Expense categories with activity this month or last, or a plan; biggest first.
  - currency (string, required): The ledger's base currency; every amount is in it.
  - expenses (Money, required)
  - goals (array of GoalProgress, required)
  - income (Money, required)
  - income_sources (array of IncomeSource, required)
  - is_current (boolean, required): The month is still running: figures are month-to-date.
  - is_future (boolean, required): The month has not started: everything but the plan is zero.
  - largest_expenses (array of LargestExpense, required)
  - month (string, required): `YYYY-MM`.
  - net (Money, required): `income` − `expenses`.
  - net_worth (NetWorthChange, required)
  - previous_expenses (Money, required)
  - previous_income (Money, required)
  - previous_month (string, required)
  - previous_net (Money, required)
  - recurring (RecurringSummary, required)
  - savings_rate (string | null, required): `net` / `income`; null when nothing came in. Pattern: `^-?\d+(\.\d+)?$`.

#### NetWorthChange
  - change (Money, required)
  - end (Money, required): At the period's end, or today while it is running.
  - start (Money, required): Net worth the night before the period began.

#### Recap
  - currency (string, required)
  - is_current (boolean, required)
  - moments (array of RecapMoment, required)
  - month (string, required): `YYYY-MM`.
  - previous_month (string, required)

#### RecapMoment
  - data (object, required): The moment's figures, by kind. Ratios (`change`, `rate`, `share`) are 0–1 decimal strings; amounts are decimal strings in `currency`.
  - kind (string, required): `spending_change`, `savings_rate`, `budgets_kept`, `net_worth_up`, `biggest_drop`, `top_category` or `goals`.
  - shareable (boolean, required): Safe to share: it carries percentages and counts only, never an amount, a merchant or an account.
  - tone (string, required): `positive` (a win) or `neutral` (a fact worth knowing).

#### RecurringSummary
  - charges (array of ReportRecurringCharge, required)
  - total (Money, required)

#### ReportBudgetSummary
  - available (Money, required)
  - budgeted (Money, required)
  - over_count (integer, required): Plans that ended the month over.
  - planned (integer, required): Categories with a plan this month.
  - remaining (Money, required)
  - spent (Money, required)
  - unbudgeted_spent (Money, required): Spending in categories with no plan.

#### ReportCategory
  - available (Money | null, required): The plan plus what rolled over; null without a plan.
  - budgeted (Money | null, required): What the budget allows; null without a plan.
  - category_id (integer, required): The expense account (`accounts.list?type=expense`).
  - code (string, required)
  - name (string, required)
  - parent_id (integer | null, required): The category this is a subcategory of.
  - previous_spent (Money, required): Last month.
  - remaining (Money | null, required): `available` − `spent`; null without a plan.
  - spent (Money, required)

#### ReportRecurringCharge
  - amount (Money, required): What it charged this month, summed if more than once.
  - cadence (string, required): `weekly`, `monthly`, `annual`, …
  - merchant_key (string, required)
  - name (string, required)
  - occurrences (integer, required)

#### YearCategory
  - category_id (integer, required): The expense account (`accounts.list?type=expense`).
  - code (string, required)
  - monthly_average (Money, required)
  - name (string, required)
  - previous_spent (Money, required): The same months of the year before.
  - share (string, required): Share of the year's spending, 0–1. Pattern: `^-?\d+(\.\d+)?$`.
  - spent (Money, required)

#### YearHighlights
  - best_month (string | null, required): The month that put the most aside.
  - highest_spending_month (string | null, required): `YYYY-MM`.
  - lowest_spending_month (string | null, required)
  - months_in_the_black (integer, required): Months that ended with income above spending.

#### YearMonth
  - expenses (Money, required)
  - income (Money, required)
  - month (string, required): `YYYY-MM`.
  - net (Money, required)

## Rules

### GET /v1/rules — List rules (`rules.list`, read)

The ledger's rules, in the order they run.

Parameters:
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - X-Organization-Id (string | null, header)

Returns 200 RuleList.

### POST /v1/rules — Create a rule (`rules.create`, write)

Save a rule. It applies to transactions that arrive from now on; call
`rules.run` to apply it to the ones already in the ledger.

Parameters:
  - Idempotency-Key (string | null, header): Any unique string (a UUID is ideal). A retry with the same key returns the first attempt's response instead of doing the work twice. Keys are kept for 24 hours.
  - X-Organization-Id (string | null, header)

Body (RuleInput):
  - actions (array of RuleActionInput)
  - conditions (array of RuleConditionInput)
  - is_active (boolean): Default: `true`.
  - match_type (string): `all` or `any`. Pattern: `^(all|any)$`. Default: `"all"`.
  - name (string, required)
  - priority (integer): Lower runs first. -1000–1000. Default: `0`.
  - stop_processing (boolean): Default: `false`.

Returns 201 Rule.

### DELETE /v1/rules/{rule_id} — Delete a rule (`rules.delete`, write, destructive)

Delete a rule. What it already did stays done: a category it set is
the transaction's category now.

Parameters:
  - rule_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 204.

### GET /v1/rules/{rule_id} — Get a rule (`rules.get`, read)

One rule, with its conditions and actions.

Parameters:
  - rule_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 200 Rule.

### PUT /v1/rules/{rule_id} — Replace a rule (`rules.update`, write)

Replace a rule wholesale: send the whole rule, as `rules.create`
takes it. Its conditions and actions are rewritten (and get new ids).
What the rule already did stays done.

Parameters:
  - rule_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Body (RuleInput):
  - actions (array of RuleActionInput)
  - conditions (array of RuleConditionInput)
  - is_active (boolean): Default: `true`.
  - match_type (string): `all` or `any`. Pattern: `^(all|any)$`. Default: `"all"`.
  - name (string, required)
  - priority (integer): Lower runs first. -1000–1000. Default: `0`.
  - stop_processing (boolean): Default: `false`.

Returns 200 Rule.

### POST /v1/rules/{rule_id}/run — Apply a rule to past transactions (`rules.run`, write)

Apply one rule to the transactions already in the ledger. Runs within
the request and is bounded: when `truncated` is true there is older
history left, and calling again continues. Applying a rule twice changes
nothing the second time.

Parameters:
  - rule_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 200 RuleRunResult.

### POST /v1/rules/preview — Preview what a rule would match (`rules.preview`, read)

Which recent transactions a rule's conditions would match, without
saving the rule or changing anything. A read: any member may call it.

Parameters:
  - X-Organization-Id (string | null, header)

Body (RulePreviewInput):
  - conditions (array of RuleConditionInput, required)
  - match_type (string): `all` or `any`. Pattern: `^(all|any)$`. Default: `"all"`.

Returns 200 RulePreview.

### POST /v1/rules/run-all — Apply every rule to transactions no rule has seen (`rules.run_all`, write)

Run every active rule over the transactions the rules have not seen
yet (new ones are normally handled as they sync). Runs synchronously
within the request, one bounded batch per call: call again while
`pending` is above zero.

Parameters:
  - X-Organization-Id (string | null, header)

Returns 200 RuleRunPendingResult.

### GET /v1/rules/schema — Get the rule vocabulary (`rules.get_schema`, read)

Every condition field (with the operators it admits and the kind of
value it takes), every action, and the limits a rule must stay within.
Read it before writing a rule: a key that is not here is refused.

Parameters:
  - X-Organization-Id (string | null, header)

Returns 200 RuleSchema.

#### Money
  - amount (string, required): Pattern: `^-?\d+\.\d{2}$`.
  - currency (string, required): ISO 4217 code. Pattern: `^[A-Z]{3}$`.

#### Rule
  - actions (array of RuleAction, required)
  - applied_count (integer, required): Transactions the rule has changed, all time.
  - conditions (array of RuleCondition, required)
  - created_at (datetime, required)
  - id (integer, required)
  - is_active (boolean, required): An inactive rule is kept but never applied.
  - last_applied_at (datetime | null, required)
  - match_type (string, required): `all` or `any`: how the rule reads its condition groups.
  - name (string, required)
  - priority (integer, required): Lower runs first. Ties run in id order.
  - stop_processing (boolean, required): When it matches, no lower-priority rule runs on that transaction.
  - updated_at (datetime, required)

#### RuleAction
  - action (string, required): An action key from `rules.get_schema` (`set_category`, `add_tags`, …).
  - id (integer, required)
  - params (object, required): Named inputs, for an action that declares `params` in the schema.
  - value (string | null, required): The action's one value, when it takes one (a category id, an address).
  - values (array of string, required): The action's values, when it takes several (tag ids, split lines).

#### RuleActionInput
  - action (string, required): An action key from `rules.get_schema`.
  - params (object): Named inputs the action declares in `rules.get_schema`.
  - value (string | null): For an action that takes one value.
  - values (array of string): For an action that takes several.

#### RuleActionParamSpec
  - key (string, required)
  - required (boolean, required)
  - value_type (string, required)

#### RuleActionSpec
  - app (string | null, required): The connected app the action writes to, if any.
  - conflicts_with (array of string, required): Actions that cannot share a rule with this one.
  - key (string, required)
  - multi (boolean, required): Takes `values` rather than `value`.
  - params (array of RuleActionParamSpec, required)
  - value_type (string, required)

#### RuleCondition
  - field (string, required): A field key from `rules.get_schema` (`description`, `amount`, …).
  - group_index (integer, required): Conditions in the same group must all hold (AND); any group may match (OR).
  - id (integer, required)
  - operator (string, required): One of the operators that field admits.
  - value (string | null, required): The value, for a one-value operator. Canonical text: decimals as `12.50`, dates ISO, ids as digits.
  - value_to (string | null, required): The upper bound, for a range operator.
  - values (array of string, required): The values, for a list operator.

#### RuleConditionInput
  - field (string, required): A field key from `rules.get_schema`.
  - group_index (integer): AND within a group, OR across groups. 0–5. Default: `0`.
  - operator (string, required): One of the operators that field admits.
  - value (string | null): For a one-value operator.
  - value_to (string | null): The upper bound, for a range operator.
  - values (array of string): For a list operator.

#### RuleFieldSpec
  - choices (array of string, required): The only values allowed, when the field is an enumeration.
  - key (string, required)
  - multi (boolean, required): Takes a list operator's `values`.
  - operators (array of string, required)
  - value_type (string, required): What a value is: `text`, `number`, `date`, `category`, `tag`, …

#### RuleInput
  - actions (array of RuleActionInput)
  - conditions (array of RuleConditionInput)
  - is_active (boolean): Default: `true`.
  - match_type (string): `all` or `any`. Pattern: `^(all|any)$`. Default: `"all"`.
  - name (string, required)
  - priority (integer): Lower runs first. -1000–1000. Default: `0`.
  - stop_processing (boolean): Default: `false`.

#### RuleLimits
  - actions (integer, required)
  - conditions (integer, required)
  - groups (integer, required)
  - rules (integer, required)
  - tags (integer, required)

#### RuleList
  - data (array of Rule, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

#### RuleOperatorArity
  - list_operators (array of string, required): Operators that take `values`.
  - nullary_operators (array of string, required): Operators that take no value (`is_empty`).
  - range_operators (array of string, required): Operators that take `value` and `value_to`.

#### RulePreview
  - matched (integer, required): How many of those the conditions match.
  - sample (array of RulePreviewMatch, required): A few of the matches, newest first.
  - scanned (integer, required): Recent transactions looked at (the newest 2,000).

#### RulePreviewInput
  - conditions (array of RuleConditionInput, required)
  - match_type (string): `all` or `any`. Pattern: `^(all|any)$`. Default: `"all"`.

#### RulePreviewMatch
  - account_id (integer, required): The real account it is on.
  - amount (Money, required): Always positive; `event_type` says which way it moved.
  - category_id (integer | null, required): The category it is filed under now.
  - date (date, required)
  - description (string, required)
  - event_type (string, required): `expense`, `income`, `transfer`, `refund`, `fee`, …
  - transaction_id (integer | null, required): The transaction (journal entry) id, for `transactions.get`. Null while the bank transaction has not been posted to the ledger yet.

#### RuleRunPendingResult
  - changed (integer, required)
  - matched (integer, required)
  - pending (integer, required): Transactions still waiting. Call again while this is above zero.
  - scanned (integer, required)

#### RuleRunResult
  - changed (integer, required): Transactions the rule actually changed.
  - matched (integer, required)
  - scanned (integer, required)
  - truncated (boolean, required): The run hit its bound; call again to reach older history.

#### RuleSchema
  - actions (array of RuleActionSpec, required)
  - fields (array of RuleFieldSpec, required)
  - limits (RuleLimits, required)
  - operators (RuleOperatorArity, required)
  - tag_colors (array of string, required): The colors `tags.create` accepts.

## Savings Goals

### GET /v1/savings-goals — List savings goals (`savings_goals.list`, read)

The ledger's savings goals, each measured against its account's
balance today.

Parameters:
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - X-Organization-Id (string | null, header)

Returns 200 SavingsGoalList.

### POST /v1/savings-goals — Create a savings goal (`savings_goals.create`, write)

Set a target on an asset account: "6,000 in savings by June". Two
goals may share an account; each measures the same balance.

Parameters:
  - Idempotency-Key (string | null, header): Any unique string (a UUID is ideal). A retry with the same key returns the first attempt's response instead of doing the work twice. Keys are kept for 24 hours.
  - X-Organization-Id (string | null, header)

Body (SavingsGoalCreate):
  - account_id (integer, required): An active asset account in this ledger (see `accounts.list?type=asset`).
  - baseline_amount (string | null): The balance the goal counts from. Omitted: the account's balance today, so only what is saved from now on counts. `"0"` counts the whole balance. Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - name (string, required)
  - target_amount (string, required): More than zero; decimal string, at most two decimal places. Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - target_date (date | null)

Returns 201 SavingsGoal.

### DELETE /v1/savings-goals/{savings_goal_id} — Delete a savings goal (`savings_goals.delete`, write, destructive)

Remove a savings goal. The account and its money are untouched.

Parameters:
  - savings_goal_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 204.

### GET /v1/savings-goals/{savings_goal_id} — Get a savings goal (`savings_goals.get`, read)

One savings goal by its id.

Parameters:
  - savings_goal_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 200 SavingsGoal.

### PATCH /v1/savings-goals/{savings_goal_id} — Update a savings goal (`savings_goals.update`, write)

Rename a goal, move it to another account, or change its target,
baseline or date. Only the fields sent change.

Parameters:
  - savings_goal_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Body (SavingsGoalUpdate):
  - account_id (integer | null): Move the goal to another asset account.
  - baseline_amount (string | null): Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - name (string | null)
  - target_amount (string | null): Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - target_date (date | null)

Returns 200 SavingsGoal.

#### Money
  - amount (string, required): Pattern: `^-?\d+\.\d{2}$`.
  - currency (string, required): ISO 4217 code. Pattern: `^[A-Z]{3}$`.

#### SavingsGoal
  - account_balance (Money, required): The account's balance right now.
  - account_id (integer, required): The asset account the money sits in (see `accounts.list`).
  - account_name (string, required)
  - baseline_amount (Money, required): The account balance the goal counts from.
  - id (integer, required)
  - monthly_needed (Money | null, required): What has to go in each month to make the date. Null with no date, or once the goal is reached or overdue.
  - name (string, required)
  - overdue (boolean, required): The date has passed and the goal was not reached.
  - progress (string, required): `saved` / `target_amount`, a decimal string from `0.0000` to `1.0000`. Pattern: `^\d\.\d{4}$`.
  - reached (boolean, required)
  - remaining (Money, required)
  - saved (Money, required): `account_balance` − `baseline_amount`, between zero and the target.
  - target_amount (Money, required)
  - target_date (date | null, required)

#### SavingsGoalCreate
  - account_id (integer, required): An active asset account in this ledger (see `accounts.list?type=asset`).
  - baseline_amount (string | null): The balance the goal counts from. Omitted: the account's balance today, so only what is saved from now on counts. `"0"` counts the whole balance. Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - name (string, required)
  - target_amount (string, required): More than zero; decimal string, at most two decimal places. Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - target_date (date | null)

#### SavingsGoalList
  - data (array of SavingsGoal, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

#### SavingsGoalUpdate
  - account_id (integer | null): Move the goal to another asset account.
  - baseline_amount (string | null): Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - name (string | null)
  - target_amount (string | null): Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - target_date (date | null)

## Schedules

### GET /v1/schedules — List schedules (`schedules.list`, read)

The ledger's standing orders, newest first, each with its last run.
Amounts are in the ledger's base currency.

Parameters:
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - X-Organization-Id (string | null, header)

Returns 200 ScheduleList.

### POST /v1/schedules — Create a schedule (`schedules.create`, write)

Set up a standing order. Its first period ends on `next_run_on`,
and every period posts once, by itself, on the day it ends. Send an
`Idempotency-Key` so a retry cannot create it twice.

Parameters:
  - Idempotency-Key (string | null, header): Any unique string (a UUID is ideal). A retry with the same key returns the first attempt's response instead of doing the work twice. Keys are kept for 24 hours.
  - X-Organization-Id (string | null, header)

Body (ScheduleInput):
  - amount (string | null): The amount for `fixed`, or the payment for `amortized`. Decimal string. Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - anchor_day (integer | null): The day of the month a period ends on, 1–28, or -1 for the last day. -1–28.
  - annual_rate (string | null): For interest: the annual rate in percent, as a decimal string (`"6.5"`). Pattern: `^\d{1,3}(\.\d{1,4})?$`.
  - cadence (string, required): `daily`, `weekly`, `monthly`, `quarterly`, `annually`. Pattern: `^(daily|weekly|monthly|quarterly|annually)$`.
  - computation (string, required): `fixed`, `simple_interest`, `amortized`. Pattern: `^(fixed|simple_interest|amortized)$`.
  - day_count (string): `act365`, `act360`, `thirty360`. Pattern: `^(act365|act360|thirty360)$`. Default: `"act365"`.
  - end_date (date | null)
  - escrow_amount (string | null): Decimal string. Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - legs (object, required): Role → account id (see `accounts.list`).
  - name (string, required)
  - principal_account_id (integer | null): For interest: the balance it is computed on.
  - start_date (date | null): Defaults to today.

Returns 201 Schedule.

### DELETE /v1/schedules/{schedule_id} — Delete a schedule (`schedules.delete`, write, destructive)

Delete a standing order. What it already posted stays in the ledger.

Parameters:
  - schedule_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 204.

### GET /v1/schedules/{schedule_id} — Get a schedule (`schedules.get`, read)

One standing order by its id.

Parameters:
  - schedule_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 200 Schedule.

### PATCH /v1/schedules/{schedule_id} — Update a schedule (`schedules.update`, write)

Rename a standing order, or pause or resume it.

Parameters:
  - schedule_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Body (ScheduleChange):
  - name (string | null)
  - status (string | null): `paused` stops it; `active` resumes it from the next period after today (missed periods are not posted). Pattern: `^(active|paused)$`.

Returns 200 Schedule.

### GET /v1/schedules/{schedule_id}/runs — List a schedule's runs (`schedule_runs.list`, read)

Every period a schedule has run for, latest first, with what each
posted.

Parameters:
  - schedule_id (integer, required, path)
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - X-Organization-Id (string | null, header)

Returns 200 ScheduleRunList.

### POST /v1/schedules/run — Post what schedules owe now (`schedules.run`, write)

Post every period that is due now, without waiting for the daily
run. Answers 202 with a Job (`kind` `run_schedules`); its `result`:
`posted`, the number of periods posted. A period posts once, however
often this runs.

Parameters:
  - Idempotency-Key (string | null, header): Any unique string (a UUID is ideal). A retry with the same key returns the first attempt's response instead of doing the work twice. Keys are kept for 24 hours.
  - X-Organization-Id (string | null, header)

Returns 202 Job.

#### Job
  - created_at (datetime, required)
  - error (JobError | null, required): Why it failed, once `failed`.
  - finished_at (datetime | null, required)
  - id (integer, required)
  - kind (string, required): What the job does: `import`, `categorize`, `generate_journals`, `reconcile`, `run_schedules`, `plaid_sync` or `rocket_money_import`.
  - result (object | null, required): What the work produced, once `succeeded`. Its fields depend on `kind` and are listed on the operation that started it. Counts are integers.
  - status (string, required): `queued`, `running`, `succeeded` or `failed`.

#### JobError
  - code (string, required): Stable error code, as in the error envelope.
  - message (string, required): For people, in the request's language. Never match on it.

#### Money
  - amount (string, required): Pattern: `^-?\d+\.\d{2}$`.
  - currency (string, required): ISO 4217 code. Pattern: `^[A-Z]{3}$`.

#### Schedule
  - amount (Money | null, required): The fixed amount, or the loan payment for `amortized`.
  - anchor_day (integer | null, required): The day of the month a period ends on (1–28, or -1 for the last day); null follows the start date.
  - annual_rate (string | null, required): The annual interest rate, in percent, as a decimal string.
  - cadence (string, required): `daily`, `weekly`, `monthly`, `quarterly`, `annually`.
  - computation (string, required): How each period's amount is found: `fixed`, `simple_interest`, `amortized`.
  - day_count (string, required): How interest counts days: `act365`, `act360`, `thirty360`.
  - end_date (date | null, required)
  - escrow_amount (Money | null, required): The part of an `amortized` payment that goes to escrow.
  - id (integer, required)
  - last_run (ScheduleRun | null, required)
  - legs (object, required): The accounts it posts to, by role (`debit`, `credit`, `interest_expense`, `interest_payable`, `payment_source`, `principal`, `escrow`) → account id.
  - name (string, required)
  - next_run_on (date, required): When the next period ends and posts.
  - principal_account_id (integer | null, required): The balance interest is computed on.
  - start_date (date, required)
  - status (string, required): `active`, `paused`.

#### ScheduleChange
  - name (string | null)
  - status (string | null): `paused` stops it; `active` resumes it from the next period after today (missed periods are not posted). Pattern: `^(active|paused)$`.

#### ScheduleInput
  - amount (string | null): The amount for `fixed`, or the payment for `amortized`. Decimal string. Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - anchor_day (integer | null): The day of the month a period ends on, 1–28, or -1 for the last day. -1–28.
  - annual_rate (string | null): For interest: the annual rate in percent, as a decimal string (`"6.5"`). Pattern: `^\d{1,3}(\.\d{1,4})?$`.
  - cadence (string, required): `daily`, `weekly`, `monthly`, `quarterly`, `annually`. Pattern: `^(daily|weekly|monthly|quarterly|annually)$`.
  - computation (string, required): `fixed`, `simple_interest`, `amortized`. Pattern: `^(fixed|simple_interest|amortized)$`.
  - day_count (string): `act365`, `act360`, `thirty360`. Pattern: `^(act365|act360|thirty360)$`. Default: `"act365"`.
  - end_date (date | null)
  - escrow_amount (string | null): Decimal string. Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - legs (object, required): Role → account id (see `accounts.list`).
  - name (string, required)
  - principal_account_id (integer | null): For interest: the balance it is computed on.
  - start_date (date | null): Defaults to today.

#### ScheduleList
  - data (array of Schedule, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

#### ScheduleRun
  - amount (Money, required)
  - error (string | null, required): Why it failed, when it did.
  - period_end (date, required)
  - period_start (date, required)
  - status (string, required): `posted`, `skipped` or `failed`.
  - transaction_id (integer | null, required): The transaction (journal entry) it posted. Null when skipped or failed, or when the entry was deleted since.

#### ScheduleRunList
  - data (array of ScheduleRun, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

## Stakes

### GET /v1/stakes — List stakes in other ledgers (`stakes.list`, read)

Every ledger this one owns a share of, with what the share is worth.

Parameters:
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - X-Organization-Id (string | null, header)

Returns 200 StakeList.

### DELETE /v1/stakes/{stake_id} — Unlink a stake (`stakes.delete`, write, destructive)

Stop counting the share: its value comes off this ledger from today,
and its account is retired with its history. Takes an owner or admin.

Parameters:
  - stake_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 204.

### GET /v1/stakes/{stake_id} — Get a stake (`stakes.get`, read)

One stake and what it is worth.

Parameters:
  - stake_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 200 Stake.

### PATCH /v1/stakes/{stake_id} — Change the share owned (`stakes.update`, write)

Set the percentage owned from today and revalue the stake. Takes an
owner or admin of this ledger.

Parameters:
  - stake_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Body (StakeUpdate):
  - ownership_pct (string, required): The share owned from today, as a decimal string between 0 and 100. Past values keep the share they had. Pattern: `^\d{1,3}(\.\d{1,4})?$`.

Returns 200 Stake.

### POST /v1/stakes/{stake_id}/revalue — Revalue a stake now (`stakes.revalue`, write)

Bring the stake to the owned ledger's value right now instead of at
the next daily revaluation. Posts the change, if any, to this ledger.

Parameters:
  - stake_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 200 Stake.

#### Money
  - amount (string, required): Pattern: `^-?\d+\.\d{2}$`.
  - currency (string, required): ISO 4217 code. Pattern: `^[A-Z]{3}$`.

#### Stake
  - account_id (integer, required): The stake's asset account in this ledger.
  - held_name (string, required): What the owned ledger is, or was last, called.
  - held_organization_id (string | null, required): The ledger owned. Null once that ledger has been deleted.
  - id (integer, required)
  - ownership_pct (string, required): The share owned, as a decimal string (`"50.0000"`).
  - status (string, required): One of `active`, `access_lost`, `source_deleted`. Anything but `active` means the value is frozen as of `valued_at`: the person who linked it is no longer an owner or admin of the owned ledger, or that ledger was deleted.
  - value (Money, required): What the share is worth, in this ledger's base currency: the percentage of the owned ledger's assets minus liabilities, never below zero.
  - valued_at (datetime | null, required): When the value was last brought up to date.

#### StakeList
  - data (array of Stake, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

#### StakeUpdate
  - ownership_pct (string, required): The share owned from today, as a decimal string between 0 and 100. Past values keep the share they had. Pattern: `^\d{1,3}(\.\d{1,4})?$`.

## Statements

### GET /v1/statements/balance-sheet — Balance sheet (`statements.balance_sheet`, read)

The balance sheet at the end of each of the last `months` months (the
last column is today): every asset, liability and equity account's
balance, with retained earnings derived so the statement balances, and
a currency translation adjustment when foreign balances moved with the
rates.

Parameters:
  - months (integer, query): How many calendar months, ending with the current one (1–24).
  - X-Organization-Id (string | null, header)

Returns 200 BalanceSheet.

### GET /v1/statements/cash-flow — Cash flow statement (`statements.cash_flow`, read)

Money in and out of each real account in each of the last `months`
months, translated to the base currency at each month's own rate.

Parameters:
  - months (integer, query): How many calendar months, ending with the current one (1–24).
  - X-Organization-Id (string | null, header)

Returns 200 CashFlow.

### GET /v1/statements/income — Income statement (`statements.income`, read)

The income statement: what each income and expense category took in or
spent in each of the last `months` calendar months, their totals, and net
income (income − expenses).

Parameters:
  - months (integer, query): How many calendar months, ending with the current one (1–24).
  - purpose (string | null, query): Only the `personal` or the `business` side of the ledger. Both when omitted.
  - X-Organization-Id (string | null, header)

Returns 200 IncomeStatement.

### GET /v1/statements/income/breakdown — Income statement row by counterparty (`statements.income_breakdown`, read)

One row of the income statement split by who the money went to or
came from, over the same months. A category id that is not in this
ledger has no activity and answers no rows.

Parameters:
  - category_id (integer, required, query): The income or expense account to split (`category_id` of a row).
  - months (integer, query): How many calendar months, ending with the current one (1–24).
  - purpose (string | null, query): Only the `personal` or the `business` side of the ledger. Both when omitted.
  - X-Organization-Id (string | null, header)

Returns 200 IncomeStatementBreakdown.

#### BalanceSheet
  - assets (BalanceSheetSection, required)
  - currency (string, required): The ledger's base currency.
  - dates (array of date, required): Month-ends, oldest first; the last is today.
  - equity (BalanceSheetSection, required)
  - liabilities (BalanceSheetSection, required)
  - unconverted_currencies (array of string, required): Currencies with no stored exchange rate, shown 1:1.

#### BalanceSheetRow
  - account_id (integer | null, required): Null for a derived equity row (retained earnings, currency translation adjustment), which no account carries.
  - code (string, required)
  - mask (string | null, required)
  - name (string, required)
  - native_latest (Money | null, required): The latest balance in the account's own currency; null for a derived row.
  - role (string | null, required)
  - values (array of Money, required): One per date in `dates`, in the base currency.

#### BalanceSheetSection
  - rows (array of BalanceSheetRow, required)
  - totals (array of Money, required): One per date.

#### CashFlow
  - accounts (array of CashFlowAccount, required)
  - currency (string, required)
  - months (array of string, required): `YYYY-MM`, oldest first.
  - totals (array of Flow, required): One per month.
  - unconverted_currencies (array of string, required)

#### CashFlowAccount
  - account_id (integer, required)
  - code (string, required)
  - is_liability (boolean, required)
  - mask (string | null, required)
  - name (string, required)
  - role (string | null, required)
  - series (array of Flow, required): One per month in `months`.

#### Flow
  - inflow (Money, required): Movements that grew the balance (for a card: charges).
  - net (Money, required)
  - outflow (Money, required): Movements that shrank it (for a card: payments).

#### IncomeStatement
  - currency (string, required): The ledger's base currency.
  - expenses (IncomeStatementSection, required)
  - income (IncomeStatementSection, required)
  - months (array of string, required): `YYYY-MM`, oldest first; the last is the current month.
  - net_income (NetIncome, required)

#### IncomeStatementBreakdown
  - currency (string, required)
  - months (array of string, required)
  - rows (array of StatementBreakdownRow, required): Largest first.

#### IncomeStatementRow
  - category_id (integer, required): The income or expense account.
  - code (string, required)
  - name (string, required)
  - parent_id (integer | null, required): The row this one is a subcategory of.
  - total (Money, required)
  - values (array of Money, required): One per month in `months`.

#### IncomeStatementSection
  - rows (array of IncomeStatementRow, required): In chart order, each subcategory after its parent.
  - total (Money, required)
  - totals (array of Money, required): One per month.

#### Money
  - amount (string, required): Pattern: `^-?\d+\.\d{2}$`.
  - currency (string, required): ISO 4217 code. Pattern: `^[A-Z]{3}$`.

#### NetIncome
  - total (Money, required)
  - values (array of Money, required)

#### StatementBreakdownRow
  - description (string, required): Who the money went to or came from, as the entries describe it.
  - entries (integer, required): How many transactions it sums.
  - total (Money, required)
  - values (array of Money, required): One per month in `months`.

## Tags

### GET /v1/tags — List tags (`tags.list`, read)

The ledger's tags, with how many transactions carry each.

Parameters:
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - X-Organization-Id (string | null, header)

Returns 200 TagList.

### POST /v1/tags — Create a tag (`tags.create`, write)

Create a tag. Names are unique ignoring case: asking for one that
exists returns the existing tag (its color unchanged) instead of an
error, so creating a tag by name is safe to repeat.

Parameters:
  - Idempotency-Key (string | null, header): Any unique string (a UUID is ideal). A retry with the same key returns the first attempt's response instead of doing the work twice. Keys are kept for 24 hours.
  - X-Organization-Id (string | null, header)

Body (TagCreate):
  - color (string | null): A palette key (`amber`, `blue`, `gold`, `green`, `indigo`, `orange`, `purple`, `red`, `rose`, `sky`, `slate`, `teal`), drawn in the app's theme. Null for the default.
  - name (string, required)

Returns 201 Tag.

### DELETE /v1/tags/{tag_id} — Delete a tag (`tags.delete`, write, destructive)

Delete a tag and take it off every transaction. Rules that name it
keep their row; the condition simply stops matching.

Parameters:
  - tag_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 204.

### GET /v1/tags/{tag_id} — Get a tag (`tags.get`, read)

One tag by id.

Parameters:
  - tag_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 200 Tag.

### PATCH /v1/tags/{tag_id} — Rename or recolor a tag (`tags.update`, write)

Rename or recolor a tag. Only the fields sent change. Renaming to a
name another tag has is a 409. Transactions and rules that use the tag
keep it: they refer to its id, not its name.

Parameters:
  - tag_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Body (TagUpdate):
  - color (string | null): A palette key (`amber`, `blue`, `gold`, `green`, `indigo`, `orange`, `purple`, `red`, `rose`, `sky`, `slate`, `teal`), drawn in the app's theme. Null for the default. Null clears it.
  - name (string | null)

Returns 200 Tag.

### PUT /v1/transactions/{transaction_id}/tags — Set a transaction's tags (`transactions.set_tags`, write)

Replace the tags on a transaction with exactly `tag_ids`. Tags a rule
applied are replaced too. Hand-written transactions cannot carry tags
(a 422).

Parameters:
  - transaction_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Body (TransactionTagsInput):
  - tag_ids (array of integer, required): Every tag the transaction should carry (see `tags.list`); an empty list removes them all.

Returns 200 TransactionTags.

#### Tag
  - color (string | null, required): A palette key (`amber`, `blue`, `gold`, `green`, `indigo`, `orange`, `purple`, `red`, `rose`, `sky`, `slate`, `teal`), drawn in the app's theme. Null for the default.
  - created_at (datetime, required)
  - id (integer, required): The tag id. `transactions.list` filters by it as `tag_id`.
  - name (string, required): Unique in the ledger, ignoring case.
  - transaction_count (integer, required): Transactions carrying the tag.

#### TagCreate
  - color (string | null): A palette key (`amber`, `blue`, `gold`, `green`, `indigo`, `orange`, `purple`, `red`, `rose`, `sky`, `slate`, `teal`), drawn in the app's theme. Null for the default.
  - name (string, required)

#### TagList
  - data (array of Tag, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

#### TagUpdate
  - color (string | null): A palette key (`amber`, `blue`, `gold`, `green`, `indigo`, `orange`, `purple`, `red`, `rose`, `sky`, `slate`, `teal`), drawn in the app's theme. Null for the default. Null clears it.
  - name (string | null)

#### TransactionTags
  - tags (array of Tag, required): The tags the transaction carries now, in id order.
  - transaction_id (integer, required)

#### TransactionTagsInput
  - tag_ids (array of integer, required): Every tag the transaction should carry (see `tags.list`); an empty list removes them all.

## Tasks

### GET /v1/tasks — List tasks (`tasks.list`, read)

The ledger's to-do list: what is open, what is snoozed and what was
done in the last 30 days. Tasks come from the ledger itself and
disappear when their condition does (filing the last uncategorized
transaction completes that task), so the list is recomputed on every
read. Dismissed tasks are not listed.

Parameters:
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - state (string | null, query): Only tasks in this state. Every state when omitted.
  - X-Organization-Id (string | null, header)

Returns 200 TaskList.

### POST /v1/tasks/dismiss — Dismiss a task (`tasks.dismiss`, write)

"Never": stop suggesting this task. Only tasks marked `dismissable`
can be dismissed. The verdict is written where the feature keeps it:
dismissing `confirm_recurring` marks the charge as not recurring (see
`recurring.set_status`). Undo with `tasks.restore`.

Parameters:
  - X-Organization-Id (string | null, header)

Body (TaskRef):
  - task_id (string, required): The task's `id`, from `tasks.list`.

Returns 200 TaskVerdict.

### POST /v1/tasks/restore — Restore a dismissed task (`tasks.restore`, write)

Undo `tasks.dismiss`. The task is listed again if its condition
still holds.

Parameters:
  - X-Organization-Id (string | null, header)

Body (TaskRef):
  - task_id (string, required): The task's `id`, from `tasks.list`.

Returns 200 TaskVerdict.

### POST /v1/tasks/snooze — Snooze a task (`tasks.snooze`, write)

"Not now": hide an open task for a day, a week or a month. It comes
back on its own when the time is up, if its condition still holds.

Parameters:
  - X-Organization-Id (string | null, header)

Body (TaskSnooze):
  - duration (string): One of `day`, `week`, `month`. Pattern: `^(day|week|month)$`. Default: `"week"`.
  - task_id (string, required): The task's `id`, from `tasks.list`.

Returns 200 TaskVerdict.

### POST /v1/tasks/unsnooze — Unsnooze a task (`tasks.unsnooze`, write)

Bring a snoozed task back to the open list now.

Parameters:
  - X-Organization-Id (string | null, header)

Body (TaskRef):
  - task_id (string, required): The task's `id`, from `tasks.list`.

Returns 200 TaskVerdict.

#### Task
  - count (integer, required): How many things the task stands for (e.g. unfiled transactions).
  - dismissable (boolean, required): Whether `tasks.dismiss` is allowed. The others can only be snoozed.
  - first_seen_at (datetime, required)
  - id (string, required): The task's stable key: its kind, and for per-subject kinds the subject (`confirm_recurring:netflix`). Pass it as `task_id` to the task actions.
  - kind (string, required): What to do. One of `categorize_transactions`, `match_receipts`, `relink_bank`, `sync_stale_bank`, `confirm_recurring`, `link_named_bank`, `update_valuation`, `set_first_budget`, `invite_member`, `name_p2p_payment`, `route_transfers`; new kinds may be added.
  - last_seen_at (datetime, required)
  - payload (object, required): Facts about the task, per kind (ids, names, counts; amounts are decimal strings in the ledger's base currency). Keys are snake_case.
  - priority (string, required): One of `urgent`, `normal`, `low`, most urgent first.
  - resolved_at (datetime | null, required)
  - snoozed_until (datetime | null, required)
  - state (string, required): One of `open`, `snoozed`, `done`; `done` means resolved in the last 30 days.

#### TaskList
  - data (array of Task, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

#### TaskRef
  - task_id (string, required): The task's `id`, from `tasks.list`.

#### TaskSnooze
  - duration (string): One of `day`, `week`, `month`. Pattern: `^(day|week|month)$`. Default: `"week"`.
  - task_id (string, required): The task's `id`, from `tasks.list`.

#### TaskVerdict
  - task (Task | null, required)

## Transactions

### GET /v1/transactions — List transactions (`transactions.list`, read)

The ledger's transactions, newest first. Daily market-value entries
on investment accounts are balance facts, not transactions, and are
never listed.

Parameters:
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - account_id (integer | null, query): Only transactions touching this account.
  - category_id (integer | null, query): Only transactions filed under this category account.
  - tag_id (integer | null, query): Only transactions carrying this tag.
  - since (date | null, query): On or after this date.
  - until (date | null, query): On or before this date.
  - search (string | null, query): Text in the description, the bank's statement line or the name the user gave the counterparty, or an amount (`72.80`, `72,80`; `72` is any amount from 72.00 to 72.99).
  - uncategorized (boolean, query): Only transactions still waiting for a category.
  - purpose (string | null, query): `personal` or `business` only.
  - X-Organization-Id (string | null, header)

Returns 200 TransactionList.

### POST /v1/transactions — Create a transaction (`transactions.create`, write)

Post a hand-written transaction as a balanced journal entry: a cash
purchase, a loan payment split into interest and principal, an opening
balance. Send an `Idempotency-Key` so a retry cannot post it twice.

Parameters:
  - Idempotency-Key (string | null, header): Any unique string (a UUID is ideal). A retry with the same key returns the first attempt's response instead of doing the work twice. Keys are kept for 24 hours.
  - X-Organization-Id (string | null, header)

Body (TransactionCreate):
  - date (date, required)
  - description (string, required)
  - lines (array of TransactionLineInput, required)
  - memo (string | null)

Returns 201 Transaction.

### DELETE /v1/transactions/{transaction_id} — Delete a transaction (`transactions.delete`, write, destructive)

Delete a hand-written transaction. Imported ones cannot be deleted
(the next sync would bring them back); set `ignored` instead.

Parameters:
  - transaction_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 204.

### GET /v1/transactions/{transaction_id} — Get a transaction (`transactions.get`, read)

One transaction by its id (the journal entry id).

Parameters:
  - transaction_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 200 Transaction.

### PATCH /v1/transactions/{transaction_id} — Update a transaction (`transactions.update`, write)

Correct a transaction's description, date or memo, refile it under
another category, mark it a transfer between two of the ledger's own
accounts (or take that back), or set it aside from spending totals.
Only the fields sent change. Refiling or marking an imported
transaction re-posts its entry, so the returned transaction may carry a
new `id`.

Parameters:
  - transaction_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Body (TransactionUpdate):
  - category_id (integer | null): Refile under this income or expense account. May give the transaction a new id (the entry is re-posted); the response carries it. A transaction marked as a transfer is un-marked first; a transfer the ledger paired from both of its sides cannot be refiled.
  - date (date | null)
  - description (string | null)
  - ignored (boolean | null): Leave it out of spending totals (imported transactions only).
  - memo (string | null): Null clears it.
  - stop_transfer_rule (boolean): With `transfer_account_id: null`: also stop the counterparty rule that files this payee as a transfer. What it already marked stays marked. Default: `false`.
  - transfer_account_id (integer | null): Mark it a transfer between the account it is on and this other balance-sheet account of the ledger's (see `accounts.list`), so it leaves income and spending. `null` takes a mark back and lets the categorizer file it. Imported transactions only; not with `category_id`. Re-posts the entry, so the response may carry a new `id`. Taking back a mark that a counterparty rule makes is refused with a 422 naming the rule, since the rule would mark it again at once: send `stop_transfer_rule` to stop the rule too, or `category_id` to file just this one.

Returns 200 Transaction.

### POST /v1/transactions/categorize — Categorize transactions in bulk (`transactions.categorize`, write)

File up to 200 transactions under one category in a single step.
Nothing moves if any of them cannot be filed there (an income account
for a purchase, say); ids that are not in this ledger, and transfers,
which have no category, are returned in `skipped`.

Parameters:
  - Idempotency-Key (string | null, header): Any unique string (a UUID is ideal). A retry with the same key returns the first attempt's response instead of doing the work twice. Keys are kept for 24 hours.
  - X-Organization-Id (string | null, header)

Body (CategorizeRequest):
  - category_id (integer, required): The income or expense account to file them under.
  - transaction_ids (array of integer, required)

Returns 200 CategorizeResult.

#### CategorizeRequest
  - category_id (integer, required): The income or expense account to file them under.
  - transaction_ids (array of integer, required)

#### CategorizeResult
  - moved (array of MovedTransaction, required)
  - skipped (array of integer, required)
  - updated (integer, required)

#### Money
  - amount (string, required): Pattern: `^-?\d+\.\d{2}$`.
  - currency (string, required): ISO 4217 code. Pattern: `^[A-Z]{3}$`.

#### MovedTransaction
  - from_id (integer, required)
  - to_id (integer, required): The id the transaction has now (re-posting may replace it).

#### Transaction
  - account_id (integer, required): The real account the transaction is on.
  - amount (Money, required): Negative when money left the account or debt grew, positive when money arrived or debt shrank. In the ledger's base currency.
  - category_id (integer | null, required): The income or expense account it is filed under. Null for a transfer between the ledger's own accounts.
  - counterparty (TransactionCounterparty | null, required): Who is on the other end. Null for transfers and unrecognized descriptions.
  - created_at (datetime, required)
  - date (date, required)
  - description (string, required): What the bank or the author called it.
  - id (integer, required): The journal entry id. The same id the app's transaction page uses.
  - ignored (boolean, required): Set aside by the user: listed, but left out of spending totals.
  - is_transfer (boolean, required)
  - lines (array of TransactionLine, required)
  - memo (string | null, required)
  - purpose (string, required): `personal` or `business`.
  - source (string, required): Where it came from: `plaid`, `import`, `manual`, `assistant`, …
  - statement_line (string | null, required): The line as the bank printed it on the statement (`SQ *BLUE BOTTLE 0412 OAKLAND CA`), which `search` also matches. Null on an entry nobody imported.
  - updated_at (datetime, required)

#### TransactionCounterparty
  - key (string, required): Stable key of the merchant or person on the other end.
  - name (string, required)

#### TransactionCreate
  - date (date, required)
  - description (string, required)
  - lines (array of TransactionLineInput, required)
  - memo (string | null)

#### TransactionLine
  - account_id (integer, required)
  - amount (Money, required): Always positive; `direction` says which side.
  - direction (string, required): `debit` or `credit`.

#### TransactionLineInput
  - account_id (integer, required): An account in this ledger (see `accounts.list`).
  - amount (string, required): Positive decimal string, at most two decimal places: `"42.50"`. Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - direction (string, required): `debit` or `credit`. Pattern: `^(debit|credit)$`.

#### TransactionList
  - data (array of Transaction, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

#### TransactionUpdate
  - category_id (integer | null): Refile under this income or expense account. May give the transaction a new id (the entry is re-posted); the response carries it. A transaction marked as a transfer is un-marked first; a transfer the ledger paired from both of its sides cannot be refiled.
  - date (date | null)
  - description (string | null)
  - ignored (boolean | null): Leave it out of spending totals (imported transactions only).
  - memo (string | null): Null clears it.
  - stop_transfer_rule (boolean): With `transfer_account_id: null`: also stop the counterparty rule that files this payee as a transfer. What it already marked stays marked. Default: `false`.
  - transfer_account_id (integer | null): Mark it a transfer between the account it is on and this other balance-sheet account of the ledger's (see `accounts.list`), so it leaves income and spending. `null` takes a mark back and lets the categorizer file it. Imported transactions only; not with `category_id`. Re-posts the entry, so the response may carry a new `id`. Taking back a mark that a counterparty rule makes is refused with a 422 naming the rule, since the rule would mark it again at once: send `stop_transfer_rule` to stop the rule too, or `category_id` to file just this one.

## Transfer Accounts

### GET /v1/transfer-accounts — List transfer accounts (`transfer_accounts.list`, read)

Every account a transfer can land on: the ledger's real asset and
liability accounts, closed ones included. A transaction moved to one of
these is a transfer between your own accounts, not spending.

Parameters:
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - X-Organization-Id (string | null, header)

Returns 200 TransferAccountList.

### POST /v1/transfer-accounts — Create a transfer account (`transfer_accounts.create`, write)

Create an asset or liability account for money that goes somewhere
the ledger does not track yet (a bank that is not connected, a wallet),
so a transfer there can be filed as a transfer instead of spending. Its
code is allocated for you and it starts at zero. For an account at a
named bank with a known balance, `institutions.create_account` does
more. Send an `Idempotency-Key` so a retry cannot create it twice.

Parameters:
  - Idempotency-Key (string | null, header): Any unique string (a UUID is ideal). A retry with the same key returns the first attempt's response instead of doing the work twice. Keys are kept for 24 hours.
  - X-Organization-Id (string | null, header)

Body (TransferAccountCreate):
  - name (string, required)
  - type (string, required): `asset` for somewhere money is kept, `liability` for a debt it pays down. Pattern: `^(asset|liability)$`.

Returns 201 TransferAccount.

#### TransferAccount
  - code (string, required)
  - id (integer, required): The account id (`accounts.get` takes it).
  - institution_id (integer | null, required): The bank it is held at, if any.
  - institution_name (string | null, required): That bank's name as the ledger shows it (a rename wins).
  - is_active (boolean, required): False once closed; still a valid destination for old payments.
  - mask (string | null, required): The last digits of the account number.
  - name (string, required)
  - role (string | null, required): What the account is for, when known (`checking`, `credit_card`, …).
  - type (string, required): `asset` or `liability`.

#### TransferAccountCreate
  - name (string, required)
  - type (string, required): `asset` for somewhere money is kept, `liability` for a debt it pays down. Pattern: `^(asset|liability)$`.

#### TransferAccountList
  - data (array of TransferAccount, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

## Transfer Suggestions

### GET /v1/transfer-suggestions — List transfer suggestions (`transfer_suggestions.list`, read)

Accounts in this ledger that other accounts' transactions keep
paying, while those transactions are still filed as spending or income
by a guess. A category someone chose, or a rule, is never second-guessed.
Suggestions already accepted or dismissed are left out.

Parameters:
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - X-Organization-Id (string | null, header)

Returns 200 TransferSuggestionList.

### POST /v1/transfer-suggestions/{account_id}/accept — Accept a transfer suggestion (`transfer_suggestions.accept`, write)

Re-file the suggested transactions as transfers to this account, and
route new ones like them the same way from now on. Accepting again
moves only what arrived since.

Parameters:
  - account_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 200 AcceptResult.

### DELETE /v1/transfer-suggestions/{account_id}/decision — Forget a transfer suggestion's verdict (`transfer_suggestions.reset`, write, destructive)

Forget whether this account's suggestion was accepted or dismissed,
so it can be suggested again. Transactions already re-filed stay filed.
A 404 when there was no verdict.

Parameters:
  - account_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 204.

### POST /v1/transfer-suggestions/{account_id}/dismiss — Dismiss a transfer suggestion (`transfer_suggestions.dismiss`, write)

Stop suggesting transfers to this account. Nothing is re-filed.

Parameters:
  - account_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 204.

#### AcceptResult
  - moved (integer, required): How many transactions were re-filed as transfers.

#### Money
  - amount (string, required): Pattern: `^-?\d+\.\d{2}$`.
  - currency (string, required): ISO 4217 code. Pattern: `^[A-Z]{3}$`.

#### TransferSuggestion
  - account_id (integer, required): The destination account; the suggestion's id.
  - account_name (string, required)
  - account_role (string | null, required): What the destination is for (`credit_card`, …).
  - first_date (date, required)
  - institution_name (string, required): The bank the destination is held at.
  - last_date (date, required)
  - mask (string | null, required)
  - sample_descriptions (array of string, required)
  - source_accounts (array of string, required): Names of the accounts those transactions are on, most first.
  - total (Money, required): What they add up to.
  - transaction_count (integer, required): How many transactions would be re-filed.

#### TransferSuggestionList
  - data (array of TransferSuggestion, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

## Unlinked Institutions

### GET /v1/unlinked-institutions — List banks the transactions name but the ledger lacks (`unlinked_institutions.list`, read)

Banks recent transfers keep naming that no account in this ledger is
held at: why spending can look too high (a card payment to an
unconnected bank has nowhere to go but spending). A short list, the
banks the most money moves to first. Banks dismissed in the app are left
out.

Parameters:
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - X-Organization-Id (string | null, header)

Returns 200 UnlinkedInstitutionList.

#### Money
  - amount (string, required): Pattern: `^-?\d+\.\d{2}$`.
  - currency (string, required): ISO 4217 code. Pattern: `^[A-Z]{3}$`.

#### UnlinkedInstitution
  - account_hints (array of string, required): Masked account numbers seen in the descriptions (`4821`).
  - counterparty_type (string | null, required): What the bank feed called the other party (`financial_institution`, `payment_app`), or null when inferred from a transfer marked by hand.
  - first_date (date, required)
  - last_date (date, required)
  - name (string, required): The bank as the transactions spell it.
  - sample_descriptions (array of string, required)
  - slug (string, required): Stable key of the bank's name; what a new institution for it would be keyed on.
  - total (Money, required): What those transactions add up to, in the currency most of them are in.
  - transaction_count (integer, required): How many recent transactions name it, in any currency.

#### UnlinkedInstitutionList
  - data (array of UnlinkedInstitution, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

## Vehicles

### GET /v1/vehicles — List vehicles (`vehicles.list`, read)

Every vehicle in the ledger, including suggested and dismissed ones
(see `status`), each with its loan and equity.

Parameters:
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - X-Organization-Id (string | null, header)

Returns 200 VehicleList.

### POST /v1/vehicles — Add a vehicle (`vehicles.create`, write)

Add a vehicle the household owns, optionally linked to its loan.
Send an `Idempotency-Key` so a retry cannot add it twice.

Parameters:
  - Idempotency-Key (string | null, header): Any unique string (a UUID is ideal). A retry with the same key returns the first attempt's response instead of doing the work twice. Keys are kept for 24 hours.
  - X-Organization-Id (string | null, header)

Body (VehicleCreate):
  - estimated_value (string | null): What it is worth, as a decimal string. Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - label (string, required): What to call it: `Blue Civic`.
  - loan_account_id (integer | null): Its loan: a liability account (see `accounts.list`) no other vehicle is linked to.
  - make (string | null)
  - model (string | null)
  - model_year (integer | null): 1900–2100.
  - valued_at (date | null): The date the value is from. Defaults to today when a value is sent.
  - vehicle_type (string): One of `car`, `motorcycle`, `truck`, `suv`, `van`, `rv`, `boat`, `other`. Default: `"car"`.
  - vin (string | null)

Returns 201 Vehicle.

### DELETE /v1/vehicles/{vehicle_id} — Delete a vehicle (`vehicles.delete`, write, destructive)

Delete a vehicle that was added by hand. Its loan account stays. One
found from an auto loan cannot be deleted (the next sync would find it
again): set its `status` to `dismissed` instead.

Parameters:
  - vehicle_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 204.

### GET /v1/vehicles/{vehicle_id} — Get a vehicle (`vehicles.get`, read)

One vehicle with its loan and equity.

Parameters:
  - vehicle_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 200 Vehicle.

### PATCH /v1/vehicles/{vehicle_id} — Update a vehicle (`vehicles.update`, write)

Correct a vehicle's details, revalue it, link or unlink its loan,
confirm or dismiss one found from an auto loan, or hide it or leave it
out of the totals. Only the fields sent change.

Parameters:
  - vehicle_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Body (VehicleUpdate):
  - estimated_value (string): A new value. Dated today unless `valued_at` is sent with it. Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - excluded_from_totals (boolean): Leave it out of the totals, and its loan out of net worth.
  - hidden (boolean)
  - label (string)
  - loan_account_id (integer | null): Link a loan, or null to unlink it.
  - make (string)
  - model (string)
  - model_year (integer): 1900–2100.
  - status (string): One of `confirmed`, `dismissed`, `suggested`.
  - valued_at (date): The date the value is from; sent alone, it re-dates the current one.
  - vehicle_type (string): One of `car`, `motorcycle`, `truck`, `suv`, `van`, `rv`, `boat`, `other`.
  - vin (string)

Returns 200 Vehicle.

#### Money
  - amount (string, required): Pattern: `^-?\d+\.\d{2}$`.
  - currency (string, required): ISO 4217 code. Pattern: `^[A-Z]{3}$`.

#### Vehicle
  - equity (Money | null, required): Value minus the loan. Null unless both are known.
  - estimated_value (Money | null, required): What it is worth. Null when nobody has said.
  - excluded_from_totals (boolean, required): Left out of the vehicle totals, and its loan out of net worth.
  - hidden (boolean, required): Hidden from the app's lists; still counted.
  - id (integer, required)
  - label (string, required)
  - loan (VehicleLoan | null, required)
  - make (string | null, required)
  - model (string | null, required)
  - model_year (integer | null, required)
  - source (string, required): `manual`, `inferred` (found from an auto loan) or `seed` (demo data).
  - status (string, required): `suggested` (found from an auto loan, not yet reviewed), `confirmed` or `dismissed`.
  - value_age_days (integer | null, required): Days since `valued_at`.
  - value_is_stale (boolean | null, required): The value is over six months old. Null when it was never dated: not fresh, unknown.
  - value_source (string, required): `user` or `none`.
  - valued_at (date | null, required): The date the value is true for.
  - vehicle_type (string, required): One of `car`, `motorcycle`, `truck`, `suv`, `van`, `rv`, `boat`, `other`.
  - vin (string | null, required)

#### VehicleCreate
  - estimated_value (string | null): What it is worth, as a decimal string. Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - label (string, required): What to call it: `Blue Civic`.
  - loan_account_id (integer | null): Its loan: a liability account (see `accounts.list`) no other vehicle is linked to.
  - make (string | null)
  - model (string | null)
  - model_year (integer | null): 1900–2100.
  - valued_at (date | null): The date the value is from. Defaults to today when a value is sent.
  - vehicle_type (string): One of `car`, `motorcycle`, `truck`, `suv`, `van`, `rv`, `boat`, `other`. Default: `"car"`.
  - vin (string | null)

#### VehicleList
  - data (array of Vehicle, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

#### VehicleLoan
  - account_id (integer, required): The loan's liability account (see `accounts.list`).
  - balance (Money, required): What is owed on it today, positive while owed.
  - name (string, required)

#### VehicleUpdate
  - estimated_value (string): A new value. Dated today unless `valued_at` is sent with it. Pattern: `^\d{1,12}(\.\d{1,2})?$`.
  - excluded_from_totals (boolean): Leave it out of the totals, and its loan out of net worth.
  - hidden (boolean)
  - label (string)
  - loan_account_id (integer | null): Link a loan, or null to unlink it.
  - make (string)
  - model (string)
  - model_year (integer): 1900–2100.
  - status (string): One of `confirmed`, `dismissed`, `suggested`.
  - valued_at (date): The date the value is from; sent alone, it re-dates the current one.
  - vehicle_type (string): One of `car`, `motorcycle`, `truck`, `suv`, `van`, `rv`, `boat`, `other`.
  - vin (string)

## Webhook Endpoints

### GET /v1/webhook-endpoints — List webhook endpoints (`webhook_endpoints.list`, read)

The ledger's webhook endpoints. Secrets are never listed.

Parameters:
  - limit (integer, query): Page size, 1–200.
  - cursor (string | null, query): The `next_cursor` of the previous page.
  - X-Organization-Id (string | null, header)

Returns 200 WebhookEndpointList.

### POST /v1/webhook-endpoints — Create a webhook endpoint (`webhook_endpoints.create`, write)

Start pushing the ledger's changes of `change_types` to `url`. The
response carries the signing `secret`, shown this once: store it and
verify every delivery's `LumaLedger-Signature` with it. Changes from
before the endpoint existed are not pushed (read them from
`changes.list`). A ledger may have up to 16 endpoints.

Parameters:
  - Idempotency-Key (string | null, header): Any unique string (a UUID is ideal). A retry with the same key returns the first attempt's response instead of doing the work twice. Keys are kept for 24 hours.
  - X-Organization-Id (string | null, header)

Body (WebhookEndpointCreate):
  - change_types (array of string, required): The change types to deliver: any of `transaction.created`, `transaction.updated`, `transaction.deleted`, `account.balance_updated`, `budget.exceeded`, `import.completed`, `job.completed`, or `*` for every type, including ones added later.
  - description (string | null): A note for yourself: what receives these.
  - url (string, required): An https URL on the public internet. Its host may not resolve to a private, loopback, link-local or otherwise internal address; this is checked now and again before every delivery. Redirects are not followed.

Returns 201 WebhookEndpointWithSecret.

### DELETE /v1/webhook-endpoints/{webhook_endpoint_id} — Delete a webhook endpoint (`webhook_endpoints.delete`, write, destructive)

Stop pushing to this endpoint, and drop what it is still owed. The
change feed is unaffected.

Parameters:
  - webhook_endpoint_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 204.

### GET /v1/webhook-endpoints/{webhook_endpoint_id} — Get a webhook endpoint (`webhook_endpoints.get`, read)

One webhook endpoint, with whether it is failing. The secret is not shown.

Parameters:
  - webhook_endpoint_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 200 WebhookEndpoint.

### PATCH /v1/webhook-endpoints/{webhook_endpoint_id} — Update a webhook endpoint (`webhook_endpoints.update`, write)

Change an endpoint's URL, description or change types, or turn it
off and on. Only the fields sent change. Turning a disabled endpoint
back on clears its failure record; what it missed meanwhile is in
`changes.list`, not re-sent.

Parameters:
  - webhook_endpoint_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Body (WebhookEndpointUpdate):
  - change_types (array of string | null): The change types to deliver: any of `transaction.created`, `transaction.updated`, `transaction.deleted`, `account.balance_updated`, `budget.exceeded`, `import.completed`, `job.completed`, or `*` for every type, including ones added later.
  - description (string | null)
  - status (string | null): `enabled` or `disabled`. An endpoint that has not accepted a delivery for three days is disabled for you (`disabled_reason: failing`) and the ledger's owners are emailed; set it back to `enabled` once it answers.
  - url (string | null): An https URL on the public internet. Its host may not resolve to a private, loopback, link-local or otherwise internal address; this is checked now and again before every delivery. Redirects are not followed.

Returns 200 WebhookEndpoint.

### POST /v1/webhook-endpoints/{webhook_endpoint_id}/rotate-secret — Rotate a webhook endpoint's signing secret (`webhook_endpoints.rotate_secret`, write)

Issue a new signing secret, shown in this response only. For the next
24 hours deliveries carry two `v1` signatures, one per secret, so a
receiver can switch over without refusing any; after that only the new
secret signs.

Parameters:
  - webhook_endpoint_id (integer, required, path)
  - X-Organization-Id (string | null, header)

Returns 200 WebhookEndpointWithSecret.

#### WebhookEndpoint
  - change_types (array of string, required): The change types to deliver: any of `transaction.created`, `transaction.updated`, `transaction.deleted`, `account.balance_updated`, `budget.exceeded`, `import.completed`, `job.completed`, or `*` for every type, including ones added later.
  - created_at (datetime, required)
  - description (string | null, required)
  - disabled_reason (string | null, required): Why it is disabled: `failing` (by us) or `user`. Null while enabled.
  - failing_since (datetime | null, required): The first failed delivery since the last one it accepted. Null when healthy.
  - id (integer, required): The endpoint id (`webhook_endpoint_id` in paths).
  - status (string, required): `enabled` or `disabled`. An endpoint that has not accepted a delivery for three days is disabled for you (`disabled_reason: failing`) and the ledger's owners are emailed; set it back to `enabled` once it answers.
  - updated_at (datetime, required)
  - url (string, required)

#### WebhookEndpointCreate
  - change_types (array of string, required): The change types to deliver: any of `transaction.created`, `transaction.updated`, `transaction.deleted`, `account.balance_updated`, `budget.exceeded`, `import.completed`, `job.completed`, or `*` for every type, including ones added later.
  - description (string | null): A note for yourself: what receives these.
  - url (string, required): An https URL on the public internet. Its host may not resolve to a private, loopback, link-local or otherwise internal address; this is checked now and again before every delivery. Redirects are not followed.

#### WebhookEndpointList
  - data (array of WebhookEndpoint, required)
  - has_more (boolean, required)
  - next_cursor (string | null, required)

#### WebhookEndpointUpdate
  - change_types (array of string | null): The change types to deliver: any of `transaction.created`, `transaction.updated`, `transaction.deleted`, `account.balance_updated`, `budget.exceeded`, `import.completed`, `job.completed`, or `*` for every type, including ones added later.
  - description (string | null)
  - status (string | null): `enabled` or `disabled`. An endpoint that has not accepted a delivery for three days is disabled for you (`disabled_reason: failing`) and the ledger's owners are emailed; set it back to `enabled` once it answers.
  - url (string | null): An https URL on the public internet. Its host may not resolve to a private, loopback, link-local or otherwise internal address; this is checked now and again before every delivery. Redirects are not followed.

#### WebhookEndpointWithSecret
  - change_types (array of string, required): The change types to deliver: any of `transaction.created`, `transaction.updated`, `transaction.deleted`, `account.balance_updated`, `budget.exceeded`, `import.completed`, `job.completed`, or `*` for every type, including ones added later.
  - created_at (datetime, required)
  - description (string | null, required)
  - disabled_reason (string | null, required): Why it is disabled: `failing` (by us) or `user`. Null while enabled.
  - failing_since (datetime | null, required): The first failed delivery since the last one it accepted. Null when healthy.
  - id (integer, required): The endpoint id (`webhook_endpoint_id` in paths).
  - secret (string | null, required): The signing secret (`whsec_…`): verify `LumaLedger-Signature` with it. Shown only in this response and never again; `webhook_endpoints.rotate_secret` issues a new one. Null on a replayed `Idempotency-Key` response.
  - status (string, required): `enabled` or `disabled`. An endpoint that has not accepted a delivery for three days is disabled for you (`disabled_reason: failing`) and the ledger's owners are emailed; set it back to `enabled` once it answers.
  - updated_at (datetime, required)
  - url (string, required)
