
LedgerLou Journal
The heart of LedgerLou: every posting lands here as an immutable entry in the general ledger. Via REST API or MCP — every posting follows the same GoBD rules and is fully traceable.
API ReferenceCore concepts
LedgerLou arranges the general ledger on three levels:
Journal -> Intents -> Posting lines
The journal is the overall view. It contains many intents. Each intent groups one or more posting lines that together form a single posting transaction.
| Concept | What it is | Persistence |
|---|---|---|
| Journal | Append-only overall view of all intents for a tenant | ledger_events (GoBD-compliant, hash-chained) |
| Intent | Business bracket for exactly one posting transaction within the journal | No dedicated record — carried as intent_id in ledger_events, documents, bank_transactions and others |
| Posting line | Single debit/credit line within an intent | ledger_events (1+ lines per intent) |
In practice this means:
- The journal lists all intents chronologically.
- An intent groups the lines that belong to a single transaction.
- A posting can consist of one or more posting lines, depending on splits and tax logic.
Journal
The journal is the ledger_events table — the immutable core of LedgerLou. Every confirmed posting writes append-only lines here:
- Append-only — DB triggers prevent UPDATE and DELETE (§ 146 AO / GoBD)
- Hash chain — every line contains an
audit_hashover its own fields plus the hash of the previous line — tampering would be immediately detectable - Gapless numbering —
journal_numberis a per-tenant monotonically increasing sequence with no gaps - Intent grouping — split postings write multiple lines sharing the same
intent_id
GET /v1/journal returns these lines enriched with account names, linked documents, and source metadata.
Intent
An intent is a UUID that identifies a posting transaction as a unit. It is created at the moment a posting is confirmed and links all lines of that transaction — for split postings this can be multiple ledger_events rows that nonetheless share the same intent_id.
At the same time, the intent is the common link across all modules: bank transactions and documents are assigned an intent_id after processing, pointing at the associated posting lines.
An intent cannot be deleted. Errors are corrected via a reversal: this creates a new intent with swapped accounts that refers to the original via reverses_intent_id — both entries remain visible in the journal. Without an explicit correction mode, the reversal is posted with today’s date in the current open period; the original period remains unchanged.
Posting lines and posting transactions
A posting transaction creates an intent and writes one or more posting lines to the journal. Every posting must be balanced (debit = credit) — the validator rejects unbalanced postings before anything is written to the database.
Postings arise via three paths:
| Source | Path |
|---|---|
| Directly via API | POST /v1/bookings — raw journal lines passed explicitly, posted immediately |
| Bank reconciliation | A posting is created when a bank transaction is assigned to an open item |
| Reversal | Counter-posting with swapped accounts under a new intent ID |
Intent lifecycle
An intent is immutable. Once its lines are in the journal they are never modified or deleted — not even when downstream modules such as bank reconciliation later unlink their assignment. Corrections arise exclusively through new intents that point back to the origin via reverses_intent_id.
Three typical scenarios show how this plays out in concert with bank reconciliation:
An invoice has already been posted manually or via API (Intent A, two lines). Later, the incoming payment arrives in the bank account and is matched to Intent A via bank reconciliation.
- 1Intent A already exists in the journal — its two lines are immutable.
- 2On matching, a reference is set only on the bank transaction. Intent A is not touched.
- 3When the match is undone, this reference on the bank transaction is cleared again. Intent A is untouched; no reversal is created.
For an open receivable (Intent A), a new settlement posting is created during bank reconciliation. It lives as its own Intent B in the journal and carries a settlement reference to Intent A.
- 1The reconciliation creates Intent B with source “bank reconciliation” and a settlement reference to Intent A.
- 2The bank transaction is linked to Intent B. Intent A remains exactly as before — only marked as settled.
- 3When undone, a reversal Intent C is created that mirrors Intent B with swapped accounts. Intent A is shown as open again without ever having been modified.
Intent A (for example, an invoice) has already been settled by a settlement posting B from bank reconciliation. It now turns out that Intent A was wrong and must be reversed.
- 1First, the dependent settlement posting B is reversed automatically — Intent C is created, mirroring B.
- 2Then Intent A is reversed — Intent D is created, mirroring A and pointing at A via a reversal reference.
- 3All four intents (A, B, C, D) remain visible in the journal. Linked bank transactions are marked as open and can be re-matched.
Reversal logic
In LedgerLou a reversal is always a new posting intent. The original lines remain unchanged; the reversal lines mirror debit and credit, carry over audit-relevant fields such as the FX block, external_reference, custom_metadata, and tax_code, and carry reverses_intent_id pointing at the reversed intent.
For the posting date, there are two modes:
| Mode | REST posting_mode | MCP posting_mode | Effect |
|---|---|---|---|
| Current period | current_period or omit | current_period or omit | Reversal is posted with today’s date in the current open period. This is the default for API and MCP so that existing integrations remain stable. |
| Original period | original_period | original_period | Reversal is set to the posting date of the original posting. Useful for year-end or period corrections, as long as the original period is still open. |
original_period is an explicit correction mode. Before writing, LedgerLou checks whether the target period is open. If it is soft-locked or hard-locked, the reversal is rejected with PERIOD_LOCKED. In that case the period must be deliberately reopened, or the correction stays in the current period.
For already-reconciled intents, LedgerLou first reverses dependent settlement intents and releases the bank linkage. Grouped bank reconciliations remain protected: if a settlement still clears other active intents, the group assignment must be released first.
curl -X POST https://api.ledgerlou.de/v1/journal/reverse \
-H "Authorization: Bearer $LEDGERLOU_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"intent_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"reason": "Falsche Kontierung",
"posting_mode": "original_period"
}'
The same applies for MCP.
{
"intent_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"reason": "Falsche Kontierung",
"posting_mode": "original_period"
}
Metadata & external references
Every posting can optionally carry two intent-level fields that link the transaction to external systems or provide additional context:
| Property | external_reference | custom_metadata |
|---|---|---|
| Type | string (max. 500 characters) | Flat JSON object (string | number | boolean | null as values) |
| Required | No | No |
| Immutable | Yes (GoBD) | Yes (GoBD) |
| In audit hash | Yes | Yes (deterministic ordering) |
| Filterable | GET /v1/journal?externalReference=... | — |
| Reversal behavior | Copied identically from the original | Copied identically from the original |
Typical use cases:
- Invoice number as reference —
external_reference: "RE-2025-0042"links the posting to the invoice in the ERP. Via?externalReference=RE-2025-0042the corresponding journal entry is immediately findable. - Cost center and project —
custom_metadata: { "cost_center": "CC-100", "project": "alpha" }assigns the posting organizationally without having to adapt the chart of accounts model. - Origin tags for automations —
custom_metadata: { "source_system": "shopify", "order_id": "ORD-9981" }gives MCP agents and ETL pipelines a structured return channel.
Foreign-currency postings
LedgerLou keeps the books in EUR. Postings in a foreign currency (e.g. USD, GBP, CHF) are posted natively as the EUR amount and additionally carry an FX block with the original data:
| Field | Meaning |
|---|---|
currency | ISO 4217 currency code (e.g. USD) |
foreign_amount | Gross amount in foreign currency |
rate | Conversion rate foreign currency → EUR (ECB convention) |
rate_date | Effective date of the rate |
rate_source | Source (e.g. ECB, manual, bank) |
Important rules:
- The caller provides the rate explicitly — LedgerLou does not query external rate sources.
- The EUR amount in
linesmust match the FX block:foreign_amount × rate ≈ sum of debit amounts(tolerance: max. 1 cent or 0.01%). - For split postings, the foreign-currency amount is allocated proportionally across the lines (largest-remainder method, 4 decimal places).
- Reversal postings carry over the FX block identically from the original — no new rate is calculated.
- EUR postings set no FX block (
fx: nullor field omitted). - Opening-balance values (
POST /v1/bookings/opening-balances) do not support FX.
Workflow
POST /v1/journal/reverse. By default a new reversal intent is created in the current open period; with posting_mode: “original_period” an open original period can be corrected. No silent overwriting — every correction is visible in the audit trail.