Mapping 277CA Status Category Codes to Internal Workflow Dispositions
Problem: your 277CA parser is decoding the STC01 composite cleanly, but the raw category codes — A1, A2, A3, A6, A7, P1, F0 — mean nothing to the downstream workflow until each one is resolved to a concrete disposition: accepted (let it ride to adjudication), pended (watch it), or rejected → rework (pull it into the appeals queue now). This guide builds the deterministic mapping table and resolver that turns health-care-claim-status category codes (external code list 507) into internal dispositions, and it fails closed so an unrecognized code is never mistaken for an acceptance. It targets the revenue cycle engineers and Python automation developers who own the denial-prevention pipeline.
Prerequisites
Spec Reference: STC01-1 Category Codes → Disposition
The category code is the coarse verdict. The status code refines why, but the disposition axis is driven entirely by the category. The table below is the mapping this guide encodes; values are from external code list 507 as used in the 005010X214 277CA acknowledgment.
Category (STC01-1) |
Meaning | Family | Internal disposition |
|---|---|---|---|
A0 |
Acknowledgement — forwarded to another entity | accepted-transit | PENDED |
A1 |
Acknowledgement — receipt (claim received) | accepted | ACCEPTED |
A2 |
Acknowledgement — acceptance into adjudication | accepted | ACCEPTED |
A3 |
Acknowledgement — returned to submitter, not processed | rejected | REJECTED |
A4 |
Acknowledgement — not found | rejected | REJECTED |
A6 |
Acknowledgement — rejected for missing information | rejected | REJECTED |
A7 |
Acknowledgement — rejected for invalid information | rejected | REJECTED |
A8 |
Acknowledgement — rejected, cannot identify entity | rejected | REJECTED |
P1 |
Pending — in process | pended | PENDED |
P2 |
Pending — payer review / additional info requested | pended | PENDED |
F0 |
Finalized (rare on a front-end acknowledgment) | finalized | PENDED |
| unknown | code not in list 507 | — | REJECTED (fail closed) |
Two design decisions are load-bearing. First, the accepted family is narrow — only A1 and A2 mean the claim entered adjudication. Everything in the A3–A8 range is a rejection that will never produce an 835 remittance and must go to rework immediately. Second, the default is REJECTED, not ACCEPTED: a claim silently marked accepted disappears from the rework queue and ages past timely-filing, so an unknown code must fail toward human review, never away from it.
Step-by-Step Implementation
Step 1 — Model the disposition and category family as enums
Define the three dispositions and a coarse family so the resolver can carry both the actionable disposition and a human-readable grouping for reporting.
from __future__ import annotations
import enum
import json
import logging
class Disposition(enum.Enum):
ACCEPTED = "ACCEPTED" # A1/A2 — entered adjudication, expect an 835
PENDED = "PENDED" # A0/P1/P2/F0 — suspended, keep watching
REJECTED = "REJECTED" # A3-A8 and unknown — pull into rework/appeal
class CategoryFamily(enum.Enum):
ACCEPTED = "accepted"
ACCEPTED_TRANSIT = "accepted-transit"
REJECTED = "rejected"
PENDED = "pended"
FINALIZED = "finalized"
UNKNOWN = "unknown"
Step 2 — Build the category mapping table
Encode list 507 as a single source of truth mapping each category code to its family and disposition. Keeping it a module-level dict makes the resolve a hot-path dict lookup with no I/O.
from dataclasses import dataclass
@dataclass(frozen=True)
class CategoryRule:
family: CategoryFamily
disposition: Disposition
description: str
# STC01-1 (external code list 507) -> internal routing rule.
CATEGORY_TABLE: dict[str, CategoryRule] = {
"A0": CategoryRule(CategoryFamily.ACCEPTED_TRANSIT, Disposition.PENDED,
"Forwarded to another entity"),
"A1": CategoryRule(CategoryFamily.ACCEPTED, Disposition.ACCEPTED,
"Receipt acknowledged"),
"A2": CategoryRule(CategoryFamily.ACCEPTED, Disposition.ACCEPTED,
"Accepted into adjudication"),
"A3": CategoryRule(CategoryFamily.REJECTED, Disposition.REJECTED,
"Returned to submitter, not processed"),
"A4": CategoryRule(CategoryFamily.REJECTED, Disposition.REJECTED,
"Not found"),
"A6": CategoryRule(CategoryFamily.REJECTED, Disposition.REJECTED,
"Rejected, missing information"),
"A7": CategoryRule(CategoryFamily.REJECTED, Disposition.REJECTED,
"Rejected, invalid information"),
"A8": CategoryRule(CategoryFamily.REJECTED, Disposition.REJECTED,
"Rejected, cannot identify entity"),
"P1": CategoryRule(CategoryFamily.PENDED, Disposition.PENDED,
"Pending, in process"),
"P2": CategoryRule(CategoryFamily.PENDED, Disposition.PENDED,
"Pending, payer review"),
"F0": CategoryRule(CategoryFamily.FINALIZED, Disposition.PENDED,
"Finalized"),
}
# Fail-closed default for any category not present in the table.
_UNKNOWN_RULE = CategoryRule(
CategoryFamily.UNKNOWN, Disposition.REJECTED, "Unknown category — fail closed"
)
Step 3 — Write the resolver
The resolver takes a decoded STC01 composite (category, status, entity) and returns the routing rule. It reads the category only; the status and entity codes are logged as diagnostic context but never change the disposition axis.
logger = logging.getLogger("x12.277ca.map")
logger.setLevel(logging.INFO)
@dataclass(frozen=True)
class StatusComposite:
category_code: str # STC01-1
status_code: str # STC01-2
entity_code: str | None # STC01-3
def resolve_disposition(stc: StatusComposite) -> CategoryRule:
"""Map an STC01 composite to an internal routing rule (fail-closed)."""
rule = CATEGORY_TABLE.get(stc.category_code, _UNKNOWN_RULE)
logger.info(
json.dumps({
"event": "277ca_category_resolved",
"category": stc.category_code, # code, not PHI
"status_code": stc.status_code, # diagnostic detail
"entity": stc.entity_code, # which party the status is about
"disposition": rule.disposition.value,
"family": rule.family.value,
})
)
return rule
Step 4 — Route the claim on its resolved disposition
Wire the resolver into the three sinks. The match statement keeps the branching exhaustive — every Disposition member is handled explicitly.
def route_claim(trace_no: str, stc: StatusComposite) -> str:
"""Return the target queue for a claim given its primary STC status."""
rule = resolve_disposition(stc)
match rule.disposition:
case Disposition.ACCEPTED:
return "adjudication_ledger" # nothing to do — 835 will follow
case Disposition.PENDED:
return "pended_watchlist" # re-check on the next status cycle
case Disposition.REJECTED:
# Carry status + entity so rework can auto-correct the defect.
return "rework_appeal_queue"
raise AssertionError("unreachable: unhandled disposition")
Verification
Resolve a small set of composites spanning all three dispositions, including an unknown category, and assert the fail-closed behavior. A rejected A7 and an unknown Z9 must both land in the rework queue; only A1/A2 reach the adjudication ledger.
samples = [
StatusComposite("A2", "20", "PR"), # accepted into adjudication
StatusComposite("A7", "562", "IL"), # rejected — invalid subscriber info
StatusComposite("P1", "0", None), # pended — in process
StatusComposite("Z9", "999", None), # unknown — must fail closed
]
targets = [route_claim("T-" + str(i), s) for i, s in enumerate(samples)]
assert targets == [
"adjudication_ledger",
"rework_appeal_queue",
"pended_watchlist",
"rework_appeal_queue", # unknown routed to rework, never accepted
]
Expected log output (one JSON line per resolution; only codes, no PHI):
{"event": "277ca_category_resolved", "category": "A2", "status_code": "20", "entity": "PR", "disposition": "ACCEPTED", "family": "accepted"}
{"event": "277ca_category_resolved", "category": "A7", "status_code": "562", "entity": "IL", "disposition": "REJECTED", "family": "rejected"}
{"event": "277ca_category_resolved", "category": "P1", "status_code": "0", "entity": null, "disposition": "PENDED", "family": "pended"}
{"event": "277ca_category_resolved", "category": "Z9", "status_code": "999", "entity": null, "disposition": "REJECTED", "family": "unknown"}
The fourth line is the one to watch: the unknown Z9 resolves to REJECTED/unknown, proving the table never lets an unrecognized category slip through as accepted.
Common Gotchas
- Mapping the status code instead of the category.
STC01-2(list 508, e.g.20,21,88) is the reason;STC01-1(list 507, e.g.A2,A7) is the disposition. Drive routing offSTC01-1and treatSTC01-2as diagnostic detail, or you will scatter one logical disposition across dozens of status codes. - Ignoring the entity code as if it were noise.
STC01-3(list 1066) tells you which party the reject is about —IL(subscriber),85(billing provider),PR(payer). TwoA7rejects with different entity codes route to different rework fixes; drop it and you lose the auto-correction hint. - Defaulting unknown categories to accepted. A missing table entry that returns
ACCEPTED(orNonetreated as success) silently drops the claim from rework. Always fail closed toREJECTEDso a new payer code surfaces as a review item, not a lost claim. - Reading only the first STC on a multi-defect claim. The claim-level category is the primary
STC, but itemized defectSTCsegments (andSTC10) carry additional categories. Resolve the primary for the disposition, then keep the rest for the rework detail.
Related
- Parent guide: X12 277CA Claim Acknowledgment Parsing — the segment walk and STC composite decode this mapping consumes.
- Reconciling 277CA to Original 837 Claims — join the resolved disposition back to the submitted claim and catch acknowledgments that never arrived.