Books
Package books is double-entry accounting: chart of accounts, ledger, bank reconciliation, and the reports that prove the books balance.
Package books is double-entry accounting: chart of accounts, ledger, bank reconciliation, and the reports that prove the books balance.
| Base URL | https://api.hanzo.ai |
| Operations | 25 |
| Auth | Authorization: Bearer $HANZO_API_KEY |
books
GET /v1/books/accounts
Returns the org's chart of accounts — the seeded fixed chart every posting key in the ledger refers to.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
sandbox | query | string | — | Sandbox reads the org's SANDBOX ledger when it is exactly "true"; anything else reads the live one. |
POST /v1/books/ask
Answers a plain-language question about the caller's own books — "what is my MRR?", "how long is my runway?" — with figures taken from their ledger, never a guessed number. A deterministic keyword router picks the intent and reads the real metrics, and those figures, followups and report sources are computed BEFORE any model call and are never altered by one: the optional narration seam only rephrases the sentence, and it degrades silently to the templated answer when no AI plane is wired. It is strictly read-only — it restates the books, it never posts to them.
Request body — application/json (required)
| Field | Type | Required | Description |
|---|---|---|---|
from | string | — | From is the RFC3339 start of the metric window. |
question | string | — | Question is the plain-language question about the org's books, e.g. "what is my MRR?". |
to | string | — | To is the RFC3339 end of the metric window. |
POST /v1/books/bank/exchange
Finish connecting a bank account (not yet available)
ANSWERS 501 UNCONDITIONALLY. It is the intended second hop of the bank-linking handshake — trade the provider's short-lived public token for the durable access credential and seal that credential into KMS — and nothing on the HTTP path reaches an implementation today.
The durable bank credential is the reason this hop exists: it is meant to be sealed server-side and never handed back to the caller. Since the route never succeeds, no credential is stored by it and no bank is connected through it.
Documented as refusing rather than declared with a success body, for the same reason as the first hop: it has never sent one, and stating a shape it has never produced would put a return type in every SDK for a call that always fails. A caller with no principal gets 401 before the 501.
POST /v1/books/bank/import
Import a bank statement file into your books
Takes a bank statement as RAW BYTES — the file exactly as downloaded, OFX, QFX or CSV, not wrapped in JSON — parses every row, books it against the caller org's own ledger, and answers the tally: how many rows were seen, how many vouchers posted, how many inflows reconciled, how many raised a question, how many were own-account transfers, and how many were skipped.
RE-IMPORTING THE SAME STATEMENT DOES NOT DOUBLE-BOOK. Every row goes through the same posting choke point every other source uses, keyed idempotently, so an overlapping statement — the usual case, since exports overlap at the month boundary — lands its new rows and counts the rest as skipped. Skipped is the number to read on a second import.
It is READ-ONLY against the bank: this ingests, it never sends money. Scoped to the caller's own org from the validated principal, and refused without one; sandbox=true writes the org's sandbox ledger instead of its real books. An empty body is a 400, and a file the parser cannot read is a 400 carrying the parser's reason rather than a partial import. On a deployment whose import parser is not built, this answers 501 rather than mishandling the file.
POST /v1/books/bank/sync
Pulls every connected bank (Plaid/Teller) for the caller's org, maps each fetched transaction to a posting and books it idempotently, then advances that connector's cursor so the next sync resumes where this one stopped. One connector's outage is skipped rather than failing the whole sync. It reports the batch: how many transactions were seen, how many vouchers posted, how many inflows reconciled against the processor clearing account, how many raised a question, how many were own-account transfers, and how many were already-processed no-ops. It is READ-ONLY against the bank — it ingests, it never sends money.
POST /v1/books/bank/token
Begin connecting a bank account (not yet available)
ANSWERS 501 UNCONDITIONALLY. It is the intended first hop of the bank-linking handshake — mint the short-lived session token a browser hands to the provider's link widget — and nothing on the HTTP path reaches an implementation today.
The connectors behind it are written and tested; only the wiring is missing, so an org cannot connect a bank through the API at all. Until that lands, bank data reaches the books by statement import.
It is documented as refusing rather than declared with a success body precisely because it has never sent one. A response schema here would be invention: every generated SDK would carry a return type for a call that has only ever failed. A caller with no principal gets 401 before the 501.
GET /v1/books/bank/transactions
Returns the org's normalized bank transactions, newest first — every row the import and connector paths have ingested, with its amount in exact cents, its direction, and whether it has been matched to a voucher yet.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
sandbox | query | string | — | Sandbox reads the org's SANDBOX ledger when it is exactly "true". |
limit | query | integer | — | Limit caps how many rows come back; 500 when absent or not positive. |
GET /v1/books/bank/unreconciled
Returns the org's unmatched bank inflows and their open clarifying questions — the queue a human answers so an unexplained deposit is never guessed into revenue.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
sandbox | query | string | — | Sandbox reads the org's SANDBOX ledger when it is exactly "true"; anything else reads the live one. |
GET /v1/books/export
Returns the complete financial package for the caller's org over (from, to]: the trial balance, the P&L, the balance sheet, and the GL detail behind them — the four statements a tax preparer or an investor asks for, assembled from the one ledger in a single read so they cannot disagree with each other.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
sandbox | query | string | — | Sandbox reads the org's SANDBOX ledger when it is exactly "true". |
from | query | string | — | From is the RFC3339 start of the window, exclusive. |
to | query | string | — | To is the RFC3339 end of the window, inclusive. |
format | query | string | — | Format is the export encoding. |
limit | query | integer | — | Limit caps the GL detail rows included as the audit trail; 5000 when absent or not positive. |
GET /v1/books/gl
ListGL returns the org's most recent GL Entry rows, newest first. This is the raw double-entry detail behind every statement: one row per leg, with its debit, credit, posting time and the source that booked it.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
sandbox | query | string | — | Sandbox reads the org's SANDBOX ledger when it is exactly "true". |
limit | query | integer | — | Limit caps how many rows come back; 500 when absent or not positive. |
GET /v1/books/inbox
Returns the org's open document queue — everything uploaded but not yet booked, newest first, each with its extracted summary and the confidence the scanner resolved its category at. A booked document drops out of the queue.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
sandbox | query | string | — | Sandbox reads the org's SANDBOX ledger when it is exactly "true"; anything else reads the live one. |
POST /v1/books/inbox
Queue a document for later scanning
Takes a document as RAW BYTES and queues it in the caller org's inbox as unsorted, answering the queued item. It is the drop box: get the paperwork in now, read it later.
It EXTRACTS NOTHING and calls no model — that is what separates it from the scan. Nothing is proposed and nothing is posted; the item simply waits to be scanned, and a booked document leaves the queue.
IDEMPOTENT BY CONTENT: the item's id is the file hash, so re-uploading the same bytes answers the existing item rather than adding a duplicate row — and it is the same id a scan of those bytes uses, which is how the two routes address one document. Scoped to the caller's own org from the validated principal and refused without one; sandbox=true targets the sandbox ledger, and filename is recorded for display. An empty or oversized upload is a 400.
GET /v1/books/metrics
Metrics returns the org's deterministic SaaS-metrics snapshot over an optional (from, to] window — MRR, ARR, revenue, COGS, burn, gross margin, net income, cash, deferred revenue, monthly burn and runway — as raw int64-cent figures AND the same figures already formatted. Every number is the ledger, aggregated the one way the books define it, never a guess; it is the grounded read the unified /v1/ask advisor replays.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
sandbox | query | string | — | Sandbox reads the org's SANDBOX ledger when it is exactly "true". |
from | query | string | — | From is the RFC3339 start of the window, exclusive. |
to | query | string | — | To is the RFC3339 end of the window, inclusive. |
GET /v1/books/pnl
Returns the org's accrual-basis Profit & Loss over an optional (from, to] window of RFC3339 posting times: recognized revenue, matched cost, and the net.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
sandbox | query | string | — | Sandbox reads the org's SANDBOX ledger when it is exactly "true". |
from | query | string | — | From is the RFC3339 start of the window, exclusive. |
to | query | string | — | To is the RFC3339 end of the window, inclusive. |
GET /v1/books/position
Returns the org's Balance Sheet as of to (empty = all time), with the
Assets == Liabilities + Equity equation proof.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
sandbox | query | string | — | Sandbox reads the org's SANDBOX ledger when it is exactly "true". |
to | query | string | — | To is the RFC3339 instant the statement is struck as of. |
GET /v1/books/questions
Returns the clarifying questions the caller's own recent GL raises — the unusual postings a founder should look at (outliers, reversals, round-offs, uncosted revenue, an overdrawn wallet), sharpest first. An empty list means the books look clean; the detector is deterministic over the ledger and invents nothing.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
sandbox | query | string | — | Sandbox reads the org's SANDBOX ledger when it is exactly "true"; anything else reads the live one. |
GET /v1/books/rules
Returns the org's auto-categorization rules, highest priority first. A rule is a standing instruction — "anything whose merchant contains X books to category Y" — and it overrides a vendor's default category, so this is the list that decides how a future bill classifies itself.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
sandbox | query | string | — | Sandbox reads the org's SANDBOX ledger when it is exactly "true"; anything else reads the live one. |
POST /v1/books/rules
Creates or updates one auto-categorization rule, keyed by its pattern — writing a pattern that already exists REPLACES that row's category and priority. The category is normalized to a real COA expense account, and anything unrecognized becomes 5900 Uncategorized rather than a guessed real account. It answers the row exactly as stored, so the caller sees the normalization. A rule overrides a vendor's default category, so this is the standing instruction that decides how a future bill classifies.
Request body — application/json (required)
| Field | Type | Required | Description |
|---|---|---|---|
category | string | — | Category is the COA expense account a matching bill books to. |
pattern | string | — | Pattern is the merchant substring the rule matches on, case-insensitively. |
priority | integer | — | Priority breaks ties: when several patterns match, the highest wins. |
POST /v1/books/scan/book
Posts a reviewed scanned bill to the ledger. It is the scanner's ONLY write: the voucher goes through the same post() choke point every other source uses, so it is checked to balance (Σdebit == Σcredit) and is idempotent by (scan, scanId) — re-booking the same scan answers posted=false and writes nothing. A bill whose economic identity (vendor, total, issue date) already posted under a DIFFERENT scan is refused 409 unless override is set, which is what stops the same receipt re-scanned into a new file hash from double-booking. An unbalanced voucher is refused 400.
Request body — application/json (required)
| Field | Type | Required | Description |
|---|---|---|---|
override | boolean | — | Override books this bill even when one of the SAME economic identity (vendor, total, issue date) already posted — the… |
scanId | string | — | ScanID is the scanned document's file hash, as GET /v1/books/inbox and the scan draft report it. |
voucher | Voucher | — | Voucher is the reviewed voucher to post. Its source is FORCED to (scan, scanId) server-side, so it can never be booked… |
POST /v1/books/scan
Scan a receipt or invoice into a proposed voucher
Takes a receipt or invoice as RAW BYTES — a PDF, an image or plain text, uploaded under its own content type, not wrapped in JSON — extracts what the document says, resolves the vendor's expense category, and answers a DRAFT carrying a balanced voucher proposed for it.
NOTHING IS POSTED. That split is the whole design: the model only ever produces a structured reading of the document, the voucher is assembled deterministically in Go from that reading, and the ledger is written only by the separate book call a human confirms. So a misread scan can propose a wrong draft; it cannot move money. Amounts are exact integer cents end to end — the extraction returns cents, never a decimal — so no rounding enters the ledger.
The draft's id is the FILE HASH, and that is what makes booking idempotent: re-scanning the same bytes addresses the same draft rather than queuing a second one. A row is written to the org's document inbox as a side effect, moving it from unsorted to draft. Scoped to the caller's own org from the validated principal and refused without one; sandbox=true targets the sandbox ledger, and filename is recorded for the inbox. An empty or oversized upload is a 400, and a deployment with no scanner model answers 501.
POST /v1/books/sync
Sync ingests the caller's OWN org from commerce into BOTH ledgers (live and sandbox) and reports how many new vouchers posted to each. It is idempotent — money that has already been booked posts nothing on a repeat — and it is read-only against commerce: it never mints a deposit, a credit or a payout, only the accounting twin of money that already moved.
GET /v1/books/transactions
Returns the org's booked ledger as a single-line register, newest first: one row per voucher, with its date, description, vendor, category, source and amount in exact cents. It is the double-entry ledger projected to the register a human reads, filterable by posting-time window, category and vendor. Strictly read-only — it restates the books, it never moves them.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
sandbox | query | string | — | Sandbox reads the org's SANDBOX ledger when it is exactly "true". |
from | query | string | — | From is the RFC3339 start of the posting-time window, inclusive. |
to | query | string | — | To is the RFC3339 end of the posting-time window, inclusive. |
category | query | string | — | Category filters to one COA account, named by number ("5300") or by category slug ("software"). |
vendor | query | string | — | Vendor filters to rows whose vendor or description contains this text, case-insensitively. |
limit | query | integer | — | Limit caps how many rows come back; 200 when absent or not positive. |
GET /v1/books/trial
Returns the org's trial balance over an optional [from, to] window of RFC3339 posting times, including the opening/closing columns and the TotalDebit == TotalCredit proof that the books balance.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
sandbox | query | string | — | Sandbox reads the org's SANDBOX ledger when it is exactly "true". |
from | query | string | — | From is the RFC3339 start of the window, exclusive. |
to | query | string | — | To is the RFC3339 end of the window, inclusive. |
GET /v1/books/vendors
Returns the org's vendor book: each canonical vendor, the alias spellings a receipt may print it under, and the expense account new bills from it default to. A vendor here is what makes a scanned bill self-classify instead of asking again.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
sandbox | query | string | — | Sandbox reads the org's SANDBOX ledger when it is exactly "true"; anything else reads the live one. |
POST /v1/books/vendors
Creates or updates one vendor in the org's vendor book, keyed by its canonical name — writing a canonical name that already exists REPLACES that row's aliases and default category. A category given as a slug ("software") is normalized to its real COA expense account, and anything unrecognized becomes 5900 Uncategorized rather than a guessed real account. It answers the row exactly as stored, so the caller sees the normalization. Recording a vendor is what makes future bills from it self-classify instead of asking again.
Request body — application/json (required)
| Field | Type | Required | Description |
|---|---|---|---|
aliases | string[] | — | Aliases are the other spellings a receipt may print the vendor under; a scan matching any of them resolves to this… |
canonical | string | — | Canonical is the vendor's one true name, and the key an upsert writes by. |
defaultCategory | string | — | DefaultCategory is the COA expense account new bills from this vendor book to. |
How is this guide?