Skip to content
Learn Agentic AI11 min read0 views

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

Share this article
C

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.