Somebody hands you a folder of 835 files and asks two questions. How much did we get paid, and why was the rest not paid? Then a third one: which of our claims have no payment at all?
An 835 is not a table. One claim spans a CLP segment, several SVC lines, and a handful of CAS adjustment segments hanging off each. Open the file in Excel and you get one long column of segments. This post flattens an 835 into rows you can filter, then matches those payments to the claims you sent. All files here are synthetic.
What is in the file
A trimmed synthetic 835, one check, two claims:
BPR*I*17867.80*C*CHK************20261231~ TRN*1*CHKTESTA000001*1999999999~ N1*PR*TESTPAYER*XV*PAYER001~ N1*PE*TESTPAYEE*XX*9999999999~ CLP*PCN00000001*1*409.57*245.74*40.95*12*PAYCTL000001~ SVC*HC:99213*245.74*147.44**1~ CAS*CO*97*73.73~ CAS*PR*1*24.57~ SVC*HC:99214*163.83*98.30**1~ CAS*CO*97*49.15~ CAS*PR*1*16.38~ CLP*PCN00000003*1*200.13*180.11*0.00*12*PAYCTL000003~ SVC*HC:99213*120.08*108.07**1~ CAS*CO*45*12.01~ Reading it: BPR is the payment (amount, date). CLP starts a claim: your claim ID, a status, the charge, the amount paid and the patient's share. SVC is a service line: procedure, charged, paid. CAS explains the gap between charged and paid as a group code (CO and PR are the two you will see most), a reason code and an amount.
The arithmetic is the useful part. For the first line: charged 245.74, paid 147.44, and the two adjustments 73.73 and 24.57 add up to exactly the difference. If your files do not balance like that, something is wrong with the file or with how you read it.
The flat shape that works
One row per service line. A claim with three lines becomes three rows, and the claim-level fields repeat. Adjustments go in one cell, written GROUP:REASON:AMOUNT and joined with |, so a line with two adjustments still stays on one row. Now Excel can sort it, filter it and pivot it.
Two details trip people up:
- Claim-level adjustments. Some payers put
CASat the claim level (right afterCLP) instead of on the lines. If you only read line-level adjustments, the sums do not tie to the check. Keep both, in separate columns. - The check has its own arithmetic. The payment amount in
BPRshould equal the sum of the claim payments minus any provider-level adjustments (PLB). Test that per check.
Doing it with x12diff
x12diff is a free command-line tool for X12 healthcare files. Disclosure: it is mine, MIT licensed, and offline (no network code). A paid desktop version is planned, but this post only uses the free CLI.
$ x12diff remit remits_1.835 remits_2.835 --csv lines.csv --rollup adjustments.csv Files: 2 835 checks: 3 Claims: 185 Service lines: 370 Charged: 40539.31 Paid: 29097.59 Paid/charged: 71.8% Patient responsibility: 2542.56 Adjustments by group-reason (count, amount): CO-50 14 3548.08 CO-45 250 2786.25 CO-97 74 2564.83 PR-1 92 2542.56 Warnings: 0 CSV written: lines.csv Rollup written: adjustments.csv You can also give it a folder, or a wildcard in the file name such as "C:/remits/*.835". The first rows of lines.csv:
file,check_number,payment_date,payer_id,payee_id,claim_id,claim_status,claim_charge,claim_paid,patient_resp,payer_claim_ctrl,service_qualifier,service_code,service_modifiers,units,line_charge,line_paid,line_adjustments,claim_adjustments remits_1.835,CHKTESTA000001,20261231,PAYER001,9999999999,PCN00000001,1,409.57,245.74,40.95,PAYCTL000001,HC,99213,,1,245.74,147.44,CO:97:73.73|PR:1:24.57, remits_1.835,CHKTESTA000001,20261231,PAYER001,9999999999,PCN00000001,1,409.57,245.74,40.95,PAYCTL000001,HC,99214,,1,163.83,98.30,CO:97:49.15|PR:1:16.38, remits_1.835,CHKTESTA000001,20261231,PAYER001,9999999999,PCN00000003,1,200.13,180.11,0.00,PAYCTL000003,HC,99213,,1,120.08,108.07,CO:45:12.01, A fully denied claim carries its adjustment at the claim level, so it appears in the last column:
remits_1.835,CHKTESTA000001,20261231,PAYER001,9999999999,PCN00000007,4,441.39,0.00,0.00,PAYCTL000007,HC,99213,,1,264.83,0.00,,CO:50:441.39 To open it in Excel, use Data, From Text/CSV rather than double-clicking, and filter on claim_status. The file starts with a UTF-8 byte-order mark so accented characters show correctly. The rollup file (group,reason,label,count,amount) is ready to chart. It shows only the codes: x12diff ships no description text for them. If you want readable names, add adjlabel.CO-45 = your text lines to a profile and pass --profile.
The balance checks
I changed one claim's paid amount in a copy of the file, from 180.11 to 190.11, and ran it again:
Warnings: 3 WARNING: check CHKTESTA000001 in remits_1.835, ST02 0001, claim PCN00000003: charge 200.13 - paid 190.11 - adjustments 20.02 leaves -10.00 unexplained WARNING: check CHKTESTA000001 in remits_1.835, ST02 0001, claim PCN00000003: service-line paid amounts add up to 180.11 but CLP04 is 190.11 WARNING: check CHKTESTA000001 in remits_1.835, ST02 0001: BPR02 17867.80 differs from sum of CLP04 17878.80 minus PLB adjustments 1.00 = 17877.80 One altered number is caught three ways: the claim does not balance, its lines do not add up to the claim, and the check total no longer matches. That is the same test you would do by hand, applied to every claim in the folder.
From payments to "what is missing": the reconciliation
Flattening answers "what was paid". The next question needs your 837s. Pass them along with the remits:
$ x12diff recon claims.837 remits_1.835 remits_2.835 --asof 20260601 --csv recon.csv --show 0 Files: 3 837 claims: 200 in 5 sets 835 claim entries: 185 on 3 checks Acknowledgement sets: 0 (0 837 sets rejected) 277CA claim statuses: 0 PAID: 99 PARTIAL: 37 DENIED: 14 PATIENT_RESP: 9 REVERSED: 8 ADJUSTED: 3 UNMATCHED: 30 REJECTED: 0 ORPHAN: 1 Charged (837): 50866.73 Paid (matched 835s, net): 29030.08 UNMATCHED aging (days since claim date): 0-30: 0 31-60: 5 61-90: 9 91+: 16 future: 0 unknown: 0 Warnings: 0 Thirty claims have no payment, and 16 of them are more than 90 days old. Some of those may never have been accepted in the first place: add the 999 and 277CA files to the same command and the tool separates REJECTED from truly waiting. The companion post on tracing rejections shows how.
Claims match remits on CLM01 = CLP01. Here is how the statuses are assigned, per the tool's documented rules:
- PAID: money came in and the only adjustments are patient responsibility plus routine write-offs. The default write-off list is CO-45 and CO-253, and you can extend it in a profile (
recon.contractual = CO-45, CO-253, CO-42). - PARTIAL: paid, but some other adjustment reduced the payment. If PARTIAL looks too high for a payer, the adjustment columns in the
remitoutput show which reason codes to add to the list. - DENIED: nothing paid, and either the status says denied or something like CO-50 took the whole amount.
- PATIENT_RESP: nothing paid because the whole amount went to the patient (a deductible, say). That claim was processed, not denied.
- REVERSED / ADJUSTED: amounts are summed across all remit entries for a claim, so an original payment, a reversal and a repayment net out.
- UNMATCHED: no remit yet, aged by days from the claim date to
--asof. - ORPHAN: a payment with no matching 837 claim.
Examples from the output file:
PARTIAL,PCN00000001,409.57,409.57,245.74,40.95,20260102,,, DENIED,PCN00000007,441.39,441.39,0.00,0.00,20260108,,, REVERSED,PCN00000015,302.25,302.25,0.00,0.00,20260116,,, UNMATCHED,PCN00000002,419.23,,,,20260103,149,, ORPHAN,ORPH000001,,75.01,67.51,0.00,,,,"no matching 837 claim; check CHKTESTA000002 in remits_1.835, ST02 0002" PCN00000015 shows why the netting matters. It was paid 272.02 on one check and then reversed on another with a negative CLP04:
remits_1.835:CLP*PCN00000015*1*302.25*272.02*0.00*12*PAYCTL000015~ remits_2.835:CLP*PCN00000015*22*-302.25*-272.02*0.00*12*PAYCTL000015~ Counting the first line as "paid" would overstate your cash. The second line cancels it.
Group the file by status in PowerShell for a quick view:
PS> Import-Csv recon.csv | Group-Object status | Select-Object Name,Count Where it can still be wrong
- Duplicate claim IDs. If your 837 data holds the same CLM01 more than once (resubmissions), the tool warns and matches by charge, then payer control number. Two duplicates with the same charge can still be matched wrongly, and the warning says so.
- Your payer's habits. Which adjustment codes count as routine is a policy choice. The default list is only a starting point.
- Real money. A PAID status is a classification of the file, not an audit. Spot-check a few against the raw 835.
Checklist before you send the numbers
- Do the claims and remits cover the same period?
- Zero warnings, or do you understand each one?
- Did you spot-check three PAID, three PARTIAL and three UNMATCHED claims by hand?
- Is the CSV stored where PHI is allowed to live?
Source and downloads: https://github.com/datawharf-tools/x12diff. Bug reports (synthetic files only, never real data): hello@datawharftools.com. A desktop version with Excel reports is planned; details and a waitlist are at https://datawharftools.com/x12/

