Skip to main content
This guide shows how to implement omnibus account patterns in Formance Ledger for managing client funds across multiple financial institutions.
This guide demonstrates practical implementation patterns for omnibus accounts using Formance Ledger. The platform’s flexibility allows you to adapt these patterns to your specific business requirements, financial practices, and regulatory obligations.

What is an Omnibus Account?

An omnibus account is a pooled account held at a financial institution that aggregates assets belonging to multiple end-users. This structure enables companies to provide financial services to their clients while maintaining relationships with banking partners. From an accounting perspective, the assets held in the omnibus account represent liabilities to your end-users—they have a legal claim on the funds you hold on their behalf.

Common Use Cases

  • Banking Services: Local and international wire transfers, multi-currency accounts
  • Financial Markets: Stock brokerage, currency exchange, collateral management
  • Cryptocurrency: Deposits, issuing, reserve assets, withdrawals, remittances, redemption wallets
  • Insurance & Risk: Premium collection, claims management, liquidity pools

Key Terminology

Account Classification Terms:
  • Nostro Account: “Our account with them” - your institution’s accounts held at partner banks
  • Vostro Account: “Their account with us” - client accounts held at your institution
  • Loro Account: “Their money with you” - viewed from the partner institution’s perspective
Accounting Fundamentals:
  • Normal Debit Accounts: Used for assets. Debits increase the balance, credits decrease it
  • Normal Credit Accounts: Used for liabilities. Credits increase the balance, debits decrease it
  • Double-Entry: Every transaction affects at least two accounts, maintaining the accounting equation
Operational Concepts:
  • Multi-Asset Support: Handle USD, EUR, GBP, and other currencies in the same system
  • Multi-Bank Operations: Maintain nostro accounts across different correspondent banks
  • Nostro Reconciliation: Ensure your ledger representation matches the bank’s actual statements
In Formance Ledger, assets are held in debit accounts (normal debit accounts), while client liabilities are tracked in credit accounts (normal credit accounts). This maintains the accounting equation: Assets = Liabilities + Equity.

Implementation Guide

This section demonstrates how to model omnibus account operations in Formance Ledger using real-world examples.

System Architecture

1

Define your actors

Identify the key participants in your omnibus account system.
  • Banks & Financial Institutions
  • End Users (Clients)
  • Platform Accounts
Normal Debit Accounts - Hold actual assets at partner banks.These accounts represent your nostro accounts and are identified by their unique identifiers:
  • European accounts: IBAN format (e.g., FR7630004028379876543210943)
  • US accounts: ABA routing + account number (e.g., 021000089:123456789)
Account structure example:
@banks:{bank_id}:main
@banks:{bank_id}:payout
Use sub-accounts to handle asynchronous operations where transactions are recorded across multiple events (instruction, processing, settlement).
2

Configure asset handling

Understand how Formance represents monetary values.Formance uses Universal Monetary Notation (UMN) to ensure precision:
Traditional FormatUMN FormatDescription
\$12.34USD/2 12342 decimal places, value in cents
€56.78EUR/2 56782 decimal places, value in cents
¢1234USD/2 1234Already in smallest unit
UMN eliminates floating-point errors by always working with integers in the smallest currency unit.

Transaction Patterns

The following Numscript examples demonstrate common omnibus account operations. Adapt these patterns to match your specific business requirements and operational workflows.
These examples use Numscript features available in Ledger v2.3+. Ensure your deployment is running a compatible version.
Download the T-Account Movements Excel Template to visualize all accounting entries with detailed T-account diagrams for each omnibus flow described in this guide.
Zoom 🔍

Client Deposit (Payin)

Record a deposit when funds arrive at your bank account with clear client identification. Scenario: A client deposits money into your omnibus account, and you can identify them from the transaction reference.

Variables

  • $asset (asset): Asset in Universal Monetary Notation (e.g., EUR/2, USD/2)
  • $amount (number): Transaction amount in the smallest currency unit (e.g., cents)
  • $bank_number (account): Bank account identifier (IBAN or routing:account format)
  • $client_id (account): Unique client identifier for account routing
  • $reference (string): Transaction reference for audit trail and reconciliation

Accounts Used

  • @banks:$bank_number:main : Nostro account holding actual funds (normal debit)
  • @clients:$client_id:main : Client liability account (normal credit)

Numscript

vars {
    asset $asset
    number $amount
    account $bank_number
    account $client_id
    string $reference
}

// Record the deposit by moving funds from bank to client account
send [$asset $amount] (
    source = @banks:$bank_number:main allowing unbounded overdraft
    destination = @clients:$client_id:main
)

// Attach reference metadata for reconciliation
set_tx_meta("reference", $reference)
The allowing unbounded overdraft clause permits the bank account to go negative, accommodating scenarios where ledger updates occur before bank statement reconciliation.

