Schema Care

X12 835 Parsing: Segments, Loops and Sample Code

By Schema CarePublished Updated

X12 835 parsing takes four steps. Read the delimiters from the ISA envelope, split the file into segments, walk the loops so each CLP claim and SVC service line keeps its CAS adjustments, then check that the lines, claims and payment total balance. A parser that skips the balance check will post wrong numbers without noticing.

Where does the 835 envelope define its delimiters?

An 835 file defines its own delimiters in its first segment, the ISA interchange header. The character right after "ISA" is the element separator, ISA11 holds the repetition separator, ISA16 holds the component separator, and the character after ISA16 ends the segment. Read them from the file every time instead of assuming them.

The envelope has three layers. The Medicare contractor CGS's 835 companion guide, written for version 005010X221A1, describes the interchange header and trailer (ISA and IEA), the functional group header and trailer (GS and GE), and the transaction set header and trailer (ST and SE). Each 835 transaction sits inside an ST and SE pair, inside a GS and GE group, inside one ISA and IEA interchange.

Delimiters really do vary by sender. The CGS guide lists its outbound delimiters as * for data elements, ^ for repetitions, > for components and ~ to end segments, and says the characters will not be used inside data elements. It also tells trading partners to contact CGS for the delimiters to expect, and lists the ISA11 repetition separator as defined by the submitter. The fictional file in this post uses : as its component separator, so a parser that hard-codes either : or > would misread every procedure code from the other sender.

Here is the top of the example file used in this post. It is fictional, and every identifier in it is an example.

ISA*00*          *00*          *ZZ*EXAMPLEPAYER   *ZZ*EXAMPLECLINIC  *260920*1200*^*00501*000000101*0*T*:~
GS*HP*EXAMPLEPAYER*EXAMPLECLINIC*20260920*1200*101*X*005010X221A1~
ST*835*0001~

In that header, the fourth character is *, ISA11 is ^, ISA16 is : and the next character, ~, ends the segment. GS08 carries 005010X221A1, the implementation guide version the CGS guide says Medicare sends. If a file has a line break after each terminator, as the examples in this post do for readability, strip whitespace around each segment before splitting it into elements.

The 835 loop and segment map

An 835 transaction has a header, a detail area and a summary. The header carries the payment (BPR), its trace number (TRN), and the payer (loop 1000A) and payee (loop 1000B). The detail area repeats loop 2100 for each claim, starting with CLP, and nests loop 2110 for each service line, starting with SVC. The summary holds provider adjustments (PLB).

X12 has no explicit loop markers in the file. A loop starts when its first segment appears and ends when another loop's first segment does. That is why the order matters: a CAS segment means a claim adjustment if it follows CLP, and a line adjustment if it follows SVC.

The map below follows the loop tables in the CGS guide and the Illinois Medicaid 835 companion guide, both written for 5010. Several element meanings (BPR16, CLP01, CLP05, SVC05, AMT with B6 and LQ02) come from an archived 2001 CMS program memorandum with an 835 crosswalk, written for the older 4010 version. Treat those as a starting point and confirm them against the 5010 implementation guide you license from X12.

Area or loopSegmentWhat it carries
HeaderSTStart of the transaction set
HeaderBPRPayment information; BPR02 is the payment amount, BPR16 the check or EFT date
HeaderTRNReassociation trace; TRN02 is the check or EFT trace number
1000A PayerN1, N3, N4, PERPayer name, address and contacts
1000B PayeeN1, N3, N4Payee name and address
2000 Header numberLXGroups the claims that follow
2100 ClaimCLPClaim payment: CLP01 patient control number, CLP03 billed, CLP04 paid, CLP05 patient responsibility, CLP07 payer claim control number
2100 ClaimNM1, REF, CASPatient name, other identifiers, claim-level adjustments
2110 Service lineSVCLine payment: SVC01 procedure, SVC02 billed, SVC03 paid, SVC05 units
2110 Service lineDTM, CAS, AMT, LQService date, line adjustments, allowed amount (AMT with B6), remark codes
SummaryPLBProvider-level adjustments not tied to one claim
TrailerSEEnd of the transaction set

Payers also leave segments out. The CGS guide marks the Loop 2100 insured name and rendering provider segments as not used by Medicare, and limits Medicare to its own subset of codes in several elements. A parser should treat every segment in the map as optional unless the implementation guide requires it, and test against each payer's companion guide rather than one sample file.

Parsing 835 CLP, SVC and CAS segments into records

A practical 835 parser turns each SVC service line into one row that carries its claim's identifiers and a list of adjustments. Each CAS segment has a group code in CAS01, then repeating reason code and amount pairs, so one CAS segment can hold several adjustments under the same group.

