Reconcile
Match Funding Transactions

Match Funding Transactions

Associate Payload funding transactions with bank statement entries for accurate reconciliation


Matching funding transactions to bank statements is essential for accurate financial reconciliation. Payload funding transactions (deposits and withdrawals) appear on merchant bank statements, and matching these entries ensures your records align with actual bank activity. Use reference numbers, amounts, dates, descriptors, and custom attributes to match transactions efficiently.

Prerequisites

Before matching funding transactions, understand:


Understanding Funding Transactions


Funding transactions move money between Payload and bank accounts.

What are funding transactions

Funding transactions in Payload represent the movement of funds to and from merchant bank accounts. These are created automatically through Automated Funding when paying transactions (payments and payouts) occur:

AttributeDeposit TransactionsWithdrawal Transactions
Typedepositwithdrawal
DirectionPayload → Bank AccountBank Account → Payload
Appears onMerchant bank statement as depositMerchant bank statement as withdrawal
PurposeSettlement of processed payments (credits)Settlement of refunds and chargebacks (debits)
Created whenPayment transactions are processedRefund or chargeback transactions occur

Funding characteristics:

  • Appear on bank statements with descriptor
  • Include reference number for tracking
  • Have estimated cleared date for settlement timing
  • May be netted, gross, or itemized based on funding style
  • Automatically orchestrated via Automated Funding configuration

Funding styles

Your processing account funding style determines how transactions settle and affects your matching strategy:

Funding StyleBank Statement EntriesMatching Strategy
NettedSingle deposit per settlement periodMatch one bank entry to multiple transactions (batch matching)
GrossSeparate deposits for credits/debitsMatch one or two bank entries to multiple transactions (batch matching)
ItemizedOne deposit per transactionMatch one bank entry per Payload transaction (one-to-one)
ManualDeposits when funds releasedMatch released batches to bank entries

Learn more about configuring funding styles in Automated Funding.

Matching Strategies


Different strategies for matching funding transactions to bank statements.

import payload
pl = payload.Session('secret_key_3bW9...', api_version='v2')
 
# Match funding transactions by reference number
 
# Query transactions by reference number from bank statement
def match_by_reference_number(bank_statement_ref_number):
    # Bank statements often include reference numbers
    # Match Payload transactions using ref_number field
    transactions = pl.Transaction.filter_by(
        q=f'ref_number == "{bank_statement_ref_number}" && type == "deposit"'  # Funding transactions
    ).all()
 
    if len(transactions) == 1:
        print('Match found!')
        print(f'Transaction ID: {transactions[0].id}')
        print(f'Amount: ${transactions[0].amount}')
        print(f'Date: {transactions[0].created_at}')
        return transactions[0]
    elif len(transactions) == 0:
        print('No matching transaction found')
        return None
    else:
        print('Multiple transactions found - investigate')
        return transactions
 
# Match multiple bank statement entries at once
def batch_match_by_reference(bank_statement_entries):
    matches = []
    unmatched = []
 
    for entry in bank_statement_entries:
        # Bank statement entry: {'ref': 'REF123456', 'amount': 1500.00, 'date': '2024-01-15'}
        transaction = match_by_reference_number(entry['ref'])
 
        if transaction and not isinstance(transaction, list):
            matches.append({
                'bank_entry': entry,
                'payload_transaction': transaction,
                'matched': True
            })
        else:
            unmatched.append({
                'bank_entry': entry,
                'matched': False,
                'reason': 'multiple_matches' if isinstance(transaction, list) else 'no_match'
            })
 
    print(f'Matched: {len(matches)}')
    print(f'Unmatched: {len(unmatched)}')
 
    return {'matches': matches, 'unmatched': unmatched}
 
# Example usage
bank_ref_number = 'REF123456'
match_by_reference_number(bank_ref_number)

The most reliable matching method uses the system-generated reference number.

How it works:

  • Payload generates unique ref_number for each transaction
  • Bank statements often include reference numbers in description
  • Query Payload transactions by ref_number field
  • Exact matches ensure accuracy

Advantages:

  • Unique identifier guarantees exact match
  • Fast querying by single field
  • Works regardless of amount or date
  • No ambiguity

Limitations:

  • Requires bank statement to include reference
  • Not all banks show reference numbers prominently
  • May need to parse reference from statement text
import payload
pl = payload.Session('secret_key_3bW9...', api_version='v2')
from datetime import datetime, timedelta
 
# Match funding transactions by amount and date
 
# Query transactions by amount and date range
def match_by_amount_and_date(amount, statement_date):
    # Bank statement date may differ from transaction created date
    # Check a date range (e.g., ±2 business days for settlement timing)
    date_obj = datetime.fromisoformat(statement_date)
    start_date = date_obj - timedelta(days=2)
    end_date = date_obj + timedelta(days=2)
 
    transactions = pl.Transaction.filter_by(
        q=f'amount == {amount} && type == "deposit" && created_at >= date("{start_date.date()}") && created_at <= date("{end_date.date()}")',
        order_by='desc(created_at)'
    ).all()
 
    print(f'Found {len(transactions)} potential matches')
 
    if len(transactions) == 1:
        print('Exact match found!')
        print(f'Transaction ID: {transactions[0].id}')
        print(f'Amount: ${transactions[0].amount}')
        print(f'Created: {transactions[0].created_at}')
        print(f'Est Cleared: {transactions[0].est_cleared_date}')
        return transactions[0]
    elif len(transactions) == 0:
        print('No matching transaction found')
        print('Check: amount, date range, transaction type')
        return None
    else:
        print('Multiple potential matches - need additional criteria')
        return transactions
 
# Match by amount with tighter date range using est_cleared_date
def match_by_amount_and_cleared_date(amount, cleared_date):
    # est_cleared_date is the estimated settlement date
    # Should match bank statement date more closely
    transactions = pl.Transaction.filter_by(
        q=f'amount == {amount} && type == "deposit" && est_cleared_date == date("{cleared_date}")',
        order_by='desc(created_at)'
    ).all()
 
    print(f'Found {len(transactions)} matches by amount and cleared date')
 
    if len(transactions) == 1:
        return transactions[0]
    elif len(transactions) > 1:
        # If multiple matches, try to narrow by descriptor or other fields
        print('Multiple matches - consider additional matching criteria')
        return transactions
 
    return None
 
# Match with amount tolerance for fees
def match_by_amount_with_tolerance(amount, statement_date, tolerance=5.00):
    # Bank statement amount may differ due to fees
    # Allow small tolerance (e.g., $5 for processing fees)
    min_amount = amount - tolerance
    max_amount = amount + tolerance
 
    date_obj = datetime.fromisoformat(statement_date)
    start_date = date_obj - timedelta(days=2)
    end_date = date_obj + timedelta(days=2)
 
    transactions = pl.Transaction.filter_by(
        q=f'amount >= {min_amount} && amount <= {max_amount} && type == "deposit" && created_at >= date("{start_date.date()}") && created_at <= date("{end_date.date()}")',
        order_by='desc(created_at)'
    ).all()
 
    print(f'Found {len(transactions)} matches within tolerance')
 
    # Return closest amount match
    if len(transactions) > 0:
        closest = min(transactions, key=lambda t: abs(t.amount - amount))
        print(f'Closest match: ${closest.amount} (diff: ${abs(closest.amount - amount):.2f})')
        return closest
 
    return None
 
# Example usage
bank_amount = 1500.00
bank_date = '2024-01-15'
match_by_amount_and_date(bank_amount, bank_date)

Match transactions using amount and settlement date with tolerance ranges.

How it works:

  • Query by transaction amount
  • Use date range (±2 days) for settlement timing differences
  • Match against created_at or est_cleared_date
  • Narrow results with additional criteria if multiple matches

Advantages:

  • Works with basic bank statement data
  • Intuitive matching criteria
  • Flexible date ranges accommodate timing differences
  • Can include tolerance for fees

Limitations:

  • Multiple transactions same amount/date = ambiguous
  • Requires date range due to timing variations
  • Amount mismatches if fees deducted
  • Manual review needed for multiple matches

Best practices:

  • Use est_cleared_date for more accurate matching
  • Apply ±2 day range for settlement timing
  • Combine with descriptor for disambiguation
import payload
pl = payload.Session('secret_key_3bW9...', api_version='v2')
from datetime import datetime, timedelta
 
# Match funding transactions by descriptor
 
# Query transactions by descriptor from bank statement
def match_by_descriptor(bank_statement_descriptor):
    # Bank statement shows descriptor field
    # Match Payload transactions using descriptor
    transactions = pl.Transaction.filter_by(
        q=f'descriptor == "{bank_statement_descriptor}" && type == "deposit"',
        order_by='desc(created_at)'
    ).all()
 
    print(f'Found {len(transactions)} transactions with descriptor: {bank_statement_descriptor}')
 
    return transactions
 
# Match by descriptor with partial match
def match_by_descriptor_partial(bank_statement_text):
    # Bank statement text may be truncated or formatted differently
    # Use contains operator for partial matching
    transactions = pl.Transaction.filter_by(
        q=f'descriptor ?= "*{bank_statement_text}*" && type == "deposit"',
        order_by='desc(created_at)'
    ).all()
 
    print(f'Found {len(transactions)} transactions containing: {bank_statement_text}')
 
    return transactions
 
# Combine descriptor with amount and date for unique match
def match_by_descriptor_amount_date(descriptor, amount, statement_date):
    date_obj = datetime.fromisoformat(statement_date)
    start_date = date_obj - timedelta(days=2)
    end_date = date_obj + timedelta(days=2)
 
    transactions = pl.Transaction.filter_by(
        q=f'descriptor == "{descriptor}" && amount == {amount} && type == "deposit" && created_at >= date("{start_date.date()}") && created_at <= date("{end_date.date()}")',
        order_by='desc(created_at)'
    ).all()
 
    if len(transactions) == 1:
        print('Exact match found using descriptor + amount + date!')
        print(f'Transaction ID: {transactions[0].id}')
        print(f'Descriptor: {transactions[0].descriptor}')
        print(f'Amount: ${transactions[0].amount}')
        return transactions[0]
    elif len(transactions) == 0:
        print('No match found - check descriptor formatting')
        return None
    else:
        print(f'Found {len(transactions)} matches - investigate')
        return transactions
 
# Match when descriptor includes template variables
def match_by_descriptor_with_template(amount, ref_number, statement_date):
    # If descriptor uses template like "Payload - {ref_number}"
    # Bank statement shows: "Payload - REF123456"
    # Query by ref_number instead
    date_obj = datetime.fromisoformat(statement_date)
    start_date = date_obj - timedelta(days=2)
    end_date = date_obj + timedelta(days=2)
 
    transactions = pl.Transaction.filter_by(
        q=f'ref_number == "{ref_number}" && amount == {amount} && type == "deposit" && created_at >= date("{start_date.date()}") && created_at <= date("{end_date.date()}")',
        order_by='desc(created_at)'
    ).all()
 
    if len(transactions) == 1:
        print('Match found using ref_number from descriptor!')
        print(f'Transaction ID: {transactions[0].id}')
        print(f'Ref Number: {transactions[0].ref_number}')
        print(f'Descriptor: {transactions[0].descriptor}')
        return transactions[0]
 
    return transactions if len(transactions) > 0 else None
 
# Example usage
bank_descriptor = 'ACME PAY'
match_by_descriptor(bank_descriptor)

Use the statement descriptor to identify and match transactions.

How it works:

  • Bank statement shows descriptor text
  • Query Payload transactions by descriptor field
  • Use exact or partial matching
  • Combine with amount/date for precision

Advantages:

  • Human-friendly identification
  • Useful for multi-brand scenarios
  • Visible on all bank statements
  • Can use partial matching for truncated text

Limitations:

  • Bank may truncate descriptor (32 char limit)
  • Formatting differences possible
  • Not unique alone (needs amount/date too)
  • Template variables make exact matching harder

Best practices:

  • Use unique descriptors per funding source
  • Include identifiers in descriptor template
  • Combine descriptor with amount and date
  • Account for truncation at 32 characters
import payload
 
pl = payload.Session("secret_key_3bW9...", api_version="v2")
from datetime import datetime, timedelta
 
# Match batch funding transactions (netted/gross funding)
 
 
# Query netted/gross funding batch by date and total amount
def match_netted_funding(statement_date, total_amount):
    # Netted funding: single deposit with combined amount
    # Match by date and total amount
    date_obj = datetime.fromisoformat(statement_date)
    start_date = date_obj - timedelta(days=1)
    end_date = date_obj + timedelta(days=1)
 
    transactions = (
        pl.Transaction.filter_by(
            q=f'type == "payout" AND amount == {total_amount} AND created_at >= date("{start_date.date()}") AND created_at <= date("{end_date.date()}")',
        )
        .order_by("desc(created_at)")
        .all()
    )
 
    if len(transactions) > 0:
        print(f"Found {len(transactions)} netted funding transactions")
        print(f"Transaction ID: {transactions[0].id}")
        print(f"Amount: ${transactions[0].amount}")
        print(f"Date: {transactions[0].created_at}")
        return transactions[0]
 
    print("No netted funding match found")
    return None
 
 
