Documentation
What Ledger17 is
Ledger17 is a headless double-entry ledger: an append-only record of balanced accounting transactions, exposed exclusively through a REST API. There is no bundled UI and no workflow engine — your systems record facts, and the ledger keeps the books.
It is built on CQRS and event sourcing, split into two services:
| Service | Default port | Role |
|---|---|---|
| Command API | :8080 | Writes — record transactions, submit business events, reverse mistakes |
| Query API | :8081 | Reads — balances, transaction lines, subledgers, live journal |
All business endpoints live under /ledger/v1. Both services publish a live OpenAPI document at /q/openapi and Swagger UI at /q/swagger-ui.
The query side is built for speed: balances and rollups are pre-calculated as transactions post — across every dimension in parallel — so reads are constant-time lookups rather than scans over the event history.
Five things to know before your first request
- Everything is append-only. Posted transactions are immutable. Corrections are new, compensating transactions — never edits. See Core concepts.
- Every block must balance. A transaction is a block of two or more entries where the sum of debits equals the sum of credits, exactly — integer arithmetic, no rounding. Unbalanced blocks are rejected.
- Amounts are scaled integers.
125000withunit: "EUR"(scale 2) means €1,250.00 — no decimals anywhere in the API. The accounting side of an entry comes fromentryType(DEBITorCREDIT); the scale comes from the unit, which can also be non-monetary (kgCO2e,hours, …). - Requests are tenant-scoped. Send the organization id in the
X-Tenant-IDheader on every call. See Authentication. - Writes are idempotent.
POST /ledger/v1/transactionsrequires anX-Idempotency-Keyheader; retrying with the same key can never post twice.
Your first transaction
Record a balanced block — a sales invoice of €1,250.00 including 25% VAT:
curl -X POST http://localhost:8080/ledger/v1/transactions \
-H "Content-Type: application/json" \
-H "X-Tenant-ID: org-84120557" \
-H "X-Idempotency-Key: inv-2026-0042" \
-d '[{
"name": "Sales invoice #42",
"transactionDate": "2026-07-01",
"unit": "EUR",
"entries": [
{ "accountReference": "1500-receivables", "entryType": "DEBIT", "amountMinorUnits": 125000,
"dimensions": { "CUSTOMER": "cust-001" } },
{ "accountReference": "3000-sales", "entryType": "CREDIT", "amountMinorUnits": 100000 },
{ "accountReference": "2700-vat-out", "entryType": "CREDIT", "amountMinorUnits": 25000 }
]
}]'
The ledger answers 201 Created with the recorded transaction:
[{
"transactionId": "7d9e1a4c-…",
"tenantId": "org-84120557",
"name": "Sales invoice #42",
"transactionDate": "2026-07-01",
"unit": "EUR",
"entryCount": 3,
"totalDebitMinorUnits": 125000,
"recordedAt": "2026-07-01T09:14:03.512Z"
}]
Then read it back from the query service:
curl "http://localhost:8081/ledger/v1/balances?accountReference=1500-receivables&timePeriodType=MONTHLY&timePeriodValue=2026-07" \
-H "X-Tenant-ID: org-84120557"
Where to go next
- Core concepts — blocks, entries, minor units, dimensions, corrections
- Authentication — OIDC tokens, tenant header, permissions
- Errors & status codes — the error envelope and the full error-code catalog
- Command API — every write endpoint
- Query API — every read endpoint, including the WebSocket journal