A vertical software-as-a-service (SaaS) platform runs embedded payments infrastructure the moment it takes card payments for its merchants and manages merchant balances through Friday payouts. The organization chart may not have a payments team yet, but the platform still owns the problem.
Then month-end arrives. The payment service provider (PSP) dashboard reports one balance for the settlement account. The balances table in Postgres, which the application updates in place, reports a different one. The bank statement someone downloaded on Monday shows a third balance, and the three statements show different five-figure amounts.
Neither the finance lead nor the on-call engineer can say which balance is right, because each system holds a partial view and none of the three systems was built to arbitrate.
You need to make five embedded payments infrastructure decisions (ledger ownership, omnibus attribution, idempotency, daily reconciliation, and rail fallback) to determine whether payment code stays a feature or becomes reliable infrastructure.
1) Ledger ownership: where should the system of record for embedded payments live?
The system of record belongs in a double-entry accounting core ledger the platform controls, with PSP reports and bank statements treated as inputs to reconcile against the ledger. The platform-controlled ledger owns the arbiter role because provider and bank records each offer only a partial view of the flow of funds.
Why a provider state cannot be the source of truth
Provider state cannot be the source of truth because a PSP record covers only the movements the provider observed. It does not record whether bank settlement landed, whether refunds were funded from platform revenue, or whether the platform waived fees.
The PSP record covers authorization and capture, followed by the settlement the provider initiates to your bank account.
Bank statements have the mirror problem: cash arrives and leaves in lump sums with no record of which sub-merchant each dollar belongs to.
Choosing either the provider or the bank as the source of truth makes every movement the other side didn't witness an unexplained difference at month-end.
Four ledger-database properties for embedded payments
The ledger's database layer must enforce four properties:
1Double-entry invariants are enforced at the database level, so the system rejects a posting that doesn't sum to zero before it exists. Enforced only in the application layer, the same posting is rejected only on the code paths someone remembered to guard.
2Append-only immutability preserves history. An in-place balance cell keeps the current number but removes the sequence of postings that produced it. When the number is wrong, nothing can be replayed.
3Write idempotency relies on a database-level uniqueness constraint on the idempotency key, which stops a duplicate retry or redelivered webhook from posting twice.
4Per-entity account isolation scopes every read and write to the owner's account namespace, so one sub-merchant's balance cannot be mutated by a query or write targeting another.
These four properties turn the ledger into an arbiter the platform can trust, but only if a single ledger instance arbitrates across every movement type.
One ledger for PSP-side, bank-side, and internal movements
One ledger links three movement types: PSP-side settlement, bank-side credit, and the internal fee split.
A single query surfaces the postings confirmed by the PSP or bank statement. It exposes any remaining clearing-account balance, while the general ledger functions, including ownership of the profit and loss (P&L), remain in your enterprise resource planning (ERP) system.
Formance Ledger is an open-source, programmable core ledger that unifies fiat and digital assets with regulatory-grade traceability, linking PSP-side settlement, bank-side credit, and the internal fee split in a single system of record.
2) Omnibus attribution: how do you attribute omnibus account funds to each account owner?
Every movement into or out of an omnibus account (a single bank account holding funds on behalf of many named account owners) must be allocated to those owner accounts in the same atomic transaction. The omnibus balance and the sum of owner balances cannot drift apart.
Executing attribution means understanding why omnibus attribution must be atomic, meeting PSD2 Article 10 and EBA safeguarding requirements, and expressing atomic omnibus allocation in Numscript.
Why omnibus attribution must be atomic
Without atomicity, a non-atomic write strands money with no owner or credits owner balances that exceed the cash backing them.
A crash between the bank-side write and the per-owner allocation leaves unallocated money in the omnibus account, and a duplicate settlement webhook produces the same result.
PSD2 Article 10 and EBA safeguarding requirements
PSD2 Article 10(1)(a) sets out the segregation method, one of two ways a payment institution can safeguard funds under Article 10.
Under the segregation method, the institution must keep relevant funds separate from all other funds, and deposit funds still held at the end of the business day following receipt in a separate account with an authorized institution.
The European Banking Authority (EBA) also requires safeguarding to include an administration and reconciliation process that ensures payment service users' funds are insulated.
Platforms should maintain current per-owner attribution throughout the month to support PSD2 safeguarding and EBA reconciliation requirements.
Atomic omnibus allocation in Numscript
In an omnibus account, a single PSP settlement lands as one lump-sum credit that must be split across many named owner accounts in the same posting.
Numscript, Formance's purpose-built language for describing financial transactions, expresses that split as one atomic transaction.
Here, a $1,250,000 PSP settlement is allocated across two sub-merchants and the platform's fee revenue account.
@counterparties:psps:provider:clearing tracks the PSP clearing position, while @merchants:8814:available and @merchants:2207:available hold merchant funds.
In this case, @platform:revenue:fees holds platform fee revenue.
The transaction moves the full amount out of the PSP clearing account and allocates it among the sub-merchant available balances and the platform's fee revenue account.
It records the provider's settlement ID and idempotency key as transaction metadata:
The three amounts ($987,500, $245,000, and $17,500) sum to $1,250,000. The clearing account is the intermediate account between the provider's claim and the bank's cash.
When the $1,250,000 credit appears on the bank statement, you post it into the same clearing account, so the balance returns to zero.
A non-zero clearing-account balance the next morning signals a reconciliation break. Atomic allocation depends on posting the transaction exactly once, which is the idempotency problem.
3) Idempotency: how do you keep payment postings idempotent across retries and webhooks?
A payment posting must apply exactly once regardless of client retries or webhook redelivery. The client generates an idempotency key with each request.
A shared layer in front of the ledger, such as the API gateway or ingress that every webhook consumer and batch job uses, records the key under a unique constraint before the posting runs.
Engineers must handle outbound API timeouts and inbound webhook redelivery, apply the IETF Idempotency-Key header contract, and decide where to store the idempotency key.
Outbound API timeouts and inbound webhook redelivery
Two retry paths must be handled. Outbound is when the client retries a payout the bank API already accepted after a timeout, and inbound is when a webhook or scheduler redelivers the same event.
A scheduler can execute the same payroll batch twice if it doesn't check the batch identity before posting. An idempotency check should reject repeated execution under the same event or batch identity.
The IETF Idempotency-Key header contract
The IETF Idempotency-Key draft sets three rules for how a server should behave when it receives the same key twice:
1Reply 409 if the first request is still running.
2Reply 422 if the second request has a different payload.
3Reject any request that carries more than one key.
Webhook consumers should apply the same three rules when they turn an event into a ledger posting.
Where to store the idempotency key
The idempotency key belongs in the shared request-processing layer, such as the API gateway (for uniqueness enforcement), and the ledger transaction as metadata (for traceability).
Store it only in the gateway, and auditors have no trail from a bank statement to the ledger transaction. Store it only on the ledger transaction, and duplicate retries can still create duplicate postings before the ledger check runs.
Webhook consumers, batch jobs, and internal admin tools should all use the same shared uniqueness check. Exactly-once postings are a precondition for the daily reconciliation control.
4) Daily reconciliation: how do you reconcile the ledger against PSP settlement files and bank statements every day?
Daily reconciliation matches ledger postings against PSP settlement files and bank statements at a fixed cut-off. It works only if every posting includes the provider's transaction ID and both effective and recorded timestamps.
Each daily reconciliation runs three matches (transaction, balance, and settlement) and uses bi-temporality to make the cut-off reproducible after late data lands.
Three daily reconciliation matches at transaction, balance, and settlement level
Each daily reconciliation runs three comparisons against the same cut-off:
1Transaction-level: each line in the PSP settlement file matches one posting, keyed on the provider's transaction ID stored on that posting.
2Balance-level: the bank's omnibus balance equals the sum of user balances plus the clearing accounts.
3Settlement-level: the aggregate the PSP says it owes you equals what the bank received.
A break at the settlement-level match with clean transaction-level and balance-level matches points to the provider's fee computation.
The 1:1 and N:1 shapes these comparisons take across several PSPs follow the account reconciliation patterns used in high-volume fintech.
Why bi-temporality makes a cut-off reproducible
Bi-temporal ledger records, which track both when an event was effective and when the system recorded it, make a cut-off reconciliation reproducible after late data arrives.
A settlement effective on the 8th that the provider's file delivers on the 10th belongs in the 8th's reconciliation.
With a single timestamp, rerunning the 8th's reconciliation on the 11th produces a different result than the run on the 9th did, and the auditor who asks which number you reported cannot reproduce either result.
Bi-temporal records turn "what did the ledger know at the cut-off" into a deterministic query.
Five questions to run against your embedded payments infrastructure
The five decisions (ledger ownership, omnibus attribution, idempotency, daily reconciliation, and rail fallback) are only useful if you can pressure-test them against what you already run.
Each question below maps to one decision and has a yes-or-no answer.
1Ledger ownership: when the PSP dashboard and your internal balance disagree, does your team default to the ledger as the arbiter, or does someone open the PSP dashboard to decide?
2Omnibus attribution: does every credit to your omnibus bank account carry a same-transaction allocation to named owner accounts, and is the clearing account for last week's settlements sitting at zero?
3Idempotency: if you replay yesterday's webhook stream twice, does the ledger end in the same state both times?
4Daily reconciliation: did this morning's reconciliation run three matches against a fixed cut-off, and can you reproduce last week's reconciliation and get the same result?
5Rail fallback: if an instant-rail payout failed over to wire yesterday, does your ledger show two separate in-flight postings, or was the original submission overwritten?
A "yes" on every question means your ledger is the arbiter, allocations are atomic, retries are safe, reconciliation is reproducible, and rail fallback preserves the original submission.
A "no" on any of them means the ledger is not doing that job yet, and something else is filling the gap. Usually that gap is a mutable balance table someone patches by hand, retry deduplication scattered across services, or reconciliation scripts nobody wants to touch before an audit.