# API reference

> Every /v1 route, from the OpenAPI spec the server generates.

Canonical: https://usetruenote.com/docs/api

Base URL `https://app.usetruenote.com/v1`. Send `Authorization: Bearer <token>` (a personal access token from Connected agents, or an OAuth access token). The spec: https://app.usetruenote.com/v1/openapi.json. Money is integer cents; days are YYYY-MM-DD; transaction amounts keep Plaid's sign (positive is money out). Errors are application/problem+json: the code, what went wrong and what fixes it (/docs/errors); 403 lists the scopes missing.

## Reads

Plain JSON with amounts in integer cents. Lists page by keyset; /v1/transactions and /v1/investment-transactions take updatedSince for what changed since your last pull.

### GET /v1/overview

Net worth, this month, the plan, bills, goals and the next estimated tax in one call

Needs any read scope. Accounts and entities the token excludes are invisible here, totals included.

### GET /v1/accounts

Accounts with latest balances (debts negative), manual accounts and net worth totals

Needs accounts:read. Accounts and entities the token excludes are invisible here, totals included.

Parameters:

- `type` (query, depository | credit | loan | investment | other): Only accounts of this type
- `includeHidden` (query, string): Include closed accounts, and accounts the person hid if this token may see hidden accounts

### GET /v1/transactions

Transactions newest first, filtered, keyset-paged (money in positive); updatedSince for what changed since your last pull, with the ids deleted

Needs transactions:read. Accounts and entities the token excludes are invisible here, totals included.

Parameters:

- `from` (query, string): First day (inclusive); default: all history
- `to` (query, string): Last day (inclusive)
- `accountId` (query, uuid): 
- `categoryId` (query, uuid | none): A category id (its subcategories included), or `none` for uncategorized
- `entityId` (query, uuid): 
- `merchant` (query, string): Merchant or description contains this (any case)
- `q` (query, string): Description, merchant or note contains this
- `minAmountCents` (query, integer | null): Size at least this, in cents (either direction)
- `maxAmountCents` (query, integer | null): 
- `direction` (query, in | out): Money in or money out
- `pending` (query, string): 
- `ownerContribution` (query, string): true: only a business's spending paid from a personal account
- `limit` (query, integer): Rows per page (default 50, max 200)
- `cursor` (query, string): `nextCursor` of the last page
- `updatedSince` (query, date-time): Only rows added or changed at or after this instant (the syncedAt of your last pull), and `deleted` lists the ids gone since. Keeps a copy in step without re-reading everything; apply by id (a row may repeat).

### GET /v1/spending

Spending grouped by category, merchant, entity or month: the household's by default (scope=all adds the businesses; entityId picks one)

Needs transactions:read. Accounts and entities the token excludes are invisible here, totals included.

Parameters:

- `from` (query, string): Default: the first of this month
- `to` (query, string): Default: today
- `groupBy` (query, category | merchant | entity | month): 
- `scope` (query, household | all): household (default): the personal entities, as the overview's month counts them; all: the household and every business together. Ignored with entityId
- `entityId` (query, uuid): Only this entity's spending (one business)
- `categoryId` (query, uuid): Only this category (and its subcategories)
- `limit` (query, integer): Groups to list (default 25); the rest are summed

### GET /v1/recurring

Recurring bills, subscriptions and income with next dates and monthly totals

Needs transactions:read. Accounts and entities the token excludes are invisible here, totals included.

Parameters:

- `direction` (query, inflow | outflow): 
- `includeInactive` (query, string): 

### GET /v1/net-worth-history

Net worth over time by account type (day, week or month points)

Needs accounts:read. Accounts and entities the token excludes are invisible here, totals included.

Parameters:

- `from` (query, string): Default: a year ago
- `to` (query, string): Default: today
- `interval` (query, day | week | month): 

### GET /v1/holdings

Investment positions by account, optionally the latest investment transactions

Needs investments:read. Accounts and entities the token excludes are invisible here, totals included.

Parameters:

- `accountId` (query, uuid): 
- `includeTransactions` (query, string): Also the latest investment transactions (buys, sells, dividends)

### GET /v1/investment-transactions

Investment transactions newest first, filtered by date, account, type, subtype or symbol, keyset-paged, with totals by kind (cash in positive); updatedSince for what changed since your last pull, with the ids deleted

Needs investments:read. Accounts and entities the token excludes are invisible here, totals included.

Parameters:

- `from` (query, string): First day (inclusive); default: all history
- `to` (query, string): Last day (inclusive)
- `accountId` (query, uuid): 
- `type` (query, buy | sell | cash | fee | transfer | cancel): 
- `subtype` (query, string): Plaid's subtype, any case: dividend, interest, buy, sell, deposit…
- `symbol` (query, string): Ticker, any case
- `q` (query, string): Description contains this
- `limit` (query, integer): Rows per page (default 50, max 200)
- `cursor` (query, string): `nextCursor` of the last page
- `updatedSince` (query, date-time): Only rows added or changed at or after this instant (the syncedAt of your last pull), and `deleted` lists the ids gone since. Keeps a copy in step without re-reading everything; apply by id (a row may repeat).

### GET /v1/liabilities

Cards and loans: owed, APR, minimums, due dates