The CAS layout is the part that trips people up. The CGS guide shows CAS01 as the claim adjustment group code, limited for Medicare to CO, OA and PR. X12's interpretation on 835 adjustments, cited in the balancing section below, lists the adjustment amounts as CAS03, CAS06, CAS09, CAS12, CAS15 and CAS18, and each reason code sits in the element just before its amount (CAS02, CAS05 and so on). Every third element starts a new adjustment, and the element between an amount and the next reason is left empty when unused.

Here is the full example file. The patient, payer, clinic and every amount are fictional.

ISA*00*          *00*          *ZZ*EXAMPLEPAYER   *ZZ*EXAMPLECLINIC  *260920*1200*^*00501*000000101*0*T*:~
GS*HP*EXAMPLEPAYER*EXAMPLECLINIC*20260920*1200*101*X*005010X221A1~
ST*835*0001~
BPR*I*96.00*C*CHK************20260920~
TRN*1*EXCHK00042*1EXAMPLE01~
N1*PR*EXAMPLE HEALTH PLAN~
N1*PE*EXAMPLE CLINIC~
LX*1~
CLP*EX-PCN-1001*1*250.00*96.00*64.00*MB*EX-ICN-778~
NM1*QC*1*EXAMPLE*JORDAN****MI*EXM000111~
SVC*HC:99213*250.00*96.00**1~
DTM*472*20260904~
CAS*CO*45*90.00~
CAS*PR*1*40.00**2*24.00~
AMT*B6*160.00~
CLP*EX-PCN-0950*22*-120.00*-80.00**MB*EX-ICN-701~
NM1*QC*1*EXAMPLE*JORDAN****MI*EXM000111~
CAS*CO*45*-40.00~
PLB*EXPROV01*20261231*WO:EX-ICN-701*-80.00~
SE*18*0001~
GE*1*101~
IEA*1*000000101~

And here is an example parser in Python, written for the fictional file above. It reads the delimiters from the ISA segment, keeps one set of totals per ST transaction, and attaches each CAS to the service line or claim it follows.

from decimal import Decimal

def read_delimiters(raw):
    if not raw.startswith("ISA"):
        raise ValueError("file does not start with an ISA segment")
    element = raw[3]
    parts = raw.split(element, 16)  # "ISA", ISA01..ISA15, then ISA16 and the rest
    return element, parts[11], parts[16][0], parts[16][1]

def money(text):
    return Decimal(text) if text else Decimal("0")

def cas_adjustments(f):
    group, found = f[1], []
    for i in range(2, len(f), 3):  # reason at 2, 5, 8 ... amount right after it
        if f[i]:
            found.append((group, f[i], money(f[i + 1] if i + 1 < len(f) else "")))
    return found

def parse_835(raw):
    element, repetition, component, terminator = read_delimiters(raw)
    transactions, tx, claim, line = [], None, None, None
    for chunk in raw.split(terminator):
        f = chunk.strip().split(element)
        tag = f[0]
        if tag == "ST":
            tx = {"bpr": Decimal("0"), "clp_paid": Decimal("0"), "plb": Decimal("0"),
                  "claims": [], "lines": []}
            transactions.append(tx)
        elif tag == "BPR":
            tx["bpr"] = money(f[2])
        elif tag == "CLP":
            claim = {"id": f[1], "status": f[2], "billed": money(f[3]),
                     "paid": money(f[4]), "cas": []}
            tx["claims"].append(claim)
            tx["clp_paid"] += claim["paid"]
            line = None
        elif tag == "SVC":
            code = f[1].split(component)
            line = {"claim": claim["id"], "procedure": code[-1] if len(code) == 1 else code[1],
                    "billed": money(f[2]), "paid": money(f[3]), "cas": []}
            tx["lines"].append(line)
        elif tag == "CAS":
            (line if line is not None else claim)["cas"].extend(cas_adjustments(f))
        elif tag == "PLB":
            for i in range(4, len(f), 2):  # PLB04, PLB06 ... are amounts
                tx["plb"] += money(f[i])
    return transactions

with open("example.835") as handle:
    for tx in parse_835(handle.read()):
        for r in tx["lines"]:
            adjustments = "; ".join(f"{g}-{c} {a}" for g, c, a in r["cas"])
            print(r["claim"], r["procedure"], r["billed"], r["paid"], adjustments)
        expected = tx["clp_paid"] - tx["plb"]
        print("BPR02", tx["bpr"], "expected", expected,
              "OK" if expected == tx["bpr"] else "MISMATCH")

Run against the example file, it prints one row and one check:

EX-PCN-1001 99213 250.00 96.00 CO-45 90.00; PR-1 40.00; PR-2 24.00
BPR02 96.00 expected 96.00 OK

The reversed claim EX-PCN-0950 has no SVC line, so it produces no row, but it stays in the claim list with its claim-level CAS. A production parser would also keep CLP05, CLP07, the DTM service date and the AMT allowed amount, which the example leaves out to stay short.

