CorebanqCorebanq Developer Docs
Ledgers

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:

  1. 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.
  2. 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:

  1. UUID (highest priority): Use ledger_id with a valid UUID
  2. Ledger Code (second priority): Use ledger_code with the accounting code
  3. Numeric ID (lowest priority): Use ledger_num_id with 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:

  1. The sum of all debit entries must equal the sum of all credit entries in LCY
  2. Each ledger must have sufficient funds for debit operations (unless overdraft is enabled)
  3. All referenced ledgers must exist and be active
  4. 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 returns 400 with direct_booking_disabled and names the ledger. direct_booking defaults 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

CodeDescriptionHTTP Status
ledger_not_foundThe specified ledger was not found404
transaction_not_foundThe specified transaction was not found404
insufficient_fundsThe ledger cannot cover the debit. Checked against the available balance (posted minus active holds and pending debits), plus the overdraft limit when overdraft is enabled402
ledger_closedThe ledger's closing date has passed and it no longer accepts postings409
ledger_inactiveThe ledger is deactivated and does not accept postings409
missing_currency_infoThe transaction names no resolvable currency (neither currency_id nor a known currency_code), or an entry's ledger has none400
journal_imbalanced_lcyThe journal entries don't balance in LCY400
journal_imbalanced_currencyThe journal entries don't balance in one of their native currencies; names the currency and both of its minor-unit totals400
invalid_entry_typeThe entry type is not valid (must be 'debit' or 'credit')400
invalid_ledger_entry_typeThe entry type is not valid for this ledger400
failed_to_create_transactionFailed to create the transaction500
failed_to_create_ledger_entryFailed to create a ledger entry500
failed_to_update_ledgerFailed to update a ledger's balance500
conversion_failedCurrency conversion failed500
direct_booking_disabledThe entry books to a ledger whose direct_booking is false; names that ledger400
ledger_has_subledger_childrenThe entry books to a ledger that has subledgers, which is never a direct-booking target400

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.

On this page