Handling Reject Code 76: Plan Limitations Exceeded
Reject code 76 means the drug is covered and the member is eligible — the only thing wrong is the amount. It fires when a claim asks for more than the plan allows on one of three axes: too many units in 442-E7 Quantity Dispensed, too many days in 405-D5 Days Supply, or too many dollars against an accumulator cap. Because the product is payable, 76 is the reject a pharmacist can most often fix at the counter by reducing the quantity or the day supply and resubmitting, which makes precise limit arithmetic the whole game: emit 76 a unit too early and you block a legitimate fill; emit it a unit too late and you leak plan dollars on every claim. This guide, part of the NCPDP Reject Code Reference, specifies the trigger conditions for 76 and the Python handler that evaluates 442-E7 and 405-D5 against the plan limits.
The Exact Decision
76 is evaluated only after eligibility and coverage pass — there is no point checking a quantity limit on a drug the plan will not cover. The decision is a comparison: does the requested quantity, days supply, or dollar amount exceed the plan’s configured limit for this GPI? A limit set is drawn from the versioned formulary snapshot and typically carries a maximum quantity per fill, a maximum days supply per fill, and sometimes a rolling dollar cap tracked in an accumulator. If any axis is exceeded, the claim rejects 76; the handler records which axis tripped so the pharmacy message and the Quantity Limit & Days Supply Validation analytics can distinguish a quantity breach from a days-supply breach.
The disambiguation that trips teams up is 76 versus 79 and 70. The table draws the lines.
| Symptom on the claim | Correct code | Why not 76 |
|---|---|---|
442-E7 quantity above the per-fill max |
76 Plan Limitations Exceeded |
Canonical 76 quantity case |
405-D5 days supply above the per-fill max |
76 Plan Limitations Exceeded |
Canonical 76 days-supply case |
| Rolling dollar accumulator cap exceeded | 76 Plan Limitations Exceeded |
Canonical 76 dollar case |
| Refill requested before earliest-refill date | 79 Refill Too Soon |
A timing limit, not a per-fill amount limit |
| GPI is excluded from the formulary | 70 Product/Service Not Covered |
Not coverable at any quantity |
| GPI covered but needs prior authorization | 75 Prior Authorization Required |
Gated on PA, not on amount |
The operating rule: 76 is about how much on a covered drug, 79 is about how soon, 70 is about whether at all, and 75 is about whether gated. The handler must never let a refill-timing problem masquerade as 76, because the pharmacy remedies them differently — a 76 is fixed by dispensing less, a 79 by waiting.
Step-by-Step Implementation
The handler receives the parsed claim quantities and the plan limit set for the GPI, then checks each axis in a fixed order so the reported breach is deterministic.
1. Carry money as decimal.Decimal. Any accumulator or dollar cap is decimal.Decimal, quantized explicitly — never float. A cent of float drift compounds across an accumulator and produces off-by-one reject decisions.
2. Compare 442-E7 Quantity Dispensed against the quantity max. A quantity above the per-fill limit is a 76 on the quantity axis.
3. Compare 405-D5 Days Supply against the days-supply max. A days supply above the limit is a 76 on the days_supply axis.
4. Compare the projected accumulator against the dollar cap. If this fill would push the rolling accumulator past the cap, that is a 76 on the dollar axis.
5. Emit 76 with the tripped axis, or pass. Set 501-F1 Header Response Status to R, attach 511-FB = "76", tag the axis, and log PHI-safely.
import json
import logging
import enum
from decimal import Decimal, ROUND_HALF_UP
from typing import Optional
from pydantic import BaseModel, ConfigDict
logging.basicConfig(format="%(message)s", level=logging.INFO)
logger = logging.getLogger("reject_76")
CENTS = Decimal("0.01")
class LimitAxis(str, enum.Enum):
NONE = "none"
QUANTITY = "quantity"
DAYS_SUPPLY = "days_supply"
DOLLAR = "dollar"
class PlanLimit(BaseModel):
model_config = ConfigDict(frozen=True, extra="forbid")
max_quantity: Decimal # per-fill 442-E7 cap
max_days_supply: int # per-fill 405-D5 cap
dollar_cap: Optional[Decimal] = None # rolling accumulator cap
class Reject76Input(BaseModel):
model_config = ConfigDict(frozen=True, extra="forbid")
claim_ref: str # tokenized 402-D2, non-PHI
quantity_442e7: Decimal # 442-E7 Quantity Dispensed (Decimal)
days_supply_405d5: int # 405-D5 Days Supply
accrued_dollars: Decimal # accumulator to date (Decimal)
fill_dollars: Decimal # this fill's plan cost (Decimal)
class Reject76Result(BaseModel):
status: str # "PASS" or "REJECT"
reject_511fb: Optional[str] = None
axis: LimitAxis = LimitAxis.NONE
def handle_reject_76(inp: Reject76Input, limit: PlanLimit) -> Reject76Result:
axis = LimitAxis.NONE
# 2. Quantity axis: 442-E7 vs per-fill max.
if inp.quantity_442e7 > limit.max_quantity:
axis = LimitAxis.QUANTITY
# 3. Days-supply axis: 405-D5 vs per-fill max.
elif inp.days_supply_405d5 > limit.max_days_supply:
axis = LimitAxis.DAYS_SUPPLY
# 4. Dollar axis: projected accumulator vs cap (Decimal math only).
elif limit.dollar_cap is not None:
projected = (inp.accrued_dollars + inp.fill_dollars).quantize(
CENTS, rounding=ROUND_HALF_UP)
if projected > limit.dollar_cap:
axis = LimitAxis.DOLLAR
if axis is LimitAxis.NONE:
result = Reject76Result(status="PASS")
else:
result = Reject76Result(status="REJECT", reject_511fb="76", axis=axis)
# PHI GUARDRAIL: tokenized ref + 511-FB + axis only. Never 302-C2
# Cardholder ID, 310-CA Patient Name, or raw claim bytes.
logger.info(json.dumps({
"event": "reject_76_eval",
"claim_ref": inp.claim_ref,
"reject_511fb": result.reject_511fb,
"axis": result.axis.value,
}))
return resultThe axis order — quantity, then days supply, then dollars — is deterministic so a claim that breaches two limits always reports the same primary axis, which keeps replayed adjudications identical. The dollar comparison is the one place money enters: it uses decimal.Decimal end to end and quantizes to cents with ROUND_HALF_UP before comparing, because a float accumulator would drift a claim across the cap boundary non-deterministically. That discipline is the same one applied across the money paths in Rule Engine Threshold Tuning & Optimization.
Figure: The three plan-limit axes are checked in a fixed order — quantity, days supply, then the decimal.Decimal dollar accumulator — and the first breach reports the primary 76 axis.
Verification and Testing Pattern
Test each axis, the pass case, and the deterministic axis ordering when two limits breach at once.
import pytest
LIMIT = PlanLimit(max_quantity=Decimal("90"), max_days_supply=30,
dollar_cap=Decimal("500.00"))
def _inp(qty="30", days=30, accrued="0.00", fill="50.00"):
return Reject76Input(claim_ref="tok-76", quantity_442e7=Decimal(qty),
days_supply_405d5=days, accrued_dollars=Decimal(accrued),
fill_dollars=Decimal(fill))
def test_within_all_limits_passes():
assert handle_reject_76(_inp(), LIMIT).status == "PASS"
def test_quantity_over_rejects_76():
r = handle_reject_76(_inp(qty="120"), LIMIT)
assert r.reject_511fb == "76" and r.axis is LimitAxis.QUANTITY
def test_days_supply_over_rejects_76():
r = handle_reject_76(_inp(days=90), LIMIT)
assert r.axis is LimitAxis.DAYS_SUPPLY
def test_dollar_cap_over_rejects_76():
r = handle_reject_76(_inp(accrued="480.00", fill="50.00"), LIMIT)
assert r.axis is LimitAxis.DOLLAR
def test_quantity_wins_when_two_axes_breach():
# Quantity is checked first, so it is the reported axis.
r = handle_reject_76(_inp(qty="120", days=90), LIMIT)
assert r.axis is LimitAxis.QUANTITYtest_quantity_wins_when_two_axes_breach pins the deterministic ordering that keeps a replayed claim reporting the same axis, and test_dollar_cap_over_rejects_76 exercises the decimal.Decimal accumulator path where float drift would otherwise put the verdict in doubt.
Gotchas and PHI Guardrails
- Never use
floatfor the accumulator. A dollar cap compared against afloatsum drifts across the boundary unpredictably; usedecimal.Decimaland quantize to cents withROUND_HALF_UPbefore every comparison. 76is not79. A quantity or days-supply breach is76; a refill requested before the earliest-refill date is79Refill Too Soon. They read similarly at the counter but the remedy differs — dispense less versus wait — so keep the timing check in its own handler.- Fixed axis order. Check quantity, then days supply, then dollars, always in that order. Reordering makes a two-axis breach report different codes on replay and breaks audit reproducibility.
- Per-fill versus rolling limits. Quantity and days-supply caps are per-fill; the dollar cap is a rolling accumulator that must be read from a versioned, member-scoped snapshot. Do not compare a per-fill quantity against a rolling total.
405-D5is an integer count of days. Treat days supply as anint; a fractional or float days supply is a data defect that belongs to schema validation, not to the limit comparison. For variable dosing that produces a non-integer, resolve it upstream in Calculating Days Supply for Variable Dosing.- Log the axis, not the amounts in context. Log the tokenized
402-D2reference, the511-FBcode, and the axis; never log the accumulator alongside302-C2Cardholder ID,310-CAPatient Name, or raw claim bytes.
For the decimal-arithmetic rationale behind the accumulator math, see Python’s decimal module documentation.
Related
- NCPDP Reject Code Reference — the registry and dispatcher that rank
76below coverage and clinical codes. - Quantity Limit & Days Supply Validation — the plan-limit rules that supply the caps this handler compares against.
- Rule Engine Threshold Tuning & Optimization — how the limit thresholds behind
76are calibrated without over-rejecting. - Handling Reject Code 70: Product/Service Not Covered — the not-coverable case
76must not absorb. - Handling Reject Code 75: Prior Authorization Required — the gated-but-coverable sibling handler.
← Back to NCPDP Reject Code Reference