Core concepts
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:
| Field | Meaning |
|---|---|
accountReference | The account the entry posts to, e.g. 1920-bank |
entryType | DEBIT or CREDIT |
amountMinorUnits | A signed integer amount at the unit’s scale — see Amounts and units |
dimensions | Optional analytical tags, e.g. {"CUSTOMER": "cust-001"} |
lineDescription | Optional free-text line description |
quantity | Optional 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:
unit | Scale | "amountMinorUnits": 125000 means |
|---|---|---|
EUR, NOK, USD, GBP | 2 | 1,250.00 |
JPY | 0 | ¥125,000 |
KWD | 3 | KWD 125.000 |
kgCO2e | 3 | 125 kg CO₂e |
tCO2e | 6 | 0.125 t CO₂e |
units | 0 | 125,000 pieces |
kg, liters | 3 | 125 kg / 125 l |
hours, days | 2 | 1,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/202and 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:
timePeriodType | Example timePeriodValue |
|---|---|
YEAR | 2026 |
TERTIAL | 2026-T1 |
QUARTER | 2026-Q1 |
BI_MONTHLY | 2026-B1 |
MONTHLY | 2026-01 |