Skip to content

API Docs

braatzledger API, version v1. 23 endpoints, listed below and machine-readable at /openapi.json (OpenAPI 3.1).

One list, no second

This page, /openapi.json and the MCP tool set are all generated from the same endpoint index the server serves at /api/v1. None of them can name an endpoint the others don't.

Authentication

Authorization: Bearer -- mint one at /settings (one year, read-only or read-write, revocable there); a browser session cookie from /signin also works

Writes

POST with Content-Type: application/json; a read-only token is refused (403); the same capability functions the rooms call, the actor recorded as human: (session) or token::

Endpoints

GET /api/v1/me

MCP tool: get_me

the books' summary: lines, booked, archived, pending (listed, not yet posted, in no total), total_cents, by_category, and auth (which credential let you in)

Parameters:

GET /api/v1/transactions

MCP tool: list_transactions

rows of C, paged; total_cents is over the WHOLE matched set, never the page; each row carries in_books, exclude_reason and state (in books | pending | superseded | retracted | archived) -- a pending line is listed and in no total

Parameters: from — ISO date, inclusive lower bound on posted_on
to — ISO date, inclusive upper bound
cat — exact category label; 'uncategorised' for the queue
q — substring of the description
page — 1-based
per_page — rows per page, capped at 100

GET /api/v1/transactions/{side}/{src_id}

MCP tool: get_transaction

one line, its REQ-8 lineage in words, and for an A line the provider's verbatim record (raw)

Parameters: side (in path) one of A, B — A = the feed's line, B = its archived twin
src_id (in path) — the line's id on that side

GET /api/v1/accounts

MCP tool: list_accounts

