Solving the Real-Time Settlement Reconciliation Problem with Line-Level 835 Parsing

Real-time settlement solves transit. It does not solve identification.

As practices migrate to real-time settlement rails like FedNow, the bottleneck shifts from fund transit to data reconciliation. Under ACH, the remittance advice usually arrived first and the money followed days later — annoying, but the file was sitting there when the deposit landed, so posting was a lookup. Compress settlement to seconds and the order inverts. Money hits the operating account Thursday evening; the 835 shows up in the clearinghouse mailbox Friday morning. For those hours you are holding cash you cannot post, cannot apply to a patient ledger, and cannot report on.

That is not a bank problem. It is a parsing problem, and it gets worse in proportion to how fast your rails are.

This article covers why the gap opens, what an 835 actually contains, one synthetic remittance where the deposit is 145.40 against 230.40 in claim payments, and the three-key matching hierarchy that closes the gap without a human reading segments.

Why faster money makes reconciliation harder

Adjudication produces two artifacts on two pipes. The payment instruction goes to the payer's payment vendor and out to a bank. The X12 835 goes to your clearinghouse mailbox, on its own schedule. Nothing coordinates them.

On a real remittance the sequence looks like this:

  • Thu 20 Aug, 18:47 — adjudication completes; DTM*405 is stamped
  • Thu 20 Aug, 19:04 — $145.40 settles, final and available
  • Fri 21 Aug, 06:12 — the 835 is retrievable
  • Fri 21 Aug, 09:30 — someone posts it, by hand, at claim level

Eleven hours of unidentified cash, then a manual posting step. The settlement leg went from days to seconds and the information leg did not move at all. Worse, the instant rails do not carry the fix: the ACH CCD+ addenda record that CORE's reassociation rule uses to bind a deposit to a remittance has no equivalent of usable size in ISO 20022 remittance fields [verify current CORE rule version and RmtInf field lengths]. You get a reference, not a remittance.

So the matching has to happen in your system, from the file, at the line.

Anatomy of an 835, and where legacy parsers lose the thread

An 835 is a nested envelope, and the nesting is the whole point:

  • Transaction header — BPR (payment amount, method, bank routing), TRN (the reassociation trace number), DTM*405 (production date), N1*PR / N1*PE payer and payee loops
  • Loop 2100 — claim — CLP (your patient control number, claim status, total charge, paid, patient responsibility, payer ICN), claim-level CAS, NM1, MOA/MIA, LQ for remark codes
  • Loop 2110 — service line — SVC (procedure, charge, paid, units), line-level CAS, REF*6R (line item control number), AMT, DTM
  • `PLB` — provider-level adjustments that sit outside the claims entirely

Three balancing identities hold in a well-formed file, and each one is a test you can run:

  1. Line: SVC02 = SVC03 + the sum of that line's CAS amounts
  2. Claim: CLP03 − CLP04 = the sum of all claim- and line-level CAS amounts
  3. Transaction: BPR02 = the sum of all CLP04 − the sum of all PLB amounts

Legacy posting logic breaks in six recurring places:

It posts at claim level. CLP04 goes to the ledger and loop 2110 is discarded. Every line-level denial, bundle, and downcode inside a claim that paid something becomes invisible. This is the single largest source of silent revenue loss, and it is a design choice, not a bug report.

It ignores `REF*6R`. Without the line item control number you echoed on the 837, the parser matches on procedure code and charge amount. That heuristic collides the moment a claim carries the same CPT twice — bilateral procedures, repeat labs, modifier splits.

