Skip to main content
Implement a complete financial host system for card issuing with real-time authorization management, hold accounts, and scheme settlement.
This comprehensive guide demonstrates how to build a financial host system for card issuing using Formance Ledger. Learn to manage cardholder balances, process authorizations in real-time, handle holds, and settle with card schemes.

What is a Financial Host?

A Financial Host is the authoritative system for cardholder available balances when authorization requests arrive from card networks. It determines in real-time whether to approve or decline transactions based on the cardholder’s current balance and risk parameters.

Evolution of Financial Hosts

  • Legacy Systems
  • Modern Real-Time
Batch Processing Era:
  • Balances managed outside financial institutions
  • Using floors and ceilings to indicate available balance to spend
  • Daily or periodic synchronization with core banking
  • Limited real-time capabilities
  • Higher fraud risk due to stale balance data
  • Still used to inform processing subsystems of balances snapshots to allow Stand-In Processing or STIP
This approach was necessary due to performance limitations of distributed systems.

Scope of This Guide

This guide focuses on card issuing use cases, demonstrating patterns for:
  • Authorization Management: Real-time approval/decline decisions with hold accounts
  • Balance Tracking: Available vs. actual balance with authorization holds
  • Scheme Settlement: Managing liabilities to Visa, Mastercard, and other networks
  • Transaction Lifecycle: From authorization through presentment to settlement
While this guide focuses on card issuing, these patterns could also apply to any payment scheme requiring real-time balance authority: instant payments, digital wallets, BNPL products, and more.

Implementation Guide

This section demonstrates how to model financial host operations for card issuing in Formance Ledger.

System Actors

  • Cardholders
  • Banks
  • Schemes
  • Platform
  • Program Manager
Normal Credit Accounts - Track cardholder available balances.These are the main customers of the Financial Institution providing issuing services. The financial host manages their available balance and processes transactions they initiate.These accounts represent liabilities towards cardholders. In retail offerings to consumers, each customer has their own account with directly associated available balance. In corporate or B2B setups (e.g., managing expense cards), there might be additional accounts per card. This example focuses on retail ‘pre-paid’ card scenarios.Account structure:
@cardholder:{account_id}:main
@cardholder:{account_id}:hold:{authorization_id}
This can be linked to the Payment Acceptance example, where they can top-up the account, providing immediate balance available for card spending.

Balance & Available Balance

By default, Numscript applies deterministic postings and implicitly considers the available balance before bookings are accepted and registered. If you only have $20 in a user’s account, a transaction/authorization for $21 will be refused. Numscript has provisions to consider overdraft, which changes the available balance and adjusts the zero point accordingly. This enables credit-like products or authorized overdraft facilities.
The authorization hold pattern demonstrated below ensures that funds are reserved for authorized transactions, preventing double-spending while maintaining accurate available balance tracking.

Transaction Patterns

Below are example Numscripts that capture the intent of the movements and bookings needed to handle typical Financial Host cases for cards. This is merely an example and your business might need different implementations, additional details, different steps and events to handle, etc.
Download the T-Account Movements Excel Template to visualize all accounting entries with detailed T-account diagrams for each issuing financial host flow described in this guide.
Zoom 🔍

Card Authorization (Approved for full amount)

Variables

  • $asset (asset): asset in UMN
  • $amount (number): Transaction amount
  • $account_id (account): Account identifier
  • $authorization_id (string): Unique authorization reference
  • $pii_id (string): Payment instrument token
  • $trx_details: JSON blob with transaction details from the processor

Accounts Used

@cardholder:accountid:main@cardholder:account_id:main @cardholder:account_id:hold:$authorization_id

Numscript

vars {
    asset $asset
    number $amount
    number $overdraft
    account $account_id
    string $authorization_id
    string $pii_id
    string $trx_details
}

send [$asset $amount] (
    source = @cardholder:$account_id:main allowing overdraft up to [$asset $overdraft]
    destination = @cardholder:$account_id:hold:$authorization_id
)

set_tx_meta("authorization_id", $authorization_id)
set_tx_meta("pii_id", $pii_id)
set_tx_meta("trx_details", $trx_details)
Funds are moved from the cardholder’s main account to a hold account, making them unavailable for other transactions until the authorization is settled or reversed. The hold account uses the authorization_id to uniquely identify this authorization, allowing for proper tracking even with multiple concurrent authorizations.

Example Usage

Authorization without overdraft - Try in Playground →
{
    "variables": {
        "asset": "EUR/2",
        "amount": "10000",
        "overdraft": "0",
        "account_id": "user123",
        "authorization_id": "auth_abc123xyz789",
        "pii_id": "10765432100123456789",
        "trx_details": "{\"mti\":\"0100\",\"amount\":\"100.00\",\"merchant\":\"BEST COFFEE SHOP\",\"mcc\":\"5814\"}"
    }
}
Authorization with overdraft - Try in Playground →
{
    "variables": {
        "asset": "EUR/2",
        "amount": "10000",
        "overdraft": "5000",
        "account_id": "user123",
        "authorization_id": "auth_abc123xyz789",
        "pii_id": "10765432100123456789",
        "trx_details": "{\"mti\":\"0100\",\"amount\":\"100.00\",\"merchant\":\"BEST COFFEE SHOP\",\"mcc\":\"5814\"}"
    }
}
The overdraft parameter allows you to define how much the cardholder can spend beyond their available balance. Set to "0" to require sufficient funds, or a positive amount to allow overspending up to that limit. This is useful for credit-like products or authorized overdraft facilities.

