> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vinmake.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Ledger and statements

> Every read route over the book: journal entries, the trial balance, sub-ledger views, the three statements, transactions, dimensions, reconciliation and EPM - on both surfaces.

# Ledger and statements

The read surface over the book of record, on both APIs.

<Warning>
  Both services mount a router at `/gl`, and they are different endpoints.
  The **backend GL API** is the canonical read API.
  The **DevCenter** `/gl/*` serves the Books screens and preserves a legacy response contract.
  Check which base URL you are pointed at before matching a shape.
</Warning>

## The backend GL API - 7 routes

Every route requires a valid bearer (a service token or a Supabase JWT) or a trusted proxy identity.
GL data is never exposed unauthenticated.

### `GET /gl/meta`

The filter vocabulary and the totals, in one call: orgs with their base currency, the distinct source types, periods and statuses, the whole chart of accounts, and `{entries, debit_base, credit_base}`.

### `GET /gl/entries`

| Query    | Default | Notes                                                 |
| -------- | ------- | ----------------------------------------------------- |
| `org`    | -       | org uuid                                              |
| `source` | -       | `bill`, `invoice`, `payment`, `manual`, `reversal`, … |
| `period` | -       | `YYYY-MM`                                             |
| `status` | -       |                                                       |
| `limit`  | `50`    | 1-500                                                 |
| `offset` | `0`     |                                                       |

```json theme={null}
{
  "entries": [
    { "id": "…", "entry_number": "JE-000412", "entry_date": "2026-07-20",
      "description": "Supplier bill VN-2026-0417", "source_type": "bill",
      "source_id": "a3f1…:VN-2026-0417", "status": "posted",
      "created_at": "…", "debit_base": 1100000.0, "credit_base": 1100000.0 }
  ],
  "count": 412, "limit": 50, "offset": 0
}
```

`count` is the unpaginated total, so a client can page without a second call.

### `GET /gl/entries/{entry_id}`

The entry header - including its `checksum` and `created_by` - plus every line with its account code and name, native and base amounts, currency, and the `activity` and `flow` dimensions.
`404` when the id is unknown.

This is the route that answers "who posted this, and what does it hash to".

### `GET /gl/trial-balance`

Every account with a movement, and the tie-out.

```json theme={null}
{
  "accounts": [ { "code": "1200", "name": "Inventory - raw materials", "type": "asset",
                  "debit_base": 1000000.0, "credit_base": 0.0, "net": 1000000.0 } ],
  "net": 0.0,
  "balanced": true
}
```

Accounts with no movement are omitted.
`balanced` is the assertion a book of record has to be able to make about itself.

### `GET /gl/reconciliation`

The same tie in one row: `debit_base`, `credit_base`, `net`, `balanced`.

### `GET /gl/subledgers/{kind}`

A thin sub-ledger view over a well-known control account.

| `kind`         | Account |
| -------------- | ------- |
| `ap`           | `2000`  |
| `ar`           | `1100`  |
| `cash`, `bank` | `1010`  |

```json theme={null}
{ "kind": "ap", "account": "2000", "balance": 34800100.0, "lines": [ … ] }
```

Up to 200 movements, newest first.
An unknown kind is a `404`.

<Note>
  This is a **GL movement view**, not the AP/AR sub-ledger itself.
  The real sub-ledger lives in `obligations`, with a row per bill and per invoice, and that is what the control-account Close checksums tie against.
  Do not read this route's balance as "open AP by party".
</Note>

### `POST /gl/sync-teable`

The one operational route on this surface: posts approved operational records into the GL through the ledger engine.
**Idempotent** - safe to call repeatedly, on a schedule or after an approval.

```json theme={null}
{ "org": "…", "posted": 14, "skipped": 3, "suspense": 0 }
```

<Warning>
  `suspense` is the number that matters here.
  This legacy poster is the one path in the tree that still has a suspense fallback for a kind it cannot code, which is exactly what the agent posting path refuses to do.
  A non-zero `suspense` is a finding, not a statistic.
</Warning>

## The DevCenter operational read surfaces

Served from the canonical GL, with the UI's response keys preserved exactly.

<Warning>
  **Amount keys named `*_usd` are the org's base currency, which is dong.**
  The key names were kept for the client contract; every response that carries a base amount therefore also carries **`base_currency`**, and a client must label money with that field.
  The Ledger screen rendered 11,000,000 dong as `$11,000,000.00` for weeks - about 25,000× off at face value - which is why this is stated on every page that touches these routes.
  A client that cannot read the currency should render `BASE`, never a dollar sign.
</Warning>

### Ledger - `/gl`

