---
title: "Reconcile a single bank transaction"
method: PUT
path: "/bank-transactions/{id}"
tags: ["Bank Imports"]
---

# Reconcile a single bank transaction

`PUT /bank-transactions/{id}`

## Request

- Base URL: `https://api.lumabill.ch/v1`
- URL: `https://api.lumabill.ch/v1/bank-transactions/{id}`
- Auth: HTTP bearer

Update a transaction's reconciliation state. Setting `status=MATCHED` with one or more invoices, an expense or a revenue posts the corresponding payment journal entries (one per invoice). Setting it back to `UNMATCHED` or `IGNORED` reverses all of them. Setting `status=CATEGORIZED` with `bookToAccountId` posts a direct booking against the chosen chart-of-accounts account. Optional `adjustments` book bank fees / rounding alongside the main entry. Mutations on transactions whose journal date falls in a closed fiscal year are rejected.

## Request body

- BankTransactionUpdate: Reconcile a single bank transaction. Setting `status=MATCHED` requires one of `matchedInvoiceIds` (or the legacy `matchedInvoiceId`), `matchedExpenseIds` (or the legacy `matchedExpenseId`), or `matchedRevenueId`. One bank payment can settle several invoices: the bank amount plus adjustments is allocated in the order of `matchedInvoiceIds`, each invoice getting min(open balance, money left), so only the last one can end partially paid. Errors (400): PAYMENT_EXCEEDS_OPEN_BALANCE (more money than the combined open balance), INVOICE_ALLOCATION_ZERO (an invoice would receive nothing), ADJUSTMENTS_EXCEED_ALLOCATION (adjustments are booked on the largest allocation and must be smaller than it), DUPLICATE_INVOICE, INVALID_MATCHED_INVOICES (both fields sent, or not 1-50 UUIDs). One DEBIT bank transaction can likewise pay several expenses (`matchedExpenseIds`, same rule; invoices only match CREDIT lines, expenses only DEBIT lines). An expense payment credits the bank with its allocation plus the signed adjustments, so positive adjustments (bank fees) are paid on top and negative ones (FX gain) reduce the bank amount. Expense-side codes: EXPENSE_ALLOCATION_ZERO, DUPLICATE_EXPENSE, INVALID_MATCHED_EXPENSES, ADJUSTMENTS_EXCEED_BANK_AMOUNT. Every journal entry of a group gets the bank date and a description starting with "Sammelzahlung" plus the bank reference. Undo a group by setting the transaction back to UNMATCHED; undoing one of its documents is refused with 409 (PAYMENT_IN_BANK_GROUP_MATCH / EXPENSE_PAYMENT_IN_BANK_GROUP_MATCH). Setting `status=CATEGORIZED` requires `bookToAccountId`. The journal entry is posted automatically and rejected if the affected fiscal year is closed.
  - `status` 'MATCHED' | 'UNMATCHED' | 'CATEGORIZED' | 'IGNORED', required
  - `matchedInvoiceId` string, uuid, nullable: Legacy single-invoice match, treated as `matchedInvoiceIds = [id]`. Do not send together with `matchedInvoiceIds`.
  - `matchedInvoiceIds` string[], nullable: Invoices paid by this bank transaction, in allocation order.
  - `matchedExpenseId` string, uuid, nullable: Legacy single-expense match, treated as `matchedExpenseIds = [id]`. Do not send together with `matchedExpenseIds`.
  - `matchedExpenseIds` string[], nullable: Expenses paid by this DEBIT bank transaction, in allocation order.
  - `matchedRevenueId` string, uuid, nullable
  - `bookToAccountId` string, uuid, nullable: Chart-of-accounts UUID (required when status=CATEGORIZED)
  - `adjustments` BankTransactionAdjustment[], nullable: On a foreign-currency line the adjustments are in the line currency.
    - `description` string, required
    - `amount` number, double, required: Signed. Positive books a DEBIT (bank fee, Skonto, FX loss), negative a CREDIT (FX gain, overpayment). Must not be zero.
    - `chartAccountId` string, uuid, required
  - `dryRun` boolean: Preview: runs the exact booking (every check and refusal, the CHF value of the line, the released book value, the realized FX, the zero-saldo realization) in a transaction that is rolled back. Answers 200 `{ message, dryRun: true, fx? }`; nothing is booked. Only for a request that books (status MATCHED with a document, or CATEGORIZED with `bookToAccountId`). A rate lookup during the preview may still fill the shared rate cache. Read strictly: `true`/`false` or the strings "true"/"false" (any case); any other value answers 400 and nothing is booked.
  - `allowCrossCurrency` boolean: Confirms a cross-currency payment: a base-currency (CHF) line that settles a foreign-currency document. Without it such a match is refused with BANK_CURRENCY_MISMATCH and `crossCurrencyPossible: true` (see BankCurrencyMismatch). The CHF moved is the bank amount; the difference to the document's CHF book value is booked as realized FX. The rate it implies (bank amount per FW settled) must lie within 0.5x to 2x of the ESTV/BAZG rate of the value date, else 400 EXCHANGE_RATE_IMPLAUSIBLE (with `impliedRate`, `officialRate`, `min`, `max`). Read strictly like `dryRun`.
  - `fwAmount` number, double: Cross-currency match only: the foreign-currency amount this line settles across the selected documents (default: everything still open). Positive; 400 INVALID_FW_AMOUNT otherwise. A part payment must send it: against everything open its implied rate is refused as EXCHANGE_RATE_IMPLAUSIBLE.
  - `exchangeRate` number, double: Line on a foreign-currency bank account: override its CHF value with this rate (CHF per 1 unit of the line currency). Default: the statement's own CHF figure, else the ESTV/BAZG rate of the value date. Ignored in a cross-currency match.
  - `baseAmount` number, double: Line on a foreign-currency bank account: the CHF amount that actually moved; wins over `exchangeRate`. Ignored in a cross-currency match.