Card Authorization (Declined)

When an authorization is declined due to insufficient funds, fraud rules, or other reasons, no postings are created in the ledger. The decline response is sent to the processor, but no balance changes occur.
Declined authorizations don’t create any ledger transactions. However, you may want to log declined authorization attempts in metadata or a separate audit system for fraud monitoring, risk analysis, and customer support purposes. The playground example below shows the numscript attempting to execute but failing due to insufficient funds (balance + overdraft < amount).

Example Usage

Authorization declined - insufficient funds - Try in Playground →
{
    "variables": {
        "asset": "EUR/2",
        "amount": "20000",
        "overdraft": "5000",
        "account_id": "user123",
        "authorization_id": "auth_abc123xyz789",
        "pii_id": "10765432100123456789",
        "trx_details": "{\"mti\":\"0100\",\"amount\":\"200.00\",\"merchant\":\"BEST COFFEE SHOP\",\"mcc\":\"5814\",\"decline_reason\":\"insufficient_funds\"}"
    }
}

Card Authorization (Approved for partial amount)

In case you would want to support Partial Authorizations, typically requested/required in deferred payments like automatic fuel dispensers

Variables

  • $asset (asset): asset in UMN
  • $amount (number): Transaction amount
  • $account_id (account): Account identifier
  • $authorization_id (string): Unique authorization reference
  • $pii_id (string): Payment instrument token
  • $trx_details: JSON blob with transaction details from the processor

Accounts Used

@cardholder:accountid:main@cardholder:account_id:main @cardholder:account_id:hold:$authorization_id

Numscript

vars {
    asset $asset
    number $amount
    number $overdraft
    account $account_id
    string $authorization_id
    string $pii_id
    string $trx_details
}

send [$asset *] (
    source = max [$asset $amount] from @cardholder:$account_id:main allowing overdraft up to [$asset $overdraft]
    destination = @cardholder:$account_id:hold:$authorization_id
)

set_tx_meta("authorization_id", $authorization_id)
set_tx_meta("pii_id", $pii_id)
set_tx_meta("trx_details", $trx_details)
Using send [$asset *] with max [$asset $amount] from... allows the system to authorize up to the requested amount but only what’s actually available (including overdraft). The actual authorized amount may be less than requested. Your system must communicate the approved partial amount back to the terminal/merchant so they can decide whether to accept it or cancel the transaction.
Not all merchants support partial authorizations. Before implementing, verify that your processor and the merchant’s terminal can handle partial approval responses. Some MCCs (like gas stations - 5542) commonly support this, while others may not.

Example Usage

Authorization for partial amount - Try in Playground →
{
    "variables": {
        "asset": "EUR/2",
        "amount": "10000",
        "overdraft": "1000",
        "account_id": "user123",
        "authorization_id": "auth_abc123xyz789",
        "pii_id": "10765432100123456789",
        "trx_details": "{\"mti\":\"0100\",\"amount\":\"100.00\",\"merchant\":\"GAS STATION\",\"mcc\":\"5542\",\"authorization_type\":\"partial\"}"
    }
}

Card Authorization (Incremental)

Incremental authorizations allow merchants to increase the authorization amount after the initial approval. Common in hospitality (adding room charges), car rentals (extending rental period), and gas stations (continued fueling) where the final amount isn’t known upfront.
Each incremental authorization uses the same authorization_id as the initial authorization. This causes all increments to accumulate in the same hold account, making it easy to track the total held amount and eventually settle or reverse the complete authorization.

Variables

  • $asset (asset): asset in UMN
  • $amount (number): Additional authorization amount (incremental)
  • $account_id (account): Account identifier
  • $authorization_id (string): Original authorization reference (same for all increments)
  • $pii_id (string): Payment instrument token
  • $trx_details: JSON blob with transaction details from the processor

Accounts Used

@cardholder:accountid:main@cardholder:account_id:main @cardholder:account_id:hold:$authorization_id

Numscript

vars {
    asset $asset
    number $amount
    number $overdraft
    account $account_id
    string $authorization_id
    string $pii_id
    string $trx_details
}

send [$asset $amount] (
    source = @cardholder:$account_id:main allowing overdraft up to [$asset $overdraft]
    destination = @cardholder:$account_id:hold:$authorization_id
)

set_tx_meta("authorization_id", $authorization_id)
set_tx_meta("pii_id", $pii_id)
set_tx_meta("trx_details", $trx_details)
set_tx_meta("authorization_type", "incremental")

Example Usage

Initial authorization for €50 - Try in Playground →
{
    "variables": {
        "asset": "EUR/2",
        "amount": "5000",
        "overdraft": "0",
        "account_id": "user456",
        "authorization_id": "auth_xyz987abc456",
        "pii_id": "10765432100987654321",
        "trx_details": "{\"mti\":\"0100\",\"amount\":\"50.00\",\"merchant\":\"HOTEL GRAND\"}"
    }
}
Incremental authorization - additional €30 - Try in Playground →
{
    "variables": {
        "asset": "EUR/2",
        "amount": "3000",
        "overdraft": "0",
        "account_id": "user456",
        "authorization_id": "auth_xyz987abc456",
        "pii_id": "10765432100987654321",
        "trx_details": "{\"mti\":\"0120\",\"amount\":\"30.00\",\"merchant\":\"HOTEL GRAND\"}"
    }
}
The same authorization_id is used for both the initial and incremental authorizations. The hold account accumulates all authorization amounts. In this example, after the incremental authorization, the total held amount would be €80.00 (€50 + €30).