Example Usage

EUR deposit - Try in Playground →
{
  "variables": {
    "asset": "EUR/2",
    "amount": "1234",
    "client_id": "123",
    "bank_number": "FR7630004028379876543210943",
    "reference": "Client 123 payin"
  }
}
USD deposit - Try in Playground →
{
  "variables": {
    "asset": "USD/2",
    "amount": "3456",
    "client_id": "456",
    "bank_number": "021000089:123456789",
    "reference": "Client 456 payin"
  }
}

Unidentified Deposit (Suspense Account)

Handle deposits when you cannot immediately identify the client from the transaction details. Scenario: Funds arrive at your omnibus account but the payment reference doesn’t clearly identify the sending client.
You cannot refuse incoming funds to an omnibus account. Always book the transaction immediately to maintain accurate records, even if client attribution is pending.
Why This Happens:
  • Missing or incorrect payment references
  • Bank statement processing delays
  • Third-party payment intermediaries
  • Manual or cash deposits

Variables

  • $asset (asset): Asset in Universal Monetary Notation
  • $amount (number): Transaction amount in smallest currency unit
  • $bank_number (account): Bank account identifier receiving the funds
  • $platform_name (account): Platform identifier for organizing suspense accounts
  • $reference (string): Available transaction reference (even if incomplete)

Accounts Used

  • @banks:$bank_number:main : Nostro account receiving funds (normal debit)
  • @platform:$platform_name:suspense:payin : Temporary holding account for unidentified deposits (normal credit)

Numscript

vars {
    asset $asset
    number $amount
    account $bank_number
    account $platform_name
    string $reference
}

// Temporarily book to suspense account until client is identified
send [$asset $amount] (
    source = @banks:$bank_number:main allowing unbounded overdraft
    destination = @platform:$platform_name:suspense:payin
)

// Store all available reference information for future matching
set_tx_meta("reference", $reference)
Monitor your suspense accounts regularly. High balances or aging transactions may indicate problems with your payment reference requirements or client communication.

Example Usage

EUR unidentified deposit - Try in Playground →
{
  "variables": {
    "asset": "EUR/2",
    "amount": "1234",
    "platform_name": "service_provider",
    "bank_number": "FR7630004028379876543210943",
    "reference": "Client payin 1 2 3"
  }
}
USD unidentified deposit - Try in Playground →
{
  "variables": {
    "asset": "USD/2",
    "amount": "3456",
    "platform_name": "service_provider",
    "bank_number": "021000089:123456789",
    "reference": "Client 45 payin 6"
  }
}

Resolving Suspense Transactions

Once you identify the client, create a second transaction to move funds from suspense to their account:
vars {
    asset $asset
    number $amount
    account $platform_name
    account $client_id
    string $reference
}

// Move from suspense to identified client account
send [$asset $amount] (
    source = @platform:$platform_name:suspense:payin
    destination = @clients:$client_id:main
)

set_tx_meta("reference", $reference)
set_tx_meta("resolution", "matched_to_client")
This creates a clear audit trail showing both the initial receipt and the subsequent client attribution.

Client Withdrawal (Payout)

Process client withdrawals using a three-stage flow that handles the asynchronous nature of banking operations.
Formance Ledger maintains consistency at all times, adhering to the accounting equation. However, real-world bank transfers are not instantaneous, requiring careful management of in-flight funds.
The Payout Lifecycle:
1

Step 1: Instruction

Client initiates withdrawal. Immediately reserve funds in a payout sub-account to prevent double-spending.Status: Funds reserved, available balance reduced
2

Step 2: Transmission

Submit payout instruction to the bank via API or batch file (PAIN.001 in SEPA, ACH in US).Status: Pending bank processing
3

Step 3: Settlement

Bank confirms funds have been debited from the omnibus account.Status: Payout complete, funds removed from ledger
This three-step process applies to any asynchronous financial operation. The same pattern can be used for payment acceptance when you control the technical initiation receiving valid successful authorizations and capturing amounts.

Step 1: Reserve Funds

Immediately reserve funds when a payout is initiated to prevent the client from spending the same money twice.
Variables
  • $asset (asset): Asset in Universal Monetary Notation
  • $amount (number): Withdrawal amount in smallest currency unit
  • $bank_number (account): Bank account identifier for the payout
  • $client_id (account): Client initiating the withdrawal
  • $reference (string): Internal reference for the transaction
  • $payref (string): Unique payout reference for bank reconciliation (end-to-end ID)
Accounts Used
  • @clients:$client_id:main : Client liability account (normal credit)
  • @banks:$bank_number:payout:$payref : In-flight funds holding account (normal debit)
Numscript
vars {
    asset $asset
    number $amount
    account $bank_number
    account $client_id
    string $reference
    string $payref
}

