Settlements
An open entry is a document or payment that has not been fully settled. Settling links entries that cancel each other out and closes them, so each stops counting toward what the counterparty owes.
Three calls do the whole job: list the open entries, preview what settling them
would do, then settle the identical body. Route paths here are relative to the
API mount, so the first is https://api.ledgerbee.com/api/v1/open-entries in
full.
Name exactly one counterparty
Every call takes either customerId or supplierId, never both and never
neither. An entry belongs to one side of the books, so a request carrying both
is refused rather than answered about whichever id was read first.
Read the open entries
GET /v1/open-entries?customerId={id}, scope journal-entries-read — see the
API Reference.
Code
Code
Three fields carry into the settle call: kind, id and version.
remainingAmount is how much of the entry is still open, and the ceiling on
what you may apply to it.
asOf answers as of the end of a given day: ?asOf=2026-08-31 reports the
entries as they stood then, and measures daysOverdue against that date.
Paid documents with an amount left
An invoice or credit note can be recorded as paid while part of it is still
left on it, for example 0.11 left after a payment that fell a few øre short.
The listing includes such a document with settledWithRemainder: true, and its
remainingAmount and baseRemainingAmount are only the amount left. That
amount is part of the counterparty's balance, so totals.perCurrency counts it.
Such an entry settles only together with entries that cancel it exactly in the
base currency, so the settlement posts nothing. Apply all of what is left, as
Math.abs(remainingAmount) like any other entry; any other selection that
includes it is refused with
SETTLEMENT_SETTLED_DOCUMENT_REQUIRES_BALANCED_MATCH (see Errors).
Read totals.baseCurrencyOutstanding for the balance, and never sum the
rows. The rows are the individual open documents; the total also includes the
counterparty's opening balance, which no row carries.
Preview the settlement
POST /v1/settlements/preview, scope journal-entries-write — see the
API Reference. Nothing is posted, and the response is a 200.
The preview takes the same body as the settle call, so you send one payload twice. Nothing has to be rebuilt between them, and nothing can differ between what you were shown and what posts.
Code
amountToApply is a magnitude — always positive, and at most that entry's
remainingAmount. Which side of the settlement an entry lands on follows from
what the entry is, so there is no sign to state.
Send the absolute value. A credit note and a payment are reported with a
negative remainingAmount, and passing that figure straight back is refused
with SETTLEMENT_AMOUNT_TO_APPLY_EXCEEDS_REMAINING. Take
Math.abs(remainingAmount). The response signs the amounts for you: an entry
being consumed comes back negative, the one consuming it positive.
Echo each entry's version. It refuses the settlement if the entry was edited
or reversed between reading the list and settling, instead of applying against a
document that has since changed. It does not move when another settlement
applies to the same entry, so an unchanged version is not a promise that as
much is still open — applying more than remains is refused on its own, measured
at the moment of the write.
Read blockers before settling. It carries the codes that would refuse the
settlement, and an empty list is what says the settle call will go through.
warnings are worth checking but do not stop it.
The difference
Settled entries rarely cancel to zero in the company's base currency. The
preview reports what is left over in difference, and the settle call posts it.
difference.kind | What it means | Where it posts |
|---|---|---|
BALANCED | The entries cancel. | Nothing posts. |
FX | They cancel in their own currency but not in the company's, because they were booked at different rates. Also entries in the company's currency and one foreign currency whose difference is within 5% of the foreign entries' total at the posting date's exchange rate. Also a payment in a third currency, such as a USD invoice paid in EUR: no entry is in the company's currency, every entry is settled in full, and the two currencies are worth within 5% of each other at the posting date's exchange rate. | The exchange gain/loss account. |
WRITEOFF | A real difference in value. A payment in a third currency that is more than 5% short or over, or that leaves an entry partly open, is one. | The debtor-loss account on the customer side, the creditor-loss account on the supplier side. |
difference.account names the account, and postings lists every line that
would be written.
An FX difference on entries booked at rates far apart is usually a mis-booked
rate rather than a real exchange difference. The preview says so in warnings
and names the currencies in rateGap, with the lowest and highest rate any
selected entry in that currency was booked at.
A difference is posted only while it is a real exchange difference. An entry whose base-currency amount is its own amount at the rate it was booked at has nothing to check. When an entry carries more than 10 in the base currency beyond that, every exchange rate the extra amount rests on must be a market rate: the entry's own rate, the rate each earlier payment on it was converted at, the rate of a payment's open rest, and, for an amount an invoice and a credit note linked to it hand to each other, the own rate of each linked document the amount comes from and the rates of the payments on it. Each is checked against the market rates LedgerBee stores for the day the rate belongs to, the rate date the entry was booked with or else its document date, or the nearest stored day up to three days away. A rate within 2% of them for EUR, or 10% for any other currency, is a market rate, and the difference is then booked however large it is, as a currency that really moved produces. A rate further off, such as a payment converted at a rate stored per 100, is a wrong figure and is refused (see Errors). Where no market rate is stored near a rate's day, or the amount comes from a linked document in another currency, the difference is posted only while the amount resting on that rate is within 10 in the base currency or 5% of the entry's base-currency amount. Settling an entry against entries that cancel it in the base currency posts nothing and is always accepted.
Settle
POST /v1/settlements, scope journal-entries-write — see the
API Reference. Send the body you previewed.
Code
Each settled entry now shows a smaller remainingAmount on the open-entries
listing, or leaves it entirely once fully applied.
Send idempotencyKey to make a retry safe: the same key with the same body
returns the settlement already recorded instead of posting a second one.
Read the settlements back
GET /v1/settlements?customerId={id} lists every settlement recorded against
one counterparty, newest first; GET /v1/settlements/{id} returns one in full,
with the entries it linked and how much of each it applied. Both take scope
journal-entries-read.
Reversed settlements are included in the list rather than hidden, because they
are part of the trail. Check reversed before treating one as still standing: a
reversed settlement contributes nothing, and the entries it linked are open
again and back on the open-entries listing.
An entry's appliedAmount can be smaller than its originalAmount. A payment
is consumed across as many settlements as it takes, and whatever is left stays
open.
Errors
| Condition | What you observe | Fix |
|---|---|---|
| The request names both a customer and a supplier, or neither | 400 SETTLEMENT_COUNTERPARTY_REQUIRED | Name exactly one |
| The counterparty id is not one of yours | 404 CUSTOMER_NOT_FOUND or 404 SUPPLIER_NOT_FOUND | Resolve ids from GET /v1/customers or GET /v1/suppliers |
| Fewer than two entries | 400 SETTLEMENT_INSUFFICIENT_SELECTIONS | A settlement links something owed to something that pays it |
| An entry id is not open, or not this counterparty's | 400 SETTLEMENT_SELECTION_NOT_FOUND | Re-read GET /v1/open-entries and use the ids it reports |
| An entry was settled in full since you read the list | 400 SETTLEMENT_SELECTION_ALREADY_FULLY_CONSUMED | Re-read the listing; the entry is gone from it |
amountToApply exceeds the entry's remainingAmount | 400 SETTLEMENT_AMOUNT_TO_APPLY_EXCEEDS_REMAINING | Apply at most remainingAmount |
amountToApply is zero | 400 SETTLEMENT_AMOUNT_TO_APPLY_ZERO | Apply a positive amount, or drop the entry from the request |
The entry changed since you read its version | 400 SETTLEMENT_OPTIMISTIC_LOCK_VIOLATION | Re-read the listing and settle against the current entry |
| The document behind an entry has been reversed | 400 SETTLEMENT_SELECTION_DOCUMENT_REVERSED | The document is voided and owes nothing |
| An entry is a document already recorded as paid that still has an amount left, and the other entries do not cancel that amount exactly | 400 SETTLEMENT_SETTLED_DOCUMENT_REQUIRES_BALANCED_MATCH | Apply the whole amount left, against entries that net it to zero in the base currency (they may be in another currency than the document), with no account to book a difference to |
Such a match has a postingDate on or before the day the document was recorded as paid | 400 SETTLEMENT_SETTLED_DOCUMENT_POSTING_DATE_NOT_AFTER_STAMP | Post the match on a later date |
The settlement would book a difference, an entry it closes carries more than 10 in the base currency beyond its own amount at its booked rate, and a rate that amount rests on (the entry's own, an earlier payment's, a payment's open rest, a linked invoice's or credit note's) is outside the band around the market rates stored for that rate's day. Also a payment in a third currency (an FX difference with no entry in the base currency) where an entry's own booked rate is outside that band: the difference would be that rate's error, not an exchange difference | 400 SETTLEMENT_RESIDUAL_ENTRY_RATE_IMPLAUSIBLE. details.reference names the entry whose rate is wrong; details.rate is the rate its figures imply and details.lowestStoredRate / details.highestStoredRate the stored market rates for details.rateDate, as strings in details.currency per one details.foreignCurrency; details.selectionIds lists every refused entry | Correct the entry's amounts, or settle it against entries that cancel it in the base currency, so nothing is booked |
| The settlement would book a difference, an entry it closes carries a base-currency amount its own currency does not account for at its booked rate, above 10 in the base currency and above 5% of the entry's base-currency amount, or an earlier payment on it is that far off its own amount, and that amount cannot be checked against a market rate: none is stored near the day a rate it rests on belongs to, or a linked document in another currency hands it over | 400 SETTLEMENT_RESIDUAL_CARRIES_UNEXPLAINED_BASE_GAP. details.reference names the first such entry; details.baseGap and details.baseAmount are its figures, or the earlier payment's when that payment is what is off, as unsigned two-decimal strings in details.currency, the base currency; details.selectionIds lists every refused entry | Settle the entry against entries that cancel it in the base currency, so nothing is booked, or correct its base-currency amounts |
| The settlement would book a difference, and an entry it closes is in the base currency but its base-currency amount differs from its own amount | 400 SETTLEMENT_RESIDUAL_CARRIES_BASE_GAP_ON_BASE_CURRENCY_DOCUMENT, with the same details fields | Settle the entry against entries that cancel it, so nothing is booked, or correct its amounts |
| The applied amounts pay more than is owed | 400 SETTLEMENT_OVERPAYMENT_REQUIRES_PARTIAL_CONSUME | Apply the amount actually owed and leave the rest of the payment open |
postingDate falls in a closed period or year | 400, one of LEDGER_ACCOUNTING_PERIOD_CLOSED_FOR_POSTING, LEDGER_ACCOUNTING_YEAR_CLOSED, LEDGER_ACCOUNTING_PERIOD_NOT_CONFIGURED, LEDGER_ACCOUNTING_YEAR_NOT_RESOLVED, LEDGER_ACCOUNTING_YEAR_PENDING_RECLOSE | Post to a date in an open period |
| The difference needs an account your company has not configured | 400, one of EXCHANGE_GAIN_LOSS_ACCOUNT_NOT_CONFIGURED, DEBTOR_LOSS_ACCOUNT_NOT_CONFIGURED, CREDITOR_LOSS_ACCOUNT_NOT_CONFIGURED | Assign the account in the gated app under Settings, then retry |
| No debtors/creditors control account covers the entries | 400 SETTLEMENT_CONTROL_ACCOUNT_NOT_CONFIGURED | Assign the control account to the customer or supplier group |
The same idempotencyKey was reused with a different body | 400 SETTLEMENT_IDEMPOTENCY_KEY_CONFLICT | Use a new key for a different settlement |
Reference
Field-level documentation for every endpoint and response shape is in the API Reference.