Skip to content
Learn Agentic AI14 min read0 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.

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.

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.

See AI Voice Agents Handle Real Calls

Book a free demo or calculate how much you can save with AI voice automation.

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.

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 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.