# Query all funding transactions for a date range to calculate batch total
def calculate_batch_total(start_date, end_date):
    # Get all funding transactions in date range
    transactions = (
        pl.Transaction.filter_by(
            q=f'type == "payout" AND created_at >= date("{start_date}") AND created_at <= date("{end_date}")',
        )
        .order_by("asc(created_at)")
        .all()
    )
 
    # Calculate total
    total = sum(txn.amount for txn in transactions)
 
    print(f"Date range: {start_date} to {end_date}")
    print(f"Transaction count: {len(transactions)}")
    print(f"Total amount: ${total:.2f}")
 
    return {"transactions": transactions, "total": total, "count": len(transactions)}
 
 
# Match itemized funding (separate transactions)
def match_itemized_funding(bank_statement_entries):
    # Itemized funding: one bank entry per transaction
    # Match each entry individually
    matches = []
    unmatched = []
 
    for entry in bank_statement_entries:
        # entry: {'amount': 150.00, 'date': '2024-01-15', 'descriptor': 'Product Sale'}
        date_obj = datetime.fromisoformat(entry["date"])
        start_date = date_obj - timedelta(days=1)
        end_date = date_obj + timedelta(days=1)
 
        transactions = (
            pl.Transaction.filter_by(
                q=f'type == "payout" AND amount == {entry["amount"]} AND created_at >= date("{start_date.date()}") AND created_at <= date("{end_date.date()}")',
            )
            .limit(5)
            .all()
        )
 
        if len(transactions) == 1:
            matches.append(
                {"bank_entry": entry, "transaction": transactions[0], "matched": True}
            )
        else:
            unmatched.append(
                {
                    "bank_entry": entry,
                    "potential_matches": transactions,
                    "matched": False,
                    "reason": (
                        "no_match" if len(transactions) == 0 else "multiple_matches"
                    ),
                }
            )
 
    print(f"Itemized funding matches: {len(matches)}")
    print(f"Unmatched entries: {len(unmatched)}")
 
    return {"matches": matches, "unmatched": unmatched}
 
 
# Match by funding batch with custom attributes
def match_by_funding_batch(batch_id, statement_date):
    # If using custom attributes to track funding batches
    # Query by batch ID in attrs
    transactions = (
        pl.Transaction.filter_by(
            q=f'type == "payout" && attrs[funding_batch_id] == "{batch_id}"',
        )
        .order_by("asc(created_at)")
        .all()
    )
 
    if len(transactions) > 0:
        total = sum(txn.amount for txn in transactions)
 
        print(f"Found funding batch: {batch_id}")
        print(f"Transaction count: {len(transactions)}")
        print(f"Total amount: ${total:.2f}")
        print(f"Expected date: {statement_date}")
 
        return {
            "batch_id": batch_id,
            "transactions": transactions,
            "total": total,
            "count": len(transactions),
        }
 
    print(f"No transactions found for batch: {batch_id}")
    return None
 
 
# Example usage
bank_statement_date = "2024-01-15"
bank_statement_amount = 15000.00
match_netted_funding(bank_statement_date, bank_statement_amount)

Match netted or gross funding batches to combined bank statement entries.

How it works:

  • Query all funding transactions in settlement period
  • Calculate total amount for batch
  • Match batch total to bank statement amount
  • Use custom attributes to track batch IDs

Advantages:

  • Matches funding style behavior
  • Single bank entry maps to multiple transactions
  • Can verify all transactions included
  • Useful for period-based reconciliation

Limitations:

  • Requires querying multiple transactions
  • More complex than single transaction matching
  • Timing differences affect batch composition
  • Discrepancies require investigation

Best practices:

  • Use attrs.funding_batch_id for explicit tracking
  • Query by date range matching settlement period
  • Verify transaction count and total match expectations
  • Investigate discrepancies systematically

Matching Examples


Practical examples of matching scenarios.

Bank statement:

  • Date: 2024-01-15
  • Amount: $1,500.00
  • Description: "Payload - REF123456"

Matching strategy:

ref_number = "REF123456"
 
transactions = pl.Transaction.filter_by(
    q=f'ref_number == "{ref_number}" && type == "deposit"',
    limit=1
).all()
 
