Drug Prior Auth Goes FHIR: What Developers Need to Build
CMS-0062-P extends FHIR prior auth mandates to drugs. Here's every terminology lookup your implementation needs — with working TypeScript code.

The CMS Interoperability and Prior Authorization Final Rule (CMS-0057-F) already requires payers to stand up FHIR-based prior authorization APIs for non-drug items and services by January 2027. In April 2026, CMS published the Interoperability Standards and Prior Authorization for Drugs Proposed Rule (CMS-0062-P), extending those same mandates to drug prior authorization. The comment period closes June 15, 2026.
If you're building prior auth infrastructure at a payer, PBM, or health IT vendor, drugs just landed on your roadmap. This post walks through the terminology lookups a drug prior auth implementation actually needs — and shows working code for each one.
Who's affected
CMS-0062-P covers the same payer categories as CMS-0057-F:
- Medicare Advantage (MA) organizations
- Medicaid managed care plans
- CHIP (Children's Health Insurance Program) agencies and managed care entities
- Qualified Health Plan (QHP) issuers on federally facilitated exchanges
These payers must support electronic prior authorization for drugs with shorter decision timeframes, report API usage metrics to CMS, and implement updated FHIR Implementation Guides (IGs) for the Prior Authorization API, Provider Access API, and Payer-to-Payer API.
The terminology problem
A drug prior auth request is a bundle of coded data. The prescriber sends a drug identifier (NDC or RxNorm code), a diagnosis justifying the prescription (ICD-10-CM), their provider identity (NPI), and sometimes associated procedure codes (HCPCS). Your system needs to validate, enrich, and adjudicate against all of these.
Each of those code systems has its own source, its own update cadence, and its own edge cases. The rest of this post shows how to handle each one with the @fhirfly-io/terminology SDK.
Setup
import { Fhirfly, NotFoundError, RateLimitError } from "@fhirfly-io/terminology";
const client = new Fhirfly({
apiKey: process.env.FHIRFLY_API_KEY!,
});
Step 1: Identify the drug (NDC)
The National Drug Code (NDC) is the universal drug identifier in US healthcare. When a prior auth request arrives, the first thing you need is structured data about the drug being requested.
const drug = await client.ndc.lookup("0069-0151-01", { shape: "full" });
console.log(drug.data.brand_name); // "Lipitor"
console.log(drug.data.generic_name); // "Atorvastatin Calcium"
console.log(drug.data.dosage_form); // "TABLET"
console.log(drug.data.strength); // "10 mg"
console.log(drug.data.dea_schedule); // null (not a controlled substance)
console.log(drug.data.is_active); // true
console.log(drug.data.pharm_class); // ["HMG-CoA Reductase Inhibitors"]
console.log(drug.data.active_ingredients); // [{ name: "Atorvastatin Calcium", strength: "10 mg", unit: "mg" }]
The shape: "full" option returns everything: active ingredients, pharmacologic class, DEA schedule, marketing category, and cross-system mappings. For prior auth, you need most of this — the drug class tells you which formulary tier to check, the DEA schedule determines controlled substance handling, and is_active catches requests for discontinued products.
Every response includes a FHIR-ready coding object:
console.log(drug.data.fhir_coding);
// {
// system: "http://hl7.org/fhir/sid/ndc",
// code: "0069-0151-01",
// display: "Lipitor 10mg Tablet"
// }
This drops directly into a FHIR MedicationRequest or Claim resource without transformation.
Step 2: Validate the diagnosis (ICD-10-CM)
Every drug prior auth needs a supporting diagnosis. The ICD-10-CM code justifies why this medication is being prescribed. Your system needs to verify the code is valid, billable, and appropriate for the drug being requested.
const diagnosis = await client.icd10.lookup("E11.9", { shape: "full" });
console.log(diagnosis.data.type); // "cm" (clinical modification, not procedure)
console.log(diagnosis.data.display); // "Type 2 diabetes mellitus without complications"
console.log(diagnosis.data.billable); // true
console.log(diagnosis.data.chapter); // "4"
console.log(diagnosis.data.fhir_coding);
// {
// system: "http://hl7.org/fhir/sid/icd-10-cm",
// code: "E11.9",
// display: "Type 2 diabetes mellitus without complications"
// }
The SDK auto-detects whether a code is ICD-10-CM (diagnosis) or ICD-10-PCS (procedure) — no separate endpoints. The billable field is critical: non-billable header codes (like E11 without a fourth character) should be flagged during intake rather than rejected downstream.
With shape: "full", you also get HCC (Hierarchical Condition Category) mappings, which matter for MA plans doing risk adjustment alongside prior auth:
if (diagnosis.data.hcc?.length) {
for (const hcc of diagnosis.data.hcc) {
console.log(`HCC ${hcc.cc_number} (${hcc.model_type} ${hcc.model_version})`);
}
}
And SNOMED CT cross-references for clinical interoperability:
if (diagnosis.data.snomed?.length) {
for (const mapping of diagnosis.data.snomed) {
console.log(`SNOMED ${mapping.concept_id}: ${mapping.display} (${mapping.map_type})`);
}
}
Step 3: Verify the prescriber (NPI)
The National Provider Identifier (NPI) identifies who is prescribing the drug. Prior auth systems need to confirm the provider is active, check their specialty (is a cardiologist prescribing a dermatology drug?), and verify their practice location.
const provider = await client.npi.lookup("1234567890");
console.log(provider.data.entity_type); // "individual"
console.log(provider.data.is_active); // true
console.log(provider.data.name?.first); // "Jane"
console.log(provider.data.name?.last); // "Smith"
console.log(provider.data.name?.credential_text); // "MD"
// Check primary specialty
const primary = provider.data.taxonomies.find(t => t.primary);
console.log(primary?.classification); // "Internal Medicine"
console.log(primary?.specialization); // "Endocrinology"
// Practice location
console.log(provider.data.practice_address?.state); // "CA"
The taxonomy data includes the provider's specialty classification and specialization, which you can match against formulary policies that restrict certain drugs to specific prescriber types.
Step 4: Cross-reference with RxNorm
RxNorm is the standard drug terminology used in FHIR resources and clinical decision support. NDC codes are package-level identifiers; RxNorm provides the clinical concept layer — ingredient, dose form, and branded vs. generic relationships. The NDC response includes rxcui values that bridge directly into RxNorm:
// The NDC lookup already gave us rxcui values
const rxcui = drug.data.rxcui[0]; // e.g., "213169"
const rxnorm = await client.rxnorm.lookup(rxcui, { shape: "full" });
console.log(rxnorm.data.name); // "atorvastatin 10 MG Oral Tablet"
console.log(rxnorm.data.tty); // "SCD" (Semantic Clinical Drug)
console.log(rxnorm.data.prescribable); // true
console.log(rxnorm.data.ingredients); // [{ rxcui: "83367", name: "atorvastatin" }]
console.log(rxnorm.data.drug_classes); // [{ class_name: "HMG-CoA Reductase Inhibitors", ... }]
// Contraindications from enrichment
if (rxnorm.data.contraindications?.length) {
for (const ci of rxnorm.data.contraindications) {
console.log(`${ci.disease_name} (${ci.severity}): ${ci.relationship}`);
}
}
// FHIR coding for the clinical drug concept
console.log(rxnorm.data.fhir_coding);
// {
// system: "http://www.nlm.nih.gov/research/umls/rxnorm",
// code: "213169",
// display: "atorvastatin 10 MG Oral Tablet"
// }
The tty (term type) field tells you what level of the RxNorm hierarchy you're looking at. SCD is a generic clinical drug, SBD is a branded drug, IN is a bare ingredient. For formulary matching, you typically want the SCD or SBD level.
Step 5: Handle injectable drugs (J-Code crosswalk)
For drugs administered by injection in a clinical setting (Medicare Part B), billing uses HCPCS J-codes rather than NDC codes. The J-Code/NDC crosswalk maps between the two systems. This matters for prior auth when a request comes in with an NDC but your coverage policy references J-codes, or vice versa.
// NDC → J-Code direction
const jcodeResult = await client.jcode.byNdc("00004110002");
console.log(jcodeResult.ndc); // "00004110002"
console.log(jcodeResult.ndc_hyphenated); // "0000-4110-02"
for (const entry of jcodeResult.hcpcs_codes) {
console.log(entry.hcpcs_code); // "J9035"
console.log(entry.hcpcs_description); // "Injection, bevacizumab, 10 mg"
console.log(entry.hcpcs_dosage); // "10 MG"
console.log(entry.bill_units); // Number of billing units
}
Going the other direction — from a J-code to all associated NDCs:
// J-Code → NDC direction
const ndcResult = await client.jcode.byHcpcs("J9035");
console.log(`${ndcResult.hcpcs_code}: ${ndcResult.ndc_count} NDCs`);
for (const entry of ndcResult.entries) {
console.log(` ${entry.ndc_hyphenated} | ${entry.drug_name} | ${entry.labeler_name}`);
}
Step 6: Check coverage and billing rules
Once you've identified the drug, diagnosis, and provider, the next step is checking whether the drug is covered and what billing constraints apply. The claims intelligence APIs cover the four main CMS edit systems.
Coverage policies (LCD/NCD)
Local Coverage Determinations (LCDs) and National Coverage Determinations (NCDs) define what Medicare covers and under what conditions. For injectable drugs billed under HCPCS codes, coverage checks are essential:
const coverage = await client.claims.checkCoverage("J9035");
console.log(coverage.data.hcpcs_code); // "J9035"
console.log(coverage.data.policies_found); // Number of matching policies
console.log(coverage.data.summary); // Human-readable summary
for (const policy of coverage.data.policies) {
console.log(policy.policy_type); // "lcd" or "ncd"
console.log(policy.display_id); // e.g., "L33822"
console.log(policy.policy_title);
console.log(policy.is_active);
console.log(policy.effective_date);
}
Maximum units (MUE)
Medically Unlikely Edits set per-code unit limits. A request exceeding the MUE for a code triggers automatic denial in many adjudication systems:
const mue = await client.claims.lookupMue("J9035");
for (const limit of mue.data.limits) {
console.log(limit.service_type); // "practitioner"
console.log(limit.mue_value); // Max units per line
console.log(limit.adjudication_indicator_display); // How the limit is applied
console.log(limit.rationale);
}
NCCI edits (code pair validation)
When a prior auth request includes multiple procedure codes alongside the drug, NCCI Procedure-to-Procedure edits determine whether those codes can be billed together:
const ncci = await client.claims.validateNcci("J9035", "96413");
console.log(ncci.data.can_bill_together); // true or false
console.log(ncci.data.summary); // Human-readable explanation
for (const edit of ncci.data.edits) {
console.log(edit.claim_type); // "practitioner" or "hospital"
console.log(edit.modifier_allowed); // Can a modifier override the edit?
console.log(edit.rationale);
}
Physician Fee Schedule / RVU pricing
For understanding the payment implications of a prior auth decision, the PFS lookup gives you Relative Value Units and calculated payment amounts:
const pfs = await client.claims.lookupPfs("96413"); // Chemo admin, first hour
console.log(pfs.data.description);
console.log(pfs.data.rvu.work); // Work RVU
console.log(pfs.data.rvu.total_non_facility); // Total RVU (non-facility)
console.log(pfs.data.conversion_factor); // Current conversion factor
console.log(pfs.data.calculated_payment.non_facility); // Calculated payment in USD
Batch processing
Prior auth systems process requests in volume. Every terminology endpoint supports batch lookups, so you can validate an entire request's worth of codes in parallel rather than making serial calls.
// Validate all diagnosis codes in a single call (up to 100)
const diagnoses = await client.icd10.lookupMany(["E11.9", "I10", "E78.5"]);
for (const item of diagnoses.results) {
if (item.status === "ok") {
console.log(`${item.input}: ${item.data?.display} | billable: ${item.data?.billable}`);
} else if (item.status === "not_found") {
console.log(`${item.input}: NOT FOUND — flag for review`);
} else if (item.status === "invalid") {
console.log(`${item.input}: INVALID — ${item.error}`);
}
}
// Batch NDC lookups (up to 500)
const drugs = await client.ndc.lookupMany([
"0069-0151-01",
"0069-0151-02",
"0000-0000-00" // invalid
]);
// Batch MUE lookups (up to 100)
const mues = await client.claims.lookupMueMany(["J9035", "96413", "96415"]);
Each batch response uses the same structure: every item includes the original input, a status field ("ok", "not_found", or "invalid"), and the data or error payload. Invalid or missing codes don't fail the entire batch — they're reported individually so your intake logic can flag them without blocking the rest of the request.
Error handling
The SDK provides typed error classes for every failure mode you'll encounter in production:
import {
Fhirfly,
NotFoundError,
RateLimitError,
QuotaExceededError,
ValidationError,
} from "@fhirfly-io/terminology";
try {
const result = await client.ndc.lookup(ndcFromRequest);
} catch (error) {
if (error instanceof NotFoundError) {
// NDC not in the database — may be newly assigned or incorrectly formatted
console.log(`Unknown NDC: ${error.code_value} (${error.code_type})`);
} else if (error instanceof ValidationError) {
// Malformed input — wrong length, invalid characters
console.log(`Invalid input: ${error.message}`);
} else if (error instanceof RateLimitError) {
// Back off and retry
console.log(`Rate limited. Retry after ${error.retryAfter}s`);
} else if (error instanceof QuotaExceededError) {
// Monthly quota hit — upgrade plan or wait for reset
console.log(`Quota exceeded: ${error.quotaUsed}/${error.quotaLimit}`);
}
}
For prior auth systems, the distinction between NotFoundError and ValidationError matters. A NotFoundError means the code is well-formed but doesn't exist in the dataset — this could be a newly assigned NDC that hasn't been ingested yet, which is different from a ValidationError where the code format itself is wrong. Your adjudication logic should handle these differently: pend for review vs. reject at intake.
Putting it together
Here's a condensed version of what a drug prior auth validation function looks like when you wire these lookups together:
import { Fhirfly, NotFoundError } from "@fhirfly-io/terminology";
const client = new Fhirfly({ apiKey: process.env.FHIRFLY_API_KEY! });
interface PriorAuthRequest {
ndc: string;
diagnosisCodes: string[];
prescriberNpi: string;
}
interface ValidationResult {
drug: { brandName: string | null; genericName: string | null; isActive: boolean };
diagnoses: Array<{ code: string; display: string; billable: boolean }>;
prescriber: { name: string; specialty: string; isActive: boolean };
flags: string[];
}
async function validateDrugPriorAuth(
request: PriorAuthRequest
): Promise<ValidationResult> {
const flags: string[] = [];
// Run lookups in parallel
const [drugResult, diagnosisResult, providerResult] = await Promise.all([
client.ndc.lookup(request.ndc, { shape: "full" }),
client.icd10.lookupMany(request.diagnosisCodes),
client.npi.lookup(request.prescriberNpi),
]);
// Flag inactive drugs
if (!drugResult.data.is_active) {
flags.push(`Drug NDC ${request.ndc} is inactive/discontinued`);
}
// Flag controlled substances
if (drugResult.data.dea_schedule) {
flags.push(`Controlled substance: DEA Schedule ${drugResult.data.dea_schedule}`);
}
// Flag non-billable or missing diagnoses
const validDiagnoses = [];
for (const item of diagnosisResult.results) {
if (item.status === "ok" && item.data) {
if (!item.data.billable) {
flags.push(`Diagnosis ${item.input} is not billable (header code)`);
}
validDiagnoses.push({
code: item.input,
display: item.data.display,
billable: item.data.billable ?? false,
});
} else {
flags.push(`Diagnosis ${item.input} not found — requires manual review`);
}
}
// Flag inactive prescribers
if (!providerResult.data.is_active) {
flags.push(`Prescriber NPI ${request.prescriberNpi} is not active`);
}
const primary = providerResult.data.taxonomies.find(t => t.primary);
return {
drug: {
brandName: drugResult.data.brand_name,
genericName: drugResult.data.generic_name,
isActive: drugResult.data.is_active,
},
diagnoses: validDiagnoses,
prescriber: {
name: `${providerResult.data.name?.first} ${providerResult.data.name?.last}`,
specialty: primary?.classification ?? "Unknown",
isActive: providerResult.data.is_active,
},
flags,
};
}
This validates the core data elements in parallel with Promise.all, collects flags for anything that needs human review, and returns a structured result your adjudication engine can act on. In production, you'd extend this with the RxNorm, J-Code, and claims intelligence lookups shown earlier based on your specific formulary rules.
Key takeaways
-
CMS-0062-P extends FHIR prior auth mandates to drugs. If your organization already started work on CMS-0057-F for non-drug prior auth, the drug extension uses the same Da Vinci Implementation Guides (PAS, CRD, DTR) — but the terminology requirements expand significantly because drugs bring NDC, RxNorm, and J-Code into scope.
-
Every prior auth decision touches multiple code systems. A single drug request requires NDC (drug identification), ICD-10-CM (diagnosis validation), NPI (prescriber verification), and potentially RxNorm (clinical classification), HCPCS/J-Codes (billing), and SNOMED CT (interoperability). Batch APIs let you validate all of these in parallel rather than making sequential calls.
-
FHIR coding objects come built-in. Every lookup returns a
fhir_codingobject with the correctsystem,code, anddisplayvalues — ready to embed in FHIR resources without manual mapping. This matters when you're building the Da Vinci PAS response bundles that CMS-0062-P requires. -
The comment period closes June 15, 2026. The proposed rule is not yet final, but the direction is clear: drug prior auth is getting the same FHIR API treatment that non-drug items received in CMS-0057-F. Building against these APIs now means you're ready regardless of the final effective date.
Further reading
- CMS-0062-P Fact Sheet — the proposed rule summary
- ONC Proposals in CMS-0062-P — updated IG requirements
- FHIRfly NDC API docs — full NDC lookup and search reference
- FHIRfly NPI API docs — provider lookup and search
- FHIRfly SDK on npm — install and get started
Related Posts
Written by The FHIRfly Team — a collective of healthcare data experts, AI specialists, and industry veterans building better clinical coding APIs.