NADAC Drug Pricing API — Weekly NDC Price Movers
Pricing
from $5.50 / 1,000 results
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
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
0
Monthly active users
3 days ago
Last modified
Categories
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:
| Column | What it is |
|---|---|
as_of_date | the weekly publication date — 30 snapshots in 2026 so far |
effective_date | the 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:
| Goal | Input |
|---|---|
| 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
| Field | Description |
|---|---|
ndc | 11-digit National Drug Code. |
ndc_description | Drug name/description as published in NADAC. |
pricing_unit | Unit the per-unit price is expressed in (EA, ML, GM, ...). |
classification_for_rate_setting | NADAC brand/generic classification (G = generic, B = brand). |
otc | Over-the-counter indicator (Y/N). |
pharmacy_type_indicator | Pharmacy type the survey covers (e.g. C/I = chain and independent). |
explanation_code | NADAC explanation code for how the rate was derived. |
The price move
| Field | Description |
|---|---|
latest_price | Most recent NADAC per-unit price in the window. |
latest_date | Effective date of the latest price. |
previous_price | The previous distinct per-unit price — never a duplicate of the latest. |
previous_date | Effective date of the previous price. Can never equal latest_date. |
abs_change | latest_price − previous_price, in pricing units. |
pct_change | Percent change between those two price points. Null when there is only one price point in the window, or the prior price was zero. |
direction | up, down, flat, or new (a single price point). |
days_between_prices | Calendar days between the previous and latest effective dates. |
prior_pct_change | The change immediately before the latest one — tells a one-off correction from a sustained climb. |
cumulative_pct_change | First price point in the window to the latest. |
volatility_pct | Standard deviation of the point-to-point percent changes. Null with fewer than three price points. |
The series
| Field | Description |
|---|---|
weeks_of_history | Number of weekly NADAC observations found for this NDC in the window. |
distinct_price_points | Number of distinct effective dates — the count that drives the delta. Always ≤ weeks_of_history. |
price_changes_in_window | How many times the price actually changed value. |
weeks_at_latest_price | How many weekly publications have carried the current price so far. |
first_price / first_date | Earliest price point in the window. |
min_price / min_price_date | Lowest price observed, and when. |
max_price / max_price_date | Highest price observed, and when. |
Brand vs generic
| Field | Description |
|---|---|
corresponding_generic_drug_nadac_per_unit | For 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_date | Effective date of that generic price. |
brand_generic_spread_pct | How 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
| Field | Description |
|---|---|
as_of_date | Newest NADAC publication week this NDC appears in. |
feed_as_of_date | Newest publication week present in the source for this run. |
is_latest_published_week | True when this NDC is in the current week. Null when the feed date could not be established. |
window_start / window_end | The price-history window this row was computed over. |
data_complete | true 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_scope | Plain-English version of the above. |
truncation_note | What limited the run (page cap, row cap, time budget, failed year). Null on a clean read. |
rows_retrieved / rows_reported_total | What we hold vs what CMS said matches. |
datasets_queried | Per-year outcome, as year:status pairs. |
datasets_failed | Years whose fetch failed, with the error. Null here is good news, not a dead column. |
dataset_status | ok, partially_unavailable, or all_unavailable. |
source_system / source_url | Attribution. |
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.
| Check | What it protects against |
|---|---|
yearly_datasets_discovered | CMS renaming the yearly-table title format. |
requested_year_dataset_present | The year covering your window having no dataset. |
required_columns_present | A column rename silently emptying every price. |
sort_parameter_applied | DKAN 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_present | The dataset being replaced by a stub. |
feed_freshness_days | CMS stopping publication — a stale feed silently answers last month's question. |
positive_canary_rows / _description / _price_band | A known-good NDC (00093505610, ATORVASTATIN 10 MG TABLET) resolving to the right drug at a sane price. |
negative_control_empty | A known-nonexistent NDC returning rows — which would mean the filter is no longer being applied. |
price_zero_sentinel_absent | NADAC 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.
Related Actors
- 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.