Needs liabilities:read. Accounts and entities the token excludes are invisible here, totals included.

### GET /v1/context

Profile, entities, income sources, goals, categories, rules, notes, plans

Needs context:read. Accounts and entities the token excludes are invisible here, totals included.

### GET /v1/plan

The spending plan: regular take-home split into groups and lines

Needs context:read. Accounts and entities the token excludes are invisible here, totals included.

### GET /v1/plan/actuals

Plan vs actual for a month: household spending per plan group and category line against the plan, the trailing 3 months' average, and spending no line names

Needs context:read, transactions:read. Accounts and entities the token excludes are invisible here, totals included.

Parameters:

- `month` (query, string): A month like 2026-10; default this month

### GET /v1/taxes/projection

The year's federal tax projected from today: the statement line by line, the refund or amount owed, the extra withholding per paycheck that breaks even, the safe harbor

Needs context:read, transactions:read. Accounts and entities the token excludes are invisible here, totals included.

Parameters:

- `year` (query, integer): 

### GET /v1/tasks

The checklist: open tasks by due date (status=done|all for the rest)

Needs context:read. Accounts and entities the token excludes are invisible here, totals included.

Parameters:

- `status` (query, open | done | all): 

### GET /v1/buckets

Buckets: each account's virtual split, balances from Truenote's ledger, targets and what's Unassigned

Needs accounts:read, context:read. Accounts and entities the token excludes are invisible here, totals included.

Parameters:

- `accountId` (query, uuid): Only this account's buckets
- `includeArchived` (query, string): Also archived buckets
- `recentEntries` (query, integer | null): Latest ledger entries per bucket (default 3)

### GET /v1/review-items

The Review queue: deposits to split, spending that looks like a bucket's, money taken out of a bucketed account, each with a proposal

Needs accounts:read, transactions:read, context:read. Accounts and entities the token excludes are invisible here, totals included.

Parameters:

- `status` (query, open | resolved): open (default): waiting for a decision; resolved: the latest decided

### GET /v1/bucket-rules

Bucket rules: spending they charge to a bucket automatically, first match wins

Needs accounts:read, context:read. Accounts and entities the token excludes are invisible here, totals included.

### GET /v1/documents

Documents the person uploaded (paystubs, tax forms, statements), newest first

Needs documents:read. Accounts and entities the token excludes are invisible here, totals included.

Parameters:

- `q` (query, string): Words in the name, notes or text (Social Security and account numbers masked)
- `kind` (query, paystub | w2 | form_1099 | form_1098 | tax_return | benefits | statement | receipt | other): 
- `taxYear` (query, integer): 

### GET /v1/documents/{id}

One document's text (masked, paged by offset); an image's bytes are at /file

Needs documents:read. Accounts and entities the token excludes are invisible here, totals included.

Parameters:

- `id` (path, required, uuid): Id
- `offset` (query, integer | null): Where to continue a long text (the last call's nextOffset)

### GET /v1/documents/{id}/file

A document's file as uploaded (unmasked)

Needs documents:read.

Parameters:

- `id` (path, required, uuid): Id

### GET /v1/export

Everything this token can see, every table, as JSON (truenote-export/1)

Needs accounts:read, transactions:read, investments:read, liabilities:read, context:read. Accounts and entities the token excludes are invisible here, totals included.

### GET /v1/export.zip

The same export as a zip of CSV files, one per table

Needs accounts:read, transactions:read, investments:read, liabilities:read, context:read.

## Writes

Every MCP write tool as POST /v1/tools/<name>, with the tool's own body: amounts in dollars (650 for $650.00), ids from the reads, the answer in cents. Truenote's own records only; nothing here moves money.

### POST /v1/documents

Keep a document for the person (multipart: file + metadata, a SaveDocument as JSON)

Needs documents:write. A PDF, PNG, JPEG or WebP image, at most 10 MB; the same file twice is a 409. Its entity, income source and account must be ones this token can see. Example: curl -H 'Authorization: Bearer tn_pat_…' -F file=@paystub.pdf -F 'metadata={"name":"Paystub Oct 16","kind":"paystub","taxYear":2026}' <origin>/v1/documents

Body (multipart/form-data):

- `file` (required) (binary)
- `metadata` (required) (string)

### POST /v1/tools/update_tax_settings

Set the tax year's household figures

Needs context:write. The same operation as the MCP tool update_tax_settings; amounts in the body are dollars (650 for $650.00), ids come from the GET routes, and the answer is the tool's data with amounts in integer cents. Change the household's figures for a tax year that the projection (get_tax_projection) can't find in the accounts: priorYearTax (last year's total tax, Form 1040 line 24) and priorYearAgi (for the 100%/110% safe harbor), otherPayments (estimated payments made from accounts Truenote can't see, this year's), priorYearPayments (of the IRS payments get_tax_projection found this year, the dollars that paid last year's return, a balance due or an extension payment, so they don't count toward this year), otherIncome (this year's interest, ordinary dividends, short-term gains and other ordinary income; negative for a net capital loss, up to 3000), capitalGains (this year's long-term capital gains and qualified dividends, taxed at 0/15/20%; both count toward the 3.8% net investment income tax), itemizedDeductions (used only when larger than the standard deduction), qualifyingChildren (children under 17 at year end; null: counted from the profile's household, a baby due by December 31 included) and notes. Only the fields you pass change; null clears. Per-job and per-business figures belong on the income source (upsert_income_source). Changes the person's data: say what you set.

Body (application/json):

- `year` (integer): Default: this one
- `priorYearTax` (number | null)
- `priorYearAgi` (number | null)
- `otherPayments` (number)
- `priorYearPayments` (number)
- `otherIncome` (number)
- `capitalGains` (number)
- `itemizedDeductions` (number | null)
- `qualifyingChildren` (integer | null)
- `notes` (string)

### POST /v1/tools/upsert_goal

Set a goal

Needs context:write. The same operation as the MCP tool upsert_goal; amounts in the body are dollars (650 for $650.00), ids come from the GET routes, and the answer is the tool's data with amounts in integer cents. Create or change a savings or payoff goal: by id, else by name (case-insensitive), else new (then kind and targetAmount are required). links are what counts toward it, each with the percent of it that counts: an account (accountId from list_accounts), a manual account (manualAccountId from list_accounts' manualAccounts: the house, an HSA) or a bucket (bucket: a name or id from list_buckets, for money kept in a savings bucket like an emergency fund); exactly one per link; giving links replaces them. Changes the person's data: say what you set.

