Parses X12 835 healthcare remittance files into structured JSON, with CARC-level denial rollups. TypeScript, zero dependencies, runs in the browser.
Running in the browser is the point. An 835 is full of PHI: member names, member IDs, claim numbers, dates of service. Parsing it client-side means it never has to reach a server, which changes what you are under HIPAA. You become a file processor rather than a data custodian.
No install required. Node 22+ strips the types natively.
node --experimental-strip-types examples/demo.tsPayer SYNTHETIC HEALTH PLAN
Check date 20260805
Claims 5 (3 denied)
Billed $ 1350.00
Paid $ 480.00
Denied $ 825.00
Denials by reason code
────────────────────────────────────────────────────────────────────────
CARC Claims Denied Reason
────────────────────────────────────────────────────────────────────────
29 1 $ 400.00 Timely filing limit exceeded
197 1 $ 220.00 Precertification/authorization/notification absent
96 1 $ 180.00 Non-covered charge
45 1 $ 25.00 Charge exceeds fee schedule or contracted/legislated fee
────────────────────────────────────────────────────────────────────────
The sample in examples/ is synthetic. Every name, member ID, claim number and
dollar amount in it is invented.
import { parse835 } from './src/index.ts';
const result = parse835(rawFileContents);
result.totalBilled // 1350
result.totalPaid // 480, taken from the BPR check amount
result.totalDenied // 825
result.payerName // 'SYNTHETIC HEALTH PLAN'
result.checkDate // '20260805'
result.claimCount // 5
result.deniedClaimCount // 3
result.claims // Claim[], each with adjustments and service lines
result.byCARC // CARCSummary[], rolled up denial reasons
result.parseErrors // string[], non-fatal problems worth surfacingIn a browser, read the file with the File API and hand the text straight in:
const text = await file.text();
const result = parse835(text);
// No upload. No fetch. Nothing leaves the page.Delimiters are read from the envelope, not assumed. X12 declares its own element
delimiter at byte 3 and its segment terminator at byte 105 of the ISA segment.
Plenty of parsers hardcode * and ~ and then break on the first payer who uses
something else. This one reads them.
Adjustment groups are distinguished. CO (contractual obligation) and OA
(other adjustment) roll into the denial summary. PR (patient responsibility) does
not, because a copay is not a denial and counting it as one inflates every number a
billing manager cares about.
Claims carry their service lines. SVC segments attach to the CLP they belong
to, with their own adjustments, so you can see which procedure inside a claim got
denied rather than only that the claim did.
Unknown reason codes degrade instead of throwing. A code with no label returns
Reason code X (contact payer for details). Payers add codes; a parser that crashes
on an unrecognized one is useless in production.
Malformed input fails with a message a human can act on, not a stack trace.
src/carc.ts maps Claim Adjustment Reason Codes to plain-language descriptions,
weighted toward the codes that actually appear in behavioral health remittances
rather than the full published set.
import { getCARCLabel } from './src/carc.ts';
getCARCLabel('29'); // 'Timely filing limit exceeded'
getCARCLabel('ZZ99'); // 'Reason code ZZ99 (contact payer for details)'node --experimental-strip-types tests/parse835.test.tsEleven tests covering envelope delimiter detection, claim and total reconciliation, denied-claim identification, CARC rollup correctness, the patient-responsibility exclusion, service line attachment, and graceful handling of unknown codes and malformed input.
This parses 835s. It does not do denial workflow, appeal generation, payer submission, or claim scrubbing.
It was extracted from a larger denial-management tool built for Pennsylvania behavioral health agencies. The parser is the reusable half and it is the half worth sharing.
I spent twelve years inside a HIPAA-regulated behavioral health provider, three of them as Director of IT, processing 835 remittances across Medicaid, Medicare and managed-care payers. I ran the 4010 to 5010 transition. I can read X12 by eye, which is mostly what made writing this tractable.
MIT. See LICENSE.