Authorization Reversal

Authorization reversals release previously authorized funds back to the cardholder. This occurs when a merchant cancels a transaction, at network request, or when the processor requires it if no presentment is received within the expected timeframe.

Variables

  • $asset (asset): asset in UMN
  • $amount (number): Amount to reverse (can be partial or full authorization amount)
  • $account_id (account): Account identifier
  • $authorization_id (string): Original authorization reference to reverse
  • $reversal_id (string): Unique reversal transaction reference
  • $pii_id (string): Payment instrument token
  • $trx_details: JSON blob with reversal details from the processor

Accounts Used

@cardholder:accountid:main@cardholder:account_id:main @cardholder:account_id:hold:$authorization_id

Numscript

vars {
    asset $asset
    number $amount
    account $account_id
    string $authorization_id
    string $reversal_id
    string $pii_id
    string $trx_details
}

send [$asset $amount] (
    source = @cardholder:$account_id:hold:$authorization_id
    destination = @cardholder:$account_id:main
)

set_tx_meta("authorization_id", $authorization_id)
set_tx_meta("reversal_id", $reversal_id)
set_tx_meta("pii_id", $pii_id)
set_tx_meta("trx_details", $trx_details)
set_tx_meta("transaction_type", "authorization_reversal")

Example Usage

Full authorization reversal - Try in Playground →
{
    "variables": {
        "asset": "EUR/2",
        "amount": "7500",
        "account_id": "user789",
        "authorization_id": "auth_def123xyz456",
        "reversal_id": "rev_abcxyz123456",
        "pii_id": "10765432100555555555",
        "trx_details": "{\"mti\":\"0400\",\"reason\":\"merchant_cancel\",\"original_amount\":\"75.00\"}"
    }
}
Partial authorization reversal - Try in Playground →
{
    "variables": {
        "asset": "EUR/2",
        "amount": "2000",
        "account_id": "user789",
        "authorization_id": "auth_def123xyz456",
        "reversal_id": "rev_partial123456",
        "pii_id": "10765432100555555555",
        "trx_details": "{\"mti\":\"0420\",\"reason\":\"partial_cancel\",\"original_amount\":\"75.00\",\"reversed_amount\":\"20.00\"}"
    }
}
Authorization reversals move funds from the hold account back to the main account, making them available again to the cardholder. Full reversals release the entire authorization amount, while partial reversals release only a portion, leaving the remaining amount on hold.
For partial reversals where you need to release a specific amount, use this numscript. For releasing all remaining funds (recommended for most cases), see the Authorization Hold Reversal section which uses [$asset *] to automatically release whatever is left in the hold account.

Postings / Completions / Presentments

Presentment (also called clearing, posting, or completion) is when the merchant submits the transaction for final settlement. This typically happens at the end of the business day or when the merchant batches their transactions. The held funds are released and the transaction moves to settlement with the scheme.

Variables

  • $asset (asset): asset in UMN
  • $amount (number): Final settlement amount (may differ from original authorization)
  • $account_id (account): Account identifier
  • $authorization_id (string): Original authorization reference
  • $presentment_id (string): Unique presentment transaction reference
  • $scheme_id (account): Card scheme identifier (Visa, Mastercard, etc.)
  • $pii_id (string): Payment instrument token
  • $trx_details: JSON blob with presentment details from the processor

Accounts Used

@cardholder:accountid:hold:account_id:hold:authorization_id @schemes:$scheme_id:main

Numscript

vars {
    asset $asset
    number $amount
    account $account_id
    string $authorization_id
    string $presentment_id
    account $scheme_id
    string $pii_id
    string $trx_details
}

send [$asset $amount] (
    source = @cardholder:$account_id:hold:$authorization_id
    destination = @schemes:$scheme_id:main
)

set_tx_meta("authorization_id", $authorization_id)
set_tx_meta("presentment_id", $presentment_id)
set_tx_meta("pii_id", $pii_id)
set_tx_meta("trx_details", $trx_details)
set_tx_meta("transaction_type", "presentment")

Example Usage

Presentment matching authorization amount - Try in Playground →
{
    "variables": {
        "asset": "EUR/2",
        "amount": "10000",
        "account_id": "user123",
        "authorization_id": "auth_abc123xyz789",
        "presentment_id": "pres_def456xyz123",
        "scheme_id": "visa",
        "pii_id": "10765432100123456789",
        "trx_details": "{\"mti\":\"0220\",\"amount\":\"100.00\",\"merchant\":\"BEST COFFEE SHOP\",\"mcc\":\"5814\",\"auth_code\":\"123456\"}"
    }
}
When a presentment is received, funds move from the hold account to the scheme’s liability account. If the presentment is for less than the authorization, the remaining balance in the hold account should be released back to the cardholder’s main account via a reversal.
This numscript handles presentments that match or are less than the authorization amount. For presentments that exceed the authorization (e.g., restaurant tips), see the next section on Presentment with Higher Amount.

Presentment with Higher Amount (Tip Adjustment)

When a presentment amount exceeds the original authorization (common with restaurant tips), you need to debit the additional amount from the cardholder’s main account. This requires a multi-step transaction to handle both the authorized hold and the additional charge.

Variables

  • $asset (asset): asset in UMN
  • $auth_amount (number): Original authorization amount held
  • $additional_amount (number): Additional amount beyond authorization (e.g., tip)
  • $account_id (account): Account identifier
  • $authorization_id (string): Original authorization reference
  • $presentment_id (string): Unique presentment transaction reference
  • $scheme_id (account): Card scheme identifier
  • $pii_id (string): Payment instrument token
  • $trx_details: JSON blob with presentment details including tip breakdown

Accounts Used

@cardholder:accountid:main@cardholder:account_id:main @cardholder:account_id:hold:authorizationid@schemes:authorization_id @schemes:scheme_id:main

Numscript

vars {
    asset $asset
    number $auth_amount
    number $additional_amount
    account $account_id
    string $authorization_id
    string $presentment_id
    account $scheme_id
    string $pii_id
    string $trx_details
}

// Move the authorized amount from hold to scheme
send [$asset $auth_amount] (
    source = @cardholder:$account_id:hold:$authorization_id
    destination = @schemes:$scheme_id:main
)

// Debit the additional amount (tip) directly from main account
send [$asset $additional_amount] (
    source = @cardholder:$account_id:main
    destination = @schemes:$scheme_id:main
)

set_tx_meta("authorization_id", $authorization_id)
set_tx_meta("presentment_id", $presentment_id)
set_tx_meta("pii_id", $pii_id)
set_tx_meta("trx_details", $trx_details)
set_tx_meta("transaction_type", "presentment_with_tip")

Example Usage

Presentment with tip adjustment - €100 auth + €25 tip - Try in Playground →
{
    "variables": {
        "asset": "EUR/2",
        "auth_amount": "10000",
        "additional_amount": "2500",
        "account_id": "user456",
        "authorization_id": "auth_xyz987abc456",
        "presentment_id": "pres_ghi789def321",
        "scheme_id": "mastercard",
        "pii_id": "10765432100987654321",
        "trx_details": "{\"mti\":\"0220\",\"amount\":\"125.00\",\"original_auth\":\"100.00\",\"tip\":\"25.00\",\"merchant\":\"RESTAURANT GOURMET\",\"mcc\":\"5812\",\"auth_code\":\"789012\"}"
    }
}
Before processing presentments that exceed the original authorization, implement business logic to validate the overage:
  • Check if the additional amount is within acceptable limits (e.g., tips ≤ 20-25% of original amount)
  • Verify sufficient funds are available in the main account for the additional charge
  • Consider scheme-specific rules for tip tolerances
  • Some issuers may require the additional amount to be authorized separately
This two-step posting creates a single transaction that debits both the hold account (authorized amount) and the main account (tip), crediting the total to the scheme. The transaction metadata includes both the original authorization amount and the tip breakdown for reconciliation purposes.

Offline Transaction Presentment

Offline transactions occur when the card terminal cannot communicate with the issuer in real-time (e.g., airplane purchases, remote locations, or chip fallback). The transaction is approved by the card chip based on card risk parameters, and the presentment arrives without a prior authorization. These transactions must be debited directly from the cardholder’s main account.

Variables

  • $asset (asset): asset in UMN
  • $amount (number): Transaction amount to debit
  • $account_id (account): Account identifier
  • $presentment_id (string): Unique presentment transaction reference
  • $scheme_id (account): Card scheme identifier
  • $pii_id (string): Payment instrument token
  • $trx_details: JSON blob with offline transaction details from the processor

Accounts Used

@cardholder:accountid:main@schemes:account_id:main @schemes:scheme_id:main

Numscript

vars {
    asset $asset
    number $amount
    account $account_id
    string $presentment_id
    account $scheme_id
    string $pii_id
    string $trx_details
}

// Mandatory debit from main account - no prior authorization exists
send [$asset $amount] (
    source = @cardholder:$account_id:main allowing unbounded overdraft 
    destination = @schemes:$scheme_id:main
)

set_tx_meta("presentment_id", $presentment_id)
set_tx_meta("pii_id", $pii_id)
set_tx_meta("trx_details", $trx_details)
set_tx_meta("transaction_type", "offline_presentment")
set_tx_meta("authorization_mode", "offline")

Example Usage

Offline transaction - airplane purchase - Try in Playground →
{
    "variables": {
        "asset": "EUR/2",
        "amount": "4500",
        "account_id": "user789",
        "presentment_id": "pres_offline123456",
        "scheme_id": "visa",
        "pii_id": "10765432100555555555",
        "trx_details": "{\"mti\":\"0220\",\"amount\":\"45.00\",\"merchant\":\"AIRLINE SKYWAYS\",\"mcc\":\"4511\",\"offline\":true,\"arqc\":\"00\"}"
    }
}
Offline transaction with insufficient funds - Try in Playground →
{
    "variables": {
        "asset": "EUR/2",
        "amount": "8000",
        "account_id": "user999",
        "presentment_id": "pres_offline789123",
        "scheme_id": "mastercard",
        "pii_id": "10765432100999999999",
        "trx_details": "{\"mti\":\"0220\",\"amount\":\"80.00\",\"merchant\":\"REMOTE CAMP STORE\",\"mcc\":\"5999\",\"offline\":true,\"arqc\":\"00\"}"
    }
}
Offline transactions bypass real-time balance checks and fraud detection. Key considerations:
  • The card chip approves based on card risk parameters (offline limits, velocity checks)
  • Transactions can result in negative balances if the cardholder spent more than available
  • Unbounded overdraft ensures these mandatory debits always succeed, but requires debt collection processes for negative balances
  • Monitor offline transaction patterns for unusual activity
  • Consider implementing card-level offline transaction limits and counters
Unlike online authorizations, offline transactions have no hold account - the debit is direct and final. The transaction is marked with authorization_mode: "offline" to distinguish it from regular presentments. Some offline transactions may arrive days or even weeks after they occurred, depending on when the merchant batches them.

Refund ‘Authorization’

A refund authorization occurs when a merchant initiates a credit transaction back to the cardholder (returns, cancellations after settlement, etc.). Unlike regular authorizations, the funds are not immediately available to the cardholder - they only become available after the refund is posted/settled. This requires a two-step process.

Step 1: Refund Authorization (Pending)

The refund is authorized but funds are held in a pending state until settlement.
Variables
  • $asset (asset): asset in UMN
  • $amount (number): Refund amount
  • $account_id (account): Account identifier
  • $refund_auth_id (string): Unique refund authorization reference
  • $scheme_id (account): Card scheme identifier
  • $pii_id (string): Payment instrument token
  • $trx_details: JSON blob with refund authorization details
Accounts Used
@schemes:schemeid:main@cardholder:scheme_id:main @cardholder:account_id:refund:pending:$refund_auth_id
Numscript
vars {
    asset $asset
    number $amount
    account $account_id
    string $refund_auth_id
    account $scheme_id
    string $pii_id
    string $trx_details
}

// Move from scheme to pending refund account (not yet available to cardholder)
send [$asset $amount] (
    source = @schemes:$scheme_id:main allowing unbounded overdraft
    destination = @cardholder:$account_id:refund:pending:$refund_auth_id
)

set_tx_meta("refund_auth_id", $refund_auth_id)
set_tx_meta("pii_id", $pii_id)
set_tx_meta("trx_details", $trx_details)
set_tx_meta("transaction_type", "refund_authorization")
set_tx_meta("refund_status", "pending")
Example Usage
Refund authorization - €75 return - Try in Playground →
{
    "variables": {
        "asset": "EUR/2",
        "amount": "7500",
        "account_id": "user123",
        "refund_auth_id": "rfauth_abc123xyz789",
        "scheme_id": "visa",
        "pii_id": "10765432100123456789",
        "trx_details": "{\"mti\":\"0220\",\"amount\":\"75.00\",\"merchant\":\"ONLINE SHOP\",\"mcc\":\"5999\",\"reason\":\"product_return\"}"
    }
}

Step 2: Refund Posting/Settlement

Once the refund is settled, funds are released from the pending account to the cardholder’s main account, making them available for use.
Variables
  • $asset (asset): asset in UMN
  • $amount (number): Refund amount to release
  • $account_id (account): Account identifier
  • $refund_auth_id (string): Original refund authorization reference
  • $refund_posting_id (string): Unique refund posting reference
  • $trx_details: JSON blob with refund posting details
Accounts Used
@cardholder:accountid:refund:pending:account_id:refund:pending:refund_auth_id @cardholder:$account_id:main
Numscript
vars {
    asset $asset
    number $amount
    account $account_id
    string $refund_auth_id
    string $refund_posting_id
    string $trx_details
}

// Release funds from pending to main account (now available to cardholder)
send [$asset $amount] (
    source = @cardholder:$account_id:refund:pending:$refund_auth_id
    destination = @cardholder:$account_id:main
)

set_tx_meta("refund_auth_id", $refund_auth_id)
set_tx_meta("refund_posting_id", $refund_posting_id)
set_tx_meta("trx_details", $trx_details)
set_tx_meta("transaction_type", "refund_posting")
set_tx_meta("refund_status", "completed")
Example Usage
Refund posting - funds released to cardholder - Try in Playground →
{
    "variables": {
        "asset": "EUR/2",
        "amount": "7500",
        "account_id": "user123",
        "refund_auth_id": "rfauth_abc123xyz789",
        "refund_posting_id": "rfpost_def456xyz123",
        "trx_details": "{\"mti\":\"0220\",\"amount\":\"75.00\",\"merchant\":\"ONLINE SHOP\",\"mcc\":\"5999\",\"reason\":\"product_return\"}"
    }
}
The two-step refund process ensures proper accounting and tracking:
  1. Authorization: Funds move from scheme liability to a pending refund account (not yet available to spend)
  2. Posting: Funds move from pending to the cardholder’s main account (now available)
This matches the real-world timing where refunds are authorized immediately but may take several days to appear as available balance.

Authorization Hold Reversal (Partial or Expired)

An authorization hold reversal releases remaining funds from the hold account back to the cardholder’s main account. This occurs when a partial presentment leaves funds in the hold, or when an authorization expires without being presented. Using the wildcard amount [$asset *] ensures all remaining held funds are released, regardless of the exact amount.

Variables

  • $asset (asset): asset in UMN
  • $account_id (account): Account identifier
  • $authorization_id (string): Original authorization reference
  • $reversal_id (string): Unique reversal transaction reference
  • $pii_id (string): Payment instrument token
  • $trx_details: JSON blob with reversal details (reason: partial_presentment, expired, merchant_cancel, etc.)

