Skip to main content

Balances: ledger vs available

Tell the three balances apart, and know which one belongs in your interface.

Before you begin​

Nothing to set up — but this page is worth reading before you build a balance display rather than after. Getting it wrong is the single most common source of support tickets on a new card program, and the symptom appears only once real authorizations start flowing.

How it works​

Every account carries three figures:

BalanceMeaning
ledger_balanceMoney that has settled. What the account actually holds
held_balanceMoney reserved by approved authorizations that have not yet cleared
available_balanceledger_balance − held_balance. What can still be spent

The relationship is arithmetic and always holds:

available_balance = ledger_balance − held_balance

The rule​

Show available_balance to end users. Reconcile against ledger_balance.

An interface built on ledger_balance shows money the cardholder cannot spend, because it ignores every hold placed earlier that day. The cardholder sees S$250, gets declined at S$220, and calls support. Nothing is broken — the display was answering a different question from the one being asked.

Conversely, reconciling against available_balance is also wrong: holds are provisional and can vanish without any money moving. Reconcile on settled money only.

A worked example​

One account, followed through a complete authorization lifecycle. Amounts are minor units, as the API returns them; the currency is SGD throughout.

Step 1 — Fund the account with S$250.00​

POST /v1/accounts/{id}/credit { "amount": { "amount": 25000, "currency": "SGD" } }
ledgerheldavailable
After funding25000025000

Nothing surprising yet. This is the state most balance displays are built and tested against, which is why the bug ships.

Step 2 — Cardholder buys coffee, S$42.50​

The merchant requests authorization. We approve it and place a hold.

ledgerheldavailable
After authorization25000425020750

No money has moved. ledger_balance is untouched. The cardholder can now spend S$207.50, not S$250.00.

A display reading ledger_balance still says S$250.00 here. That is the bug, and it is invisible until someone tries to spend the difference.

Step 3 — The merchant clears, at S$46.00​

Next day, a tip is added and the merchant submits the transaction for real. The hold is released and an actual debit is posted — for a different amount than was authorized.

ledgerheldavailable
After clearing20400020400

25000 − 4600 = 20400. The S$42.50 hold released in full; the S$46.00 charge posted against the ledger.

This is normal. Restaurants, fuel pumps, and hotels all authorize one amount and clear another. Any reconciliation that assumes authorized equals cleared will break on the first tip.

The whole sequence​

EventledgerheldavailableMoney moved?
Opening000—
Credit S$250.0025000025000Yes
Authorize S$42.5025000425020750No
Clear at S$46.0020400020400Yes

Two edge cases worth knowing​

Holds that never clear. An authorization that is neither cleared nor reversed eventually expires, and the hold is released with no money moving. Until then those funds are unavailable — which is the usual explanation when a cardholder insists money is missing after cancelling an online order. authorization.expired fires when it resolves.

available_balance can go negative. If a transaction clears for more than it was authorized for, and the difference exceeds what is left, the account goes overdrawn. Take the example above with only S$44.00 funded:

Eventledgerheldavailable
Credit S$44.00440004400
Authorize S$42.5044004250150
Clear at S$46.00−2000−200

Handle a negative balance in your interface. Do not assume the field is unsigned, do not clamp it to zero, and do not crash on it. Subsequent authorizations decline with insufficient_funds until the account is credited.

Verifying​

Read the account at each step and assert on all three fields:

curl "$ALETA_BASE_URL/v1/accounts/acct_01HX8ZQ2K4M7P9RSTVWXYZAB" \
-H "Authorization: Bearer $ALETA_ACCESS_TOKEN"

In sandbox you can drive the whole sequence yourself — and the assertions in the middle are the ones that catch real bugs:

POST /v1/simulate/authorization { "card_id": "...", "amount": { "amount": 4250, "currency": "SGD" } }
# assert available dropped by 4250, ledger unchanged

POST /v1/simulate/clearing { "authorization_id": "...", "amount": { "amount": 4600, "currency": "SGD" } }
# assert held is 0, ledger dropped by 4600

An implementation that treats a hold as a debit, or that fails to release a hold on clearing, passes every single-call test and fails this one. See sandbox and test data.

Common errors​

StatuscodeWhat it usually means
404account_not_foundReading a balance with GET /v1/accounts/{account_id}: the account doesn't exist, or belongs to another organization
402insufficient_fundsChecked against available_balance, not ledger_balance. Held funds are not spendable
422currency_mismatchComparing or combining money across currencies. Balances are only comparable within one currency