Building a Healthcare Billing Agent: AI for Patient Billing Inquiries
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.
Why Billing Is the Top Patient Complaint
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.
Modeling the Billing Domain
Healthcare billing has specific terminology and relationships that the agent must understand:
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 Billing Inquiry Agent
The agent needs to look up charges, explain them in plain language, and answer specific questions:
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}",
}
Payment Plan Management
For patients who cannot pay in full, the agent can set up structured payment plans:
See AI Voice Agents Handle Real Calls
Book a free demo or calculate how much you can save with AI voice automation.
@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
Dispute Handling
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.")
FAQ
How does the billing agent handle sensitive financial information securely?
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.
Can the agent handle insurance coordination of benefits questions?
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.
What happens when a charge was genuinely made in error?
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
CallSphere Team
Expert insights on AI voice agents and customer communication automation.
Try CallSphere AI Voice Agents
See how AI voice agents work for your industry. Live demo available -- no signup required.