if transactions[0].amount == 1500.0:
    print("Match confirmed!")

Bank statement:

  • Date: 2024-01-15
  • Amount: $15,000.00
  • Description: "Daily Settlement"

Matching strategy:

batch_start = "2024-01-14"
batch_end = "2024-01-14"
 
transactions = pl.Transaction.filter_by(
    q=f'type == "deposit" && created_at >= date("{batch_start}") && created_at <= date("{batch_end}")'
).all()
 
total = sum(txn.amount for txn in transactions)
 
if total == 15000.0:
    print(f"Batch matches! {len(transactions)} transactions")

Bank statement (multiple entries):

  • Entry 1: $150.00 on 2024-01-15, "Product Sale - ORD-001"
  • Entry 2: $200.00 on 2024-01-15, "Product Sale - ORD-002"
  • Entry 3: $175.00 on 2024-01-15, "Product Sale - ORD-003"

Matching strategy:

entries = [
    {"amount": 150.0, "date": "2024-01-15", "order": "ORD-001"},
    {"amount": 200.0, "date": "2024-01-15", "order": "ORD-002"},
    {"amount": 175.0, "date": "2024-01-15", "order": "ORD-003"},
]
 
for entry in entries:
    transactions = pl.Transaction.filter_by(
        q=f'type == "deposit" && amount == {entry["amount"]} && order_number == "{entry["order"]}" && created_at >= date("{entry["date"]}")'
    ).all()
 
    if len(transactions) == 1:
        print(f"Matched: {entry['order']}")

Bank statement:

  • Date: 2024-01-15
  • Amount: $1,495.50 (after $4.50 wire fee)

Matching strategy:

bank_amount = 1495.50
tolerance = 10.0
 
min_amount = bank_amount - tolerance
max_amount = bank_amount + tolerance
 
transactions = pl.Transaction.filter_by(
    q=f'type == "deposit" && amount >= {min_amount} && amount <= {max_amount} && created_at >= date("2024-01-14") && created_at <= date("2024-01-16")'
).all()
 
closest = min(transactions, key=lambda t: abs(t.amount - bank_amount))
 
print(
    f"Match found: ${closest.amount} (diff: ${closest.amount - bank_amount:.2f})"
)

Schema Reference


Fields relevant to matching funding transactions.

Key transaction fields

ref_number
stringRead-only
A system-generated human-readable reference number for this transaction. This unique identifier can be used on receipts, customer communications, and for looking up transactions in reports and support inquiries.
Max length: 64
amount
number (double)
The monetary amount for this transaction in the currency of the processing account. This value is always positive and represents the total value being transferred, collected, or refunded. The amount is rounded to two decimal places for display.
created_at
string (date-time)Read-only
Timestamp when the resource was created. Automatically set by the system and immutable.
est_cleared_date
string (date)
The estimated date when this transaction will be cleared and funds will be available or settled. This is an estimate and not guaranteed.
Read-only permissions: "Undocumented"

Matching fields:

  • ref_number: Unique system-generated reference for exact matching
  • amount: Transaction monetary value for amount-based matching
  • created_at: Transaction creation timestamp
  • est_cleared_date: Estimated settlement date, matches bank statement dates more closely
  • descriptor: Statement text, appears on bank statements

Query operators

Use these operators when querying for matching:

OperatorUsageExample
==Exact matchref_number == "REF123456"
>=Greater than or equalcreated_at >= date("2024-01-14")
<=Less than or equalcreated_at <= date("2024-01-16")
?=Contains (partial match)descriptor ?= "*ACME*"
&&Combine conditionsamount == 1500 && type == "deposit"

Next Steps

Improve reconciliation accuracy and automate matching workflows


Complete Bank Reconciliation

Master full bank reconciliation with Bank Reconciliation guide, automate matching processes, and implement Custom Attributes for tracking reconciliation metadata across transactions.

Customize Statement Descriptors

Configure descriptors for easier matching with Custom Descriptors documentation, understand how Funding styles affect descriptors, and optimize Processing Settings for your workflow.

Build Reconciliation Reports

Create powerful reports with Reporting Overview for reconciliation analytics, track transaction status with Transaction Status documentation, and manage payouts efficiently with Payouts guide.

Automate Reconciliation

Integrate external systems with System Integration best practices, receive real-time updates via Webhooks for automatic matching, and explore API Reference for building custom reconciliation tools.


Related articles