API Docs¶
BraatzBooks API, version v1. 32 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
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:
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, auth (which credential let you in), and the visible_from floor with vault_rows/visible_rows beside it -- every number here is over what this face SERVES, never over the whole vault
Parameters: —
GET /api/v1/visibility¶
MCP tool: get_visibility
the tenant's floor and what it subtracts: visible_from (stored at signup, never moves), tier, window_hours (the free tier's rolling window in hours -- 48 -- or null), effective_from (whichever floor subtracts more), oldest_visible_row, vault_rows (everything C holds) and visible_rows (what this face serves). The vault keeps the whole initial pull and nothing is deleted to shrink a view; an empty visible_from with a null window_hours means this tenant has no floor and sees all of it
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_onto — ISO date, inclusive upper boundcat — exact category label; 'uncategorised' for the queueq — substring of the descriptionpage — 1-basedper_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 twinsrc_id (in path) — the line's id on that side
GET /api/v1/transactions/{side}/{src_id}/why¶
MCP tool: explain_transaction
why this line says what it says, in one templated sentence, with the citations it was built from -- which rule (and its revision), which model and how sure, which human and when, the duh band, and the pocket. citations is the evidence and sentence is only its rendering: a caller that wants to disagree should read the citations. engine names what decided the line; defect is non-empty when these books hold NO citation for it, which is a REPORTED gap and never a generated reason. 404 when this tenant holds no such line; 404 with a named hint when this deployment's tap does not request the read
Parameters: side (in path) one of A, B — A = the feed's line, B = its archived twinsrc_id (in path) — the line's id on that side
POST /api/v1/transactions/{side}/{src_id}/label¶
MCP tool: label_transaction
"that's wrong" -- correct ONE line and have the correction round-trip to a labelled training row in the same breath. ONE ledger.labels row AND the set_category op are written as ONE database fact, so a label without a moved line cannot exist; the OLD category is derived from the ops ledger rather than taken from this caller, because asking the thing being corrected what it had wrong is how a training row gets a fictional "before". Returns the r_ops seq that MOVED the line. 409 on side B: an archived duplicate arrival carries no category to correct -- correct its original instead
Parameters: side (in path) one of A, B — A = the feed's line; B is 409src_id (in path) — the line's id on that sidecategory required — the category the books SHOULD have given this linereason — why, in the tenant's own words -- stored on the label row as provenance
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/meter¶
MCP tool: get_meter
the cost meter, one row per calendar month, newest first: calls (tail calls, abstentions included -- an unsure answer still crossed the wire and still cost money), millicents, unpriced_calls (calls whose cost was absent or unparseable, EXCLUDED from millicents and reported rather than dropped), lines_resolved_local against lines_sent_to_api (the ratio a plan is priced on), and the cap: cap_tier, cap_millicents_month, cap_source and the computed mode. mode is null when this vault knows no cap, interactive under it and batched at or over it -- null is NOT 'under the cap', because an unknown cap reported as healthy is the one lie this meter exists to refuse
Parameters: months — how many months back, newest first, default 36, capped at 120
GET /api/v1/messages¶
MCP tool: get_messages
this tenant's own thread with the business, newest first: direction (in what they texted, out what was texted back), channel, status, body, provider_sid (the carrier's own id for an out row, empty when there is not one yet) and created_at. DRAFTS ARE NOT HERE and cannot be: a reply is invisible until a human releases it, so this face can never show a customer words nobody chose to send. count is the number of rows IN THIS DOCUMENT and not the lifetime total, so a truncated read is distinguishable from a short thread by comparing it against the limit asked for
Parameters: limit — how many messages back, newest first, default 50; the ledger ceilings it at 200 in its own body, so a larger ask is clamped rather than honoured
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 decidesas_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 BraatzBooks'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 -> transactionbraatz — optional, and an ASSERTION rather than an input: this surface recomputes its own side and 409s on a disagreementas_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, Bsrc_id required — the line's id on that sidecategory 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 idrule_key required — your name for the rule; a new revision if it existsmatch_kind required one of description_exact, description_regex, amount_signmatch_value requiredcategory requiredpriority — 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 itheader 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 existsmatch_kind required one of description_exact, description_regex, amount_signmatch_value requiredcategory requiredpriority — 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 four URLs the same books are downloaded from (transactions.csv, accounts.csv, and the QuickBooks pair: quickbooks-bank.csv for QuickBooks Online's Upload from file, quickbooks.iif for QuickBooks Desktop, which is the one that carries the categories), 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: —
POST /api/v1/auditor/grants¶
MCP tool: offer_auditor_grant
offer these books to a named person: files a grant request and TEXTS A SIX-DIGIT CODE TO THE CUSTOMER'S OWN ENROLLED PHONE (never to the viewer), returning only request_id -- the code is confirmed back at POST /api/v1/auditor/grants/{request_id}/confirm, which is what actually mints the grant. SESSION ONLY: a grant is offered by the customer, signed in, so a bearer token (and therefore this MCP tool) is refused 403 -- an auditor that could mint its own grant would renew itself forever. 503 when this deployment holds no outbound SMS credential, checked BEFORE the row is filed
Parameters: viewer_name required — who is being let in, as the customer's own audit list will show them foreverviewer_channel required — the viewer's phone in E.164 (+15551234567) -- it is half the code digest's salt, so a number spelled one way here and another at confirm makes a RIGHT code fail as a wrong one
POST /api/v1/auditor/grants/{request_id}/confirm¶
MCP tool: confirm_auditor_grant
spend the texted code and mint the grant: 200 carries grant (the live row, with grant_id) and token, an auditor-scope credential good for 034's 24 hours and no longer, which reads GET /api/v1/auditor/view and nothing else. 404 names a request id that does not exist; a 403 names the vault's own reason (wrong code, too many attempts, expired request) rather than borrowing the 404. SESSION ONLY: the code went to the customer's phone and only the customer spends it
Parameters: request_id (in path) — the id POST /api/v1/auditor/grants returnedcode required — the six digits texted to the customerviewer_name required — the same name the request was filed under
POST /api/v1/auditor/grants/{grant_id}/revoke¶
MCP tool: revoke_auditor_grant
take a grant back: the row is stamped revoked_at and every later read through it is refused BY NAME forever (a revoked grant is never resurrected -- a new look needs a new grant). It is a POST and not a DELETE on purpose: this server has do_GET and do_POST and nothing else, so a DELETE would have put the customer's most safety-critical control on the only path with none of _api_write's seven walls behind it. SESSION ONLY: only the customer takes a grant back
Parameters: grant_id (in path) — the grant to revoke, as GET /api/v1/auditor/grants lists it
GET /api/v1/auditor/view¶
MCP tool: get_auditor_view
the accountant's own read of the books under a live grant: months (calendar-month totals off the one relation the dashboard reads), pocket_flagged (the owner add-back list) and receipts (the provider's verbatim record), composed under one identity and STAMPING its own use so the customer's list can show when the grant was last read. THE GRANT ID COMES OFF THE TOKEN'S LABEL, never off a query string -- a token that could name its own grant could name somebody else's -- so this takes no params and answers 403 to any credential that is not an auditor grant, including the bearer PAT the MCP face carries. An EXPIRED or REVOKED grant is a 403 naming why and never a 404: 'no such grant' and 'your grant died' are different sentences to the person holding the link
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