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 |
HI01–HI12 |
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.
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
Step 1 — Model the CLM segment and related 2300 data with a typed dataclass
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.
CLM05is a composite delimited by the component separator (:by default, but read it from theISAenvelope). 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, notfloat; a penny of floating-point drift trips the reconciliation and, if suppressed, surfaces later as an 835 balance mismatch. - Mishandling the frequency code.
1is an original claim,7a replacement, and8a void/cancel. A7or8without the original payer claim number in aREFsegment 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.
Related
- Parent guide: X12 837P Segment Architecture Guide — the full loop hierarchy the 2300
CLMsegment sits inside. - Parsing ISA & GS Segments with Python — resolving the delimiters and version the
CLMcomposite parser depends on. - Map ICD-10-CM to CPT Using Python Dictionaries — validating the
HIdiagnosis codes carried alongsideCLMin the 2300 loop.
For the Professional implementation guide element definitions see the Washington Publishing Company X12 reference; mask claim identifiers in logs per HHS HIPAA Security Guidance.