Accounts Used

@cardholder:accountid:hold:account_id:hold:authorization_id @cardholder:$account_id:main

Numscript

vars {
    asset $asset
    account $account_id
    string $authorization_id
    string $reversal_id
    string $pii_id
    string $trx_details
}

// Release all remaining funds from hold account back to main account
send [$asset *] (
    source = @cardholder:$account_id:hold:$authorization_id
    destination = @cardholder:$account_id:main
)

set_tx_meta("authorization_id", $authorization_id)
set_tx_meta("reversal_id", $reversal_id)
set_tx_meta("pii_id", $pii_id)
set_tx_meta("trx_details", $trx_details)
set_tx_meta("transaction_type", "hold_reversal")

Example Usage

Reversal after partial presentment - €25 remaining - Try in Playground →
{
    "variables": {
        "asset": "EUR/2",
        "account_id": "user456",
        "authorization_id": "auth_xyz987abc456",
        "reversal_id": "rev_partial123456",
        "pii_id": "10765432100987654321",
        "trx_details": "{\"mti\":\"0420\",\"reason\":\"partial_presentment\",\"original_auth\":\"100.00\",\"presented\":\"75.00\",\"remaining\":\"25.00\"}"
    }
}
Reversal of expired authorization - full amount - Try in Playground →
{
    "variables": {
        "asset": "EUR/2",
        "account_id": "user789",
        "authorization_id": "auth_def123xyz456",
        "reversal_id": "rev_expired789123",
        "pii_id": "10765432100555555555",
        "trx_details": "{\"mti\":\"0400\",\"reason\":\"authorization_expired\",\"original_auth\":\"75.00\"}"
    }
}
Using [$asset *] allows the reversal to release all remaining funds from the hold account without needing to specify the exact amount. This is essential for:
  • Partial presentments: When only part of the authorization was presented, the remainder needs to be released
  • Expired authorizations: When no presentment occurs within the validity period (typically 7-30 days)
  • Merchant cancellations: When the merchant cancels before presentment
  • Incremental authorizations: When you need to release the cumulative total regardless of how many increments occurred
The wildcard * automatically calculates and releases whatever balance exists in the hold account, making the reversal logic simple and foolproof.

Chargeback

A chargeback occurs when a cardholder disputes a transaction, and the issuer accepts the claim. The process involves two steps: initially crediting the cardholder from a dedicated chargeback account, then confirming the chargeback by settling with the scheme’s main settlement account.

Step 1: Chargeback Acceptance

When a chargeback is accepted, the cardholder is immediately credited from a dedicated scheme chargeback account.
Variables
  • $asset (asset): asset in UMN
  • $amount (number): Chargeback amount
  • $account_id (account): Account identifier
  • $chargeback_id (string): Unique chargeback reference
  • $scheme_id (account): Card scheme identifier
  • $original_presentment_id (string): Reference to the original transaction being disputed
  • $pii_id (string): Payment instrument token
  • $trx_details: JSON blob with chargeback details (reason code, dispute category, etc.)
Accounts Used
@schemes:schemeid:chargeback@cardholder:scheme_id:chargeback @cardholder:account_id:main
Numscript
vars {
    asset $asset
    number $amount
    account $account_id
    string $chargeback_id
    account $scheme_id
    string $original_presentment_id
    string $pii_id
    string $trx_details
}

// Credit cardholder from scheme's chargeback account
send [$asset $amount] (
    source = @schemes:$scheme_id:chargeback allowing unbounded overdraft
    destination = @cardholder:$account_id:main
)

set_tx_meta("chargeback_id", $chargeback_id)
set_tx_meta("original_presentment_id", $original_presentment_id)
set_tx_meta("pii_id", $pii_id)
set_tx_meta("trx_details", $trx_details)
set_tx_meta("transaction_type", "chargeback_acceptance")
set_tx_meta("chargeback_status", "accepted")
Example Usage
Chargeback acceptance - unauthorized transaction - Try in Playground →
{
    "variables": {
        "asset": "EUR/2",
        "amount": "10000",
        "account_id": "user123",
        "chargeback_id": "cb_20241023_00123",
        "scheme_id": "visa",
        "original_presentment_id": "pres_def456xyz123",
        "pii_id": "10765432100123456789",
        "trx_details": "{\"mti\":\"0430\",\"amount\":\"100.00\",\"reason_code\":\"10\",\"reason\":\"fraud-unauthorized\"}"
    }
}

Step 2: Chargeback Confirmation

Once the chargeback is confirmed, the scheme deducts the chargeback amount from the settlement. This moves funds from the scheme’s main settlement account to cover the chargeback account.
Variables
  • $asset (asset): asset in UMN
  • $amount (number): Chargeback amount
  • $chargeback_id (string): Original chargeback reference
  • $scheme_id (account): Card scheme identifier
  • $settlement_ref (string): Reference to the settlement where chargeback was deducted
  • $trx_details: JSON blob with settlement details
Accounts Used
@schemes:schemeid:main@schemes:scheme_id:main @schemes:scheme_id:chargeback
Numscript
vars {
    asset $asset
    number $amount
    string $chargeback_id
    account $scheme_id
    string $settlement_ref
    string $trx_details
}

