Skip to main content

Ledger and statements

The read surface over the book of record, on both APIs.
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.

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

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.
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.
Up to 200 movements, newest first. An unknown kind is a 404.
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”.

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.
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.

The DevCenter operational read surfaces

Served from the canonical GL, with the UI’s response keys preserved exactly.
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.

Ledger - /gl

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.

Statements - /stmt

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

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. 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

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.
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.
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

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.