Where a denial actually starts

A denied claim feels, from a billing team's side, like a payer decision, something that happened on the other end of the submission. Very often, the real cause is much earlier and much smaller: a field that was malformed, missing, or inconsistent before the claim ever left the building, the kind of thing a five-second automated check would have caught.

graph LR
    A[Claim Created] --> B{Validated<br/>before submission?}
    B -->|No check run| C[Submitted As-Is]
    C --> D[Payer Review]
    D -->|Bad procedure code,<br/>missing ID, etc.| E[Denied]
    D -->|Passes| F[Paid]
    B -->|Real check run| G[Fixed Before Submission]
    G --> F

The difference between the top path and the bottom one in this picture is a validation step that either exists or doesn't. Nothing about the actual care delivered changes; only whether a real, catchable data problem gets caught before or after it costs a delay.

Five real claims, checked before submission

claims = pd.DataFrame([
    {"claim_id": "C1001", "patient_id": "P001", "procedure_code": "99213", "provider_npi": "1234567893", "amount": 150.00},
    {"claim_id": "C1002", "patient_id": "P002", "procedure_code": "9921",  "provider_npi": "1234567893", "amount": 220.00},
    {"claim_id": "C1003", "patient_id": "",     "procedure_code": "99214", "provider_npi": "1234567893", "amount": 180.00},
    {"claim_id": "C1004", "patient_id": "P004", "procedure_code": "99215", "provider_npi": "12345",      "amount": 300.00},
    {"claim_id": "C1005", "patient_id": "P005", "procedure_code": "99213", "provider_npi": "1234567893", "amount": -50.00},
])

A believable, ordinary batch, nothing here looks obviously wrong at a glance.

A real, small validation check

def validate_claim(row):
    issues = []
    if not row["patient_id"]:
        issues.append("missing patient_id")
    if not re.fullmatch(r"\d{5}", str(row["procedure_code"])):
        issues.append(f"invalid procedure_code format: {row['procedure_code']!r}")
    if not re.fullmatch(r"\d{10}", str(row["provider_npi"])):
        issues.append(f"invalid provider NPI: {row['provider_npi']!r}")
    if row["amount"] <= 0:
        issues.append(f"non-positive claim amount: {row['amount']}")
    return issues
C1002: ["invalid procedure_code format: '9921' (must be 5 digits)"]
C1003: ['missing patient_id']
C1004: ["invalid provider NPI: '12345' (must be 10 digits)"]
C1005: ['non-positive claim amount: -50.0']

4 of 5 claims would likely be rejected or denied before ever reaching the payer
real dollar amount at risk in this sample: $700.00

Four claims out of five, in this small, deliberately realistic batch, each with a specific, mechanical, catchable problem: a procedure code one digit short, a blank patient ID, a provider number the wrong length, a negative amount that shouldn't be possible. None of these are clinical judgment calls. All four are the kind of thing a validation script checks in milliseconds, and none of them were caught before this batch was built.

Why each flagged claim failed, in this sample
Bad procedure code
1
Missing patient ID
1
Invalid provider NPI
1
Non-positive amount
1

Why "the payer denied it" isn't the real root cause

A denial reason code from a payer tells you what failed. It doesn't tell you when the mistake actually happened, and in a large share of real denials, the mistake happened long before submission, at data entry, in an integration between two systems, in a code lookup table that went stale. Treating every denial as a payer-side event to dispute, rather than a data quality event to prevent, means fixing the same category of mistake over and over, downstream, after it's already cost a delay, instead of catching it once, upstream, before it ever leaves the building.

What actually changes the number

Not a bigger billing team working denials faster. A validation step, structurally identical to the five-line function above, scaled to real claim volume and real field rules, run automatically before every batch submission. The four claims caught here took under a second to check. Caught after a payer denial instead, the same four claims mean a real delay, real staff time spent investigating and resubmitting, and a real, measurable gap between when the care was delivered and when it actually gets paid for.

The takeaway

A denied claim is very often a data quality failure wearing a billing outcome's clothing. The fields that cause it, malformed codes, missing IDs, impossible amounts, are exactly the kind of thing a direct, automated check catches before submission, the same check demonstrated here, at the scale of five claims. At the scale of a real claims volume, that's the difference between catching a mistake in under a second and discovering it weeks later as a revenue gap nobody can immediately explain.