By Sagar Shankaran, Founder of CallSphere
Build an AI agent that automatically matches incoming payments to outstanding invoices using fuzzy matching, handles exceptions, and generates reconciliation reports.
Key takeaways
Accounts receivable teams spend hours matching incoming bank payments to outstanding invoices. The difficulty is that payment references are often incomplete, amounts may include partial payments or combine multiple invoices, and customer names on bank statements do not always match the billing system. An AI agent can automate the straightforward matches and surface only the genuinely ambiguous cases for human review.
Define structured models for invoices and payments.
flowchart LR
INPUT(["User intent"])
PARSE["Parse plus<br/>classify"]
PLAN["Plan and tool<br/>selection"]
AGENT["Agent loop<br/>LLM plus tools"]
GUARD{"Guardrails<br/>and policy"}
EXEC["Execute and<br/>verify result"]
OBS[("Trace and metrics")]
OUT(["Outcome plus<br/>next action"])
INPUT --> PARSE --> PLAN --> AGENT --> GUARD
GUARD -->|Pass| EXEC --> OUT
GUARD -->|Fail| AGENT
AGENT --> OBS
style AGENT fill:#4f46e5,stroke:#4338ca,color:#fff
style GUARD fill:#f59e0b,stroke:#d97706,color:#1f2937
style OBS fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b
style OUT fill:#059669,stroke:#047857,color:#fff
from pydantic import BaseModel
from datetime import date
from enum import Enum
class MatchConfidence(str, Enum):
EXACT = "exact"
HIGH = "high"
MEDIUM = "medium"
LOW = "low"
UNMATCHED = "unmatched"
class Invoice(BaseModel):
invoice_id: str
customer_name: str
customer_id: str
amount: float
currency: str
due_date: date
status: str # "open", "partial", "paid"
remaining_balance: float
class BankTransaction(BaseModel):
transaction_id: str
date: date
amount: float
currency: str
reference: str # Bank reference / memo
counterparty: str
class MatchResult(BaseModel):
transaction_id: str
invoice_id: str | None
confidence: MatchConfidence
match_reason: str
amount_difference: float
The matching engine tries several strategies in order of confidence — exact reference match, amount match, and fuzzy matching.
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
from difflib import SequenceMatcher
class ReconciliationEngine:
def __init__(
self,
invoices: list[Invoice],
transactions: list[BankTransaction],
):
self.invoices = {inv.invoice_id: inv for inv in invoices}
self.open_invoices = [
inv for inv in invoices if inv.status != "paid"
]
self.transactions = transactions
self.results: list[MatchResult] = []
def reconcile(self) -> list[MatchResult]:
"""Run all matching strategies."""
unmatched_txns = list(self.transactions)
# Strategy 1: Exact reference match
unmatched_txns = self._match_by_reference(unmatched_txns)
# Strategy 2: Exact amount + customer name match
unmatched_txns = self._match_by_amount_and_name(unmatched_txns)
# Strategy 3: Fuzzy matching for remaining
unmatched_txns = self._fuzzy_match(unmatched_txns)
# Mark remaining as unmatched
for txn in unmatched_txns:
self.results.append(
MatchResult(
transaction_id=txn.transaction_id,
invoice_id=None,
confidence=MatchConfidence.UNMATCHED,
match_reason="No matching invoice found",
amount_difference=txn.amount,
)
)
return self.results
def _match_by_reference(
self, transactions: list[BankTransaction]
) -> list[BankTransaction]:
"""Match by invoice number in bank reference."""
unmatched = []
for txn in transactions:
matched = False
for inv in self.open_invoices:
if inv.invoice_id.lower() in txn.reference.lower():
diff = abs(txn.amount - inv.remaining_balance)
self.results.append(
MatchResult(
transaction_id=txn.transaction_id,
invoice_id=inv.invoice_id,
confidence=MatchConfidence.EXACT,
match_reason=(
f"Invoice ID found in reference"
),
amount_difference=diff,
)
)
matched = True
break
if not matched:
unmatched.append(txn)
return unmatched
def _match_by_amount_and_name(
self, transactions: list[BankTransaction]
) -> list[BankTransaction]:
"""Match by exact amount and similar customer name."""
unmatched = []
for txn in transactions:
candidates = [
inv for inv in self.open_invoices
if abs(inv.remaining_balance - txn.amount) < 0.01
]
best_match = None
best_score = 0.0
for inv in candidates:
similarity = SequenceMatcher(
None,
txn.counterparty.lower(),
inv.customer_name.lower(),
).ratio()
if similarity > best_score:
best_score = similarity
best_match = inv
if best_match and best_score > 0.6:
self.results.append(
MatchResult(
transaction_id=txn.transaction_id,
invoice_id=best_match.invoice_id,
confidence=MatchConfidence.HIGH,
match_reason=(
f"Amount match + name similarity "
f"({best_score:.0%})"
),
amount_difference=0.0,
)
)
else:
unmatched.append(txn)
return unmatched
def _fuzzy_match(
self, transactions: list[BankTransaction]
) -> list[BankTransaction]:
"""Fuzzy match using amount proximity and name similarity."""
unmatched = []
tolerance = 0.05 # 5% amount tolerance
for txn in transactions:
best_match = None
best_score = 0.0
for inv in self.open_invoices:
amount_diff = abs(
txn.amount - inv.remaining_balance
)
amount_ratio = (
amount_diff / inv.remaining_balance
if inv.remaining_balance > 0
else 1.0
)
if amount_ratio > tolerance:
continue
name_sim = SequenceMatcher(
None,
txn.counterparty.lower(),
inv.customer_name.lower(),
).ratio()
combined_score = (
name_sim * 0.6 + (1 - amount_ratio) * 0.4
)
if combined_score > best_score:
best_score = combined_score
best_match = inv
if best_match and best_score > 0.5:
self.results.append(
MatchResult(
transaction_id=txn.transaction_id,
invoice_id=best_match.invoice_id,
confidence=MatchConfidence.MEDIUM,
match_reason=(
f"Fuzzy match (score: {best_score:.2f})"
),
amount_difference=abs(
txn.amount - best_match.remaining_balance
),
)
)
else:
unmatched.append(txn)
return unmatched
For items the rule-based engine cannot match, the LLM analyzes context clues.
from openai import OpenAI
client = OpenAI()
class LLMMatchSuggestion(BaseModel):
suggested_invoice_id: str | None
reasoning: str
confidence: str
def resolve_exception(
txn: BankTransaction, open_invoices: list[Invoice]
) -> LLMMatchSuggestion:
"""Use LLM to resolve an unmatched transaction."""
invoices_text = "\n".join(
f"- {inv.invoice_id}: {inv.customer_name}, "
f"${inv.remaining_balance:.2f}, due {inv.due_date}"
for inv in open_invoices
)
response = client.beta.chat.completions.parse(
model="gpt-4o",
messages=[
{
"role": "system",
"content": (
"You are an accounts receivable specialist. "
"Match the bank transaction to the most likely "
"invoice based on amount, name, date, and reference."
),
},
{
"role": "user",
"content": (
f"Transaction: ${txn.amount:.2f} from "
f"'{txn.counterparty}' ref: '{txn.reference}' "
f"on {txn.date}\n\nOpen Invoices:\n"
f"{invoices_text}"
),
},
],
response_format=LLMMatchSuggestion,
)
return response.choices[0].message.parsed
def generate_report(results: list[MatchResult]) -> dict:
"""Generate reconciliation summary report."""
total = len(results)
by_confidence = {}
for r in results:
by_confidence.setdefault(r.confidence, []).append(r)
return {
"total_transactions": total,
"exact_matches": len(by_confidence.get(MatchConfidence.EXACT, [])),
"high_confidence": len(by_confidence.get(MatchConfidence.HIGH, [])),
"medium_confidence": len(by_confidence.get(MatchConfidence.MEDIUM, [])),
"unmatched": len(by_confidence.get(MatchConfidence.UNMATCHED, [])),
"auto_match_rate": (
(total - len(by_confidence.get(MatchConfidence.UNMATCHED, [])))
/ total * 100
if total > 0
else 0
),
}
Track a remaining_balance field on each invoice. When a partial match is detected (payment amount is less than invoice amount), record the partial payment and update the remaining balance. The agent flags these for review and suggests whether to apply as partial payment or investigate further.
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
The agent should detect lump-sum payments by checking if the payment amount matches the sum of multiple open invoices from the same customer. Implement a combination search that tries subsets of open invoices to find matching totals, starting with the most likely groupings.
Include a currency conversion step using daily exchange rates. Match the converted amount rather than the raw amount, and store the exchange rate used for audit purposes. Flag any matches where the exchange rate assumption could change the outcome.
#InvoiceReconciliation #PaymentMatching #Accounting #FuzzyMatching #Automation #AgenticAI #LearnAI #AIEngineering

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
See how AI voice agents work for your industry. Live demo available -- no signup required.
Togolese professional-services firms lose new clients when calls go unanswered. See how lawyers, accountants, notaries, and consultants use CallSphere to capture and qualify every enquiry 24/7 in French and Ewe from around 30,000 XOF a month.
Maltese professional-services firms in Valletta and Sliema use CallSphere AI voice and chat agents to answer, qualify and book new-client inquiries in English, Maltese and Italian without interrupting billable work.
Tunisian professional firms lose clients and billable time to unanswered calls. See how law offices, accountants and consultancies in Tunis use CallSphere AI voice and chat agents to run first intake 24/7 in French and Arabic and protect fee earners.
A technical playbook for deploying AI voice agents that place prior authorization calls to payer IVRs, navigate hold queues, and capture auth numbers autonomously.
Learn how AI voice agents automate appointment booking, reduce no-shows by up to 35%, and free staff for higher-value work across industries.
See how AI agents automate tax document collection — chasing missing W-2s, 1099s, and receipts via calls and texts to eliminate the #1 CPA bottleneck.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco
Watch how CallSphere handles real customer calls, schedules appointments, and processes payments — live.