## Response `200`

Transaction updated (or, with `dryRun`, the booking previewed and rolled back). `fx` is present for a line on a foreign-currency bank account and for a confirmed cross-currency match.

- BankTransactionUpdateResult
  - `message` string, required
  - `dryRun` boolean: Present (true) on a preview; nothing was booked
  - `fx` BankLineFx: How a foreign-currency booking was valued: the CHF value of the bank line and, per settled document, the released CHF book value and the realized exchange difference (booked on the FX result account with `fxKind: realized`).
    - `currency` 'CHF' | 'EUR' | 'USD' | 'GBP' | 'JPY' | 'CAD' | 'AUD' | 'SEK' | 'NOK' | 'DKK' | 'HUF': ISO 4217 code of a currency the API converts (ESTV/BAZG rates). The same list applies to every currency field of the API.
    - `lineBaseAmount` number, double: CHF value of the bank line
    - `lineRate` number, double, nullable: CHF per 1 unit of the line currency
    - `rateSource` 'IDENTITY' | 'ACTUAL' | 'MANUAL' | 'STATEMENT' | 'BAZG': STATEMENT = the bank statement's own CHF figure; ACTUAL / MANUAL = your `baseAmount` / `exchangeRate`; BAZG = ESTV/BAZG rate of the value date; IDENTITY = base-currency line
    - `documents` object[]
      - `invoiceId` string, uuid
      - `creditNoteId` string, uuid
      - `expenseId` string, uuid
      - `revenueId` string, uuid
      - `currency` 'CHF' | 'EUR' | 'USD' | 'GBP' | 'JPY' | 'CAD' | 'AUD' | 'SEK' | 'NOK' | 'DKK' | 'HUF': ISO 4217 code of a currency the API converts (ESTV/BAZG rates). The same list applies to every currency field of the API.
      - `fwAmount` number, double: Settled amount in the document currency
      - `exchangeRate` number, double, nullable
      - `rateSource` string
      - `baseAmount` number, double: CHF moved for this document
      - `releasedBookValue` number, double: CHF book value released from the open item
      - `realizedFx` number, double: Realized exchange difference in CHF (positive = gain)
    - `zeroSaldoRealization` object: When the booking brings the foreign-currency bank account to FW 0 with a CHF rest, that rest is realized as an exchange difference. A dry run has no journalEntryId.
      - `journalEntryId` string, uuid
      - `amount` number, double
      - `side` 'DEBIT' | 'CREDIT': Side of the CHF rest on the bank account
  - `zeroSaldoSkipped` 'PRIOR_YEAR_NOT_CLOSED' | 'FX_ACCOUNT_MISSING': Present when the booking emptied a foreign-currency bank account in FW but its residual CHF book value was not realized (zero-saldo rule): `PRIOR_YEAR_NOT_CLOSED` (the previous fiscal year is open, no Saldovortrag yet) or `FX_ACCOUNT_MISSING` (no FX result account). The booking itself is done.

## Other responses

- `400`: Validation error. A currency refusal (errorCode BANK_CURRENCY_MISMATCH) carries the details of BankCurrencyMismatch; other FX codes: BANK_LINE_CURRENCY_MISMATCH, BANK_ACCOUNT_CURRENCY_UNCONFIRMED (a foreign-currency bank account still posting to a CHF account: confirm its currency first), MIXED_DOCUMENT_CURRENCIES, OPEN_ITEM_RATE_MISSING, FX_RATE_UNAVAILABLE (no ESTV/BAZG rate: send `exchangeRate` or `baseAmount`), FX_RATE_SOURCE_UNAVAILABLE (ESTV/BAZG unreachable: retry), INVALID_EXCHANGE_RATE, EXCHANGE_RATE_IMPLAUSIBLE (override or statement figure outside 0.5x to 2x of the ESTV/BAZG rate of the value date), AMOUNT_OUT_OF_RANGE, INVALID_FW_AMOUNT.
- `401`: Invalid or missing API key
- `403`: Subscription required or read-only key on a write endpoint
- `404`: Resource not found
- `409`: Concurrent change, nothing booked. The booking locks the bank line first: BANK_TRANSACTION_CHANGED when the line was matched, categorized or unbooked since it was read (reload and retry). A match also re-checks the matched documents under lock and answers with the allocation code (e.g. PAYMENT_EXCEEDS_OPEN_BALANCE) when they changed in between.
- `500`: Internal server error

---

[API](https://skmtc.dev/lumabill/apis/lumabill-api.md) · [All operations](https://skmtc.dev/lumabill/apis/lumabill-api/llms.txt) · [OpenAPI document](https://skmtc.dev/lumabill/apis/lumabill-api/revisions/e48e7099b545?raw)