Body (application/json):

- `id` (uuid)
- `name` (required) (string)
- `kind` (emergency_fund | savings | debt_payoff | purchase | tax_reserve | other)
- `targetAmount` (number): Dollars
- `targetDate` (string | null)
- `priority` (integer): 1 is the most important
- `notes` (string)
- `archived` (boolean)
- `links` (object[])

### POST /v1/tools/upsert_manual_account

Add or update a manual account

Needs context:write. The same operation as the MCP tool upsert_manual_account; amounts in the body are dollars (650 for $650.00), ids come from the GET routes, and the answer is the tool's data with amounts in integer cents. Something no bank connection reports, kept by hand: the house (asset, type real_estate) and its mortgage (liability, type loan), a car loan, an HSA, IRA or pension held elsewhere (type retirement), private company stock or a startup stake (type equity), cash. By id, else by name (any case), else new: a new one needs kind and balance. balance is dollars, positive: what it's worth, or what's owed on a liability; it's recorded on balanceOn (default today, never a day still to come) and replaces that day's entry, so call it again with a new balance to update the value. These count in net worth and appear in list_accounts' manualAccounts; a goal can link them (upsert_goal links with manualAccountId). archived: true hides one that's gone (a sold car). What you leave out stays. Truenote's records only. Tell the person what you set.

Body (application/json):

- `id` (uuid)
- `name` (required) (string)
- `kind` (asset | liability)
- `type` (string): real_estate, vehicle, loan, retirement, equity, cash, crypto, other
- `entityId` (uuid | null): Household (null) or a business
- `notes` (string)
- `archived` (boolean)
- `balance` (number)
- `balanceOn` (string): The day the balance is for (default today)

### POST /v1/tools/upsert_income_source

Set an income source