// Reserve funds by moving from client account to payout sub-account
send [$asset $amount] (
    source = @clients:$client_id:main
    destination = @banks:$bank_number:payout:$payref
)

// Store references for tracking and reconciliation
set_tx_meta("reference", $reference)
The payout reference ($payref) must be unique and recoverable from bank statements. Use formats like end-to-end IDs (SEPA) or trace numbers (ACH).
Example Usage
EUR payout initiation - Try in Playground →
{
  "variables": {
    "asset": "EUR/2",
    "amount": "1234",
    "client_id": "123",
    "bank_number": "FR7630004028379876543210943",
    "reference": "interest payment",
    "payref": "ABC123"
  },
  "balances": {
    "clients:123:main": {
      "EUR/2": 1234
    }
  }
}
USD payout initiation - Try in Playground →
{
  "variables": {
    "asset": "USD/2",
    "amount": "3456",
    "client_id": "456",
    "bank_number": "021000089:123456789",
    "reference": "interest payment",
    "payref": "CDE456"
  },
  "balances": {
    "clients:456:main": {
      "USD/2": 3456
    }
  }
}

Step 2: Transmit to Bank

This operational step occurs outside the ledger. Your systems prepare and submit payout instructions to your banking partner.
Transmission Methods: Europe (SEPA) - PAIN.001 XML Files:
  • ISO 20022 standard format for credit transfers
  • Can include multiple payments in a single file
  • Supports end-to-end identification for reconciliation
  • Typically submitted via SFTP, EBICS, or Swift FileAct
  • Timing: Same-day, daily, or multiple batches per day depending on your agreement
United States - ACH (Automated Clearing House):
  • NACHA format for batch payments
  • Standard batch cutoff times (usually multiple daily windows)
  • Trace numbers for transaction tracking
  • Typically 1-2 business days for settlement
  • Alternative: Real-time payment networks (RTP, FedNow) for instant transfers
Modern Banking/PSP APIs:
  • Many banks or PSPs now offer real-time API access
  • Instant payment initiation with real-time status updates
  • Webhook notifications for settlement
  • Examples: Adyen, Banking Circle, Column, Increase, Stripe Treasury
Duration: Varies from instant to several business days depending on your operational processes, bank partnerships, and contractual obligations to clients.

Step 3: Confirm Settlement

Record the final settlement when the bank confirms funds have been debited from the omnibus account.
Variables
  • $asset (asset): Asset in Universal Monetary Notation
  • $amount (number): Settlement amount in smallest currency unit
  • $bank_number (account): Bank account identifier
  • $reference (string): Settlement confirmation reference
  • $payref (string): Original payout reference for matching
Accounts Used
  • @banks:$bank_number:payout:$payref : In-flight payout account (normal debit)
  • @banks:$bank_number:main : Main bank account (normal debit)
Numscript
vars {
    asset $asset
    number $amount
    account $bank_number
    string $reference
    string $payref
}

// Complete the payout by clearing the reserved funds
send [$asset $amount] (
    source = @banks:$bank_number:payout:$payref
    destination = @banks:$bank_number:main
)

// Record settlement confirmation
set_tx_meta("reference", $reference)
set_tx_meta("status", "settled")
Example Usage
EUR payout settlement - Try in Playground →
{
  "variables": {
    "asset": "EUR/2",
    "amount": "1234",
    "bank_number": "FR7630004028379876543210943",
    "reference": "settlement_confirmed_ABC123",
    "payref": "ABC123"
  }
}
USD payout settlement - Try in Playground →
{
  "variables": {
    "asset": "USD/2",
    "amount": "3456",
    "bank_number": "021000089:123456789",
    "reference": "settlement_confirmed_CDE456",
    "payref": "CDE456"
  }
}
After settlement, the payout sub-account balance returns to zero, and your ledger accurately reflects the actual bank account balance.

Why Not Use @world?

The @world account appears in many documentation examples as a convenient source for assets — it can go into unlimited overdraft, simplifying transaction examples. While useful for tutorials, using real bank accounts in production provides significant advantages: Better Reconciliation:
  • Direct comparison with bank statements
  • Clear audit trail matching external records
  • Simplified compliance and reporting
Operational Visibility:
  • Track in-flight funds and pending settlements
  • Monitor actual vs. available balances
  • Identify discrepancies quickly
Business Intelligence:
  • Understand cash flow by banking partner
  • Analyze fees and transaction costs per bank
  • Optimize banking relationships with data
Technical Aspects:
  • Less single source constraint avoiding row-locking contingency on the database
  • Better parallel processing
@world remains useful for documentation examples, testing environments, internal transfers not involving external banks, prototyping before banking integrations, and use cases with sub-ledgers that are specific to managing assets or liabilities only.

Advanced Ledger Features

Enhance your omnibus account implementation with these powerful Formance Ledger capabilities: