Skip to content
Learn Agentic AI
Learn Agentic AI14 min read7 views

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.

Try Live Demo →
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

Share

Try CallSphere AI Voice Agents

See how AI voice agents work for your industry. Live demo available -- no signup required.