LedgerLou Docs is optimized for desktop.

Please open this page on a device with a wider screen.

LedgerLou Ledger Engine

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 Reference
Manual postingsRecord simple and split postings directly
Agentic postingsMCP agent posts with a confirmation loop
GoBD append-onlyNo silent changes — one SHA256 hash per line
Audit-safe reversalCounter-posting instead of overwriting
Document linkageDocument and posting in a single step

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

ConceptWhat it isPersistence
JournalAppend-only overall view of all intents for a tenantledger_events (GoBD-compliant, hash-chained)
IntentBusiness bracket for exactly one posting transaction within the journalNo dedicated record — carried as intent_id in ledger_events, documents, bank_transactions and others
Posting lineSingle debit/credit line within an intentledger_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_hash over its own fields plus the hash of the previous line — tampering would be immediately detectable
  • Gapless numberingjournal_number is 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:

SourcePath
Directly via APIPOST /v1/bookings — raw journal lines passed explicitly, posted immediately
Bank reconciliationA posting is created when a bank transaction is assigned to an open item
ReversalCounter-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:

Scenario 1Bank reconciliation against an already existing posting

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.

  1. 1Intent A already exists in the journal — its two lines are immutable.
  2. 2On matching, a reference is set only on the bank transaction. Intent A is not touched.
  3. 3When the match is undone, this reference on the bank transaction is cleared again. Intent A is untouched; no reversal is created.
Scenario 2Settlement posting from bank reconciliation

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.

  1. 1The reconciliation creates Intent B with source “bank reconciliation” and a settlement reference to Intent A.
  2. 2The bank transaction is linked to Intent B. Intent A remains exactly as before — only marked as settled.
  3. 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.
Scenario 3Reversal of an already reconciled intent

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.

  1. 1First, the dependent settlement posting B is reversed automatically — Intent C is created, mirroring B.
  2. 2Then Intent A is reversed — Intent D is created, mirroring A and pointing at A via a reversal reference.
  3. 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:

ModeREST posting_modeMCP posting_modeEffect
Current periodcurrent_period or omitcurrent_period or omitReversal 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 periodoriginal_periodoriginal_periodReversal 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:

external_referenceFree-text ID for linking to external systems — e.g. invoice number, Stripe ID, or ERP reference. Max. 500 characters.
custom_metadataFlat key-value object for arbitrary client context — e.g. cost center, project, tags. Max. 20 keys, 4 KB serialized.
Propertyexternal_referencecustom_metadata
Typestring (max. 500 characters)Flat JSON object (string | number | boolean | null as values)
RequiredNoNo
ImmutableYes (GoBD)Yes (GoBD)
In audit hashYesYes (deterministic ordering)
FilterableGET /v1/journal?externalReference=...
Reversal behaviorCopied identically from the originalCopied identically from the original

Typical use cases:

  • Invoice number as referenceexternal_reference: "RE-2025-0042" links the posting to the invoice in the ERP. Via ?externalReference=RE-2025-0042 the corresponding journal entry is immediately findable.
  • Cost center and projectcustom_metadata: { "cost_center": "CC-100", "project": "alpha" } assigns the posting organizationally without having to adapt the chart of accounts model.
  • Origin tags for automationscustom_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:

FieldMeaning
currencyISO 4217 currency code (e.g. USD)
foreign_amountGross amount in foreign currency
rateConversion rate foreign currency → EUR (ECB convention)
rate_dateEffective date of the rate
rate_sourceSource (e.g. ECB, manual, bank)

Important rules:

  • The caller provides the rate explicitly — LedgerLou does not query external rate sources.
  • The EUR amount in lines must 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: null or field omitted).
  • Opening-balance values (POST /v1/bookings/opening-balances) do not support FX.

Workflow

1
Post the transactionVia REST API or MCP. Simple posting or split with multiple positions. Optionally upload a document directly in a single request.
2
Posting lands in the journalAfter confirmation the posting is written immutably to the general ledger — with intent ID, timestamp, and SHA256 hash.
3
Inspect the journalThe journal is filterable by account, period, and search term. Every intent shows all associated posting lines and linked intents.
4
Correct errors via reversalIncorrect postings are reversed at the intent level via 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.

Access methods

Dashboard (UI)Search the journal, view posting details, and trigger reversals.
REST APICreate postings programmatically and query the journal — e.g. for integrations, ETL pipelines, or automated monthly closings.
MCPMCP agents classify transactions, create posting suggestions, and write to the journal after confirmation.

Scopes

journal:readjournal:write