# Books

URL: https://docs.hanzo.ai/docs/openapi/books

> Double-entry accounting: chart of accounts, ledger, bank reconciliation, and the reports that prove the books balance.

Also for this capability: **API** · [CLI](https://docs.hanzo.ai/docs/cli/books) · [SDKs](https://docs.hanzo.ai/docs/sdks)

Double-entry accounting: chart of accounts, ledger, bank reconciliation, and the reports that prove the books balance.

> [All API references →](https://docs.hanzo.ai/docs/openapi) · [Get started →](https://docs.hanzo.ai/docs/guides)

|                |                                        |
| -------------- | -------------------------------------- |
| **Base URL**   | `https://api.hanzo.ai`                 |
| **Operations** | 23                                     |
| **Auth**       | `Authorization: Bearer $HANZO_API_KEY` |

## Specification

> **HIP-1001 · Books — The Double-Entry Ledger** — Active · [read the specification →](https://github.com/hanzoai/hips/blob/7ed737180339cd05b5f2935eafe7f7b83d2449a2/HIPs/hip-1001-books-double-entry-ledger.md)

`/v1/books` is double-entry accounting for an org: a chart of accounts, an
append-only general ledger, bank feeds with reconciliation, receipt capture, and
the reports that prove the books balance. It is implemented in `hanzoai/cloud` at
`apps/books`. This HIP states the two properties that make it trustworthy — that
it records money it never moves, and that every posting balances or is refused.

### Motivation

The money plane holds balances and the customer-facing billing surface projects
them. Neither keeps books: a chart of accounts, a general ledger, revenue
recognition, a trial balance. Without those there is no statement anyone can audit
and no way to answer a question about last quarter that survives a restatement.

### Specification

The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as
in RFC 2119.

#### The surface, and what stays untyped

Every address is under `/v1/books` — twenty-one prefixes enumerated in the
manifest row (`manifest/apps.go:412`), twenty-two paths published in the
capability's own subset (`plugin/books/openapi.json`). Twenty operations are
typed; five are untyped by design and each is named at its registration and held in a ledger
test (`apps/books/projection_test.go`): the three raw-byte uploads —
`POST /v1/books/scan`, `POST /v1/books/inbox` and `POST /v1/books/bank/import`
take the receipt or statement file ITSELF as the body, which no JSON `In` can
describe, so each declares its byte request through `openapi.Register` with
`openapi.Binary` — and two link stubs that answer an unconditional 501 and
deliberately declare nothing. The two ledgers must sum to the served surface,
so a route added untyped goes red rather than unlisted.

#### Free, metered nowhere, publishing nothing

Every books route is **free** — the plugin declares `Price: cloud.Free`
(`plugin/books/main.go:21`) and no handler gates or meters spend. Books spends
no provider's money: it restates the ledger feed it reads. It publishes **no
events** on the bus, so a customer's webhooks receive nothing from this
capability, and it emits nothing to observability beyond the request span
every route already gets — there is no tracer, meter or log surface of books'
own in `apps/books`.

#### Stage

Books is a vertical application, not the agentic-OS core: its stage is
**beta**, declared in the manifest row (`manifest/apps.go:412`, `Stage: Beta`;
HIP-0139 §8).

#### Upstreams

One OSS library is embedded: `rsc.io/pdf` (BSD-3-Clause), which parses bank
statements on the PDF import path (`apps/books/import_pdf.go:46`) — the text
extraction survives in HEAD, nothing else of it does. The Plaid and Teller
bank connectors (`apps/books/plaid.go`, `apps/books/teller.go`) are
hand-written read-only clients to those services, not forks of anything.

#### It records money; it never moves money

Three sources post into the ledger: the platform's own transaction feed, a
read-only bank connector, and a reviewed receipt capture. All three land through
one choke point, `store.post` (`apps/books/store.go:238`), and none of them can
mint a deposit, a credit or a payout (`apps/books/books.go:14-19`).

This is the invariant the whole capability rests on: \*_books can restate money,
never create it._\* Any new posting source MUST arrive through the same choke point
and MUST carry no authority to move funds.

#### A voucher balances or it is refused

A posting is a set of legs in exact integer minor units. Floating point is
forbidden: cents are exact under add, subtract and negate, which are the only
operations double-entry performs, so there is no rounding error to accumulate
(`apps/books/gl.go:11-14`).

The pipeline is pure and database-free (`apps/books/gl.go:55-65`): merge legs on
the same account, reduce each leg to a single non-negative side, absorb a residual
difference within the round-off allowance, then assert `Σdebit == Σcredit`. A
difference larger than the allowance MUST fail closed rather than be plugged
against equity (`apps/books/gl.go:17-21`). The allowance exists only to soak up a
one- or two-cent artifact of an upstream split.

Idempotency is `(sourceKind, sourceID)`: the same source event posts exactly once,
so replaying a feed is a no-op rather than a duplicate.

#### Each org's books are physically separate

Every read resolves the caller's own org from the validated principal, and each
org's ledger is its own database file, with the sandbox ledger a second, separate
file (`apps/books/books.go:22-26`, `apps/books/books.go:44-47`). A test-mode row
therefore cannot reach real revenue, and one org cannot read another's ledger even
in the presence of a query defect, because the other org's rows are not in the
file being queried.

#### The language surface may rephrase a figure; it may never source one

The plain-language question surface routes deterministically to metrics computed
from the ledger, and the model — when one is configured at all — rewrites prose
without touching a number (`apps/books/ask.go:4-16`). With the model absent or
down, the figures are identical. The brain is strictly read-only and MUST NOT
reach the posting path.

#### Bank credentials live in the key service or the operation fails

Connector access tokens are stored and fetched through KMS and MUST NOT be
persisted in the ledger database. A deployment with no key service wired fails
every credentialed bank operation closed (`apps/books/books.go:55-59`).

#### What this refuses

- **No manual journal door.** Postings come from the three declared sources.
- **No float.** An amount is integer cents or it does not enter.
- **No cross-org read.** The tenant is not an input.
- **No model-sourced figure.** Narration is prose only.

### Rationale

The alternative to a separate posting choke point is to let each source write its
own legs. It is easier and it is how the imbalance gets in: three writers means
three places where the balance assertion can be skipped, and the assertion is the
only thing that makes a trial balance mean anything.

The alternative to per-org database files is one database with a tenant column.
That works until one query forgets the predicate. Separate files make the
forgetting harmless, and the cost is bounded because the ledger is small.

### Security Considerations

What an attacker gets from the wrong implementation is one of two things: a
posting source that carries authority to move funds turns a bookkeeping bug
into minted money, which is why every source lands through the one choke point
that cannot mint (`apps/books/books.go:14-19`); and a tenancy defect here is
not a leak of preferences but of a company's finances.

A ledger is a disclosure surface: revenue, vendors, payroll shape. Tenancy is
therefore enforced by physical separation rather than by a filter, and the tenant
key is read from the validated principal rather than from any caller-supplied
field.

The bank connector is the one component holding a third-party credential. Keeping
those in the key service means a copy of an org's ledger file is not a copy of its
bank access.

The read-only posture of the question surface is a security property, not only a
correctness one: it is what makes it safe to hand a language model an ability to
answer questions about the ledger.

## Four surfaces

| Surface  | Reaches this capability as                    | Coverage                                                                   |
| -------- | --------------------------------------------- | -------------------------------------------------------------------------- |
| **REST** | `books` at its own prefix                     | 23 operations                                                              |
| **CLI**  | `hanzo books …`                               | 23 of 23                                                                   |
| **SDK**  | `BooksApi` in every published client          | 23 methods                                                                 |
| **MCP**  | tool `books` on `https://api.hanzo.ai/v1/mcp` | 20 operations, 0 under the document's own id — ask `describe` for the rest |

## Quickstart

```bash
export HANZO_API_KEY=sk-...   # console.hanzo.ai → API keys
```

Then the first call — a read that needs nothing but the key. `GET /v1/books/gl`, operation `get_books_gl`:

**CLI**

```bash
hanzo books gl
```

**SDK**

**.ts**

```ts
import { Configuration, BooksApi } from 'hanzoai';

const api = new BooksApi(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.getBooksGl();
```

**.py**

```python
from hanzoai.cloud import ApiClient, Configuration
from hanzoai.cloud.api import BooksApi

client = ApiClient(Configuration(access_token=os.environ["HANZO_API_KEY"]))
result = BooksApi(client).get_books_gl()
```

**.go**

```go
cfg := hanzoai.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := hanzoai.NewAPIClient(cfg)

resp, _, err := client.BooksAPI.GetBooksGl(context.Background()).Execute()
if err != nil {
	return err
}
```

**.rs**

```rust
use hanzo_client::apis::{configuration::Configuration, books_api};

let mut cfg = Configuration::new();
cfg.bearer_access_token = std::env::var("HANZO_API_KEY").ok();

let result = books_api::get_books_gl(&cfg, Default::default()).await?;
```

**.java**

```java
import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.BooksApi;

ApiClient client = new ApiClient();
client.setBearerToken(System.getenv("HANZO_API_KEY"));

var result = new BooksApi(client).getBooksGl();
```

**HTTP**

```bash
curl https://api.hanzo.ai/v1/books/gl \
  -H "Authorization: Bearer $HANZO_API_KEY"
```

**MCP**

MCP reaches **books** through the `books` tool, which names its 20 operations with its own verbs — this one among them, under a name only MCP declares. `describe` explains any of them:

```bash
curl -X POST https://api.hanzo.ai/v1/mcp \
  -H "Content-Type: application/json" \
  -d '{
       "jsonrpc": "2.0",
       "id": 1,
       "method": "tools/call",
       "params": {
         "name": "describe",
         "arguments": {
           "op": "list_book_accounts"
         }
       }
     }'
```

Answers `200` with `books.GLRow[]` — ok.

## Endpoints

| Endpoint                                                                                                  | What it does                                                                                                                                                                                                                                                                                                     |
| --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`GET /v1/books/accounts`](https://docs.hanzo.ai/docs/openapi/books/get-books-accounts)                   | Returns the org's chart of accounts — the seeded fixed chart every posting key in the ledger refers to.                                                                                                                                                                                                          |
| [`POST /v1/books/ask`](https://docs.hanzo.ai/docs/openapi/books/post-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.                                                                                                                                     |
| [`POST /v1/books/bank/import`](https://docs.hanzo.ai/docs/openapi/books/post-books-bank-import)           | Import a bank statement file into your books                                                                                                                                                                                                                                                                     |
| [`POST /v1/books/bank/sync`](https://docs.hanzo.ai/docs/openapi/books/post-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.                                                                                     |
| [`GET /v1/books/bank/transactions`](https://docs.hanzo.ai/docs/openapi/books/get-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.                                                                                          |
| [`GET /v1/books/bank/unreconciled`](https://docs.hanzo.ai/docs/openapi/books/get-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.                                                                                                                                                |
| [`GET /v1/books/export`](https://docs.hanzo.ai/docs/openapi/books/get-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. |
| [`GET /v1/books/gl`](https://docs.hanzo.ai/docs/openapi/books/get-books-gl)                               | Returns the org's most recent GL Entry rows, newest first.                                                                                                                                                                                                                                                       |
| [`GET /v1/books/inbox`](https://docs.hanzo.ai/docs/openapi/books/get-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.                                                                                                                           |
| [`POST /v1/books/inbox`](https://docs.hanzo.ai/docs/openapi/books/post-books-inbox)                       | Queue a document for later scanning                                                                                                                                                                                                                                                                              |
| [`GET /v1/books/metrics`](https://docs.hanzo.ai/docs/openapi/books/get-books-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.                                          |
| [`GET /v1/books/pnl`](https://docs.hanzo.ai/docs/openapi/books/get-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.                                                                                                                                                        |
| [`GET /v1/books/position`](https://docs.hanzo.ai/docs/openapi/books/get-books-position)                   | Returns the org's Balance Sheet as of `to` (empty = all time), with the Assets == Liabilities + Equity equation proof.                                                                                                                                                                                           |
| [`GET /v1/books/questions`](https://docs.hanzo.ai/docs/openapi/books/get-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.                                                                                                     |
| [`GET /v1/books/rules`](https://docs.hanzo.ai/docs/openapi/books/get-books-rules)                         | Returns the org's auto-categorization rules, highest priority first.                                                                                                                                                                                                                                             |
| [`POST /v1/books/rules`](https://docs.hanzo.ai/docs/openapi/books/post-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.                                                                                                                                                         |
| [`POST /v1/books/scan/book`](https://docs.hanzo.ai/docs/openapi/books/post-books-scan-book)               | Posts a reviewed scanned bill to the ledger.                                                                                                                                                                                                                                                                     |
| [`POST /v1/books/scan`](https://docs.hanzo.ai/docs/openapi/books/post-books-scan)                         | Scan a receipt or invoice into a proposed voucher                                                                                                                                                                                                                                                                |
| [`POST /v1/books/sync`](https://docs.hanzo.ai/docs/openapi/books/post-books-sync)                         | Ingests the caller's OWN org from commerce into BOTH ledgers (live and sandbox) and reports how many new vouchers posted to each.                                                                                                                                                                                |
| [`GET /v1/books/transactions`](https://docs.hanzo.ai/docs/openapi/books/get-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.                                                                                                                                    |
| [`GET /v1/books/trial`](https://docs.hanzo.ai/docs/openapi/books/get-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.                                                                                                              |
| [`GET /v1/books/vendors`](https://docs.hanzo.ai/docs/openapi/books/get-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.                                                                                                                                                    |
| [`POST /v1/books/vendors`](https://docs.hanzo.ai/docs/openapi/books/post-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.                                                                                                                             |

---

[All Hanzo APIs](https://docs.hanzo.ai/docs/openapi) · [Interactive reference](https://docs.hanzo.ai/docs/openapi/reference)
