NADAC Drug Pricing API — Weekly NDC Price Movers avatar

NADAC Drug Pricing API — Weekly NDC Price Movers

Pricing

from $5.50 / 1,000 results

Go to Apify Store
NADAC Drug Pricing API — Weekly NDC Price Movers

NADAC Drug Pricing API — Weekly NDC Price Movers

Drug pricing API built on CMS/Medicaid NADAC weekly data: per-NDC price time series, week-over-week deltas, and biggest-mover spike/drop detection. Filter by NDC or drug name over any date window to track generic drug price changes. Keyless, official source.

Pricing

from $5.50 / 1,000 results

Rating

0.0

(0)

Developer

Kyle Maloney

Kyle Maloney

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

0

Monthly active users

3 days ago

Last modified

Share

Turn the CMS/Medicaid NADAC weekly drug-price file into a per-NDC time series with real price-change deltas and a biggest-mover ranking. Keyless, official source, no scraping.

Most NADAC actors on the Store dump the current week's prices. This one stitches the weekly publications together, collapses them into actual price points, and tells you what moved, by how much, and how long the current price has been in force.


Who this is for

  • PBMs and pharmacy analytics teams — catch a generic spiking before it shows up in a reimbursement dispute.
  • 340B TPAs and covered entities — track acquisition-cost drift on the drugs you dispense most.
  • Formulary and rebate teams — brand-vs-generic per-unit spread comes back on the same row.
  • State Medicaid / MCO rate setters — NADAC is the benchmark; this is the delta.
  • AI agents — see "Use as an MCP tool" below.

How NADAC actually works (and why this matters for your query)

A NADAC yearly dataset is the stack of that year's weekly publications:

ColumnWhat it is
as_of_datethe weekly publication date — 30 snapshots in 2026 so far
effective_datethe date the current price took effect, which repeats in every weekly snapshot until the price actually changes

Live example, NDC 00093505610 (ATORVASTATIN 10 MG TABLET):

as_of 2026-06-17 .. 2026-07-15 -> effective_date 2026-06-17, price 0.02452 (5 snapshots)
as_of 2026-07-22 .. 2026-07-29 -> effective_date 2026-07-22, price 0.02403 (2 snapshots)

So "the last two rows" are usually the same price seen twice. This Actor collapses each series to distinct effective dates first (the newest publication wins if CMS restates a price), then computes the delta. pct_change for that NDC is −2.00%, not 0.00%.

Corrected in v1.1. Version 1.0 compared the raw last two rows. Measured on this Actor's own default input on 2026-08-01, that reported 273 of 278 NDC series (98.2%) as a 0.00% flat move, and zero as having moved at all, when the truth was 265 real movers and 13 genuinely flat. If you pulled data before 2026-08-01, re-run it.


Example input

{
"drugName": "atorvastatin",
"daysBack": 180,
"minPctChange": 0,
"direction": "all",
"maxResults": 50
}

Other useful shapes:

GoalInput
One NDC's full history{ "ndc": "0093-5056-10", "daysBack": 365 } — hyphens, spaces and dropped leading zeros are all accepted
Spikes only{ "drugName": "metformin", "minPctChange": 10, "direction": "up" }
Brand-vs-generic spread{ "drugName": "abilify", "daysBack": 180 } — populates corresponding_generic_drug_nadac_per_unit and brand_generic_spread_pct
Multi-year window{ "drugName": "insulin", "sinceDate": "2024-01-01" } — spans the yearly datasets automatically

Leaving both ndc and drugName empty scans every NDC in the window (~700,000 weekly rows over 180 days). That run may hit the page or time budget, and if it does every emitted row will say so via data_complete: false.


Output fields

One row per NDC, ranked by biggest absolute percent move.

Drug identity

FieldDescription
ndc11-digit National Drug Code.
ndc_descriptionDrug name/description as published in NADAC.
pricing_unitUnit the per-unit price is expressed in (EA, ML, GM, ...).
classification_for_rate_settingNADAC brand/generic classification (G = generic, B = brand).
otcOver-the-counter indicator (Y/N).
pharmacy_type_indicatorPharmacy type the survey covers (e.g. C/I = chain and independent).
explanation_codeNADAC explanation code for how the rate was derived.

The price move

FieldDescription
latest_priceMost recent NADAC per-unit price in the window.
latest_dateEffective date of the latest price.
previous_priceThe previous distinct per-unit price — never a duplicate of the latest.
previous_dateEffective date of the previous price. Can never equal latest_date.
abs_changelatest_price − previous_price, in pricing units.
pct_changePercent change between those two price points. Null when there is only one price point in the window, or the prior price was zero.
directionup, down, flat, or new (a single price point).
days_between_pricesCalendar days between the previous and latest effective dates.
prior_pct_changeThe change immediately before the latest one — tells a one-off correction from a sustained climb.
cumulative_pct_changeFirst price point in the window to the latest.
volatility_pctStandard deviation of the point-to-point percent changes. Null with fewer than three price points.

The series

