Why was this 837 rejected? Tracing 999 and 277CA responses back to the claim
A claim batch went out three months ago. Some claims were paid, some were denied, and a few simply never came back. No remittance, no denial, nothing. Before anyone calls the payer, there is a cheaper question: did those claims ever get past the front door?
Two files answer that, and neither one looks like a remittance:
- the 999, which says whether a whole transaction set (one ST/SE block of claims) was accepted, and
- the 277CA, which reports claim by claim whether each one was accepted or rejected before adjudication.
A claim rejected at either stage never produces an 835. So "no remit" and "rejected" look the same in your aging report unless you connect the files. This post shows how to do that by hand, then with a free CLI I wrote. Everything below uses synthetic files.
What a rejection looks like on the wire
A 999 answers one 837 transaction set at a time. Each set gets an AK2 segment naming it (the transaction type and the ST control number from your file), then an IK5 segment with the verdict. Optional IK3/IK4 segments point at the segment and element that failed. This is a synthetic 999 where I rejected the second set:
AK1*HC*1*005010X222A1~ AK2*837*0001*005010X222A1~ IK5*A~ AK2*837*0002*005010X222A1~ IK3*CLM*14*2300*8~ IK4*2*1028*7*NOTAVALIDID~ IK5*R*5~ AK2*837*0003*005010X222A1~ IK5*A~ ... AK9*P*5*5*4~ IK5*A is accepted, IK5*R is rejected, and AK9*P says the group was only partly accepted: 5 sets received, 4 accepted. The whole second set, all its claims, was bounced.
A 277CA works at the claim level. Each claim gets a TRN segment carrying your patient control number (the CLM01 you sent), followed by an STC status segment:
TRN*2*PCN00000032~ STC*A3:21*20260102*U*224.56~ The first element of STC starts with a category code (here A3). Which categories mean "rejected" depends on your payer's companion guide, so check yours. x12diff treats A3, A4, A6, A7 and A8 as rejections by default and A0, A1 and A2 as acceptances, and both lists are configurable in the profile.
The by-hand method
- Open the 999. For every
AK2with anIK5*R, note the ST control number. - In your 837 file, find the set with that ST02. Every
CLMbetween thatSTand itsSEwas in the rejected set. - Open the 277CA. Collect every
TRN02whoseSTCcategory is a rejection. Those are claim IDs, which are CLM01 from the 837. - Remove all of those from your "unpaid" list. What is left really is waiting on the payer.
That works for one batch. It stops working when you have a month of files, several sets per file, and resubmissions that reused control numbers. Which brings the first trap.
Trap: control numbers are not unique
ST02 and GS06 are only unique inside their interchange. If you start numbering at 1 in every file, a 999 for ST02 = 0002 could belong to a dozen different 837s. Matching safely needs more than the number: the group control number, the version string, the date, and who sent it. Being sure which file a 999 answers is the hard part, not reading the IK5.
Also: a 277CA acceptance can arrive after an earlier rejection (you fixed and resent the claim). The newest status should win. If you just take the first STC you see, you flag claims that are fine.
Doing it with x12diff
x12diff is a free command-line tool for 834, 835, 837 and the acknowledgement files. Disclosure: it is mine, it is MIT licensed, and it runs entirely offline. There is a paid desktop version planned, but everything in this post is the free CLI.
Give recon the 837, your 835s, the 999 and the 277CA in any order. It recognizes each file by its contents:
$ x12diff recon claims.837 remits_1.835 remits_2.835 ack.999 status.277 --asof 20260601 --csv recon.csv --show 0 Files: 5 837 claims: 200 in 5 sets 835 claim entries: 185 on 3 checks Acknowledgement sets: 1 (1 837 sets rejected) 277CA claim statuses: 26 PAID: 99 PARTIAL: 37 DENIED: 14 PATIENT_RESP: 9 REVERSED: 8 ADJUSTED: 3 UNMATCHED: 18 REJECTED: 12 ORPHAN: 1 Charged (837): 50866.73 Paid (matched 835s, net): 29030.08 UNMATCHED aging (days since claim date): 0-30: 0 31-60: 3 61-90: 6 91+: 9 future: 0 unknown: 0 Warnings: 0 CSV written: recon.csv Run the same claims and remits with no 999 and no 277CA and the picture is different. Three of the claims look like this:
UNMATCHED,PCN00000032,224.56,,,,20260202,119,, UNMATCHED,PCN00000048,78.75,,,,20260218,103,, UNMATCHED,PCN00000072,263.44,,,,20260314,79,, Three claims, 119, 103 and 79 days old, all "unmatched, waiting". With the acknowledgements added, the same three become:
REJECTED,PCN00000032,224.56,,,,20260202,119,277CA category A3, REJECTED,PCN00000048,78.75,,,,20260218,103,999 set rejected, REJECTED,PCN00000072,263.44,,,,20260314,79,999 set rejected, The columns are status, claim_id, claim_charge, remit_charge, paid, patient_resp, claim_date, age_days, reject_source, detail. The reject_source column tells you which door turned the claim away. Those three are not slow payer follow-ups. They need a corrected resubmission.
Two behaviors worth knowing, both from the tool's documented rules:
- A claim is only marked REJECTED when no remit exists for it. If a 999 rejected a set but a claim in it was paid anyway (in my synthetic data, 38 of the 40 claims in the rejected set were), it keeps its payment status and the
detailcolumn says999 rejected the set but a remit exists, so you can see the conflict instead of having it silently overwritten. - If it cannot be sure which 837 a 999 belongs to, it marks nothing and prints a warning explaining why, rather than guessing. When two sets share every identifier and date it cannot rule out a wrong match, so keep one month per folder if in doubt.
The same table in PowerShell, no Excel needed (the CSV starts with a byte-order mark, which Import-Csv handles):
PS> Import-Csv recon.csv | Where-Object status -eq REJECTED | Select-Object claim_id,claim_charge,age_days,reject_source claim_id claim_charge age_days reject_source -------- ------------ -------- ------------- PCN00000032 224.56 119 277CA category A3 PCN00000040 318.37 111 277CA category A3 PCN00000048 78.75 103 999 set rejected PCN00000072 263.44 79 999 set rejected What it does not do
It tells you that a claim was rejected, by which file, and when it was dated. It does not explain the rejection in words: x12diff ships no code descriptions, so look the category and reason up in your payer's guide. It also does not read the IK3/IK4 detail into the CSV. For the "which element failed" question you still read the 999 itself.
Checklist before you send the list
- Do the 999 and 277CA actually belong to this month's 837? Check the dates, and keep months in separate folders.
- Did any rejected claim get resubmitted? A later acceptance should clear the rejection.
- Spot-check three REJECTED claims by hand against the raw 277CA or 999.
- Is the CSV stored where PHI is allowed to live?
Source and downloads: https://github.com/datawharf-tools/x12diff. Questions or bug reports (synthetic files only, never real data): hello@datawharftools.com. If you would rather click than type commands, there is a planned Pro desktop version, with details and a waitlist at https://datawharftools.com/x12/