It reads `SVC01` and stops. When a payer adjudicates a different code than you submitted, the submitted code appears in SVC06 and the adjudicated code in SVC01 [verify against the X12 835 TR3 and your payer's companion guide]. A parser reading only SVC01 records the payer's version of what you did and never flags the substitution.

It takes absolute values of `CAS`. Negative CAS amounts reverse a prior adjustment and increase payment. Absolute-value handling silently corrupts identity #1 and #2.

It drops `PLB`. Provider-level adjustments — overpayment recovery (WO), forwarding balance (FB), interest (L6) [verify the current reason code list] — are why BPR02 does not equal the sum of your claim payments. Drop them and every deposit is "off by some amount" forever.

It treats reversals as new payments. CLP02 = 22 marks a reversal of a previous payment, normally followed by a corrected claim. Post both as receipts and you have double-counted.

Worked example: a 145.40 deposit against 230.40 in claims

Two claims, four lines, one recoupment. All values are synthetic and contain no patient data.

code
BPR*I*145.40*C*ACH*CCP*01*091000019*DA*0001234567*1932847561**01*021000021*DA*9876543210*20260821~
TRN*1*20260820A9931447*1932847561~
DTM*405*20260820~
CLP*PCN-88214*1*375.00*134.40*73.60*12*ICN-2026-0820-4471*11~
SVC*HC:99215*320.00*113.60**1~
CAS*CO*45*138.00~
CAS*PR*1*40.00~
CAS*PR*2*28.40~
REF*6R*L-88214-01~
SVC*HC:96372*55.00*20.80**1~
CAS*CO*45*29.00~
CAS*PR*2*5.20~
REF*6R*L-88214-02~
CLP*PCN-88235*1*325.00*96.00*24.00*12*ICN-2026-0820-4472*11~
SVC*HC:99203*295.00*96.00**1*HC:99204~
CAS*CO*45*175.00~
CAS*PR*2*24.00~
REF*6R*L-88235-01~
SVC*HC:81003*30.00*0.00**1~
CAS*CO*97*30.00~
REF*6R*L-88235-02~
PLB*1932847561*20261231*WO>ICN-2026-0612-9930*85.00~

Every line balances — SVC02 = SVC03 + the sum of that line's CAS:

  • 99215 — 320.00 = 113.60 + 138.00 (CO-45) + 40.00 (PR-1) + 28.40 (PR-2) ✓
  • 96372 — 55.00 = 20.80 + 29.00 (CO-45) + 5.20 (PR-2) ✓
  • 99203 — 295.00 = 96.00 + 175.00 (CO-45) + 24.00 (PR-2) ✓
  • 81003 — 30.00 = 0.00 + 30.00 (CO-97) ✓

Both claims tie out. PCN-88214: 375.00 − 134.40 = 240.60, equal to 167.00 contractual plus 73.60 patient; CLP05 = 73.60 ✓. PCN-88235: 325.00 − 96.00 = 229.00, equal to 205.00 contractual plus 24.00 patient; CLP05 = 24.00 ✓.

And the transaction ties out — which is the part claim-level posting can never do. Claim payments sum to 134.40 + 96.00 = 230.40. BPR02 is 145.40. The $85.00 difference is the PLB: a WO overpayment recovery referencing ICN-2026-0612-9930, a claim from June that is already closed on your books. The deposit is not short. It is net.

Three things are only visible at the line:

A partial payment. PCN-88214 paid, but $73.60 sits in the PR group — $40.00 deductible plus $33.60 coinsurance. That figure, and only that figure, is billable to this patient. The $167.00 in CO-45 is your contractual write-off.

A denial and a downcode inside a claim that paid. Line 81003 was zero-paid under CO-97 (bundled into another service). Line 99203 was submitted as 99204 — SVC06 holds what you billed, SVC01 holds what the payer adjudicated, and the entire differential is buried in CO-45 with no distinct CARC announcing the substitution [verify how each of your payers signals a procedure code change]. Claim-level posting records "$96.00 received" and moves on. Line-level parsing records a code change worth appealing.

A recoupment against a closed claim. The $85.00 belongs to a June encounter, not to either claim in this file. Posting it against PCN-88214 or PCN-88235 corrupts two ledgers to make one deposit balance.

Note what is not billable to the patient: the CO-97 bundled line and every CO-45 dollar. CO is contractual obligation — provider liability. Only the PR group crosses to a statement.

Matching in three keys, and what it does to the ledger

Automated line-level matching is not one join. It is three, executed in order, each with a defined fallback and a defined failure state.

LayerKeyIn the 835On your sideIf the key is missing
Deposit ↔ remittanceReassociation traceTRN02, checked against BPR02ACH addenda, or the reference field on the instant-rail creditCash sits in suspense; match by amount + date is a guess [verify carriage with your FI]
Claim ↔ claimPatient control number, payer ICNCLP01, CLP07Your PCN as submitted; store the ICN on first receiptReplays and corrections double-post
Line ↔ chargeLine item control numberREF*6R in loop 2110The REF*6R you sent on the 837Fall back to SVC01 + units + charge — and flag the claim, because repeated codes collide
Adjustment ↔ prior claimReason code + referencePLB03 composite, PLB04 amountThe original ICN on an already-closed claimDeposit never ties; someone hunts an "off by $X" every month

Two rules make this safe to run unattended. Idempotency: derive a key from payer ID, CLP07, CLP02, TRN02, and the line control number, so a re-sent file posts nothing twice. Suspense before certainty: an instant credit with no matched 835 lands in unapplied cash and never touches a patient ledger — no statement, no balance, no call.

What changes downstream is measurable on your own data, and you should measure it rather than take a number from a vendor: hours per week spent reconciling deposits to remittances, count of deposits that required manual intervention, days between funds-available and line-level-posted, and the count of SVC06-differs-from-SVC01 lines your current system has never recorded. Baseline those four before you change anything.

FAQ

Our PM system posts 835s automatically. Isn't this already handled? Test it rather than assume. Pull a remittance containing a downcode and check whether the system stored both the submitted code and the adjudicated code, or only what the payer sent back. Then pull one with a PLB and check whether the deposit reconciled without a manual adjusting entry. Most auto-posting is claim-level auto-posting: it applies CLP04 correctly and discards loop 2110. That is not the same capability, and the difference only shows up in the lines you never see.

The deposit hit before the 835. What should the ledger do in the meantime? Post it to unapplied cash against the bank record, keyed by the rail's reference. Do not guess an encounter, do not touch a patient balance, and do not let a statement run against an unmatched credit. When the 835 arrives, match on TRN02 first and BPR02 second, then release from suspense and post at the line. The rule that keeps this clean is that suspense is a real ledger state with an owner and an age report — not a spreadsheet someone maintains.

Can we just match deposits to remittances by amount? No, and the example above is why. BPR02 was $145.40 while the claims totaled $230.40, because a PLB netted an unrelated June recoupment out of the same deposit. Amount matching also collides whenever a payer sends multiple remittances the same day, nets interest, or splits one file across transaction sets. Amount is a useful check on a match you made with a key. It is not a key.

Run it against your own remittance

The shteg.ai sandbox takes a de-identified 835 and returns the parse: service-line balancing validated on every SVC (SVC02 = SVC03 + CAS), claim tie-out against CLP03 − CLP04, transaction tie-out of BPR02 against claim payments net of PLB, the PR-group total isolated as the billable patient amount, every line where SVC06 differs from SVC01, and every line that resolved by heuristic because REF*6R was absent.

Bring one file where the deposit and the remittance landed on different days, and one where the deposit didn't match the claims. The second file is the one that will tell you something.

[Open the shteg.ai sandbox →](/sandbox) [verify link target before publish]


Educational content for physicians, practice administrators, and revenue-cycle staff. Not legal, billing, coding, or compliance advice. All segment values, claim identifiers, control numbers, dates, and dollar figures in this article are synthetic illustrations and contain no patient data. X12 835 specifications, CARC and RARC code lists, CAQH CORE operating rules, Nacha rules, and FedNow and RTP capabilities and message support change over time — verify current specifications with your clearinghouse, payers, and financial institution before making operational decisions. Payment timing obligations, patient billing, and balance-billing restrictions are governed by your payer contracts and by applicable state and federal law; consult counsel for your jurisdiction.


Draft notes: ~1,640 words. H1 + 6 H2s (4 body + FAQ + CTA). Covers all five outline items from fast-payment-mssjoj9x.md §Brief 2 — async arrival, 835 anatomy plus parsing failures, line-level matching strategy, partial/denial/recoupment handling, ledger and reconciliation-hours impact — with items 4 and 5 distributed across the worked example and the matching section to stay inside the 4–6 H2 spec. Carries the brief's prescribed 2 → 1 internal link in the opening.

Worked example carries both required forms: SVC02 = SVC03 + CAS on all four lines, claim tie-out on both CLPs, plus a transaction-level tie-out (BPR02 145.40 = 230.40 claim payments − 85.00 PLB WO) and an async settlement timeline. The BPR02 ≠ ΣCLP04 construction is the load-bearing differentiator against fast-payment-1-msskmg9y.md, which never reaches PLB. Single comparison table: the four-layer matching key hierarchy.

Nine [verify] flags — no statistics asserted anywhere; §5 "impact" is framed as four metrics to baseline, not a claimed reduction. PR-vs-CO billability guardrail stated twice, including explicitly for the CO-97 bundled line. Dates check against a real 2026 calendar (Aug 20 Thursday, Aug 21 Friday). Primary-source pointers for review: X12 835 TR3 (loop 2110, SVC06, PLB), CAQH CORE reassociation rule, WPC CARC/RARC lists.

Differentiation check: overlaps fast-payment-1-msskmg9y.md on the reconciliation-inversion premise (one paragraph there, §1 here) and underpayment-recovery-* on downcode detection. Worth a dedupe pass on the intro before both fast-payment pieces ship in the same cluster.

Blocking before publish: §5 requires a named physician or revenue-cycle reviewer — currently unassigned. Both internal link targets (/blog/migrating-practice-revenue-to-real-time-rails, /sandbox) are guesses at eventual slugs.


Want me to save this to examples/seo-presence/workspace/drafts/fast-payment-2-<id>.md to match the pipeline convention? The existing drafts use a short base-36 timestamp slug (mssjoj9x) — give me one in that format, or say the word and I'll generate it.

<!-- generated_by: claude-draft-1 -->