the sub-accounts the feed has delivered lines for; pending is counted from the BOOKS (lines the feed's own judgement still holds as pending), so a line that has since posted is not in it

Parameters:

GET /api/v1/sync-history

MCP tool: list_sync_history

bank-sync runs, newest first

Parameters: limit — runs to return, capped at 200

GET /api/v1/rules

MCP tool: list_rules

the live rulebook, retired revisions included, in the order it DECIDES: the bottom row wins when two rules match a line (priority descending -- the matcher's walk, reversed)

Parameters:

GET /api/v1/exceptions

MCP tool: list_exceptions

lines no live rule and no live op has explained

Parameters:

GET /api/v1/categories

MCP tool: list_categories

the tenant's own categories as the books use them, each with its booked total_cents and line count, most money first; 'uncategorised' is the queue (there is no house chart of accounts here: a label exists once a line carries it)

Parameters:

GET /api/v1/taxonomy

MCP tool: list_category_taxonomy

the tenant's category TAXONOMY as the New category dialog defines it -- name, cat_key, header_key, header_name and the header's sort_order -- live revisions only (a retired category is one the books have stopped deriving, and offering it back is offering a word the taxonomy has retracted), ordered by header then name so it renders the same way twice. This is what create_category and create_categories write into, and it answers whether or not any line carries the label yet; list_categories is the BOOKS' aggregate and answers only labels lines actually carry, with their totals. 404 when this deployment's tap does not request the categories read

Parameters:

GET /api/v1/usage

MCP tool: get_usage

the two meters a plan would bill on (inventory E4): bank connections in use against the cap this surface enforces, and transaction volume over a trailing window -- the LARGER of money in and money out, never their sum and never the net, which is the basis the meter measured on 2026-09-01 uses

Parameters: days — trailing window in days, default 30, capped at 365

GET /api/v1/reports/pnl-rolling

MCP tool: get_pnl_rolling

the Daily Rolling P&L as STRUCTURE, not prose: two windows (7d, 30d), each with the same six sections (revenue, cogs, gross_margin, biz_expenses, net_income, owners_draw) carrying current and prior, their category lines, and every booked transaction that contributed to each line. gross_margin and net_income are DERIVED (derived: true) and re-carry their components' lines with the sign each component enters with, so the twelve section current numbers are what a coefficient compares -- do not also sum the derived sections' lines. lineage_basis says whether the paper-verified lineage was readable (it is unreadable from this door: v_c_ledger does not carry reconciled_state, which changes no section number); lists_source names which category lists classified the rows

Parameters: tenant — must be the credential's own tenant; omit it and the credential decides
as_of — ISO date the two windows end on, in America/Chicago; default today

POST /api/v1/reports/pnl-parity

MCP tool: record_pnl_parity

grade the DIY n8n oracle's Daily Rolling P&L against braatzledger's own and file ONE deterministic verdict for the day: the difference coefficient over the twelve section numbers, the per-section deltas, and the localization of every delta down to the transactions that explain it (RECATEGORIZED / AMOUNT_DIFF / WINDOW_EDGE / MISSING_ON_BRAATZ / MISSING_ON_ORACLE). POST oracle alone and this surface computes ITS OWN side from its own books -- our numbers are not something a caller gets to supply. braatz may be sent anyway, and is then CONFIRMED rather than used: if its twelve section numbers disagree with what these books say, the request is 409 and nothing is filed. A second POST for the same day UPSERTS and returns a higher revision

Parameters: oracle required — the ORACLE's STRUCTURED report -- what GET /api/v1/reports/pnl-rolling emits in shape, computed by the n8n workflow. A rendered card or the n8n node output is 400: the comparator needs section -> category -> transaction
braatz — optional, and an ASSERTION rather than an input: this surface recomputes its own side and 409s on a disagreement
as_of — ISO date the windows end on; default the oracle report's own as_of, else today in America/Chicago

POST /api/v1/categorize

MCP tool: categorize

set one line's category -- an APPENDED judgement in the op ledger, never an edit; 'uncategorized' unsets; the archive side (B) is refused by the ledger

Parameters: side required one of A, B
src_id required — the line's id on that side
category required

POST /api/v1/adjudicate

MCP tool: adjudicate

resolve ONE exception and mint a versioned rule from it (REQ-8): the line gets its category and every future match is decided by the rule

Parameters: a_id required — the exception's A-side id
rule_key required — your name for the rule; a new revision if it exists
match_kind required one of description_exact, description_regex, amount_sign
match_value required
category required
priority — lower decides first; default 100

POST /api/v1/run-rules

MCP tool: run_rules

run every live rule over the uncategorised lines; returns how many it decided (a second run decides 0 -- idempotent by construction)

Parameters:

POST /api/v1/move-rule-to-end

MCP tool: move_rule_to_end

make ONE rule the decider for every line it matches (fintable's 'move to end'): an APPENDED revision whose priority is below every other live rule's, never an edit; a rule already deciding appends nothing; touches no line already decided -- the next run-rules is where it shows

Parameters: rule_key required — the rule's key as list_rules shows it

POST /api/v1/retire-rule

MCP tool: retire_rule

switch ONE rule off (fintable's 'Delete this rule'): an APPENDED revision with retired=true, never a delete -- the transactions it already decided KEEP their categories, it decides no further lines, and the retired revision stays in list_rules so "we turned it off" never looks like "there was never a rule"

Parameters: rule_key required — the rule's key as list_rules shows it

POST /api/v1/new-category

MCP tool: create_category

add ONE category to the tenant's taxonomy under an existing header (fintable's New category dialog): an appended definer call, the ledger's own refusal relayed verbatim when the name already lives under another header; 404 when this deployment's tap does not request the verb

Parameters: name required — the category as the books will spell it
header required one of cogs, expense, income, draw, transfer — the header key it groups under

POST /api/v1/new-categories

MCP tool: create_categories

add SEVERAL categories in one call (fintable's create_bulk_categories): every row is validated BEFORE anything is written, then appended in order -- the response carries created (the rows that landed, with their ids) and stops at the first ledger refusal, which it names

Parameters: categories required — objects with name and header, as POST /api/v1/categories takes them, at most 100

POST /api/v1/create-rule

MCP tool: create_rule

file a rule with NO transaction to hang it on (fintable's Create rule; G1's ledger.create_rule): it decides every matching line nobody has already decided, and every future match -- born_from_a_id NULL, which adjudicate can never produce; an APPENDED revision when the key already exists, never an edit (ruling #135)

Parameters: rule_key required — your name for the rule; a new revision if it exists
match_kind required one of description_exact, description_regex, amount_sign
match_value required
category required
priority — lower decides first; default 100

POST /api/v1/create-rules

MCP tool: create_rules

file SEVERAL rules in one call (fintable's create_bulk_rules): every row is validated BEFORE anything is written, then appended in order -- the response carries created and stops at the first ledger refusal, which it names

Parameters: rules required — objects with rule_key, match_kind, match_value, category and optional priority, as POST /api/v1/rules takes them, at most 100

POST /api/v1/export

MCP tool: export_books

push the books to their destination (fintable's Force Re-Sync / resync_spreadsheets): when a spreadsheet destination is connected this APPENDS a resync op against it and returns the op id; otherwise it renders the CSV bundle ledgerExport ships and returns its receipt -- the row counts and the two URLs the same books are downloaded from, plus destinations: known when the destination list was read and found empty, unreadable when it could not be read at all -- the CSV bundle is real either way, but only known means nothing was connected. Booked lines only; pending is never exported

Parameters: destination_id — which connected destination to re-sync; the newest live one by default

POST /api/v1/sync-now

MCP tool: sync_now

ask for a bank sync now: 202 with state recorded|already-pending (one request per tenant at a time), 409 when nothing is connected; it is a REQUEST the watcher consumes -- list_sync_history is what says it ran

Parameters:

In build now

Named rather than drawn, so the list above is only what answers today:

  • public data API
  • OAuth app registry
  • disconnect a bank -- one-way at the provider, so it stays the Accounts room's two-click form and is not an API verb