CARC / RARC Denial Routing
Every dollar a payer withholds on an electronic remittance arrives wrapped in a code, and the difference between a healthy revenue cycle and a growing accounts-receivable backlog is whether your automation reads that code correctly and moves the claim to the right desk. Claim Adjustment Reason Codes (CARCs) and Remittance Advice Remark Codes (RARCs) are the standardized vocabulary payers use on the X12 835 to explain why a billed amount was reduced, denied, or shifted to the patient — and treating all of them as “denials” is the single most common mistake in denial-management automation. A CARC 45 write-down is a contractual adjustment you agreed to and can never appeal; a CARC 197 authorization denial is fully recoverable if a specialist acts before the timely-filing window closes; a PR-group CARC 1 deductible is not a denial at all but the patient’s balance. For revenue cycle engineers, medical billing developers, and healthcare IT teams building on the Denial Management & Appeals Automation stack, denial routing is the switchboard that turns raw remittance codes into deterministic, owner-assigned work. This page defines the CAS segment structure, the common CARC/RARC vocabulary, and a typed Python model that parses every adjustment triplet and routes it to the correct queue.
Architectural Placement: Where Routing Sits on the Remittance
Denial routing consumes the output of remittance parsing and feeds the appeal and analytics stages downstream. The adjustment codes it reads live inside the CAS (Claims Adjustment) segment of the 835, which is fully described in the X12 835 Remittance Structure Breakdown; a routing engine never re-parses the interchange envelope itself but receives already-structured CLP claim loops and their child CAS and SVC service loops. For each adjustment the router evaluates the group code, the reason code, any attached remark code, and the monetary amount, then emits a single routing decision: which work queue, which owner role, and what priority. The decision is stateless with respect to patient data — it keys only on the CLP01 patient control number and the code values — so any remittance can be re-routed deterministically after a code-list update without touching protected health information.
CAS01 group code is the primary switch: it decides whether an adjustment is a contractual write-down, the patient's balance, a reworkable defect, a payer reduction, or a correction — before the CARC in CAS02 refines the destination.Core Specification: The CAS Segment and the CARC / RARC Vocabulary
The CAS segment is a repeating structure. CAS01 names the group code — the broad category of the adjustment — and the remaining elements form up to six triplets, each a reason code, a monetary amount, and an optional quantity. A single CAS segment therefore can carry six distinct adjustments against one claim or service line, and a claim can contain multiple CAS segments (one at the claim level under CLP, and one per service line under each SVC). The routing engine must iterate every triplet in every CAS segment; collapsing them to “the first reason code” silently drops adjustments and breaks the balancing arithmetic the 835 guarantees.
| Element | Name | Requirement | Valid values / notes |
|---|---|---|---|
CAS01 |
Claim Adjustment Group Code | Mandatory | CO contractual obligation, PR patient responsibility, OA other adjustment, PI payer-initiated reduction, CR correction & reversal |
CAS02 |
Adjustment Reason Code (CARC) | Mandatory | A value from the external CARC list (e.g. 45, 96, 197) |
CAS03 |
Adjustment Amount | Mandatory | Signed monetary amount; positive reduces provider payment |
CAS04 |
Adjustment Quantity | Conditional | Units affected (e.g. denied service units) |
CAS05–CAS19 |
Triplets 2–6 | Conditional | Repeat of reason / amount / quantity, up to six triplets total |
Group codes answer who owns the balance; CARCs answer why. The most common CARCs a denial router must recognize span contractual write-downs, informational denials, and hard non-coverage rejections:
| CARC | Meaning | Typical group | Actionability |
|---|---|---|---|
16 |
Claim/service lacks information or has submission/billing error | CO / OA |
Reworkable — supply missing data, often paired with a RARC naming the field |
18 |
Exact duplicate claim or service | OA |
Suppress — verify against original 837, do not resubmit blindly |
45 |
Charge exceeds fee schedule / maximum allowable | CO |
Not appealable — contracted write-down, post as adjustment |
50 |
Non-covered; not deemed a medical necessity | CO / PR |
Appealable with documentation or medical records |
96 |
Non-covered charge(s) | CO / PR |
Depends on the RARC — check benefit vs. coding cause |
97 |
Payment included in the allowance for another service (bundled) | CO |
Reworkable — review NCCI bundling, consider modifier |
197 |
Precertification / authorization / notification absent | CO |
Appealable — retro-authorization or auth documentation |
RARCs supplement CARCs with specific detail and travel in different places on the 835. Claim-level remark codes ride in the MOA (Outpatient Adjudication Information) and MIA (Inpatient Adjudication Information) segments — for example MOA03 through MOA07 each hold a remark code — while the same remark vocabulary reappears in the LQ segment when the acknowledgment path is the X12 277CA Claim Acknowledgment Parsing transaction rather than the 835. A RARC such as N130 (“consult plan benefit documents”) or M15 (“separately billed services bundled”) turns a generic CARC 96 or 97 into an actionable instruction. A robust router therefore keys its decision on the pair — group code plus CARC — and consults the RARC only to refine priority and owner, never as the primary switch.
Implementation: A Typed CAS Parser and Group-Aware Router
The router is built from three typed pieces: an enum of group codes, a dataclass for a single parsed adjustment, and a routing function that maps (group, reason) to a work-queue decision. The parser splits each CAS segment into its triplets and joins any claim-level RARC; the router then classifies. Logging records only the CLP01 patient control number, the CLP02 status, and the code values — never patient names, member IDs, or diagnoses — satisfying the HIPAA Security Rule §164.312(b) audit-control requirement while still producing a per-decision evidentiary trail.
from __future__ import annotations
import enum
import json
import logging
from dataclasses import dataclass, field
from decimal import Decimal
# ---------------------------------------------------------------------------
# HIPAA-safe structured logging — control numbers and codes only, never PHI.
# ---------------------------------------------------------------------------
class JSONFormatter(logging.Formatter):
def format(self, record: logging.LogRecord) -> str:
payload = {
"ts": self.formatTime(record, self.datefmt),
"level": record.levelname,
"msg": record.getMessage(),
}
for attr in ("clp01", "group", "carc", "rarc", "queue"):
if hasattr(record, attr):
payload[attr] = getattr(record, attr)
return json.dumps(payload)
logger = logging.getLogger("denial_router")
logger.setLevel(logging.INFO)
_handler = logging.StreamHandler()
_handler.setFormatter(JSONFormatter(datefmt="%Y-%m-%dT%H:%M:%SZ"))
logger.addHandler(_handler)
# ---------------------------------------------------------------------------
# CAS01 group codes — the primary routing switch.
# ---------------------------------------------------------------------------
class GroupCode(enum.Enum):
CO = "CO" # Contractual Obligation
PR = "PR" # Patient Responsibility
OA = "OA" # Other Adjustment
PI = "PI" # Payer Initiated Reduction
CR = "CR" # Correction and Reversal
@dataclass(frozen=True)
class Adjustment:
"""One CAS triplet joined to its claim context and any remark code."""
group: GroupCode
carc: str # CAS02 adjustment reason code
amount: Decimal # CAS03 adjustment amount (signed)
quantity: Decimal | None = None # CAS04
rarc: str | None = None # from MOA/MIA (or LQ on a 277CA)
def parse_cas_segment(
elements: list[str],
claim_rarcs: list[str] | None = None,
) -> list[Adjustment]:
"""Split a CAS segment into typed adjustments.
`elements` is the CAS segment already tokenized on the element
separator, e.g. ['CAS', 'CO', '45', '31.40', '', 'CO', '253', '2.60'].
Triplets begin at index 2 and repeat every three elements, up to six.
"""
group = GroupCode(elements[1]) # CAS01
rarc = (claim_rarcs or [None])[0]
adjustments: list[Adjustment] = []
# Walk index 2, 5, 8, ... — each triplet is (reason, amount, quantity).
for i in range(2, min(len(elements), 20), 3):
reason = elements[i]
if not reason:
continue
amount = Decimal(elements[i + 1] or "0")
qty_raw = elements[i + 2] if i + 2 < len(elements) else ""
quantity = Decimal(qty_raw) if qty_raw else None
adjustments.append(
Adjustment(group=group, carc=reason, amount=amount,
quantity=quantity, rarc=rarc)
)
return adjustments
# ---------------------------------------------------------------------------
# Routing table: (group, carc) -> work queue. Fallback is group-level.
# ---------------------------------------------------------------------------
@dataclass(frozen=True)
class RouteDecision:
queue: str
owner: str
priority: int # 1 = highest
is_denial: bool # distinguishes denial from adjustment / patient bill
_SPECIFIC_ROUTES: dict[tuple[GroupCode, str], RouteDecision] = {
(GroupCode.CO, "45"): RouteDecision("contractual_writeoff", "auto_post", 5, False),
(GroupCode.CO, "97"): RouteDecision("coding_rework", "coder", 2, True),
(GroupCode.CO, "197"): RouteDecision("auth_appeal", "appeals", 1, True),
(GroupCode.CO, "16"): RouteDecision("missing_info_rework", "biller", 2, True),
(GroupCode.OA, "18"): RouteDecision("duplicate_review", "biller", 3, False),
(GroupCode.CO, "50"): RouteDecision("medical_necessity_appeal", "clinical", 1, True),
(GroupCode.CO, "96"): RouteDecision("noncovered_review", "biller", 2, True),
}
_GROUP_FALLBACK: dict[GroupCode, RouteDecision] = {
GroupCode.CO: RouteDecision("contractual_review", "biller", 3, True),
GroupCode.PR: RouteDecision("patient_statement", "patient_billing", 4, False),
GroupCode.OA: RouteDecision("general_rework", "biller", 3, True),
GroupCode.PI: RouteDecision("payer_initiated_review", "biller", 2, True),
GroupCode.CR: RouteDecision("reversal_repost", "cash_posting", 2, False),
}
def route(adj: Adjustment, clp01: str) -> RouteDecision:
"""Resolve one adjustment to a work queue, most-specific first."""
decision = _SPECIFIC_ROUTES.get((adj.group, adj.carc)) \
or _GROUP_FALLBACK[adj.group]
logger.info(
"routed adjustment",
extra={
"clp01": clp01,
"group": adj.group.value,
"carc": adj.carc,
"rarc": adj.rarc or "",
"queue": decision.queue,
},
)
return decision
Driving the parser with a realistic claim-level CAS segment shows why iterating every triplet matters — a single segment produces two independent routing decisions:
segment = ["CAS", "CO", "45", "31.40", "", "97", "12.00", ""]
adjustments = parse_cas_segment(segment, claim_rarcs=["N130"])
for adj in adjustments:
decision = route(adj, clp01="PCN-000481")
print(adj.group.value, adj.carc, adj.amount, "->", decision.queue,
"denial" if decision.is_denial else "adjustment")
The CARC 45 triplet posts as a contractual write-off with no human touch, while the CARC 97 bundling triplet routes to the coding rework queue as a genuine denial worth pursuing — two very different dispositions extracted from one segment. The mapping table itself is intentionally data, not code: promoting it to a config-driven, versioned structure is the subject of Building a CARC/RARC Routing Engine, and the group-code classification logic that decides is_denial is expanded in Classifying Denials by CARC Group Code.
Payer Rule and Compliance Constraint
CARCs and RARCs are not owned by any single payer — they are external code lists maintained under X12 and published by the code-list maintainers (the CARC and RARC lists are updated three times a year, in March, July, and November). Under the HIPAA Administrative Simplification transaction and code-set standards, covered entities must use the code list in effect on the date the remittance was issued. This makes every code list versioned and effective-dated, and a routing engine that hard-codes a static snapshot will mis-route the moment a code is deactivated or its meaning is narrowed. Two compliance constraints follow directly:
- Honor effective dates. A CARC that was reworkable last year may be split into two more specific codes this cycle, or deactivated with a “use CARC X instead” note. Store each code list with its start and end effective dates, and resolve the code against the remittance production date (
BPR16/ the 835DTMproduction date), not today’s date. Replaying a stored denial against a newer list without re-resolving can send a claim to the wrong desk. - Never conflate the three dispositions. A
PR-group adjustment is the patient’s responsibility — a deductible, copay, or coinsurance — and is not a denial; routing it into an appeal queue wastes staff time and delays patient billing. ACOCARC 45 is a contractual write-down you agreed to in the payer contract and is not appealable. Only adjustments that represent a recoverable payer decision — authorization, medical necessity, bundling, missing information — belong in an appeal or rework queue. Encoding this distinction as theis_denialflag on every route keeps denial analytics and reporting honest, because a denial-rate KPI that counts contractual write-downs and patient balances is meaningless.
Because these boundaries move on published dates, version-control the routing table alongside the payer-specific rule boundary configuration it complements, and stamp each routed adjustment with the code-list version that classified it so an audit can reconstruct why a claim went where it did.
Error Handling and Balancing
The 835 is a self-balancing transaction: for every claim, the submitted charge (CLP03) must equal the paid amount (CLP04) plus the patient responsibility plus the sum of every CAS adjustment amount across all group codes. A router that drops a triplet — or coerces a signed amount incorrectly — breaks this identity, and the mismatch is the earliest signal that parsing went wrong. Validate the balance per claim before dispatching any routing decision, and quarantine an unbalanced claim rather than routing partial adjustments:
def assert_balanced(clp03_charge: Decimal, clp04_paid: Decimal,
patient_resp: Decimal, adjustments: list[Adjustment]) -> None:
total_adj = sum((a.amount for a in adjustments), Decimal("0"))
if clp03_charge != clp04_paid + patient_resp + total_adj:
# Do not route a claim whose adjustments do not reconcile.
raise ValueError("CAS adjustments do not balance against CLP charge")
An unrecognized group code (a value outside CO/PR/OA/PI/CR) should raise rather than default silently — it signals a malformed or non-standard 835 that belongs in the dead-letter path described in Error Categorization & Retry Logic Design, not a work queue. An unrecognized CARC, by contrast, is expected and safe: it falls through to the group-level fallback route and is flagged for a code-list refresh rather than dropped.
Performance and Scale
At clearinghouse volume a nightly 835 batch can carry hundreds of thousands of claim loops, each with several CAS segments. Keep the routing table as an in-memory dictionary keyed on (GroupCode, carc) for O(1) resolution rather than scanning a list per adjustment, and load it once at worker startup from the versioned store. Routing itself is CPU-light and embarrassingly parallel; fan the parsed CLP loops across a worker pool as described in Asynchronous Batch Processing for High-Volume Claims, and emit routing decisions to a message broker so the appeal, rework, and patient-billing queues drain independently. Because the router is stateless and idempotent — the same adjustment always yields the same decision for a given code-list version — a whole batch can be re-routed after a code-list update simply by replaying it, with no risk of double-posting.
By parsing every CAS triplet, keying decisions on the group-plus-reason pair, and enforcing the denial / contractual / patient-responsibility distinction, healthcare IT teams turn an opaque wall of remittance codes into a deterministic, auditable work-assignment engine.
Related
- Build the mapping as a versioned, config-driven service in Building a CARC/RARC Routing Engine.
- Separate write-offs from appealable denials in Classifying Denials by CARC Group Code.
- Locate the CAS, MOA, and MIA segments on the wire in the X12 835 Remittance Structure Breakdown.
- Turn routed denials into recoverable dollars through Automated Appeal Letter Generation.
- Measure the outcome with Denial Analytics and Reporting.