The codes in the row need their own lookup. The X12 claim adjustment reason codes list gives CARC 45 as a charge that exceeds the fee schedule or contracted arrangement, 1 as a deductible amount and 2 as a coinsurance amount, and names the group codes CO, PR, OA and PI. What each pairing means for the patient balance is covered in the guide to claim adjustment reason codes on an EOB. How CARC, RARC and procedure codes are normalized alongside other code systems is covered in which code systems appear in extracted medical data.

How should an 835 parser balance totals?

An 835 parser should balance at three levels. Each service line's billed amount minus its adjustments should equal its paid amount. Each claim's billed amount minus all its adjustments should equal its paid amount. The sum of claim payments minus provider-level adjustments should equal the payment amount in BPR02.

The service-line rule is stated directly by X12. Its interpretation on 835 claim payment adjustments says the submitted service billed amount (SVC02) minus all service adjustment amounts, from CAS03 through CAS18, should equal the service paid amount (SVC03). In the example, $250.00 minus $90.00, $40.00 and $24.00 is $96.00, which matches SVC03.

The claim and transaction rules here rest on an older source: the archived 2001 CMS memorandum, written for 4010 and for Medicare's paper remittance, which says that, as with an 835, remittance amounts must balance at the transaction, claim and service line levels. Its claim rule subtracts adjustments from the submitted claim charge to reach the claim payment. Its transaction rule says the sum of claim payments minus the total provider adjustments equals the check amount. In the example, the claims pay $96.00 and minus $80.00, which sum to $16.00. The PLB amount is minus $80.00, so $16.00 minus minus $80.00 gives $96.00, matching BPR02.

The sign on PLB is the usual source of bugs. Subtracting PLB amounts means a positive PLB reduces the check, such as a recovery of an old overpayment, and a negative PLB increases it. Get the sign wrong and every file with provider adjustments fails, or worse, a broken file passes.

What to do when a check fails:

  • Stop posting that transaction. A mismatch means the parser misread something or the file is incomplete. Do not post part of it.
  • Report the level. Name the line, claim or transaction that failed, so a person can find it quickly.
  • Use exact decimals. Floating-point sums of cents drift, which is why the example uses Python's Decimal.

Turning the parsed rows into a posting workflow, with matching and exception handling, is covered in ERA 835 processing. If a billing team only needs to see the data, the guide to converting an 835 file to a spreadsheet shows how without code.

835 parsing edge cases: split files, reversals and PLB adjustments

Three cases break naive 835 parsers. One file can hold several payments, each its own ST transaction with its own BPR, so totals must reset per transaction. A reversal arrives as a claim with negative amounts, often with a corrected claim beside it. And PLB adjustments change the payment without belonging to any claim.

Split and combined files come first. The CGS and Illinois guides both place BPR in the transaction header, so the payment belongs to the ST and SE pair, not to the file. Do not assume a file holds exactly one ST; loop over every ST and SE pair the file contains. A parser that keeps one running total across a whole file with two payments will fail its balance check. The example parser starts fresh totals on every ST for that reason, and the TRN trace number, not the file name, is the key to match each payment to a deposit.

Reversals come next. The Illinois Medicaid guide shows a voided claim reported with a CLP status code of 22 and negative billed and paid amounts, and notes that, according to the 835 implementation guide, a reversal does not contain any patient responsibility amount. When a claim is reprocessed, the same guide shows the reversal followed by a new CLP for the corrected claim. A parser that ignores status 22 will count the old payment twice.

PLB adjustments are last. The Illinois guide uses a PLB with reason code WO, for overpayment recovery, and a negative amount to offset a reversal whose money is not being recovered in that file, so the payment total does not drop. When the recovery happens, in the same file or a later one, another PLB carries a positive WO amount. The CGS guide lists the PLB03-1 reason codes Medicare uses, including WO, 72, L6 and others. PLB03 is a composite: the reason code, then an identifier such as the original claim number, split by the component separator.

Other cases worth a test file:

  • Zero-payment transactions. A BPR02 of zero is valid. The Illinois guide sets BPR04 to NON in that case.
  • Claim-level CAS. Adjustments can sit at the claim level as well as the line level, so include both when balancing a claim.
  • Unknown segments. Skip segments your parser does not use, but never skip one that changes amounts.

Paper EOBs are the other half of remittance data. Schema Care reads EOB service lines, allowed amounts, plan payments and remark codes into fields and sends low-confidence ones to a reviewer, as the threshold demo shows on a fictional EOB. Schema Care offers no API, SDK, webhooks, FHIR output, 835 output or 837 output. Teams that need one from paper usually start with paper EOB to 835 conversion, and the claim side of X12 is covered in X12 837 generation.

All posts in APIs, FHIR and Integration