Skip to content
Learn Agentic AI13 min read0 views

AI Agent for Dental Insurance Verification: Automated Eligibility and Benefits Checking

Build an AI agent that automates dental insurance verification by integrating with payer APIs, parsing complex plan structures, and explaining coverage details to patients in plain language.

The Insurance Verification Bottleneck

Insurance verification is one of the most time-consuming tasks in a dental office. Staff call insurance companies, wait on hold, and manually transcribe benefit information. A single verification can take 10 to 15 minutes. With 20 patients per day, that is over three hours of staff time just on hold.

An AI insurance verification agent automates this by connecting directly to payer APIs through a dental clearinghouse, parsing the structured response, and presenting the information in a format that is immediately useful to both staff and patients.

Clearinghouse Integration Layer

Dental clearinghouses like DentalXChange, NEA, and Availity provide standardized APIs that connect to hundreds of insurance payers through a single integration point. The agent communicates with these clearinghouses using the X12 270/271 eligibility transaction format.

from dataclasses import dataclass, field
from datetime import date, datetime
from typing import Optional
from enum import Enum
import httpx


class BenefitCategory(Enum):
    PREVENTIVE = "preventive"
    BASIC = "basic"
    MAJOR = "major"
    ORTHODONTICS = "orthodontics"
    ENDODONTICS = "endodontics"
    PERIODONTICS = "periodontics"
    ORAL_SURGERY = "oral_surgery"
    DIAGNOSTICS = "diagnostics"


@dataclass
class BenefitDetail:
    category: BenefitCategory
    coverage_percent: int
    waiting_period_months: int = 0
    annual_max_remaining: Optional[float] = None
    frequency_limit: str = ""
    requires_preauth: bool = False


@dataclass
class EligibilityResult:
    is_eligible: bool
    subscriber_name: str
    plan_name: str
    group_number: str
    effective_date: date
    termination_date: Optional[date]
    annual_maximum: float
    annual_max_remaining: float
    deductible: float
    deductible_met: float
    benefits: list[BenefitDetail] = field(
        default_factory=list
    )
    raw_response: dict = field(default_factory=dict)
    verified_at: datetime = field(
        default_factory=datetime.utcnow
    )


class ClearinghouseClient:
    def __init__(
        self, api_url: str, username: str,
        password: str, submitter_id: str,
    ):
        self.api_url = api_url
        self.auth = (username, password)
        self.submitter_id = submitter_id

    async def check_eligibility(
        self, subscriber_id: str, subscriber_dob: date,
        subscriber_name: str, provider_npi: str,
        payer_id: str, service_date: date,
    ) -> dict:
        payload = {
            "submitter_id": self.submitter_id,
            "provider": {"npi": provider_npi},
            "subscriber": {
                "member_id": subscriber_id,
                "date_of_birth": subscriber_dob.isoformat(),
                "name": subscriber_name,
            },
            "payer": {"payer_id": payer_id},
            "service_date": service_date.isoformat(),
            "service_type_codes": ["35"],  # dental
        }

        async with httpx.AsyncClient(timeout=30) as client:
            resp = await client.post(
                f"{self.api_url}/eligibility/inquiry",
                json=payload,
                auth=self.auth,
            )
            resp.raise_for_status()
            return resp.json()

Parsing the Eligibility Response

Payer responses are complex nested structures. The parser extracts the information that matters — coverage percentages, deductible status, frequency limits, and waiting periods — and organizes it by benefit category.

class EligibilityParser:
    CATEGORY_CODES = {
        "35": BenefitCategory.PREVENTIVE,
        "36": BenefitCategory.BASIC,
        "37": BenefitCategory.MAJOR,
        "38": BenefitCategory.ORTHODONTICS,
        "23": BenefitCategory.DIAGNOSTICS,
    }

    def parse(self, raw: dict) -> EligibilityResult:
        subscriber = raw.get("subscriber", {})
        plan = raw.get("plan", {})
        benefits_raw = raw.get("benefits", [])

        benefits = []
        for b in benefits_raw:
            category = self.CATEGORY_CODES.get(
                b.get("service_type_code")
            )
            if not category:
                continue

            benefits.append(BenefitDetail(
                category=category,
                coverage_percent=self._extract_percent(b),
                waiting_period_months=b.get(
                    "waiting_period_months", 0
                ),
                annual_max_remaining=b.get(
                    "remaining_amount"
                ),
                frequency_limit=self._extract_frequency(b),
                requires_preauth=b.get(
                    "preauthorization_required", False
                ),
            ))

        return EligibilityResult(
            is_eligible=raw.get("active", False),
            subscriber_name=subscriber.get("name", ""),
            plan_name=plan.get("description", "Unknown"),
            group_number=plan.get("group_number", ""),
            effective_date=date.fromisoformat(
                plan.get("effective_date", "2020-01-01")
            ),
            termination_date=self._parse_optional_date(
                plan.get("termination_date")
            ),
            annual_maximum=plan.get("annual_maximum", 0),
            annual_max_remaining=plan.get(
                "annual_max_remaining", 0
            ),
            deductible=plan.get("deductible", 0),
            deductible_met=plan.get("deductible_met", 0),
            benefits=benefits,
            raw_response=raw,
        )

    def _extract_percent(self, benefit: dict) -> int:
        pct = benefit.get("coinsurance_percent")
        if pct is not None:
            return int(pct)
        copay = benefit.get("copay_type", "")
        if copay == "no_charge":
            return 100
        return 0

    def _extract_frequency(self, benefit: dict) -> str:
        freq = benefit.get("frequency")
        if not freq:
            return ""
        return (
            f"{freq.get('count', '')} per "
            f"{freq.get('period', 'year')}"
        )

    def _parse_optional_date(self, val):
        if not val:
            return None
        return date.fromisoformat(val)