FieldDescription
weeks_of_historyNumber of weekly NADAC observations found for this NDC in the window.
distinct_price_pointsNumber of distinct effective dates — the count that drives the delta. Always ≤ weeks_of_history.
price_changes_in_windowHow many times the price actually changed value.
weeks_at_latest_priceHow many weekly publications have carried the current price so far.
first_price / first_dateEarliest price point in the window.
min_price / min_price_dateLowest price observed, and when.
max_price / max_price_dateHighest price observed, and when.

Brand vs generic

FieldDescription
corresponding_generic_drug_nadac_per_unitFor a brand record, the per-unit price of the matching generic. Null on a generic record (normal). Populate it with {"drugName": "abilify"}.
corresponding_generic_drug_effective_dateEffective date of that generic price.
brand_generic_spread_pctHow much more the brand costs per unit than its generic, as a percent of the generic price.

Provenance and completeness — read these before you act on a ranking

FieldDescription
as_of_dateNewest NADAC publication week this NDC appears in.
feed_as_of_dateNewest publication week present in the source for this run.
is_latest_published_weekTrue when this NDC is in the current week. Null when the feed date could not be established.
window_start / window_endThe price-history window this row was computed over.
data_completetrue when every matching row CMS reported was retrieved. false when the fetch was truncated — the ranking is then over a subset. null when CMS reported no total, so completeness could not be established. Never silently true.
ranking_scopePlain-English version of the above.
truncation_noteWhat limited the run (page cap, row cap, time budget, failed year). Null on a clean read.
rows_retrieved / rows_reported_totalWhat we hold vs what CMS said matches.
datasets_queriedPer-year outcome, as year:status pairs.
datasets_failedYears whose fetch failed, with the error. Null here is good news, not a dead column.
dataset_statusok, partially_unavailable, or all_unavailable.
source_system / source_urlAttribution.

null means "not checked". false means "checked, and negative". If every yearly dataset fails, the run fails loudly and emits nothing rather than telling you no drugs moved.


Live source-integrity checks

Before a single billable row is produced, the Actor runs 11 checks against the live CMS datastore and fails the run if any regresses. Offline tests cannot catch a silently degraded upstream — the fixtures were shaped from the degraded data.

CheckWhat it protects against
yearly_datasets_discoveredCMS renaming the yearly-table title format.
requested_year_dataset_presentThe year covering your window having no dataset.
required_columns_presentA column rename silently emptying every price.
sort_parameter_appliedDKAN accepts an unapplied sort parameter, echoes it back verbatim and returns HTTP 200. If it ever stops honouring sorts, pagination becomes unordered and a truncated fetch becomes an arbitrary subset.
reported_total_presentThe dataset being replaced by a stub.
feed_freshness_daysCMS stopping publication — a stale feed silently answers last month's question.
positive_canary_rows / _description / _price_bandA known-good NDC (00093505610, ATORVASTATIN 10 MG TABLET) resolving to the right drug at a sane price.
negative_control_emptyA known-nonexistent NDC returning rows — which would mean the filter is no longer being applied.
price_zero_sentinel_absentNADAC starting to publish 0.00000 as a null sentinel, which would fabricate −100% moves.

Every measured value is logged on every run. If you hit what you believe is a false positive, skipDriftAssertions: true runs anyway — rows still carry dataset_status.


Pricing

Pay per result: $0.01 per row ($10 per 1,000), with graduated discounts on paid Apify plans. One row = one NDC's complete price-movement record. Use maxResults to cap spend, and minPctChange to pay only for drugs that actually moved.


Use as an MCP tool

Available to AI agents via mcp.apify.com. Every input and output field carries a description, so an agent can chain this cleanly:

"Has the acquisition cost of metformin 500 mg moved more than 5% in the last quarter?" → { "drugName": "metformin 500", "daysBack": 90, "minPctChange": 5 }

Agents should read data_complete and dataset_status before asserting a ranking.


FAQ

Why is previous_date weeks before latest_date instead of exactly one week? Because NADAC prices only change when they change. previous_date is the previous distinct price point. days_between_prices and weeks_at_latest_price tell you the gap.

Why does weeks_of_history say 24 but distinct_price_points say 6? 24 weekly publications carried this NDC; the price took 6 distinct values across them. Both are true and both are useful.

Why did my broad query come back with data_complete: false? An unfiltered 180-day window is roughly 700,000 weekly rows. The run hit a budget. Narrow by drugName, ndc, or a shorter window — or accept the row's own statement that the ranking is over a subset.

Zero results for my NDC. NADAC covers outpatient drugs reimbursed by Medicaid, and codes retire. The Actor logs whether the query reached CMS, so a genuine empty answer is distinguishable from an outage. Try a wider window or search by name.

Is this the same as WAC or AWP? No. NADAC is the survey-based acquisition cost retail pharmacies actually pay. That is the point.


  • Drug Shortage API — Delta Monitor & FDA Shortage Tracker — openFDA shortage feed with new/resolved/reappeared deltas. Same buyer, adjacent question.
  • CMS Open Payments and Medicaid Exclusion Screener — the rest of the CMS shelf.

Data: CMS / Medicaid.gov NADAC (National Average Drug Acquisition Cost), a public, keyless dataset. This Actor is a screening and monitoring tool, not a pricing determination or reimbursement advice.