The Ledger Behind Every Authorization in Card Issuing Platforms
By Formance Staff
Ledger
Connectivity
Payments
Loading article content
Loading table of contents
Loading author
_FORMANCE LEDGER/
Post your first authorization hold
Running a card issuing platform means running a ledger that answers the authorization question in milliseconds and reconciles cleanly against every settlement file. Clone Formance Ledger, run it locally, and post your first authorization hold against a test cardholder account.
A $48,500.00 corporate purchasing card webhook arrives, your service decrements the balance row, and then the processor misses your 200 response and retries. The retry carries a new request ID, passes your request-level check, and decrements the row again. The cardholder is now $48,500.00 short on a ledger that believes both holds are real. No control will flag the duplicate hold until the settlement file arrives.
In a card issuing platform, the ledger is the component your engineering team controls, and it decides whether every authorization is approved or declined. The ledger sits behind the issuer processor, answers the balance question in milliseconds, holds funds while the network clears, and reconciles against settlement files that arrive hours or days later.
The sections below cover where the ledger sits in the authorization path, how to model the hold lifecycle, how to stop concurrent authorizations from double-approving, which key deduplicates retried webhooks, and how to reconcile against late settlement files.
Where your ledger sits in the card issuing authorization path
Your ledger sits at the end of the authorization path, called by the issuer processor after the network's request has been decoded, and it owns the balance of record.
The ledger’s position determines both which roles surround it in the authorization path and what happens when the balance of record is wrong, or the processor fails.
The roles in the authorization path
An authorization travels from the network to the BIN sponsor's scheme connection, through the issuer processor that decodes the message, to your ledger, which approves or declines and returns the answer along the same route.
An authorization reserves funds, and clearing and settlement move the funds later. A card issuing platform typically separates four roles around the ledger:
1A bank identification number (BIN) sponsor that holds the network membership.
2An issuer processor that decodes the authorization message.
3A program manager that runs onboarding and disputes.
4The ledger itself.
Of these four roles, the BIN sponsor is the card issuer of record to the network. The scheme recognizes it as the regulated entity liable for every authorization your ledger approves. The processor, program manager, and ledger all operate under the BIN sponsor's program agreement.
Balance of record and processor availability failure modes
The two failure modes are a wrong balance of record (yours to prevent) and processor unavailability (yours to plan around).
When a third-party issuer processor suffers a database failure or extended outage, card and ATM transactions for every downstream program can go down for hours until the processor is restored and the backlog is drained.
Your recovery design has to decide which stand-in rules apply, whether the processor approves on your behalf against a stale balance, and how you replay the backlog through the same ledger checks when the processor returns.
How to model the authorization hold lifecycle for captures, reversals, expiries, and increments
Model each hold as a ledger posting from the cardholder's available account to a per-authorization-hold account, and record every later lifecycle event (capture, reversal, expiry, or increment) as another posting.
Postings are immutable and hash-chained, so every later lifecycle event must be a new posting rather than an in-place update.
The cardholder balance shows available funds, held funds sum across the cardholder's hold accounts, and the posted balance shows what has been sent to scheme settlement. All three balances are derived from the same posting history. A ledger balance stored as a mutable column has no equivalent derivation.
Modeling the lifecycle in practice means understanding how to post an authorization hold and its capture as atomic transactions, then how to release, expire, and increment that hold as the network sends follow-up messages.
How to post an authorization hold and its capture
Post an authorization as a single transaction from available to a hold account keyed on the authorization identifier.
For a $48,500.00 authorization, the transaction moves the funds from the cardholder's available account into a hold account whose path ends in the authorization ID. The transaction attaches the network transaction identifier, scheme, and merchant category as metadata.
The examples use Numscript, Formance's domain-specific language for describing financial transactions in terms readable by both engineers and finance teams.
// CARD_AUTH// Event: place a $48,500.00 hold against the cardholder's available balancesend [USD/2 4850000] ( source = @users:88213:available destination = @users:88213:hold:5f9c1)set_tx_meta("event_type", "card_auth")set_tx_meta("authorization_id", "5f9c1")set_tx_meta("network_transaction_id", "5f9c1234")set_tx_meta("scheme", "visa")set_tx_meta("merchant_category", "5045")
The transaction rejects atomically when @users:88213:available holds less than $48,500.00, and the atomic rejection is the decline.
A capture creates two postings when the cleared amount is below the authorized amount. The captured amount moves from the hold account to scheme settlement, and the remainder returns to available.
If the $48,500.00 authorization clears for $46,200.00, the $46,200.00 goes from @users:88213:hold:5f9c1 to @counterparties:cardSchemes:visa:settlement, and $2,300.00 returns to @users:88213:available.
Both postings belong in a single atomic multi-posting (the transaction succeeds or fails as a single unit), so the hold account ends at zero, and no partial state is ever readable.
How to release, expire, and increment a hold
Release a reversed hold with a single posting from the hold account back to available.
Expire an uncleared hold the same way once the authorization window closes. The window length depends on the card network and merchant type, so store it in the ledger database.
Date the release posting at the window boundary so a lookup for the day before shows the hold open and a lookup for the day after shows it released.
An incremental authorization adds another posting from available into the same hold account. Card networks give every increment the same transaction identifier as the original authorization, so keying the hold account on that identifier means every increment lands in the same account. A single reversal then empties it no matter how many increments built it up.
How to stop concurrent card authorizations from double-approving
Make the balance check and the hold posting one atomic write in the ledger database layer, because two authorizations that read the same available balance at the same instant can both be approved.
Read-compare-write fails under concurrency because retries can't patch the rerun. The atomic-write and append-only alternatives to read-compare-write are the two database-layer options that actually close it.
Why read-compare-write fails
A cardholder with $50,000.00 available and two $48,500.00 authorizations in flight can end up with both approved and a $47,000.00 overdraft that neither request could see.
Between the read and the write, a second request reads the same balance before the first one commits.
Retry logic layered on top does not close the read-to-write race window. The retry just re-runs the same read against a balance that may already be stale.
Atomic-write and append-only alternatives to read-compare-write
The atomic-write approach rejects the transaction at the database level whenever the source account would go negative, so the balance check and the hold posting land as one indivisible operation.
The append-only-log approach never stores balances in a mutable row. Every posting is appended to an immutable log, and the current balance is derived by summing the log on read, which makes concurrent writes safe by construction.
Serializing writes per account creates contention on hot accounts, such as a corporate card shared across a large team, so one option is to shard hot accounts into sub-accounts and sum them on read.
The ledger integrity invariants for both alternatives need to ensure ledger integrity. Formance Ledger uses the append-only-log approach.
Which key deduplicates retried issuer processor webhooks
Use the network transaction identifier, the message type, and the processor's stable message ID (the ID that stays the same across retries) as the idempotency key. Then store the idempotency key in the same database transaction as the posting under a unique constraint.
The correct idempotency key for authorization webhooks prevents a retry from posting a second hold, and storing it safely with the posting keeps the ledger consistent if the two writes diverge.
Choosing the idempotency key for authorization webhooks
Use the network transaction identifier plus the message type as the idempotency key, and not the HTTP request ID.
A processor retry can carry a new request ID and the same network transaction identifier, so a key built on the HTTP request header treats the retry as a fresh authorization and posts a second hold.
Visa requires incremental authorizations, and any reversal that covers them, to carry the same Transaction Identifier as the original authorization (Sections 5.7.2.5 and 5.7.3.6).
The identifier repeats across events, and every increment uses the same message type; add the processor's stable message ID so each event gets its own key while retries of the same event still match.
Storing the idempotency key safely with the posting
Store the idempotency key in the same database transaction as the posting, under a unique constraint, so a divergence between the two writes cannot leave one write without the other.
When a duplicate webhook arrives, return the stored result of the first delivery with a success status. Returning a 409 or decline can prompt the processor to decline the cardholder for a purchase your ledger already approved.
When two retries arrive simultaneously, the unique constraint admits one retry. The other blocks, reads the committed result, and returns it.
An idempotency key enforced at write time would have collapsed the retries into a single posting before any Commonwealth Bank customer ever saw the second charge.
How to reconcile the authorization ledger against late settlement files
Match settlement records to authorizations on the authorization identifier, treat any unmatched record as a break, and post every correction as a compensating posting dated to the settlement day.
Matching authorizations to settlement records
For matched records, match first on the authorization identifier. For unmatched records, compare amount and apply a timing window.
Financial reconciliation runs on two clocks: effective time (when the network says the transaction settled) and recorded time (when the file reached your ledger). Both clocks preserve the bi-temporal card timeline for chargebacks and cross-border captures.
Each unmatched record is a break with its own compensating posting. Foreign exchange (FX) variance (authorization in one currency and settlement in another) is a common break. Partial captures, incremental authorizations, and reversals that arrive out of order relative to the original auth are other common breaks.
Scheme rules acknowledge that reconciliation messages and net settlement advisements will not always agree line-for-line, so a zero-break day is not the baseline.
Design the matching pipeline to expect a steady rate of breaks, classify them by type, and route each class to the compensating-posting pattern below.
Compensating postings for corrections
Post every correction as a compensating posting, and never as an edit to the original. A compensating posting is a new entry, effective on the settlement day and recorded when the break was detected, that reverses or offsets a prior posting while leaving the original intact.
If a settlement record lands with the wrong amount, book a second posting that moves the difference in the correct direction. If a capture posts to the wrong internal account, book one posting that reverses the mistaken leg and another that books the correct leg.
Editing the original in place destroys the audit trail. It breaks point-in-time queries because a lookup for any date before the correction can no longer return what the ledger actually believed at that time.
Compensating postings keep the original readable, the correction discoverable next to it, and the full history reconstructable for regulators, disputes, and reconciliation.
The five design decisions behind a card issuing authorization ledger
Every authorization your platform approves passes through the same five design points. The platforms that hold up through scheme audits, processor outages, and settlement drift are the ones that resolved all five before shipping.
The failures show up later, usually as a reconciliation break nobody can trace back to a decision.
1Ledger position and failover: Own the balance of record, and decide up front whether stand-in rules approve against stale balances or the platform goes dark until the backlog drains.
2A hold account per authorization: Auths, captures, reversals, expiries, and increments all land as immutable postings against the same per-auth account, with balances derived from the log rather than a mutable row.
3Atomic writes or an append-only log: Both close the read-to-write race, so pick the one that fits your contention profile and shard hot accounts when a single card carries too much traffic.
4Idempotency on the network transaction ID plus message type: Never use the HTTP request ID, and always persist the key in the same database transaction as the posting under a unique constraint.
5Bi-temporal reconciliation with compensating postings: Match on effective and recorded time, and correct only by appending a new posting dated to the day the break was detected.