| Route                        | Returns                                                                                                                                             |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET /gl/freshness`          | when the numbers on screen were last refreshed. The canonical GL has no ingest layer, so source stamps are `null` and freshness is the last GL post |
| `GET /gl/meta`               | the filter vocabulary for the Ledger screen                                                                                                         |
| `GET /gl/entries`            | entries, filtered by `entity`, `source`, `status`, `account`, `period`, with `limit` and `offset`                                                   |
| `GET /gl/entries/{entry_id}` | one entry and its lines                                                                                                                             |
| `GET /gl/subledgers/ap`      | open AP derived from GL movements on the trade-AP control account                                                                                   |
| `GET /gl/subledgers/cash`    | cash position by account and currency                                                                                                               |
| `GET /gl/coverage`           | how much P\&L value lands in real accounts versus the `4900`/`6900` suspense plugs                                                                  |
| `GET /gl/reconciliation`     | the tie-out banner                                                                                                                                  |

<Note>
  Fields with no canonical source - the source-payload id, the counterparty name, a transaction hop - are returned as **`null`**, never invented.
  `GET /gl/reconciliation` states plainly that no external source feed is wired rather than reporting a phantom break, and `/gl/subledgers/ap` labels its vendor from the entry description because that is the only counterparty label the GL itself carries.
  Honest coverage beats a number with nothing behind it.
</Note>

### Statements - `/stmt`

| Route                     | Query          |
| ------------------------- | -------------- |
| `GET /stmt/cash_position` | -              |
| `GET /stmt/pnl`           | `start`, `end` |
| `GET /stmt/balance_sheet` | `as_of`        |
| `GET /stmt/cash_flow`     | `start`, `end` |

Computed live from the ledger by joining lines to accounts on type and normal side, and dating them by the entry date.
**Balanced by construction**, because they are a projection of a balanced book rather than a separate calculation.

On this chart, expense is `cogs + opex` - there is no `expense` account type - and cash and bank are `1000`, `1010` and `1030`.

### Transactions - `/tx`

| Route                           | Notes                                                                                                |
| ------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `GET /tx/meta`                  | the filter vocabulary                                                                                |
| `GET /tx/transactions`          | filter by `type`, `source`, `posting`, `q`, `dim`, with `limit` and `offset`                         |
| `GET /tx/transactions/{txn_id}` | one transaction, its economic lines, the entries posted from it, and lineage back to the raw payload |

The browsable layer between raw ingest and the ledger.

### Dimensions - `/dim`

`GET /dim/catalog` - the dimension definitions and their hierarchical values, so the ledger and transaction views can render filterable dimensions.

### Reconciliation - `/recon`

The human-in-the-loop rail: a `suggested` match is a proposal a human accepts or rejects.

| Route                                   | Notes                                                                 |
| --------------------------------------- | --------------------------------------------------------------------- |
| `GET /recon/summary`                    | the two-pane screen's header and the consolidation-correctness banner |
| `GET /recon/matches`                    | filter by `status` and `type`                                         |
| `GET /recon/matches/{match_id}`         | one match, evidence on the left and normalised records on the right   |
| `POST /recon/matches/{match_id}/accept` | → `matched`                                                           |
| `POST /recon/matches/{match_id}/reject` | → `rejected`                                                          |
| `POST /recon/matches/{match_id}/undo`   | back to `suggested` - only a *decided* match can be undone            |

Confidence stays internal.
The UI is given discrete states only, so a reviewer is never shown a score to defer to.

`undo` is the safety net that makes a mistaken accept recoverable.
The three mutating routes take the derived actor.

### EPM - `/epm`

| Route                       | Query                 |
| --------------------------- | --------------------- |
| `GET /epm/budget_vs_actual` | `dim`, `value`        |
| `GET /epm/forecast`         | `months`, default `3` |

Portable Postgres queries over the dimensioned GL, deliberately not tied to one cube engine.

## Schema resilience, and where it stops

The DevCenter's read routers carry a wrapper that turns a missing legacy table or column into a `200` with an empty body, so a gated UI page loads rather than crashing.

<Warning>
  It is attached to the **read** routers only, and that is load-bearing.
  Mounted app-wide it also covered `/intake/{id}/post-approve`, `/post-reject` and `/posting`, turning schema drift on a money route into an HTTP `200` with an empty body.
  The posting routes fail closed with a real error.
</Warning>

Aggregate endpoints are cached with a TTL, and `POST /cache/invalidate` drops that cache - called after the GL is re-posted so the statement numbers refresh immediately.

## Where it is enforced

| Concern                       | Code                                                                                          |
| ----------------------------- | --------------------------------------------------------------------------------------------- |
| the canonical read API        | `backend/app/routers/external/gl.py`                                                          |
| the backend's authentication  | `backend/app/core/auth.py`                                                                    |
| the DevCenter read routers    | `devcenter/gl_api.py`, `stmt_api.py`, `tx_api.py`, `dim_api.py`, `recon_api.py`, `epm_api.py` |
| the schema-resilience wrapper | `devcenter/legacy_schema.py`                                                                  |

## The test that would fail if it broke

`backend/tests/test_gl_api.py` for the canonical routes including the auth gate.
`langgraph_chat/devcenter/tests/` for the read contract, including that every base-amount response carries `base_currency`.

## Related

* [Read the ledger](/greatbook/guides/read-the-ledger) - the same data in the UI
* [How a journal entry is formed](/greatbook/gl/index) - what each entry in these responses is
* [Statutory reporting](/greatbook/capabilities/statutory-reporting) - the TT200 projection over the same book
* [Derived balances](/greatbook/capabilities/derived-balances) - why these totals are computed, not stored
