Journal Transactions
Journal Transactions API
The Journal Transactions API allows you to create balanced multi-entry journal transactions across multiple ledgers with automatic currency conversion to the system's base currency.
Purpose and use
Journal transactions are the controlled way to post balanced debit and credit entries when a banking event cannot be represented as a single simple transfer. They are used for adjustments, fees, settlement corrections, suspense clearance, treasury movements, and accounting operations that must stay double-entry compliant.
Who uses this. Finance operations, treasury, reconciliation teams, and senior operations users use journal transactions when posting or reviewing manual and system-generated accounting movements.
How it works. Every journal contains at least two entries. Total debits must equal total credits in local-currency value, and each entry posts to a ledger that allows direct booking. Posted entries update the target subledger and roll up to control ledgers.
What users do. Users select the ledgers, debit and credit amounts, currency, value date, reference, and supporting note, then review the journal before posting.
Outcomes and side effects. A posted journal immediately changes ledger balances and creates an audit trail. Reversals or corrections should be posted as new journals, not by editing history.
See also:
Object Relationships
Journal transactions are the primary mechanism for complex, multi-party movements of funds. They integrate into the wider ledger hierarchy as follows:
Key Rules
- Settlement: Journal entries immediately update the Posted Balance of the target ledgers.
- Aggregation: If a journal entry is posted to a Subledger, the change automatically cascades up to its Parent Control Ledger(s).
- Balance Constraint: For a transaction to be committed, debits must equal credits per native currency (in minor units) and in the system's Local Currency (LCY).
Key Features
- Create multi-entry journal transactions with any number of ledger entries (minimum 2)
- Add journal entries to existing transactions or create new ones
- Automatic balance validation per native currency and in the system's base currency (LCY)
- Support for different currencies across entries with automatic conversion
- Flexible ledger identification (by UUID, code, or numeric ID)
- Support for transaction metadata and custom properties
- Automatic parent ledger balance updates
- DirectBooking validation - journal entries are rejected on ledgers with DirectBooking=false
Endpoints
Create Journal Transaction
POST /v1/ledgers/journals
Create a balanced multi-entry journal transaction with automatic currency conversion. You can either create a new transaction or add entries to an existing one.
Adding Entries to Existing Transactions
To add journal entries to an existing transaction, include the transaction_id field in your request:
{
"transaction_id": "70a07097-22a2-457a-b6e6-84ce7129d6ca",
"transaction": {
"type": "JOURNAL",
"total_amount": 500000,
"currency_code": "CHF",
"description": "Additional entries for existing transaction"
},
"entries": [
{
"ledger_code": "EXPENSES-UTILITIES",
"amount": 500000,
"type": "debit",
"event": "utility_expense",
"description": "Additional utility expense"
},
{
"ledger_code": "CASH-MAIN",
"amount": 500000,
"type": "credit",
"event": "utility_payment",
"description": "Additional utility payment"
}
]
}If the transaction_id is not provided or is null, a new transaction will be created.
Request Body
{
"transaction": {
"type": "JOURNAL",
"total_amount": 1000000,
"currency_code": "CHF",
"description": "Monthly payroll expense allocation",
"reference_id": "PAY-2023-12",
"value_date": "2023-12-31T12:00:00Z",
"metadata": {
"department": "HR",
"payment_period": "2023-12"
}
},
"entries": [
{
"ledger_code": "EXPENSES-SALARY",
"amount": 1000000,
"type": "debit",
"event": "payroll_expense",
"description": "December 2023 payroll expenses",
"value_date": "2023-12-31T12:00:00Z"
},
{
"ledger_code": "CASH-MAIN",
"amount": 1000000,
"type": "credit",
"event": "payroll_payment",
"description": "December 2023 payroll payment",
"value_date": "2023-12-31T12:00:00Z"
}
],
"is_internal": false
}Response (201 Created)
All monetary amounts are returned in CcyAmtWithPrecision format (minor units as a string, with currency code and precision).
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "JOURNAL",
"status": "completed",
"total_amount": {
"amount": "1000000",
"currency": "CHF",
"precision": 2
},
"currency_id": "7c0dfa3d-2a9e-4d1a-8f8f-0d8d06358334",
"currency_code": "CHF",
"description": "Monthly payroll expense allocation",
"reference_id": "PAY-2023-12",
"timestamp": "2023-12-31T12:00:01Z",
"value_date": "2023-12-31T12:00:00Z",
"completed_at": "2023-12-31T12:00:01Z",
"total_debit_lcy": {
"amount": "1000000",
"currency": "CHF",
"precision": 2
},
"total_credit_lcy": {
"amount": "1000000",
"currency": "CHF",
"precision": 2
},
"metadata": {
"department": "HR",
"payment_period": "2023-12"
},
"created_at": "2023-12-31T12:00:01Z",
"modified_at": "2023-12-31T12:00:01Z",
"created_by": "1e3d0ff4-e9e3-4b6a-a3b3-4e9b97df8f34",
"modified_by": "1e3d0ff4-e9e3-4b6a-a3b3-4e9b97df8f34",
"active": true
}Usage Guidelines
Multi-Currency Entries
When creating journal entries with different currencies, the system calculates the Local Currency Equivalent (LCY) amount for each entry using the current exchange rates, and then enforces two balance rules:
- Per native currency — for every currency in the batch, the sum of debit minor units must equal the sum of credit minor units. This is the rule that decides whether a multi-currency journal is accepted.
- LCY total — the sum of all debit LCY amounts must equal the sum of all credit LCY amounts.
A journal that satisfies the first rule usually satisfies the second, because each currency's debits and credits convert at that currency's rate and so contribute the same amount to both LCY sides. They can still come apart: entries may use different rate types or value dates, and two entries in one currency converted at different rates leave the LCY totals unequal. Both rules are therefore checked, and the per-currency one is checked first.
A cross-currency movement cannot be expressed as one debit in one currency against one credit in another: it needs a leg in each currency, which is what a transit or FX position ledger is for. Any value difference between the two sides — a spread — is a leg of its own, in a single currency.
Rejections name the failure: an out-of-balance currency returns 400 with ledgers.journal_imbalanced_currency carrying the currency and both of its native totals; an out-of-balance LCY total returns 400 with ledgers.journal_imbalanced_lcy carrying both LCY totals. A batch containing an entry whose ledger has no currency at all belongs to no currency total, and returns 400 with currencies_m.missing_currency_info. All are logged with the rejected batch.
Example — a USD expense settled from a EUR account, bridged per currency:
{
"transaction": {
"type": "JOURNAL",
"total_amount": 1000,
"currency_code": "CHF",
"description": "Multi-currency journal entry"
},
"entries": [
{
"ledger_code": "EXPENSES-USD",
"amount": 1000,
"currency_code": "USD",
"type": "debit",
"event": "usd_expense",
"description": "USD expense"
},
{
"ledger_code": "TRANSIT-USD",
"amount": 1000,
"currency_code": "USD",
"type": "credit",
"event": "usd_expense",
"description": "USD side of the conversion"
},
{
"ledger_code": "TRANSIT-EUR",
"amount": 950,
"currency_code": "EUR",
"type": "debit",
"event": "eur_payment",
"description": "EUR side of the conversion"
},
{
"ledger_code": "CASH-EUR",
"amount": 950,
"currency_code": "EUR",
"type": "credit",
"event": "eur_payment",
"description": "EUR payment"
}
]
}USD balances at 1000 against 1000 and EUR at 950 against 950, so both rules hold. Dropping the two transit legs — one debit in USD against one credit in EUR — is rejected with ledgers.journal_imbalanced_currency even when the LCY equivalents match, because neither currency balances on its own.
Transaction Currency
The transaction carries its currency twice: currency_id and the denormalised currency_code. The
request may name either or both, but it must name one — a transaction the server cannot resolve a
currency for is refused with 400 currencies_m.missing_currency_info before anything is written.
currency_id is the authority. It is the field that carries the foreign key, so when both are sent
and they disagree, the transaction is stored with the code of the currency that currency_id names
and the sent code is discarded; the override is logged. The request is not refused over the
mismatch — failing a posting over a label is worse than correcting it. When only currency_code is
sent it is resolved to a currency, and a code that names none is refused with the same 400.
The stored currency_code is therefore always the code of the stored currency_id, and both are
always present on a transaction read back from GET /v1/ledgers/transactions.
Ledger Identification Options
You can identify ledgers in three different ways:
- UUID (highest priority): Use
ledger_idwith a valid UUID - Ledger Code (second priority): Use
ledger_codewith the accounting code - Numeric ID (lowest priority): Use
ledger_num_idwith the numeric identifier
The system will try to find the ledger in the order listed above, based on which fields are provided.
Balance Validation
The system performs the following validations:
- The sum of all debit entries must equal the sum of all credit entries in LCY
- Each ledger must have sufficient funds for debit operations (unless overdraft is enabled)
- All referenced ledgers must exist and be active
- DirectBooking Validation: Journal entries are rejected on ledgers where
DirectBooking=false. This ensures that only ledgers configured for direct booking can receive journal entries. The refusal returns400withdirect_booking_disabledand names the ledger.direct_bookingdefaults to false, so a ledger that has simply never had it enabled refuses just as one that lost it to a subledger child does; the message says to enable it there or post to a ledger that allows it.
Parent Ledger Updates
When an entry affects a ledger that has a parent, the parent's balance (and any ancestors up the hierarchy) will be automatically updated to reflect the change.
Error Codes
| Code | Description | HTTP Status |
|---|---|---|
ledger_not_found | The specified ledger was not found | 404 |
transaction_not_found | The specified transaction was not found | 404 |
insufficient_funds | The ledger cannot cover the debit. Checked against the available balance (posted minus active holds and pending debits), plus the overdraft limit when overdraft is enabled | 402 |
ledger_closed | The ledger's closing date has passed and it no longer accepts postings | 409 |
ledger_inactive | The ledger is deactivated and does not accept postings | 409 |
missing_currency_info | The transaction names no resolvable currency (neither currency_id nor a known currency_code), or an entry's ledger has none | 400 |
journal_imbalanced_lcy | The journal entries don't balance in LCY | 400 |
journal_imbalanced_currency | The journal entries don't balance in one of their native currencies; names the currency and both of its minor-unit totals | 400 |
invalid_entry_type | The entry type is not valid (must be 'debit' or 'credit') | 400 |
invalid_ledger_entry_type | The entry type is not valid for this ledger | 400 |
failed_to_create_transaction | Failed to create the transaction | 500 |
failed_to_create_ledger_entry | Failed to create a ledger entry | 500 |
failed_to_update_ledger | Failed to update a ledger's balance | 500 |
conversion_failed | Currency conversion failed | 500 |
direct_booking_disabled | The entry books to a ledger whose direct_booking is false; names that ledger | 400 |
ledger_has_subledger_children | The entry books to a ledger that has subledgers, which is never a direct-booking target | 400 |
direct_booking_disabled used to be invalid_ledger_configuration, whose text describes a different failure — no control ledger routes an account's customer type and currency — that this endpoint does not raise. It reached clients here unrendered, naming a setting that had nothing to do with the refusal.