Needs context:write. The same operation as the MCP tool upsert_income_source; amounts in the body are dollars (650 for $650.00), ids come from the GET routes, and the answer is the tool's data with amounts in integer cents. Create or change an income source (a W-2 job, a 1099 client, a business, a bonus): by id, else by name, else new (then type and cadence are required). expectedAmount is take-home per payment (a paycheck's net pay). regular sources (salaries; the default for w2) are the spending plan's income; anything else (a bonus, 1099 work) is a windfall allocated when it lands. earner is who in the household earns it. taxSetAsidePct is the share of each payment to set aside for taxes (30 for 30%); payerMatch is text its deposits carry ("NORTHSTAR") so Truenote can find them. For the yearly tax projection (get_tax_projection), a W-2 job takes grossPay (this year's salary before deductions, bonus apart), retirement (401(k)/403(b) for the year), otherPretax (health, HSA, FSA through payroll for the year), withheldYtd with withheldAsOf (federal income tax withheld so far and the date of the paystub it's from, both or neither), withholdingPerPaycheck (federal tax withheld from each regular paycheck), bonus, bonusOn (paid or expected) and bonusWithholdingPct (default 22); a 1099 client or business takes receivedElsewhere (paid this year into accounts Truenote can't see), expectedRestOfYear (still expected this year; null: from expectedAmount) and expensesEstimate (deductible expenses for the year; null: its entity's spending). Fields you leave out keep their value; null clears. Changes the person's data: say what you set.

Body (application/json):

- `id` (uuid)
- `name` (required) (string)
- `type` (1099 | w2 | business | other)
- `cadence` (weekly | biweekly | semimonthly | monthly | quarterly | annually | irregular)
- `expectedAmount` (number | null): Dollars per payment
- `taxSetAsidePct` (number)
- `payerMatch` (string | null)
- `entityId` (uuid | null)
- `earner` (string | null): A household member's name
- `regular` (boolean): Counts toward the spending plan's income (default: true for w2)
- `active` (boolean)
- `notes` (string)
- `grossPay` (number | null): W-2: this year's salary, bonus apart
- `retirement` (number): W-2: 401(k)/403(b) deferrals for the year
- `otherPretax` (number): W-2: health, HSA, FSA through payroll, per year
- `withheldYtd` (number | null): W-2: federal income tax withheld so far this year (from a paystub)
- `withheldAsOf` (string | null): The paystub's date (with withheldYtd)
- `withholdingPerPaycheck` (number | null): W-2: federal income tax withheld per regular paycheck
- `bonus` (number): W-2: this year's bonus
- `bonusOn` (string | null): When the bonus was or will be paid
- `bonusWithholdingPct` (number): Default 22
- `receivedElsewhere` (number): 1099/business: paid this year into accounts Truenote can't see
- `expectedRestOfYear` (number | null): 1099/business: still expected this year (null: from expectedAmount)
- `expensesEstimate` (number | null): 1099/business: deductible expenses for the year (null: the entity's spending)

### POST /v1/tools/update_profile

Update the household profile

Needs context:write. The same operation as the MCP tool update_profile; amounts in the body are dollars (650 for $650.00), ids come from the GET routes, and the answer is the tool's data with amounts in integer cents. Change the profile every agent reads first: the person's name, household members (each with a relationship, and a birthDate or, for a baby on the way, a dueDate), state (postal code like TX), filing status, risk tolerance, the free-form "about us" and time zone. Only the fields you pass change. householdMembers replaces the whole list: read get_context first and send everyone, including the person themself (relationship self). Changes the person's data: say what you set.

Body (application/json):

- `name` (string): What to call the person
- `householdMembers` (object[])
- `state` (AL | AK | AZ | AR | CA | CO | CT | DE | DC | FL | GA | HI | ID | IL | IN | IA | KS | KY | LA | ME | MD | MA | MI | MN | MS | MO | MT | NE | NV | NH | NJ | NM | NY | NC | ND | OH | OK | OR | PA | RI | SC | SD | TN | TX | UT | VT | VA | WA | WV | WI | WY | null)
- `filingStatus` (single | married_joint | married_separate | head_of_household | qualifying_surviving_spouse | null)
- `riskTolerance` (conservative | moderate | aggressive | null)
- `about` (string)
- `timeZone` (string): IANA zone, e.g. America/Chicago

### POST /v1/tools/upsert_entity

Set an entity

Needs context:write. The same operation as the MCP tool upsert_entity; amounts in the body are dollars (650 for $650.00), ids come from the GET routes, and the answer is the tool's data with amounts in integer cents. Add or rename an entity money can belong to: the household (personal) or a business (an LLC, a side business). By id, else by name (case-insensitive), else new (then kind is required). Assign accounts and transactions to it with create_rule or categorize_transactions; each business's money is then reported on its own. Changes the person's data: say what you set.

Body (application/json):

- `id` (uuid): An existing entity to rename
- `name` (required) (string)
- `kind` (personal | business): personal or business

### POST /v1/tools/upsert_plan_line

Set a spending plan line

Needs context:write. The same operation as the MCP tool upsert_plan_line; amounts in the body are dollars (650 for $650.00), ids come from the GET routes, and the answer is the tool's data with amounts in integer cents. Add or change a line of the spending plan (read get_plan first): by id, else by name (case-insensitive), else new (then group and amount are required). group is a group's name or id; amount is dollars per payment, cadence how often (default monthly; a $139 yearly Amazon Prime is amount 139, cadence annually). endsOn is the last day it's paid (a car loan's final month); startsOn delays a line that hasn't begun (daycare from April). match is text its payment's bank description or merchant contains ("Valon" for the mortgage, "CoServ" for electricity): a line with match and no destination is a bill, paid when a payment out contains it (find the text with query_transactions or get_recurring). accountId (from list_accounts) is where saved money goes (a brokerage: its own deposits count); bucketId (from list_buckets) is the bucket it fills ("Vacation / Travel" → the Vacations bucket), which also sets the account. categoryId (from get_context) only breaks everyday spending down (Groceries, Dining). Anything you leave out stays as it is. To cancel something, delete_record with kind plan_line. Changes the person's data: say what you set.

Body (application/json):

- `id` (uuid)
- `name` (required) (string)
- `group` (string): A group's name or id
- `amount` (number): Dollars per payment
- `cadence` (weekly | biweekly | semimonthly | monthly | quarterly | annually)
- `startsOn` (string | null)
- `endsOn` (string | null): The last day it's paid
- `categoryId` (uuid | null)
- `accountId` (uuid | null)
- `bucketId` (uuid | null): A bucket it fills (from list_buckets); the line then goes to its account
- `match` (string | null): Text its payment's bank description or merchant contains; null clears it
- `notes` (string)

### POST /v1/tools/upsert_plan_group

Set a spending plan group

Needs context:write. The same operation as the MCP tool upsert_plan_group; amounts in the body are dollars (650 for $650.00), ids come from the GET routes, and the answer is the tool's data with amounts in integer cents. Add, rename or retarget a group of the spending plan: by id, else by name (case-insensitive), else new (then kind is required: spending for expenses and wants, saving for savings and investing). targetPct is the share of take-home it should hold (45 for 45%); the groups' targets usually add up to 100. Changes the person's data: say what you set.

Body (application/json):

- `id` (uuid)
- `name` (required) (string)
- `kind` (spending | saving)
- `targetPct` (number)
- `position` (integer): Order, 0 first

### POST /v1/tools/upsert_bucket

Set a bucket

Needs context:write, accounts:read. The same operation as the MCP tool upsert_bucket; amounts in the body are dollars (650 for $650.00), ids come from the GET routes, and the answer is the tool's data with amounts in integer cents. Create or change a bucket (a virtual part of a savings or checking account, see list_buckets): by id, else by name (case-insensitive; pass accountId when two accounts have a bucket with that name), else new (then accountId is required: a checking or savings account from list_accounts). target is the bucket's own goal in dollars and targetDate when it should be reached (both optional; null clears). startingBalance (new buckets only) is what it holds today; to correct an existing bucket's balance use record_bucket_entry with kind set_balance. overflow makes it the account's overflow bucket: what a deposit has left after filling what each bucket is due goes there (one per account; setting it moves it from another). archived hides a bucket once it's empty. This changes only Truenote's records, never the bank: no money moves. Say what you set.

Body (application/json):

- `id` (uuid)
- `name` (required) (string)
- `accountId` (uuid): The account it splits (required for a new bucket)
- `target` (number | null): Dollars
- `targetDate` (string | null)
- `notes` (string)
- `overflow` (boolean): Extra from deposits goes here
- `archived` (boolean)
- `startingBalance` (number): Dollars it holds today (new buckets only)

### POST /v1/tools/move_between_buckets

Move money between buckets

Needs context:write, accounts:read. The same operation as the MCP tool move_between_buckets; amounts in the body are dollars (650 for $650.00), ids come from the GET routes, and the answer is the tool's data with amounts in integer cents. Move an amount from one bucket to another bucket of the same account (by name or id from list_buckets), e.g. $500 from Emergencies to Vacations. Both buckets change in one step; the account's total and Unassigned stay the same (unless it takes the from bucket below zero: what it's short comes out of Unassigned). This is Truenote's bookkeeping only: no money moves at the bank. date defaults to today (it can't be in the future). Say what you moved.

Body (application/json):

- `from` (required) (string): A bucket's name or id
- `to` (required) (string): A bucket's name or id, in the same account
- `amount` (required) (number): Dollars
- `date` (string)
- `memo` (string)

### POST /v1/tools/record_bucket_entry

Record a bucket entry

Needs context:write, accounts:read. The same operation as the MCP tool record_bucket_entry; amounts in the body are dollars (650 for $650.00), ids come from the GET routes, and the answer is the tool's data with amounts in integer cents. Record money going into or out of a bucket (by name or id from list_buckets) in Truenote's ledger. kind: contribution (money put into the account for this bucket, e.g. a transfer to savings), withdrawal (money taken out of the account from this bucket), charge (spending paid from another account, like a vet bill on checking, that this bucket covers: it needs the transactionId, and the bucket drops now while the account's total drops when the money is moved back), or set_balance (a correction: the bucket holds exactly amount as of date). transactionId (from query_transactions) ties the entry to the bank transaction it accounts for. date defaults to today. This never moves money at the bank. When unsure which bucket something belongs to, ask the person rather than guess. Say what you recorded.

Body (application/json):

- `bucket` (required) (string): A bucket's name or id
- `accountId` (uuid): When two accounts have a bucket with that name
- `kind` (required) (contribution | withdrawal | charge | set_balance)
- `amount` (required) (number): Dollars (for set_balance: the balance it holds)
- `date` (string)
- `memo` (string)
- `transactionId` (uuid): The transaction it accounts for (needed for charge)

### POST /v1/tools/resolve_review_item

Decide a review item

Needs context:write, accounts:read, transactions:read. The same operation as the MCP tool resolve_review_item; amounts in the body are dollars (650 for $650.00), ids come from the GET routes, and the answer is the tool's data with amounts in integer cents. Decide an item from list_review_items for the person: decision approve does what its proposal says (or, with allocations, puts other amounts in other buckets: bucket names or ids, dollars; for a withdrawal, paysBack lists the charge entry ids it pays back instead of the proposal's), and createRule (a charge only, to one bucket) also saves the proposed bucket rule so the next match is automatic; decision dismiss leaves the transaction out of buckets for good (a deposit stays in Unassigned). Together the amounts can't be more than the transaction. Only decide what the person asked you to, or what's plainly right; otherwise ask. This is Truenote's bookkeeping: no money moves at the bank. Say what you decided.

Body (application/json):

- `id` (required) (uuid): The item's id from list_review_items
- `decision` (required) (approve | dismiss)
- `allocations` (object[]): Instead of the proposal's buckets and amounts
- `paysBack` (uuid[]): A withdrawal: the charge entry ids it pays back, instead of the proposal's
- `createRule` (boolean): A charge: also save the proposed bucket rule

### POST /v1/tools/create_bucket_rule

Create a bucket rule

Needs context:write, accounts:read. The same operation as the MCP tool create_bucket_rule; amounts in the body are dollars (650 for $650.00), ids come from the GET routes, and the answer is the tool's data with amounts in integer cents. A rule that charges matching spending to a bucket automatically ("Banfield" → Pets), for everything since the bucket's start and every sync after: textContains is matched against the merchant and the bank's description (any case); accountId (from list_accounts), categoryId (from get_context) and minAmount/maxAmount (dollars) narrow it. Money out only. The first matching rule wins; a transaction the person already decided (charged by hand, an entry deleted, a review item answered) is left alone, and nothing is ever charged twice. Returns how many it charged right away. Prefer a specific text (a merchant's name) over a broad one: a wrong automatic charge is worse than a review item. Truenote's bookkeeping only: no money moves.

Body (application/json):

- `bucket` (required) (string): A bucket's name or id from list_buckets
- `bucketAccountId` (uuid): When two accounts have a bucket with that name
- `textContains` (required) (string)
- `accountId` (uuid): Only spending on this account
- `categoryId` (uuid): Only spending in this category
- `minAmount` (number)
- `maxAmount` (number)

### POST /v1/tools/add_note

Remember a fact

Needs context:write. The same operation as the MCP tool add_note; amounts in the body are dollars (650 for $650.00), ids come from the GET routes, and the answer is the tool's data with amounts in integer cents. Save a dated fact or decision for every agent to know later ("Lease ends 2027-06-30", "Decided to pay the Visa in full monthly"). Short tags (lowercase words) help find it. Use for durable context, not for chat history.

Body (application/json):

- `body` (required) (string)
- `occurredOn` (string | null): The day it's about, if any
- `tags` (string[])
- `pinned` (boolean): Always shown first in get_context

### POST /v1/tools/update_note

Change a note

Needs context:write. The same operation as the MCP tool update_note; amounts in the body are dollars (650 for $650.00), ids come from the GET routes, and the answer is the tool's data with amounts in integer cents. Change a note (an id from get_context): fix or rewrite its body when a fact or decision changes, re-date it, retag it, or pin or unpin it (pinned notes always come first in get_context). What you leave out stays. Prefer this over adding a second note that contradicts the first. Changes the person's data: say what you changed.

Body (application/json):

- `id` (required) (uuid)
- `body` (string)
- `occurredOn` (string | null): The day it's about; null: none
- `tags` (string[]): Replaces the note's tags
- `pinned` (boolean)

### POST /v1/tools/upsert_task

Add or change a checklist task

Needs context:write. The same operation as the MCP tool upsert_task; amounts in the body are dollars (650 for $650.00), ids come from the GET routes, and the answer is the tool's data with amounts in integer cents. Add a to-do to the person's checklist, or change one: by id, else by title (any case), else new. Use it for the dated actions a plan leaves behind ("Exercise options before the 409A", due 2026-11-06), one task per action, with a due date when there's a deadline. kind event is a dated thing that happens rather than something to do ("Brooke's due date", "JJ back to work", "Closing on the house"): it needs dueOn and has no checkbox. amount (dollars) is what it costs or brings when it's about money (the $15,000 a payment needs; it reads back as sum). done: true ticks it off, false opens it again. goalId (from get_context) or bucket (a name or id from list_buckets) say what it serves; null unlinks. spotlight: true makes it the Overview's countdown, at the top of the person's home page; there is only one, so this replaces the current one (list_tasks shows which). Spotlight the one dated event that matters most to the household's money in the coming months (a birth, a move, a wedding, a job change, a big payment being saved for), not routine to-dos; the person can change it. What you leave out stays. Truenote's records only: it never acts on the task. Tell the person what you added or changed.

Body (application/json):

- `id` (uuid)
- `kind` (task | event): task (default) or event
- `title` (required) (string)
- `dueOn` (string | null): The day it's due (an event's day); null: no date
- `notes` (string)
- `done` (boolean)
- `amount` (number | null): Dollars; null clears it
- `spotlight` (boolean): The Overview's countdown; replaces the current one
- `goalId` (uuid | null)
- `bucket` (string | null): A bucket's name or id
- `bucketAccountId` (uuid): When two accounts have a bucket with that name

### POST /v1/tools/save_plan

Save a plan

Needs context:write. The same operation as the MCP tool save_plan; amounts in the body are dollars (650 for $650.00), ids come from the GET routes, and the answer is the tool's data with amounts in integer cents. Save a named scenario for the person to review: a markdown body and structured assumptions (numbers, dates, lists) such as a parental-leave budget or a tax plan. By id, else by name, else new (status draft). Saving again with the same name updates it.

Body (application/json):

- `id` (uuid)
- `name` (required) (string)
- `summary` (string)
- `body` (string): Markdown
- `assumptions` (object)
- `status` (draft | active | archived)

### POST /v1/tools/categorize_transactions

Recategorize transactions

Needs context:write, transactions:read. The same operation as the MCP tool categorize_transactions; amounts in the body are dollars (650 for $650.00), ids come from the GET routes, and the answer is the tool's data with amounts in integer cents. Set the category, entity (household or a business) or excluded flag of up to 200 transactions at once (ids from query_transactions, category and entity ids from get_context), and/or a note. A category, entity or exclusion set here wins over rules for those transactions. All of them change or none do. To fix a whole merchant going forward, use create_rule instead.

Body (application/json):

- `transactionIds` (required) (uuid[])
- `categoryId` (uuid | null): null: back to the automatic category
- `entityId` (uuid | null)
- `excluded` (boolean): true: leave out of spending and income
- `note` (string | null)

### POST /v1/tools/create_rule

Create a categorization rule

Needs context:write, transactions:read. The same operation as the MCP tool create_rule; amounts in the body are dollars (650 for $650.00), ids come from the GET routes, and the answer is the tool's data with amounts in integer cents. A rule that sets the category, entity or excluded flag of every matching transaction, past and future (except ones edited by hand), and/or a business share: businessShare splits each match, percent of it going to a business entity as a part of its own (the rest stays the household's), for a bill partly used for business (internet 60% to the LLC). Conditions all must hold: merchant or description contains text, an account, an amount range in dollars (by size), a direction (outflow is money out). Returns how many transactions it now decides. Prefer this over categorize_transactions or split_transaction for anything recurring.

