Transaction blocks and entries

The unit of writing in Ledger17 is the transaction block: a named set of two or more accounting entries that must balance.

Each entry has:

FieldMeaning
accountReferenceThe account the entry posts to, e.g. 1920-bank
entryTypeDEBIT or CREDIT
amountMinorUnitsA signed integer amount at the unit’s scale — see Amounts and units
dimensionsOptional analytical tags, e.g. {"CUSTOMER": "cust-001"}
lineDescriptionOptional free-text line description
quantityOptional decimal quantity (e.g. number of shares)

A block is accepted only when the sum of its debit amounts equals the sum of its credit amounts. The accounting side of an entry is expressed through entryType, not through the sign of the amount.

Amounts and units

Every transaction block names a unit — what is being counted. Usually that is an ISO 4217 currency, but the ledger books non-monetary quantities the same way: carbon, inventory, time.

amountMinorUnits is the amount as a scaled integer: the display value multiplied by 10^scale, where the scale is registered per unit. There are no decimals anywhere in the API, which removes floating-point rounding from the ledger entirely.

The same integer means different display values depending on the unit:

unitScale"amountMinorUnits": 125000 means
EUR, NOK, USD, GBP21,250.00
JPY0¥125,000
KWD3KWD 125.000
kgCO2e3125 kg CO₂e
tCO2e60.125 t CO₂e
units0125,000 pieces
kg, liters3125 kg / 125 l
hours, days21,250.00

Units that are not registered fall back to scale 2. Registered currencies follow their ISO 4217 exponent — note JPY (no decimals) and KWD (three).

Negative amounts

amountMinorUnits is normally positive, with the accounting side carried entirely by entryType. A negative amount is permitted and means “reduce this side”: it lowers the account’s debit (or credit) turnover instead of inflating both sides.

DEBIT −100 and CREDIT +100 have the same net effect on the balance, but they differ in the turnover columns and in exports — reducing the original side is usually what a manual correction of a booking error wants.

Balance validation uses the signed sums: a block is accepted when Σ debit amounts = Σ credit amounts, negative entries included.

Append-only

The event store is append-only: once a transaction block is recorded it is never updated or deleted. A mistake is fixed by posting a correction — a compensating block that reverses the original (debits become credits and vice versa) — via POST /ledger/v1/transactions/{transactionId}/corrections. Both the original and the correction stay in the ledger forever, so the audit trail is complete by construction.

Multi-tenancy

Every request is scoped to one organization (tenant) through the X-Tenant-ID header, e.g. org-84120557. Data from different tenants never mixes; a token must carry the matching permission for the tenant it addresses (see Authentication).

Dimensions and subledgers

Dimensions attach analytical meaning to entries without inflating the chart of accounts. Entry-level dimensions are a map of DimensionType to an opaque value reference:

  "dimensions": { "CUSTOMER": "cust-001", "PROJECT": "proj-42" }
  

Available dimension types include CUSTOMER, SUPPLIER, EMPLOYEE, COUNTERPARTY, ASSET, PROJECT and CATEGORY. The values themselves are ids owned by your surrounding systems — the ledger treats them as opaque.

Dimensions power the query side’s subledger rollups and party exposure endpoints: per-customer receivables, per-supplier payables, per-asset holdings, and so on.

Dimension aggregates are pre-calculated as transactions post — for every dimension type in parallel. That is what keeps dimension queries fast: a per-customer rollup is a lookup in a maintained view, not a scan over history. The trade-off is that each query filters on one dimension at a time — you can slice the same data by CUSTOMER or by PROJECT, but not by both in a single query.

Blocks can additionally carry block-level dimensions as a list of {"dimensionType": "...", "valueReference": "<uuid>"} objects.

Business events and rule sets

Instead of hand-crafting entries, a client can submit a business event — “a sales invoice happened”, “payroll ran” — to POST /ledger/v1/events. Pluggable domain rule sets derive the balanced accounting entries from the event, validate it against an RDF ontology, and detect conflicts between rule sets before anything is posted.

CQRS: command and query

Writes and reads are separate services over a shared event store:

  • The command service (reference) validates and appends events; it answers with 201/202 and never serves reads beyond a simple event inquiry.
  • The query service (reference) maintains read models — balances, lines, subledgers — and streams the journal over WebSocket.

The read models are pre-aggregated: balances per account, period and dimension are updated as events arrive, so query latency does not grow with the size of the ledger.

Reads are eventually consistent with writes: a 202 Accepted business event becomes visible in query results once it has been processed.

Time periods

Balance queries slice time with a period type and value:

timePeriodTypeExample timePeriodValue
YEAR2026
TERTIAL2026-T1
QUARTER2026-Q1
BI_MONTHLY2026-B1
MONTHLY2026-01