Coverage Explanation Generator

Patients struggle to understand insurance jargon. The agent translates coverage details into plain language, specific to the procedures they need.

See AI Voice Agents Handle Real Calls

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

class CoverageExplainer:
    PROCEDURE_CATEGORIES = {
        "D0120": BenefitCategory.PREVENTIVE,   # periodic exam
        "D0274": BenefitCategory.DIAGNOSTICS,   # bitewings
        "D1110": BenefitCategory.PREVENTIVE,    # adult cleaning
        "D2391": BenefitCategory.BASIC,         # resin filling
        "D2740": BenefitCategory.MAJOR,         # porcelain crown
        "D3310": BenefitCategory.ENDODONTICS,   # root canal
        "D7210": BenefitCategory.ORAL_SURGERY,  # extraction
    }

    def explain_coverage(
        self, result: EligibilityResult,
        procedure_codes: list[str],
        fee_schedule: dict[str, float],
    ) -> str:
        lines = []
        lines.append(f"Plan: {result.plan_name}")
        lines.append(
            f"Annual Maximum: ${result.annual_maximum:,.0f} "
            f"(${result.annual_max_remaining:,.0f} remaining)"
        )
        deductible_remaining = (
            result.deductible - result.deductible_met
        )
        lines.append(
            f"Deductible: ${result.deductible:,.0f} "
            f"(${deductible_remaining:,.0f} remaining)"
        )
        lines.append("")

        total_patient = 0.0
        for code in procedure_codes:
            category = self.PROCEDURE_CATEGORIES.get(code)
            fee = fee_schedule.get(code, 0)
            benefit = self._find_benefit(
                result.benefits, category
            )
            if benefit:
                insurance_pays = fee * benefit.coverage_percent / 100
                patient_pays = fee - insurance_pays
                total_patient += patient_pays
                lines.append(
                    f"  {code}: ${fee:,.0f} fee, "
                    f"insurance covers {benefit.coverage_percent}% "
                    f"= ${insurance_pays:,.0f}, "
                    f"you pay ${patient_pays:,.0f}"
                )
            else:
                total_patient += fee
                lines.append(
                    f"  {code}: ${fee:,.0f} "
                    f"(no coverage found)"
                )

        lines.append(f"\nEstimated total out-of-pocket: "
                     f"${total_patient:,.0f}")
        return "\n".join(lines)

    def _find_benefit(self, benefits, category):
        if not category:
            return None
        return next(
            (b for b in benefits if b.category == category),
            None,
        )

Batch Verification for the Daily Schedule

Rather than verifying insurance one patient at a time, the agent processes the entire next-day schedule in a batch, flagging issues early.

class BatchVerifier:
    def __init__(self, db, clearinghouse, parser):
        self.db = db
        self.client = clearinghouse
        self.parser = parser

    async def verify_next_day(self, practice_id: str):
        tomorrow = date.today()
        appointments = await self.db.fetch("""
            SELECT a.id, a.type, p.insurance_member_id,
                   p.insurance_payer_id, p.dob,
                   p.first_name || ' ' || p.last_name AS name,
                   pr.npi
            FROM appointments a
            JOIN patients p ON p.id = a.patient_id
            JOIN providers pr ON pr.id = a.provider_id
            WHERE a.start_time::date = $1
              AND a.insurance_verified = false
              AND p.insurance_member_id IS NOT NULL
        """, tomorrow)

        results = []
        for appt in appointments:
            try:
                raw = await self.client.check_eligibility(
                    subscriber_id=appt["insurance_member_id"],
                    subscriber_dob=appt["dob"],
                    subscriber_name=appt["name"],
                    provider_npi=appt["npi"],
                    payer_id=appt["insurance_payer_id"],
                    service_date=tomorrow,
                )
                parsed = self.parser.parse(raw)
                await self.db.execute("""
                    UPDATE appointments
                    SET insurance_verified = true,
                        insurance_result = $2
                    WHERE id = $1
                """, appt["id"], parsed.is_eligible)
                results.append((appt["id"], parsed))
            except Exception as e:
                results.append((appt["id"], str(e)))
        return results

FAQ

How accurate is automated insurance verification compared to calling the insurance company?

Automated verification through clearinghouses uses the same X12 270/271 EDI transactions that insurance companies process when their own representatives look up information. The data is pulled directly from the payer's system, so it is typically more accurate than verbal communication over the phone. The main limitation is that some plans have carve-out provisions that do not appear in the electronic response.

What happens when a patient's insurance information has changed since their last visit?

The agent runs verification against whatever insurance information is on file. If the verification comes back as "not eligible," the agent automatically notifies the front desk and sends the patient a message asking them to confirm or update their insurance details. The intake form flow can be triggered again for just the insurance section.

Can the agent handle patients with dual coverage or secondary insurance?

Yes. When a patient has two insurance plans, the agent runs verification against both payers and applies coordination of benefits rules. The primary plan is verified first, and the estimated patient responsibility from the primary becomes the claim amount submitted to the secondary. The coverage explainer shows both plans side by side.


#InsuranceVerification #DentalAI #BenefitsChecking #HealthcareAutomation #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.