Body (application/json):

- `name` (required) (string)
- `conditions` (required) (object)
- `setCategoryId` (uuid | null)
- `setEntityId` (uuid | null)
- `setExcluded` (boolean | null)
- `businessShare` (object | null)
- `priority` (integer): Lower runs first

### POST /v1/tools/split_transaction

Split a transaction

Needs context:write, transactions:read. The same operation as the MCP tool split_transaction; amounts in the body are dollars (650 for $650.00), ids come from the GET routes, and the answer is the tool's data with amounts in integer cents. Split one transaction (an id from query_transactions; a part's id means its whole) into parts that add up to it exactly, each with its own category, entity and note: the business part of a mixed purchase, a bill shared with the LLC. amount is each part's size in dollars (the whole's direction is kept). The parts count in place of the whole everywhere (spending, the plan, each business's taxes), and query_transactions lists them instead (with splitOf). Replaces any split it had; rules leave it alone afterwards. unsplit: true puts it back together. For a recurring share, use create_rule's businessShare instead. Changes the person's records only: say what you split.

Body (application/json):

- `transactionId` (required) (uuid)
- `parts` (object[])
- `unsplit` (boolean)

### POST /v1/tools/prepare_upload

Prepare a document upload

Needs documents:write. The same operation as the MCP tool prepare_upload; amounts in the body are dollars (650 for $650.00), ids come from the GET routes, and the answer is the tool's data with amounts in integer cents. Get a one-time link to keep one file (a paystub, W-2, 1099, 1098, tax return, statement, receipt; PDF or image, 10 MB at most) in the person's documents. Returns curl, a command that sends a local file straight to Truenote (replace FILE with the path; the file never passes through you), and uploadUrl, a page where the person drops the file themselves (for when you can't reach their files). Say what it is: kind, taxYear, documentOn, and the incomeSourceId a paystub is for (from get_context); name, kind and taxYear left out are guessed from the file's name. One call per file; the link lasts 15 minutes. Then read_document can read it.

Body (application/json):

- `name` (string): Default: the file's name
- `kind` (paystub | w2 | form_1099 | form_1098 | tax_return | benefits | statement | receipt | other)
- `taxYear` (integer)
- `documentOn` (string): The date on the document (a paystub's pay date)
- `entityId` (uuid): Whose it is (default: the household)
- `incomeSourceId` (uuid): The income source a paystub or W-2 is for
- `accountId` (uuid): The connected account a statement is of
- `manualAccountId` (uuid): The manual account (list_accounts) it's about: the mortgage, the HSA
- `notes` (string)

### POST /v1/tools/update_document

Edit a document

Needs documents:write. The same operation as the MCP tool update_document; amounts in the body are dollars (650 for $650.00), ids come from the GET routes, and the answer is the tool's data with amounts in integer cents. Change what a kept document is and whose (an id from list_documents): name, kind, taxYear, documentOn (the date on it, e.g. a statement's date), entityId, incomeSourceId (the job a paystub or W-2 is for), accountId (a connected account a statement is of), manualAccountId (a manual account it's about, like the mortgage), notes. Fields you leave out keep their value; null clears. The file itself never changes, and documents can't be deleted here. Changes the person's records: say what you changed.

Body (application/json):

- `id` (required) (uuid)
- `name` (string)
- `kind` (paystub | w2 | form_1099 | form_1098 | tax_return | benefits | statement | receipt | other)
- `taxYear` (integer | null)
- `documentOn` (string | null)
- `entityId` (uuid | null)
- `incomeSourceId` (uuid | null)
- `accountId` (uuid | null)
- `manualAccountId` (uuid | null)
- `notes` (string)

### POST /v1/tools/delete_record

Delete a record

Needs context:write. The same operation as the MCP tool delete_record; amounts in the body are dollars (650 for $650.00), ids come from the GET routes, and the answer is the tool's data with amounts in integer cents. Delete one of the person's records by kind and id: goal, income_source, entity (its accounts and transactions go back to the household; rules that set it go too), category (transactions in it fall back to Plaid's mapping; rules that set it go too), rule (what it decided goes back to the other rules), manual_account (with its balance history), plan, plan_group (only once it has no lines), plan_line (something canceled or entered by mistake; for one that ends later, set its endsOn with upsert_plan_line so the plan shows what frees up), task (to finish one, upsert_task with done: true instead), note (when a decision changes, add the new note, then delete the old one so agents don't read both), bucket (its money goes back to Unassigned; no money moves), bucket_rule, bucket_entry (undoes one ledger entry: a move's both sides; an automatic one isn't redone; an archived bucket's entries stay; needs accounts:read). Ids come from get_context, get_plan, list_tasks, list_accounts, list_buckets and list_bucket_rules. Documents use delete_document. It can't be undone: confirm with the person first (a note or entry you made in this conversation excepted), then say what you deleted.

Body (application/json):

- `kind` (required) (goal | income_source | entity | category | rule | manual_account | plan | plan_group | bucket | bucket_rule | plan_line | task | note | bucket_entry)
- `id` (required) (uuid)

### POST /v1/tools/delete_document

Delete a document

Needs documents:write. The same operation as the MCP tool delete_document; amounts in the body are dollars (650 for $650.00), ids come from the GET routes, and the answer is the tool's data with amounts in integer cents. Permanently delete an uploaded document (an id from list_documents): the file and its text. Use when the person asks, or for a duplicate or wrong upload. It can't be undone: confirm with the person first.

Body (application/json):

- `id` (required) (uuid)

### POST /v1/tools/upsert_category

Add or change a category

Needs context:write. The same operation as the MCP tool upsert_category; amounts in the body are dollars (650 for $650.00), ids come from the GET routes, and the answer is the tool's data with amounts in integer cents. Add a spending category, or rename one, change its kind (expense, income, transfer: transfers don't count as spending) or nest it under a parent (parentId, null: top level): by id, else by name (any case), else new (then kind is required). Which of the bank's categories land in it stays the person's to set in Truenote. Then categorize_transactions or create_rule to fill it. Say what you changed.

Body (application/json):

- `id` (uuid)
- `name` (required) (string)
- `kind` (expense | income | transfer)
- `parentId` (uuid | null)

### POST /v1/tools/update_rule

Change a categorization rule

Needs context:write, transactions:read. The same operation as the MCP tool update_rule; amounts in the body are dollars (650 for $650.00), ids come from the GET routes, and the answer is the tool's data with amounts in integer cents. Change a rule (an id from get_context): its name, conditions (given conditions replace them all; amounts in dollars by size), what it sets (category, entity, excluded; null clears), its businessShare (null removes it), priority (lower runs first) or active (false pauses it). What you leave out stays. Saving re-decides every transaction nobody edited by hand. To remove a rule, delete_record with kind rule.

Body (application/json):

- `id` (required) (uuid)
- `name` (string)
- `conditions` (object)
- `setCategoryId` (uuid | null)
- `setEntityId` (uuid | null)
- `setExcluded` (boolean | null)
- `businessShare` (object | null)
- `priority` (integer)
- `active` (boolean)

### POST /v1/tools/update_account

Change a bank account's settings

Needs context:write, accounts:read. The same operation as the MCP tool update_account; amounts in the body are dollars (650 for $650.00), ids come from the GET routes, and the answer is the tool's data with amounts in integer cents. Change how Truenote treats a connected account (an id from list_accounts): nickname (null: the bank's name again), hidden (true drops it out of totals, net worth and this connection; only the person can show it again unless this connection sees hidden accounts) or entityId (the business it belongs to; null: the household). Never touches the bank. Say what you changed.

Body (application/json):

- `accountId` (required) (uuid)
- `nickname` (string | null)
- `hidden` (boolean)
- `entityId` (uuid | null)

### POST /v1/tools/reset_transactions

Undo transaction edits

Needs context:write, transactions:read. The same operation as the MCP tool reset_transactions; amounts in the body are dollars (650 for $650.00), ids come from the GET routes, and the answer is the tool's data with amounts in integer cents. Undo hand edits on transactions (ids from query_transactions): their category, entity, exclusion and any hand split go back to what the rules decide. Notes stay. Use when an earlier categorize_transactions or split_transaction was wrong.

Body (application/json):

- `transactionIds` (required) (uuid[])

### POST /v1/tools/refresh_bank

Refresh a bank connection

Needs context:write, accounts:read. The same operation as the MCP tool refresh_bank; amounts in the body are dollars (650 for $650.00), ids come from the GET routes, and the answer is the tool's data with amounts in integer cents. Ask the bank for what's new on the connection holding an account (an id from list_accounts): balances and transactions, in the background (a minute or two). Use when the person says something posted that Truenote doesn't show yet. A connection that needs the person to sign in again can't be refreshed: tell them to reconnect it in Truenote.

Body (application/json):

- `accountId` (required) (uuid)

### POST /v1/tools/disconnect_bank

Disconnect a bank

Needs context:write, accounts:read. The same operation as the MCP tool disconnect_bank; amounts in the body are dollars (650 for $650.00), ids come from the GET routes, and the answer is the tool's data with amounts in integer cents. Remove the bank connection holding an account (an id from list_accounts): Truenote stops syncing it, and every account on that connection and its history are deleted from Truenote. Truenote is read-only to banks: nothing changes at the bank itself. Connecting it again means the person signs in to the bank in Truenote. It can't be undone: confirm with the person first, naming every account it removes.

Body (application/json):

- `accountId` (required) (uuid)
