By Sagar Shankaran, Founder of CallSphere
Learn how to build an AI agent that handles patient billing questions, explains charges and benefits, sets up payment plans, processes disputes, and integrates with practice management systems.
Key takeaways
Healthcare billing is the number one source of patient complaints. Statements are confusing, explanation of benefits documents are incomprehensible, and reaching a human to explain a charge can take 30 minutes on hold. A billing AI agent answers questions about charges in plain language, explains what insurance covered and why, sets up payment plans, and handles disputes — available 24/7 without hold times.
Healthcare billing has specific terminology and relationships that the agent must understand:
flowchart LR
CALLER(["Patient or Caregiver"])
subgraph TEL["Telephony"]
SIP["Twilio SIP and PSTN"]
end
subgraph BRAIN["Healthcare AI Agent"]
STT["Streaming STT<br/>Deepgram or Whisper"]
NLU{"Intent and<br/>Entity Extraction"}
TOOLS["Tool Calls"]
TTS["Streaming TTS<br/>ElevenLabs or Rime"]
end
subgraph DATA["Live Data Plane"]
CRM[("CRM and Notes")]
CAL[("Calendar and<br/>Schedule")]
KB[("Knowledge Base<br/>and Policies")]
end
subgraph OUT["Outcomes"]
O1(["Appointment booked"])
O2(["Prescription refill request"])
O3(["Triage to clinician"])
end
CALLER --> SIP --> STT --> NLU
NLU -->|Lookup| TOOLS
TOOLS <--> CRM
TOOLS <--> CAL
TOOLS <--> KB
NLU --> TTS --> SIP --> CALLER
NLU -->|Resolved| O1
NLU -->|Schedule| O2
NLU -->|Escalate| O3
style CALLER fill:#f1f5f9,stroke:#64748b,color:#0f172a
style NLU fill:#4f46e5,stroke:#4338ca,color:#fff
style O1 fill:#059669,stroke:#047857,color:#fff
style O2 fill:#0ea5e9,stroke:#0369a1,color:#fff
style O3 fill:#f59e0b,stroke:#d97706,color:#1f2937
from dataclasses import dataclass, field
from datetime import datetime, date
from typing import Optional
from enum import Enum
class ClaimStatus(Enum):
SUBMITTED = "submitted"
IN_PROCESS = "in_process"
PAID = "paid"
DENIED = "denied"
PARTIALLY_PAID = "partially_paid"
APPEALED = "appealed"
class LineItemType(Enum):
OFFICE_VISIT = "office_visit"
PROCEDURE = "procedure"
LAB = "lab"
IMAGING = "imaging"
INJECTION = "injection"
SUPPLY = "supply"
@dataclass
class BillingLineItem:
cpt_code: str
description: str
item_type: LineItemType
service_date: date
billed_amount: float
allowed_amount: float
insurance_paid: float
patient_responsibility: float
adjustment: float = 0.0
denial_reason: Optional[str] = None
@dataclass
class PatientStatement:
statement_id: str
patient_id: str
statement_date: date
line_items: list[BillingLineItem] = field(default_factory=list)
total_billed: float = 0.0
total_insurance_paid: float = 0.0
total_patient_owes: float = 0.0
total_adjustments: float = 0.0
prior_balance: float = 0.0
payments_received: float = 0.0
current_balance: float = 0.0
def calculate_totals(self) -> None:
self.total_billed = sum(item.billed_amount for item in self.line_items)
self.total_insurance_paid = sum(item.insurance_paid for item in self.line_items)
self.total_adjustments = sum(item.adjustment for item in self.line_items)
self.total_patient_owes = sum(item.patient_responsibility for item in self.line_items)
self.current_balance = self.prior_balance + self.total_patient_owes - self.payments_received
The agent needs to look up charges, explain them in plain language, and answer specific questions:
Hear it before you finish reading
Talk to a live CallSphere AI voice agent for healthcare in your browser — 60 seconds, no signup.
class BillingInquiryAgent:
CPT_DESCRIPTIONS = {
"99213": "Standard office visit (15-30 minutes) for an existing patient",
"99214": "Extended office visit (30-40 minutes) for a moderately complex problem",
"36415": "Blood draw for lab work",
"85025": "Complete blood count (CBC) lab test",
"80053": "Comprehensive metabolic panel (blood chemistry test)",
"71046": "Chest X-ray, two views",
}
def __init__(self, statements: dict[str, PatientStatement]):
self._statements = statements
def explain_charge(self, statement_id: str, cpt_code: str) -> dict:
statement = self._statements.get(statement_id)
if not statement:
return {"error": "Statement not found"}
matching_items = [
item for item in statement.line_items if item.cpt_code == cpt_code
]
if not matching_items:
return {"error": f"No charge with code {cpt_code} found on this statement"}
item = matching_items[0]
plain_description = self.CPT_DESCRIPTIONS.get(
cpt_code, item.description
)
explanation = {
"charge": plain_description,
"service_date": item.service_date.isoformat(),
"what_was_billed": f"${item.billed_amount:.2f}",
"what_insurance_allowed": f"${item.allowed_amount:.2f}",
"what_insurance_paid": f"${item.insurance_paid:.2f}",
"your_responsibility": f"${item.patient_responsibility:.2f}",
}
if item.adjustment > 0:
explanation["discount_applied"] = f"${item.adjustment:.2f}"
explanation["why_discount"] = (
"This is the difference between what the provider billed and what "
"your insurance plan has agreed to pay. You are not responsible for this amount."
)
if item.denial_reason:
explanation["denial_reason"] = item.denial_reason
explanation["what_you_can_do"] = (
"You may appeal this denial. I can help you start that process."
)
return explanation
def get_balance_summary(self, patient_id: str) -> dict:
patient_statements = [
s for s in self._statements.values() if s.patient_id == patient_id
]
if not patient_statements:
return {"error": "No statements found for this patient"}
latest = max(patient_statements, key=lambda s: s.statement_date)
return {
"current_balance": f"${latest.current_balance:.2f}",
"last_statement_date": latest.statement_date.isoformat(),
"charges_this_period": f"${latest.total_patient_owes:.2f}",
"payments_received": f"${latest.payments_received:.2f}",
"prior_balance": f"${latest.prior_balance:.2f}",
}
For patients who cannot pay in full, the agent can set up structured payment plans:
@dataclass
class PaymentPlan:
plan_id: str
patient_id: str
total_amount: float
monthly_payment: float
num_installments: int
start_date: date
remaining_balance: float
payments_made: int = 0
status: str = "active"
class PaymentPlanManager:
MIN_MONTHLY_PAYMENT = 25.0
MAX_INSTALLMENTS = 24
def calculate_options(self, balance: float) -> list[dict]:
options = []
for months in [3, 6, 12, 18, 24]:
monthly = balance / months
if monthly < self.MIN_MONTHLY_PAYMENT:
continue
options.append({
"months": months,
"monthly_payment": round(monthly, 2),
"total": balance,
"interest": 0.0, # Most practices offer 0% interest
})
return options
def create_plan(
self, patient_id: str, balance: float, months: int
) -> PaymentPlan:
if months > self.MAX_INSTALLMENTS:
raise ValueError(f"Maximum {self.MAX_INSTALLMENTS} installments allowed")
monthly = round(balance / months, 2)
if monthly < self.MIN_MONTHLY_PAYMENT:
raise ValueError(f"Minimum monthly payment is ${self.MIN_MONTHLY_PAYMENT}")
return PaymentPlan(
plan_id=f"PP-{patient_id}-{datetime.utcnow().strftime('%Y%m%d')}",
patient_id=patient_id,
total_amount=balance,
monthly_payment=monthly,
num_installments=months,
start_date=date.today(),
remaining_balance=balance,
)
def record_payment(self, plan: PaymentPlan, amount: float) -> PaymentPlan:
plan.remaining_balance -= amount
plan.payments_made += 1
if plan.remaining_balance <= 0:
plan.remaining_balance = 0
plan.status = "completed"
return plan
When patients dispute a charge, the agent collects the necessary information and initiates the review process:
@dataclass
class BillingDispute:
dispute_id: str
patient_id: str
statement_id: str
disputed_items: list[str] # CPT codes
reason: str
patient_narrative: str
supporting_docs: list[str] = field(default_factory=list)
status: str = "submitted"
resolution: Optional[str] = None
created_at: datetime = field(default_factory=datetime.utcnow)
class DisputeHandler:
VALID_REASONS = [
"service_not_received",
"duplicate_charge",
"incorrect_amount",
"insurance_should_cover",
"already_paid",
"wrong_patient",
]
def create_dispute(
self,
patient_id: str,
statement_id: str,
cpt_codes: list[str],
reason: str,
narrative: str,
) -> BillingDispute:
if reason not in self.VALID_REASONS:
raise ValueError(f"Invalid dispute reason. Valid reasons: {self.VALID_REASONS}")
return BillingDispute(
dispute_id=f"DSP-{datetime.utcnow().strftime('%Y%m%d%H%M')}",
patient_id=patient_id,
statement_id=statement_id,
disputed_items=cpt_codes,
reason=reason,
patient_narrative=narrative,
)
def get_status_message(self, dispute: BillingDispute) -> str:
messages = {
"submitted": "Your dispute has been received and is being reviewed by our billing team.",
"under_review": "Your dispute is currently being reviewed. Expected resolution: 15-30 days.",
"resolved_in_favor": f"Your dispute has been resolved. Resolution: {dispute.resolution}",
"resolved_against": "After review, the original charge stands. You may appeal this decision.",
}
return messages.get(dispute.status, "Status unknown. Please contact our billing office.")
The agent encrypts all financial data at rest and in transit. Payment card details are never stored — they are tokenized through a PCI-compliant payment processor. The agent can reference statement amounts and balances but never displays full card numbers. All billing interactions are logged in the audit trail.
Still reading? Stop comparing — try CallSphere live.
See the healthcare AI agent handle a real call — complete, industry-specific, and live in your browser. No signup.
Yes. When a patient has multiple insurance plans, the agent explains which plan is primary and secondary, shows how each plan processed the claim, and clarifies why the patient responsibility is what it is after both plans have paid. This is one of the most confusing aspects of healthcare billing for patients, and clear explanations dramatically reduce call-backs.
If the agent identifies a clear error (duplicate charge with the same CPT code on the same date of service), it can flag the charge for immediate review and place a hold on the patient's responsibility for that line item while the review is pending. This prevents the patient from being sent to collections for a charge that is likely to be reversed.
#HealthcareAI #MedicalBilling #PaymentPlans #RevenueCycle #Python #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.
Step-by-step build of a working agent with the OpenAI Agents SDK — Agent class, tools, handoffs, tracing — plus an eval pipeline that catches regressions before merge.
AWS HealthScribe became the open scribe layer EHR vendors built on top of in 2026. Here's the API surface, the per-encounter pricing, the BAA terms.
Apollo, Manipal, and Narayana scaled AI agents across Bangalore in 2026. Here's the deployments across radiology, intake, and follow-up, the costs.
Notable's AI agents now handle scheduling, intake, and revenue cycle for 6,000+ clinics in 2026. Here's the multi-agent architecture, the per-clinic pricing.
Abridge raised $250M in April 2026 at a $2.7B valuation. We break down the deployment numbers, the EHR integrations across Epic and Cerner. The Q2 2026 buyer briefing.
Enterprise CIO Guide perspective on Hippocratic AI's deployment numbers show healthcare voice agents are moving from pilot to production across major US health systems.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco