AI Agent for Medical Billing Inquiries: Explaining Bills, Processing Payments, and Setting Up Plans
Build an AI agent that explains medical and dental bills in plain language, processes secure payments, sets up payment plans for patients, and handles billing dispute workflows with full Python implementation.
Why Billing Is the Top Source of Patient Frustration
Billing inquiries are the number one reason patients call medical and dental practices. Patients receive statements filled with CDT or CPT codes, insurance adjustments, and confusing line items. Most just want to know: what do I owe, and why? An AI billing agent answers these questions instantly, processes payments securely, and sets up payment plans — all without tying up staff.
Bill Data Model and Retrieval
The billing agent needs access to the full billing record: charges, insurance payments, adjustments, and patient responsibility.
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 date, datetime
from typing import Optional
from enum import Enum
from decimal import Decimal
class LineItemStatus(Enum):
BILLED = "billed"
INSURANCE_PAID = "insurance_paid"
ADJUSTED = "adjusted"
PATIENT_DUE = "patient_due"
PAID = "paid"
COLLECTIONS = "collections"
@dataclass
class BillLineItem:
procedure_code: str
procedure_description: str
service_date: date
tooth_number: Optional[int]
fee: Decimal
insurance_paid: Decimal
adjustment: Decimal
patient_responsibility: Decimal
status: LineItemStatus
@dataclass
class PatientBill:
bill_id: str
patient_id: str
patient_name: str
statement_date: date
line_items: list[BillLineItem]
total_charges: Decimal
total_insurance: Decimal
total_adjustments: Decimal
total_patient_due: Decimal
total_paid_by_patient: Decimal
balance_due: Decimal
payment_plan_active: bool = False
class BillingRetriever:
def __init__(self, db):
self.db = db
async def get_patient_bill(
self, patient_id: str,
) -> Optional[PatientBill]:
header = await self.db.fetchrow("""
SELECT b.id, b.patient_id,
p.first_name || ' ' || p.last_name AS name,
b.statement_date,
COALESCE(pp.id IS NOT NULL, false)
AS has_plan
FROM bills b
JOIN patients p ON p.id = b.patient_id
LEFT JOIN payment_plans pp
ON pp.bill_id = b.id AND pp.status = 'active'
WHERE b.patient_id = $1
ORDER BY b.statement_date DESC LIMIT 1
""", patient_id)
if not header:
return None
items = await self.db.fetch("""
SELECT procedure_code, procedure_description,
service_date, tooth_number,
fee, insurance_paid, adjustment,
patient_responsibility, status
FROM bill_line_items
WHERE bill_id = $1
ORDER BY service_date
""", header["id"])
line_items = [
BillLineItem(
procedure_code=r["procedure_code"],
procedure_description=r["procedure_description"],
service_date=r["service_date"],
tooth_number=r["tooth_number"],
fee=Decimal(str(r["fee"])),
insurance_paid=Decimal(str(r["insurance_paid"])),
adjustment=Decimal(str(r["adjustment"])),
patient_responsibility=Decimal(
str(r["patient_responsibility"])
),
status=LineItemStatus(r["status"]),
)
for r in items
]
total_charges = sum(i.fee for i in line_items)
total_insurance = sum(
i.insurance_paid for i in line_items
)
total_adj = sum(i.adjustment for i in line_items)
total_patient = sum(
i.patient_responsibility for i in line_items
)
payments = await self.db.fetchrow("""
SELECT COALESCE(SUM(amount), 0) AS paid
FROM payments WHERE bill_id = $1
""", header["id"])
balance = total_patient - Decimal(str(payments["paid"]))
return PatientBill(
bill_id=header["id"],
patient_id=patient_id,
patient_name=header["name"],
statement_date=header["statement_date"],
line_items=line_items,
total_charges=total_charges,
total_insurance=total_insurance,
total_adjustments=total_adj,
total_patient_due=total_patient,
total_paid_by_patient=Decimal(
str(payments["paid"])
),
balance_due=balance,
payment_plan_active=header["has_plan"],
)
Plain Language Bill Explanation
The agent translates each line item into language the patient can understand, explaining why they owe what they owe.
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
class BillExplainer:
PROCEDURE_NAMES = {
"D0120": "Regular checkup exam",
"D0274": "X-rays (4 bitewing images)",
"D1110": "Teeth cleaning (adult)",
"D2391": "Tooth-colored filling (1 surface)",
"D2740": "Porcelain crown",
"D3310": "Root canal (front tooth)",
"D7210": "Tooth extraction (surgical)",
}
def explain(self, bill: PatientBill) -> str:
lines = []
lines.append(
f"Bill Summary for {bill.patient_name}\n"
f"Statement Date: {bill.statement_date}\n"
)
for item in bill.line_items:
friendly_name = self.PROCEDURE_NAMES.get(
item.procedure_code,
item.procedure_description,
)
tooth_str = (
f" (Tooth #{item.tooth_number})"
if item.tooth_number else ""
)
lines.append(
f" {friendly_name}{tooth_str}\n"
f" Charge: ${item.fee}\n"
f" Insurance paid: ${item.insurance_paid}\n"
f" Adjustment: -${item.adjustment}\n"
f" Your cost: ${item.patient_responsibility}\n"
)
lines.append(
f"Total charges: ${bill.total_charges}\n"
f"Insurance covered: ${bill.total_insurance}\n"
f"Adjustments: -${bill.total_adjustments}\n"
f"Your total: ${bill.total_patient_due}\n"
f"Already paid: ${bill.total_paid_by_patient}\n"
f"BALANCE DUE: ${bill.balance_due}\n"
)
return "\n".join(lines)
def explain_insurance_adjustment(
self, item: BillLineItem,
) -> str:
if item.adjustment > 0:
return (
f"The ${item.adjustment} adjustment is a "
f"contractual discount. Your dentist agreed "
f"to accept a lower fee as part of the "
f"agreement with your insurance network. "
f"This reduces your out-of-pocket cost."
)
return ""
Secure Payment Processing
The agent processes payments through a PCI-compliant payment gateway. It never handles raw card numbers — instead, it directs patients to a secure tokenization form and processes the resulting token.
class PaymentProcessor:
def __init__(self, gateway_client, db):
self.gateway = gateway_client
self.db = db
async def generate_payment_link(
self, bill_id: str, amount: Decimal,
) -> str:
session = await self.gateway.create_session(
amount=float(amount),
reference=bill_id,
success_url=(
f"https://portal.example.com/payment/success"
f"?bill={bill_id}"
),
cancel_url=(
f"https://portal.example.com/payment/cancel"
),
)
return session["checkout_url"]
async def process_token_payment(
self, bill_id: str, token: str, amount: Decimal,
) -> dict:
result = await self.gateway.charge(
token=token,
amount=float(amount),
description=f"Payment for bill {bill_id}",
)
if result["status"] == "succeeded":
await self.db.execute("""
INSERT INTO payments
(bill_id, amount, method,
transaction_id, paid_at)
VALUES ($1, $2, 'card', $3, $4)
""", bill_id, float(amount),
result["transaction_id"],
datetime.utcnow())
return {
"success": result["status"] == "succeeded",
"transaction_id": result.get("transaction_id"),
"message": result.get("message", ""),
}
Payment Plan Setup
For larger balances, the agent creates structured payment plans with automatic recurring charges.
@dataclass
class PaymentPlan:
id: str
bill_id: str
total_amount: Decimal
monthly_payment: Decimal
term_months: int
start_date: date
next_payment_date: date
payments_made: int = 0
status: str = "active"
class PaymentPlanManager:
def __init__(self, db, gateway):
self.db = db
self.gateway = gateway
async def create_plan(
self, bill_id: str, total: Decimal,
term_months: int, payment_token: str,
) -> PaymentPlan:
monthly = (total / term_months).quantize(
Decimal("0.01")
)
plan_id = str(__import__("uuid").uuid4())
start = date.today()
subscription = await self.gateway.create_subscription(
token=payment_token,
amount=float(monthly),
interval="monthly",
reference=plan_id,
)
plan = PaymentPlan(
id=plan_id,
bill_id=bill_id,
total_amount=total,
monthly_payment=monthly,
term_months=term_months,
start_date=start,
next_payment_date=start,
)
await self.db.execute("""
INSERT INTO payment_plans
(id, bill_id, total_amount, monthly_payment,
term_months, start_date, next_payment_date,
subscription_id, status)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'active')
""", plan.id, bill_id, float(total), float(monthly),
term_months, start, start,
subscription["subscription_id"])
return plan
async def get_plan_status(
self, patient_id: str,
) -> Optional[dict]:
plan = await self.db.fetchrow("""
SELECT pp.*, b.patient_id
FROM payment_plans pp
JOIN bills b ON b.id = pp.bill_id
WHERE b.patient_id = $1
AND pp.status = 'active'
""", patient_id)
if not plan:
return None
remaining = (
Decimal(str(plan["total_amount"]))
- Decimal(str(plan["monthly_payment"]))
* plan["payments_made"]
)
return {
"monthly_payment": plan["monthly_payment"],
"payments_made": plan["payments_made"],
"payments_remaining": (
plan["term_months"] - plan["payments_made"]
),
"balance_remaining": float(remaining),
"next_payment": plan["next_payment_date"],
}
FAQ
How does the agent handle billing disputes?
When a patient disputes a charge, the agent creates a formal dispute record with the patient's stated reason, flags the line item for review by the billing team, and pauses collection activity on the disputed amount. The agent can resolve simple disputes — such as duplicate charges — automatically by cross-referencing the appointment record. Complex disputes are escalated to a human billing coordinator with full context attached.
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.
Is it safe to process payments through an AI agent?
The agent never sees or stores raw credit card numbers. It uses tokenization — the patient enters card details on a PCI-compliant form hosted by the payment gateway, which returns a one-time-use token. The agent uses this token to initiate the charge. All payment data flows through the gateway's encrypted infrastructure, and the practice's system only stores the transaction ID and confirmation.
What happens if a patient's automatic payment plan payment fails?
The agent retries the charge once after three days. If the retry also fails, it notifies the patient via their preferred contact method and offers alternatives: update payment information, make a manual payment, or contact the office to adjust the plan. After two consecutive failures, the plan is paused and the billing team receives an alert to follow up personally.
#MedicalBilling #PaymentProcessing #HealthcareAI #PatientFinance #Python #AgenticAI #LearnAI #AIEngineering
Try CallSphere AI Voice Agents
See how AI voice agents work for your industry. Live demo available -- no signup required.