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.

CAS triplet to work-queue routing decision treeAn 835 CAS segment carrying up to six adjustment triplets enters a parse node that splits each triplet into its CAS01 group code, CAS02 reason code (CARC), and CAS03 monetary amount, joined with any claim-level MOA or MIA remark code (RARC). The router branches on the CAS01 group code into five outcomes. Group CO, contractual obligation, routes to an appeal or write-off review queue because CARC 45 is a contractual write-down while CARC 197 is appealable. Group PR, patient responsibility, routes to the patient statement queue and is not a denial. Group OA, other adjustment, routes to the rework queue. Group PI, payer initiated reduction, routes to a payer-initiated review queue. Group CR, correction and reversal, routes to a reversal and repost queue. Each branch preserves the CAS03 amount so downstream analytics balance against the CLP charge. 835 REMITTANCE CAS segmentup to 6 triplets PARSE + JOIN REMARK Split tripletCAS01 groupCAS02 reason (CARC)CAS03 amount+ MOA/MIA RARC GROUP CODE BRANCH CO — Contractualwrite-down or appeal PR — Patient resp.not a denial OA — Other adj.needs rework PI — Payer init.payer reduction CR — Correctionreversal / repost WORK QUEUE Appeal / write-offreview Patient statementbalance billing Rework queuecorrect + resubmit Payer-initiatedreview Reversal / repostledger correction The CAS03 amount rides along every branch so denial analytics balance against the CLP04 paid amount.
The 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)
CAS05CAS19 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:

  1. 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 835 DTM production date), not today’s date. Replaying a stored denial against a newer list without re-resolving can send a claim to the wrong desk.
  2. 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. A CO CARC 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 the is_denial flag 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.

Up: Denial Management & Appeals Automation