Mapping the 837P 2300 CLM Segment in Python: Composite Parsing, Charge Reconciliation, and Frequency Codes

Problem: the 2300 loop CLM segment is the spine of every 837P professional claim, yet its most error-prone element — CLM05, a colon-delimited composite carrying the place-of-service facility type and the claim frequency code — is routinely mis-split, and CLM02 (total claim charge) is submitted without reconciling it against the sum of the service-line SV102 charges, producing a 999 IK3 reject or a downstream balance mismatch. This guide shows the exact Python to parse the 2300 CLM segment into a typed model, split the CLM05 composite safely, validate the frequency code, and map the related HI, DTP, and REF segments into an internal claim. It follows the envelope work in Parsing ISA & GS Segments with Python.

Prerequisites

Spec Reference: 2300 CLM and Neighboring Segments

The CLM segment anchors loop 2300; the surrounding segments qualify the claim. Only the elements a scrubber must read are listed.

Element Name Requirement Valid values / notes
CLM01 Patient control number Required Provider-assigned claim ID; echoed back on 277CA/835 for reconciliation
CLM02 Total claim charge amount Required Must equal the sum of every service-line SV102 in loop 2400
CLM05-1 Place of service (facility type) code Required POS code, e.g. 11 office, 22 on-campus outpatient, 21 inpatient
CLM05-3 Claim frequency type code Required 1 original, 7 replacement, 8 void/cancel
CLM07 Provider accept assignment Required A assigned, B assigned (clinical lab), C not assigned
CLM08 Benefits assignment indicator Required Y / N / W
HI01HI12 Health care diagnosis codes Required Composite ABK/ABF qualifier + ICD-10-CM code; ABK is the principal
DTP*472 Service date Situational DTP03 date or date range for the claim/line
REF*D9 Claim identifier Situational Clearinghouse trace / prior claim reference

CLM05 is a composite: CLM05-1:CLM05-2:CLM05-3. Position 2 is a fixed B (facility code qualifier) in the 5010 Professional guide, position 1 is the two-digit place-of-service code, and position 3 is the frequency code. Splitting on the wrong delimiter, or assuming three elements always arrive, is the single most common CLM-mapping bug.

CLM segment and CLM05 composite anatomyThe CLM segment is shown as a row of elements separated by asterisks: CLM01 patient control number, CLM02 total charge, then CLM05 which is a composite. CLM05 expands into three colon-separated sub-elements: position one the place of service code, position two the fixed facility code qualifier B, and position three the claim frequency code where one is original, seven is replacement, and eight is void. CLM07 provider accept assignment follows. CLM01 patient ctrl no. CLM02 total charge CLM05 composite CLM07 assignment CLM05-1 place of service e.g. 11 office CLM05-2 qualifier B fixed CLM05-3 frequency code 1 / 7 / 8
The CLM05 composite carries three colon-separated sub-elements; positions 1 and 3 (place of service and frequency) drive scrubbing, while position 2 is the fixed B qualifier.

Step-by-Step Implementation

Represent the composite as its own structure so the frequency and place-of-service codes are first-class fields, not string slices reparsed at every use. Keep CLM02 as Decimal. Log only CLM01 and control numbers — never patient names or member IDs — per HIPAA Security Rule §164.312(b).

from __future__ import annotations

import logging
from dataclasses import dataclass, field
from decimal import Decimal

logging.basicConfig(format="%(asctime)s | %(levelname)s | %(name)s | %(message)s")
logger = logging.getLogger("x12.837p.clm")


@dataclass(frozen=True, slots=True)
class Clm05:
    place_of_service: str   # CLM05-1
    facility_qualifier: str  # CLM05-2, fixed "B" in 5010 Professional
    frequency_code: str     # CLM05-3: 1 original, 7 replacement, 8 void


@dataclass(slots=True)
class Claim2300:
    patient_control_number: str          # CLM01
    total_charge: Decimal                # CLM02
    claim_code: Clm05                    # CLM05 composite
    accept_assignment: str              # CLM07
    benefits_assignment: str            # CLM08
    diagnoses: list[str] = field(default_factory=list)   # HI ICD-10-CM codes
    service_date: str | None = None      # DTP*472 DTP03
    claim_reference: str | None = None   # REF*D9

Step 2 — Split the CLM05 composite on the component separator

Use the composite (component) separator resolved from the ISA envelope, not a hard-coded :. Guard against a short composite: some senders omit the qualifier, so validate the element count before indexing.

def parse_clm05(raw: str, comp_sep: str = ":") -> Clm05:
    """Split the CLM05 composite into its three sub-elements.

    Expects '<POS><sep>B<sep><freq>'. Raises on a malformed composite so a
    truncated CLM05 fails loudly rather than silently mapping POS as frequency.
    """
    parts = raw.split(comp_sep)
    if len(parts) < 3:
        raise ValueError(f"CLM05 composite has {len(parts)} sub-elements, expected 3")
    return Clm05(
        place_of_service=parts[0].strip(),
        facility_qualifier=parts[1].strip(),
        frequency_code=parts[2].strip(),
    )

