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.

NCPDP reject-code categorization and dispatch tree A 511-FB Reject Code source node feeds a dispatcher that categorizes each adjudication verdict into four families: coverage and eligibility, carrying codes 70, 65, 69 and 608; clinical and DUR, carrying codes 88 and 75; plan limits, carrying codes 76 and 79; and data and format, carrying codes 07, 25 and 569. Each family routes to its owning subsystem and fallback workflow. 511-FB Reject Code Categorize + dispatch verdict Coverage eligibility + formulary Clinical / DUR safety + prior auth Plan Limits qty / days / dollars Data / Format M/I fields + notices 70 · not covered 65 · patient 69 · terminated 608 · expired 75 · PA required 88 · DUR reject 76 · limits 79 · refill soon 07 · M/I cardholder 25 · M/I prescriber 569 · notice Each category owns a subsystem, a precedence rank, and a fallback workflow

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.

python
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 v

The 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.

python
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 ordered

The 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-FB occurrences, 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 07 M/I Cardholder ID is transaction-level: the engine could not identify the member, so no claim in the transaction can be evaluated and the whole B1 short-circuits. A 76 Plan Limitations Exceeded is claim-level: in a multi-claim transaction, one line can reject 76 while another pays. The level field keeps these from being conflated.
  • Supplemental codes do not compete. 569 Provide Beneficiary With Notice attaches to a coverage or PA reject; it is never the primary and never suppresses one. Modeling it with a supplemental flag keeps it out of the precedence sort.
  • Message text is not the contract. Downstream automation must switch on the 511-FB code, 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

← Back to Claims Ingestion & NCPDP Parsing