NCPDP Reject Code Reference
A reject code is the single field that tells a pharmacy, in real time, exactly why a claim did not pay — and in an automated adjudication engine it is also the contract between the rule that fired and the workflow that has to react. Every terminal decision the engine makes on the NCPDP Telecommunication Standard D.0 hot path is carried back to the point of sale in 511-FB Reject Code, a two-to-three character code the pharmacy management system renders as a counter message and a routing hint. This reference specifies the reject codes an adjudication engine emits, the trigger conditions that produce each one, the precedence rules when several fire at once, and the Python registry-and-dispatcher pattern that keeps the mapping deterministic, testable, and PHI-safe. It sits inside Claims Ingestion & NCPDP Parsing because a reject code is only as trustworthy as the parse and validation that produced it.
Operational Context
A D.0 B1 Billing request either pays or it rejects, and when it rejects the response 501-F1 Header Response Status is set to R and one or more 511-FB Reject Code occurrences are attached at the transaction or claim level. There is no free-text explanation on the wire — the pharmacist sees a code and a short standard message, and any downstream automation (member messaging, prior-authorization intake, help-desk triage, plan-leakage analytics) keys off that code, not off prose. Choosing the wrong code is not cosmetic: emit 75 where 70 belongs and a pharmacy sends the member into a prior-authorization queue for a drug the plan will never cover; emit 70 where 76 belongs and the pharmacist reduces the day supply pointlessly against a formulary exclusion. The code is the interface, so the engine must select it with the same rigor it applies to pricing.
Reject selection is the last step of a longer pipeline. A B1 transaction is parsed by the strategies in NCPDP D.0 Message Parsing Strategies, structurally validated and bucketed by Schema Validation & Error Categorization, resolved to a therapeutic class through the NDC-to-GPI Crosswalk Automation, then run through coverage, clinical, and plan-limit rules. Each stage can veto the claim, and each veto is expressed as a specific 511-FB value. The reject-code layer is therefore a registry that every rule writes into and a dispatcher that decides which of several possible rejects the pharmacy actually sees.
Reject Code Categories
The codes an adjudication engine emits fall into four operational categories. The category is not part of the NCPDP standard — it is an engineering grouping that determines which subsystem owns the code, what the correct fallback workflow is, and how the code interacts with the others. A 569 supplemental notice attaches alongside another reject; a 07 M/I data reject fires before any coverage rule runs; a coverage 70 and a plan-limit 76 are mutually exclusive verdicts on the same claim. Getting the category right is what lets the dispatcher order the codes correctly.
Figure: The dispatcher categorizes every adjudication verdict into coverage, clinical/DUR, plan-limit, or data/format families before selecting the 511-FB value the pharmacy sees.
Reject Code Table
The table below is the working reference for the codes an adjudication engine emits most often. The standard message is the human-readable text NCPDP associates with the code; the trigger is the engine condition that produces it; the owning stage is the subsystem responsible for setting it. Codes are grouped by the four categories above.
| Code | Standard message | Category | Trigger condition | Owning stage |
|---|---|---|---|---|
70 |
Product/Service Not Covered | Coverage | 407-D7 NDC resolves to a GPI that is excluded from the plan formulary, or the NDC is not on file |
Formulary lookup |
65 |
Patient Not Covered | Coverage | 302-C2 Cardholder ID resolves to no active member on the date in 401-D1 Date of Service |
Eligibility |
69 |
Filled After Coverage Terminated | Coverage | Member eligibility segment ended before 401-D1 Date of Service |
Eligibility |
608 |
Filled After Coverage Expired | Coverage | Benefit/plan coverage window has expired for the member as of 401-D1 |
Eligibility |
75 |
Prior Authorization Required | Clinical / DUR | GPI requires PA, or step therapy in the rule engine is not satisfied | Rule engine |
88 |
DUR Reject Error | Clinical / DUR | A drug-utilization-review edit (drug-drug interaction, therapeutic duplication, high dose) hard-rejects | DUR engine |
76 |
Plan Limitations Exceeded | Plan limits | 442-E7 Quantity Dispensed, 405-D5 Days Supply, or an accumulator dollar cap exceeds the plan limit |
Plan-limit engine |
79 |
Refill Too Soon | Plan limits | Requested fill date precedes the earliest-refill date computed from the prior 405-D5 Days Supply |
Plan-limit engine |
25 |
Missing/Invalid Prescriber ID | Data / format | 411-DB Prescriber ID is absent, malformed, or fails NPI checksum |
Schema validation |
07 |
M/I Cardholder ID | Data / format | 302-C2 Cardholder ID is missing or malformed (representative of the M/I field family) |
Schema validation |
569 |
Provide Beneficiary With Notice | Supplemental | Attaches to a coverage/PA reject to require the pharmacy to hand the member a coverage-determination notice | Response builder |
Two structural facts govern how these are used. First, the M/I (“Missing/Invalid”) family is large — every mandatory field has its own M/I code (01 M/I Bank/Processor ID, 07 M/I Cardholder ID, 25 M/I Prescriber ID, and dozens more) — and these fire in schema validation before any coverage or clinical rule runs, because the engine cannot evaluate a rule on a field it could not read. Second, 569 is not a standalone rejection; it is a supplemental code that rides along with a coverage or prior-authorization reject to instruct the pharmacy to deliver a written notice, so it never appears alone and never suppresses the primary code.
Canonical Reject Model
Before writing a dispatcher, pin down the data model. A reject is not a bare string — it is a code, a category, a precedence rank, a level (transaction-level vs claim-level), and an optional supplemental note. Modeling it as a Pydantic v2 structure lets the engine reason about ordering and lets tests assert on the shape rather than on message text.
from __future__ import annotations
import enum
from typing import Optional
from pydantic import BaseModel, ConfigDict, field_validator
class RejectCategory(str, enum.Enum):
DATA_FORMAT = "data_format" # M/I family, checksum failures
COVERAGE = "coverage" # eligibility + formulary
CLINICAL = "clinical" # DUR + prior authorization
PLAN_LIMIT = "plan_limit" # quantity / days / dollar caps
SUPPLEMENTAL = "supplemental" # notices that ride alongside
class RejectLevel(str, enum.Enum):
TRANSACTION = "transaction" # whole B1 unreadable; no claim evaluated
CLAIM = "claim" # this claim rejected; others may pay
class Reject(BaseModel):
"""One 511-FB Reject Code occurrence with dispatch metadata."""
model_config = ConfigDict(frozen=True, extra="forbid")
code: str # 511-FB Reject Code, e.g. "70"
message: str # standard NCPDP message text
category: RejectCategory
level: RejectLevel = RejectLevel.CLAIM
precedence: int # lower = wins when several fire
supplemental: bool = False # True only for codes like 569
@field_validator("code")
@classmethod
def _code_shape(cls, v: str) -> str:
if not (v.isalnum() and 1 <= len(v) <= 4):
raise ValueError("511-FB must be a 1-4 char alphanumeric code")
return vThe precedence field is the heart of the dispatcher. When several rules reject the same claim, only one code is returned to the pharmacy first (or an ordered list, depending on the receiver’s capabilities), and the order is not arbitrary: a data/format problem outranks everything because the engine could not even read the claim; eligibility outranks formulary because there is no point telling a non-member their drug is off-formulary; and supplemental notices never compete for the primary slot.
Reject Registry and Dispatcher
The registry is a single immutable source of truth for every code the engine can emit, and the dispatcher is a pure function that turns a set of rule verdicts into an ordered response. Keeping the two separate means a new rule never has to know the message text or precedence of the code it raises — it raises the code, and the registry supplies the rest.
import json
import logging
from typing import Iterable
logging.basicConfig(format="%(message)s", level=logging.INFO)
logger = logging.getLogger("reject_dispatch")
# Immutable registry: the one place a 511-FB code's metadata is defined.
REGISTRY: dict[str, Reject] = {
"07": Reject(code="07", message="M/I Cardholder ID",
category=RejectCategory.DATA_FORMAT,
level=RejectLevel.TRANSACTION, precedence=10),
"25": Reject(code="25", message="Missing/Invalid Prescriber ID",
category=RejectCategory.DATA_FORMAT, precedence=15),
"65": Reject(code="65", message="Patient Not Covered",
category=RejectCategory.COVERAGE, precedence=20),
"69": Reject(code="69", message="Filled After Coverage Terminated",
category=RejectCategory.COVERAGE, precedence=22),
"608": Reject(code="608", message="Filled After Coverage Expired",
category=RejectCategory.COVERAGE, precedence=24),
"70": Reject(code="70", message="Product/Service Not Covered",
category=RejectCategory.COVERAGE, precedence=30),
"75": Reject(code="75", message="Prior Authorization Required",
category=RejectCategory.CLINICAL, precedence=40),
"88": Reject(code="88", message="DUR Reject Error",
category=RejectCategory.CLINICAL, precedence=42),
"76": Reject(code="76", message="Plan Limitations Exceeded",
category=RejectCategory.PLAN_LIMIT, precedence=50),
"79": Reject(code="79", message="Refill Too Soon",
category=RejectCategory.PLAN_LIMIT, precedence=52),
"569": Reject(code="569", message="Provide Beneficiary With Notice",
category=RejectCategory.SUPPLEMENTAL, precedence=99,
supplemental=True),
}
def dispatch(raised_codes: Iterable[str], claim_ref: str) -> list[Reject]:
"""Order raised 511-FB codes by precedence; keep supplementals last.
PHI GUARDRAIL: claim_ref is the tokenized 402-D2 Prescription/Service
Reference #. No 302-C2 Cardholder ID, 310-CA Patient Name, or raw claim
bytes ever reach this function or its logger.
"""
rejects = [REGISTRY[c] for c in raised_codes if c in REGISTRY]
# Transaction-level data/format reject short-circuits everything.
txn = [r for r in rejects if r.level is RejectLevel.TRANSACTION]
if txn:
ordered = sorted(txn, key=lambda r: r.precedence)
else:
primary = sorted(
(r for r in rejects if not r.supplemental),
key=lambda r: r.precedence,
)
supplemental = [r for r in rejects if r.supplemental]
ordered = primary + supplemental
logger.info(json.dumps({
"event": "reject_dispatch",
"claim_ref": claim_ref, # tokenized 402-D2, non-PHI
"codes": [r.code for r in ordered], # 511-FB values only
"primary": ordered[0].code if ordered else None,
}))
return orderedThe dispatcher never logs the payload — only the tokenized 402-D2 reference and the emitted 511-FB codes. That is the same PHI discipline enforced across Security & Compliance Boundaries for Claims Data: the outcome and the code are safe to persist; the member identity is not. Because the registry is immutable and the dispatcher is pure, the same set of raised codes always yields the same ordered response, which is exactly the property an audit replay depends on.
The M/I Field Family
The Missing/Invalid family deserves separate treatment because it behaves unlike the coverage and clinical codes. Every mandatory D.0 field has a dedicated M/I code, and these are raised in schema validation — before eligibility, before the crosswalk, before any rule engine runs — because a field that could not be read cannot be reasoned about. The family is large and mechanical; a handful of the codes an adjudication engine emits most often are shown below, but the pattern generalizes to every required field.
| Code | Field it guards | Typical cause |
|---|---|---|
07 |
302-C2 Cardholder ID |
absent, wrong length, or non-alphanumeric member ID |
25 |
411-DB Prescriber ID |
absent or failing NPI checksum |
19 |
409-D9 Ingredient Cost Submitted |
non-numeric or negative submitted cost |
21 |
407-D7 Product/Service ID |
NDC absent or wrong length |
26 |
403-D3 Fill Number |
absent or out of the valid 0–99 range |
Two properties make the M/I family special in the dispatcher. First, most M/I codes are transaction-level when they guard an identity field — an unreadable 302-C2 means the engine cannot identify the member, so no claim in the transaction can be evaluated and the whole B1 short-circuits with a single reject. Second, an M/I reject on a value field (say 409-D9 Ingredient Cost) is claim-level and can coexist with a paid sibling in a multi-claim transaction. The level field on the Reject model is what keeps these two behaviors from being conflated, and it is why the dispatcher checks for a transaction-level reject first and short-circuits before it ever sorts the coverage and clinical codes. A subtle but important consequence: because M/I codes outrank everything, an engine must never let a downstream rule “improve” the reject — telling a pharmacist the drug needs prior authorization when the real problem is an unreadable prescriber ID wastes a PA-intake cycle on a claim that was never evaluable.
Response Construction and Receiver Compatibility
The dispatcher produces an ordered list of Reject objects, but the wire response is an NCPDP D.0 response transaction, and building it correctly is where several production defects hide. The response header sets 501-F1 Header Response Status to R, and each emitted code becomes a 511-FB Reject Code occurrence in the response status segment. The count of occurrences must match the count the engine actually attaches — a mismatch between the declared reject count and the number of 511-FB fields is itself a malformed response that some receivers will drop.
Receiver capability is the constraint that shapes how many codes to send. The standard permits multiple 511-FB occurrences, but pharmacy management systems vary: some surface every code, some show only the first, and a few older systems truncate silently. The safe posture is to always order by precedence so that whatever the receiver chooses to display, the most actionable code is first, and to attach a supplemental 569 Provide Beneficiary With Notice only behind a primary coverage or PA reject where a coverage-determination notice is actually required. When a human-readable hint adds value — a covered alternative behind a 70, for instance — it goes in 526-FQ Additional Message Information, and it must never carry PHI. The response builder is also the last line of PHI defense: it echoes back only the transaction correlation fields the standard requires, never the inbound 310-CA Patient Name or a re-serialized copy of the request body.
Correctness and Audit Replay
Everything in the reject layer is built to be replayable, because a payer examination can ask the engine to re-adjudicate a claim from months ago and expect the identical 511-FB outcome. Three disciplines make that hold. First, the registry is immutable and versioned alongside the formulary and rule snapshots, so a reject decision references the exact code metadata that was live at fill time; changing a precedence rank is a new registry version, not an in-place edit. Second, the dispatcher is a pure function of the raised codes, with no clock, no I/O, and no hidden state, so replaying the same verdict set is deterministic. Third, every terminal decision is serialized to the append-only audit sink before the response leaves the engine — the tokenized 402-D2 reference, the ordered 511-FB codes, and the snapshot versions, and nothing else. A claim that rejected 76 in June must reject 76 on replay in December against the June snapshots, and the audit event is the artifact that proves it did. When a rejected claim needs repair or retry rather than replay, it routes through Fallback Routing Logic Design, which reconciles on the same snapshot stamps the audit event carries.
Engineering Constraints
- Multiple simultaneous rejects. A single claim can trip several rules at once — a member who is a valid but the drug is off-formulary and the quantity exceeds the limit. NCPDP D.0 allows multiple
511-FBoccurrences, but many pharmacy systems surface only the first, so precedence ordering determines what the pharmacist actually acts on. Return the full ordered list, but make the primary code the one the member can most productively act on. - Code precedence. Precedence is a policy decision encoded in the registry, not a heuristic scattered through the rules. Data/format outranks eligibility, eligibility outranks formulary, formulary outranks clinical PA, and plan limits sit below coverage because there is no reason to tell a member their day supply is too high for a drug the plan will not cover at all.
- Transaction-level vs claim-level. A
07M/I Cardholder ID is transaction-level: the engine could not identify the member, so no claim in the transaction can be evaluated and the wholeB1short-circuits. A76Plan Limitations Exceeded is claim-level: in a multi-claim transaction, one line can reject76while another pays. Thelevelfield keeps these from being conflated. - Supplemental codes do not compete.
569Provide Beneficiary With Notice attaches to a coverage or PA reject; it is never the primary and never suppresses one. Modeling it with asupplementalflag keeps it out of the precedence sort. - Message text is not the contract. Downstream automation must switch on the
511-FBcode, never on the message string — NCPDP message wording can be revised, and receiver systems localize it. The code is stable; the prose is not.
In This Section
- Handling Reject Code 70: Product/Service Not Covered — how a formulary-exclusion or not-on-file result becomes a
70, disambiguated from75,76, and the eligibility codes. - Handling Reject Code 75: Prior Authorization Required — emitting
75for PA-required drugs and unmet step therapy, and routing the claim into prior-authorization intake. - Handling Reject Code 76: Plan Limitations Exceeded — deriving
76from442-E7Quantity Dispensed and405-D5Days Supply against plan caps.
Related
- Schema Validation & Error Categorization — where the M/I field family, including
07and25, is raised before any coverage rule runs. - NCPDP D.0 Message Parsing Strategies — the parse that produces the fields a reject verdict is computed from.
- Step Therapy & Prior Auth Trigger Rules — the clinical logic that decides whether a claim raises
75. - Quantity Limit & Days Supply Validation — the plan-limit checks behind
76and79. - Fallback Routing Logic Design — where a rejected claim is routed for repair, retry, or manual review.
← Back to Claims Ingestion & NCPDP Parsing