Step 3 — Validate place of service and frequency code

Constrain both coded values to their allowed sets. A frequency code of 7 (replacement) or 8 (void) must carry a prior claim reference in REF*F8/REF*D9; flag it here so a replacement without a payer claim number is not sent as a fresh original.

VALID_POS: frozenset[str] = frozenset({"11", "12", "19", "21", "22", "23", "31", "49", "50", "81"})
FREQUENCY: dict[str, str] = {"1": "original", "7": "replacement", "8": "void"}


def validate_claim_code(code: Clm05, claim_reference: str | None) -> list[str]:
    errors: list[str] = []
    if code.place_of_service not in VALID_POS:
        errors.append(f"CLM05-1 place of service '{code.place_of_service}' not recognized")
    if code.frequency_code not in FREQUENCY:
        errors.append(f"CLM05-3 frequency '{code.frequency_code}' invalid (expect 1/7/8)")
    elif code.frequency_code in {"7", "8"} and not claim_reference:
        errors.append(f"frequency {code.frequency_code} ({FREQUENCY[code.frequency_code]}) "
                      "requires a prior claim reference (REF)")
    return errors

Step 4 — Reconcile CLM02 against the service-line charges and assemble the claim

CLM02 must equal the sum of the 2400 loop SV102 amounts to the cent. Reconcile with Decimal arithmetic and fail on any drift before the claim leaves the scrubber.

def build_claim(
    clm_elements: list[str],
    hi_codes: list[str],
    sv102_amounts: list[Decimal],
    service_date: str | None,
    claim_reference: str | None,
    comp_sep: str = ":",
) -> Claim2300:
    clm05 = parse_clm05(clm_elements[4], comp_sep)  # 0-based: CLM05 is index 4
    total = Decimal(clm_elements[1])                 # CLM02

    line_sum = sum(sv102_amounts, Decimal("0.00"))
    if total != line_sum:
        raise ValueError(f"CLM02 {total} != sum(SV102) {line_sum}")

    errors = validate_claim_code(clm05, claim_reference)
    if errors:
        raise ValueError("; ".join(errors))

    claim = Claim2300(
        patient_control_number=clm_elements[0],
        total_charge=total,
        claim_code=clm05,
        accept_assignment=clm_elements[6],   # CLM07
        benefits_assignment=clm_elements[7],  # CLM08
        diagnoses=hi_codes,
        service_date=service_date,
        claim_reference=claim_reference,
    )
    logger.info("clm01=%s | pos=%s | freq=%s | charge=%s | dx=%d",
                claim.patient_control_number, clm05.place_of_service,
                FREQUENCY[clm05.frequency_code], total, len(hi_codes))
    return claim

Verification

Exercise both a clean original claim and the balance/frequency failure branches with de-identified values.

from decimal import Decimal

# CLM*PCN12345*250.00***11:B:1*Y*A*Y~  (elements, 0-based)
clm = ["PCN12345", "250.00", "", "", "11:B:1", "Y", "A", "Y"]
claim = build_claim(clm, ["Z0000", "E119"], [Decimal("150.00"), Decimal("100.00")],
                    service_date="20260716", claim_reference=None)
assert claim.claim_code.frequency_code == "1"
assert claim.total_charge == Decimal("250.00")

# Charge mismatch must raise.
try:
    build_claim(clm, ["Z0000"], [Decimal("150.00")], "20260716", None)
    raise AssertionError("expected CLM02 mismatch")
except ValueError as e:
    assert "CLM02" in str(e)

Expected structured log for the clean claim, with no PHI:

2026-07-16 11:20:05 | INFO | x12.837p.clm | clm01=PCN12345 | pos=11 | freq=original | charge=250.00 | dx=2

Common Gotchas

  • Splitting CLM05 on the element separator instead of the composite separator. CLM05 is a composite delimited by the component separator (: by default, but read it from the ISA envelope). Splitting on * gives one blob; hard-coding : breaks any interchange that uses a different component separator.
  • CLM02 not equal to the sum of SV102. The total claim charge must reconcile to the cent against the 2400 service lines. Use Decimal, not float; a penny of floating-point drift trips the reconciliation and, if suppressed, surfaces later as an 835 balance mismatch.
  • Mishandling the frequency code. 1 is an original claim, 7 a replacement, and 8 a void/cancel. A 7 or 8 without the original payer claim number in a REF segment is rejected or, worse, posts as a duplicate original.
  • Assuming CLM05 always has three sub-elements. Truncated composites from some senders drop the qualifier; index defensively and validate the count so the frequency code is never read from the place-of-service position.

For the Professional implementation guide element definitions see the Washington Publishing Company X12 reference; mask claim identifiers in logs per HHS HIPAA Security Guidance.

Up: X12 837P Segment Architecture Guide