# 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](/api).

```bash
curl 'https://api.ledgerbee.com/api/v1/open-entries?customerId=0197A943-2325-7829-B835-B6C71A293065' \
  -H 'x-api-key: <your-key>'
```

```json
{
  "baseCurrency": "DKK",
  "asOf": "2026-09-30",
  "entries": [
    {
      "kind": "ANCHOR",
      "id": "0197A943-2325-7829-B835-B6C71A293070",
      "type": "INVOICE",
      "reference": "INV-1043",
      "date": "2026-08-14",
      "currency": "DKK",
      "originalAmount": 1250.0,
      "remainingAmount": 850.0,
      "version": "2026-08-14T09:12:44.000Z",
      "dueDate": "2026-09-13",
      "daysOverdue": 17
    }
  ],
  "totals": {
    "perCurrency": [{ "currency": "DKK", "remainingTotal": 850.0 }],
    "baseCurrencyOutstanding": 850.0
  }
}
```

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.

**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](/api). 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.

```bash
curl -X POST 'https://api.ledgerbee.com/api/v1/settlements/preview' \
  -H 'x-api-key: <your-key>' \
  -H 'Content-Type: application/json' \
  -d '{
    "customerId": "0197A943-2325-7829-B835-B6C71A293065",
    "postingDate": "2026-09-01",
    "entries": [
      { "kind": "ANCHOR", "id": "0197A943-2325-7829-B835-B6C71A293070", "amountToApply": 850.0, "version": "2026-08-14T09:12:44.000Z" },
      { "kind": "PAYMENT", "id": "0197A943-2325-7829-B835-B6C71A293088", "amountToApply": 850.0, "version": "2026-08-30T11:04:22.000Z" }
    ]
  }'
```

`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. | The exchange gain/loss account. |
| `WRITEOFF` | A real difference in value. | 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.

## Settle

`POST /v1/settlements`, scope `journal-entries-write` — see the
[API Reference](/api). Send the body you previewed.

```json
{
  "id": "0197A943-2325-7829-B835-B6C71A2930A1",
  "differenceJournalEntryId": "0197A943-2325-7829-B835-B6C71A2930A2",
  "vatReclassJournalEntryId": null
}
```

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 |
| 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 `ACCOUNTING_PERIOD_CLOSED_FOR_POSTING`, `ACCOUNTING_YEAR_CLOSED`, `ACCOUNTING_PERIOD_NOT_CONFIGURED`, `ACCOUNTING_YEAR_NOT_RESOLVED`, `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](/api).