// Cover the chargeback account from scheme's main settlement account
send [$asset $amount] (
    source = @schemes:$scheme_id:main allowing unbounded overdraft
    destination = @schemes:$scheme_id:chargeback
)

set_tx_meta("chargeback_id", $chargeback_id)
set_tx_meta("settlement_ref", $settlement_ref)
set_tx_meta("trx_details", $trx_details)
set_tx_meta("transaction_type", "chargeback_confirmation")
set_tx_meta("chargeback_status", "confirmed")
Example Usage
Chargeback confirmation via settlement - Try in Playground →
{
    "variables": {
        "asset": "EUR/2",
        "amount": "10000",
        "chargeback_id": "cb_20241023_00123",
        "scheme_id": "visa",
        "settlement_ref": "settle_20241025_00123",
        "trx_details": "{\"mti\":\"0240\",\"amount\":\"100.00\",\"settlement\":\"deducted\"}"
    }
}
The two-step chargeback process ensures proper accounting:
  1. Acceptance: Cardholder is credited immediately from the scheme’s chargeback account (customer service)
  2. Confirmation: The scheme’s main settlement account covers the chargeback account when settlement is adjusted
This separation allows tracking of pending vs confirmed chargebacks and aligns with how schemes typically handle chargeback settlements (deducted from net settlement amounts).
Chargeback management considerations:
  • Track chargeback reason codes for fraud pattern detection
  • Consider chargeback fees that schemes may apply
  • Some chargebacks can be reversed if the merchant provides compelling evidence (see Second Presentment)

Second Presentment (Chargeback Reversal)

A second presentment occurs when a merchant successfully contests a chargeback by providing compelling evidence. The issuer must reverse the chargeback, debiting the cardholder’s account again and crediting the scheme to re-establish the liability.

Variables

  • $asset (asset): asset in UMN
  • $amount (number): Transaction amount (original chargeback amount)
  • $account_id (account): Account identifier
  • $chargeback_id (string): Original chargeback reference being reversed
  • $second_presentment_id (string): Unique second presentment reference
  • $scheme_id (account): Card scheme identifier
  • $pii_id (string): Payment instrument token
  • $trx_details: JSON blob with second presentment details (merchant evidence, arbitration result, etc.)

Accounts Used

@cardholder:accountid:main@schemes:account_id:main @schemes:scheme_id:main

Numscript

vars {
    asset $asset
    number $amount
    account $account_id
    string $chargeback_id
    string $second_presentment_id
    account $scheme_id
    string $pii_id
    string $trx_details
}

// Debit cardholder (reverses the chargeback credit) and credit scheme liability
send [$asset $amount] (
    source = @cardholder:$account_id:main allowing unbounded overdraft
    destination = @schemes:$scheme_id:main
)

set_tx_meta("chargeback_id", $chargeback_id)
set_tx_meta("second_presentment_id", $second_presentment_id)
set_tx_meta("pii_id", $pii_id)
set_tx_meta("trx_details", $trx_details)
set_tx_meta("transaction_type", "second_presentment")
set_tx_meta("chargeback_status", "reversed")

Example Usage

Second presentment - merchant wins dispute - Try in Playground →
{
    "variables": {
        "asset": "EUR/2",
        "amount": "10000",
        "account_id": "user123",
        "chargeback_id": "cb_20241023_00123",
        "second_presentment_id": "sp_pres123456",
        "scheme_id": "visa",
        "pii_id": "10765432100123456789",
        "trx_details": "{\"mti\":\"0250\",\"amount\":\"100.00\",\"reason\":\"merchant_provided_evidence\",\"arbitration\":\"issuer_loss\"}"
    }
}
A second presentment reverses the chargeback by:
  1. Debiting the cardholder’s account (reversing the chargeback credit they received)
  2. Crediting the scheme’s liability account (re-establishing the debt to be settled)
This typically occurs after the merchant provides compelling evidence (delivery confirmation, signed receipts, etc.) or wins arbitration. The cardholder may not have sufficient funds if they’ve already spent the chargeback amount, hence the unbounded overdraft allowance.
Second presentment considerations:
  • The cardholder may have already spent the chargeback funds, creating a negative balance
  • Implement customer communication workflows to explain the reversal
  • Track arbitration outcomes to improve dispute handling processes
  • Some jurisdictions have consumer protection rules limiting second presentments
  • Consider implementing debt collection processes for resulting negative balances

Process STIP Advices

STIP (Stand-In Processing) advices are notifications of transactions that were approved by the processor or network when the issuer’s authorization system was unavailable (system downtime, network issues, etc.). The processor uses predefined rules to approve or decline these transactions on behalf of the issuer. When the issuer system comes back online, STIP advices must be processed as mandatory debits.

Variables

  • $asset (asset): asset in UMN
  • $amount (number): Transaction amount
  • $account_id (account): Account identifier
  • $stip_advice_id (string): Unique STIP advice reference
  • $scheme_id (account): Card scheme identifier
  • $pii_id (string): Payment instrument token
  • $trx_details: JSON blob with STIP transaction details (stand-in decision, approval reason, etc.)

Accounts Used

@cardholder:accountid:main@schemes:account_id:main @schemes:scheme_id:main

Numscript

vars {
    asset $asset
    number $amount
    account $account_id
    string $stip_advice_id
    account $scheme_id
    string $pii_id
    string $trx_details
}

// Mandatory debit from main account - transaction was already approved by stand-in processor
send [$asset $amount] (
    source = @cardholder:$account_id:main allowing unbounded overdraft
    destination = @schemes:$scheme_id:main
)

set_tx_meta("stip_advice_id", $stip_advice_id)
set_tx_meta("pii_id", $pii_id)
set_tx_meta("trx_details", $trx_details)
set_tx_meta("transaction_type", "stip_advice")
set_tx_meta("authorization_mode", "stand_in")

Example Usage

STIP advice - approved during system downtime - Try in Playground →
{
    "variables": {
        "asset": "EUR/2",
        "amount": "6500",
        "account_id": "user456",
        "stip_advice_id": "stip_20241023_00789",
        "scheme_id": "mastercard",
        "pii_id": "10765432100987654321",
        "trx_details": "{\"mti\":\"0220\",\"amount\":\"65.00\",\"merchant\":\"SUPERMARKET\",\"mcc\":\"5411\",\"stand_in\":true,\"stip_reason\":\"issuer_unavailable\"}"
    }
}
STIP processing considerations:
  • Transactions were approved without real-time issuer authorization - balance checks were bypassed
  • Stand-in parameters (limits, velocity) may differ from issuer’s actual risk rules
  • Can result in negative balances if stand-in limits are more permissive than issuer limits
  • Unbounded overdraft ensures these mandatory debits always succeed
  • Monitor STIP volumes and patterns to identify system availability issues
  • Review STIP parameters regularly to align with issuer risk appetite
  • Some processors charge fees for STIP services
STIP advices are similar to offline transactions - both are mandatory debits that occurred without real-time issuer authorization. The key difference is that STIP transactions were approved by the processor/network based on configured parameters, while offline transactions were approved by the card chip itself. Both require unbounded overdraft to ensure successful processing.

Querying the Ledger

The Formance Ledger allows you to query aggregated account balances using powerful filtering with wildcards. The way accounts are organized enables you to track specific aspects of your issuing system.

All Amounts Currently in Hold Accounts

Query all authorization hold accounts across all cardholders to see total funds pending settlement:
{
  "$match": { "address": "cardholder::hold:" }
}
This returns all hold sub-accounts for any cardholder, regardless of the authorization_id. Useful for:
  • Total funds authorized but not yet settled
  • Liquidity management and forecasting
  • Monitoring authorization aging

All Hold Accounts for a Specific Cardholder

Query all authorization holds for a single cardholder:
{
  "$match": { "address": "cardholder:user123:hold:" }
}
This shows all active authorizations for user123. Useful for:
  • Customer service inquiries about pending charges
  • Investigating why available balance differs from account balance
  • Identifying stuck or expired authorizations

Specific Authorization Hold

Query a single authorization hold:
{
  "$match": { "address": "cardholder:user123:hold:auth_abc123xyz789" }
}
This returns the exact hold amount for a specific authorization.

All Pending Refunds

Query all pending refunds across the system:
{
  "$match": { "address": "cardholder::refund:pending:" }
}
This shows refunds that have been authorized but not yet posted. Useful for:
  • Tracking refund processing time
  • Identifying delayed refunds
  • Reconciliation with scheme settlement reports

All Scheme Liabilities

Query total liability owed to a specific scheme:
{
  "$match": { "address": "schemes:visa:main" }
}
Or for all schemes:
{
  "$match": { "address": "schemes:" }
}
This shows total amounts to be settled with each scheme. Useful for:
  • Settlement preparation and forecasting
  • Liquidity management
  • Multi-scheme volume analysis

All Cardholder Main Accounts

Query all cardholder main account balances:
{
  "$match": { "address": "cardholder::main" }
}
This returns the available balance for all cardholders. Useful for:
  • Total cardholder liability calculation
  • Regulatory reporting
  • Reserve requirement calculations

Chargeback Accounts by Scheme

Query chargeback liabilities for a specific scheme:
{
  "$match": { "address": "schemes:visa:chargeback" }
}
Or all scheme chargeback accounts:
{
  "$match": { "address": "schemes::chargeback" }
}
This tracks pending chargebacks that need scheme settlement coverage.

Combining Filters with Metadata

Query authorizations for a specific payment instrument with balance check:
{
  "$and": [
    { "$match": { "address": "cardholder::hold:" } },
    { "$match": { "metadata[pii_id]": "10765432100123456789" } },
    { "$gt": { "balance[EUR/2]": "0" } }
  ]
}
This finds all active holds for a specific card that still have balance.

Query by Transaction Type

Find all offline transactions:
{
  "$and": [
    { "$match": { "account": "cardholder:" } },
    { "$match": { "metadata[transaction_type]": "offline_presentment" } }
  ]
}
In Formance filtering, empty segments in an address filter act as wildcards. For example:
  • cardholder::hold: matches any cardholder ID and any authorization ID in hold accounts
  • cardholder:user123:hold: matches all authorizations for user123
  • Ending with : creates an open-ended wildcard matching all subsequent segments
Combine address filters with metadata, balance, and timestamp filters using $and and $or operators for powerful queries.
These filtering patterns enable real-time monitoring of your issuing operations without needing separate tracking systems. All the information is inherently available in the ledger’s account structure, making reconciliation and operational reporting straightforward. See the Filtering Queries documentation for complete syntax reference.
These examples use Numscript features available in Ledger v2.3+. Ensure your deployment runs a compatible version.