Drug Shortage API — Delta Monitor & FDA Shortage Tracker
Pricing
from $3.30 / 1,000 results
Drug Shortage API — Delta Monitor & FDA Shortage Tracker
Drug shortage API on the official openFDA feed: normalized shortage records, delta monitor mode that emits only NEW or status-CHANGED shortages (new/resolved/reappeared), and therapeutic-class + dosage-form rollups of current shortages. Keyless.
Pricing
from $3.30 / 1,000 results
Rating
0.0
(0)
Developer
Kyle Maloney
Maintained by CommunityActor stats
0
Bookmarked
1
Total users
1
Monthly active users
19 hours ago
Last modified
Categories
Share
The official openFDA drug/shortages feed, normalized into clean rows, with a delta monitor that emits only what actually changed since your last run. Keyless, no scraping.
Live as of 2026-08-01 the feed holds 1,636 records: 1,180 Current shortages, 432 planned discontinuations, 24 recently resolved.
Who this is for
- Hospital and health-system pharmacy — a daily scheduled run that tells you what entered shortage, what resolved, and what is being discontinued permanently.
- Group purchasing and supply-chain teams — substitution planning off the established pharmacologic class on the same row.
- 340B TPAs, PBMs, formulary committees — therapeutic-class and dosage-form rollups.
- Compounders and wholesalers —
discontinued_dateis the planning horizon. - AI agents — see "Use as an MCP tool".
What v1.1 corrects
All three of these were live, billing, and reporting SUCCESS.
The delta monitor had never worked. v1.0 kept its baseline in Apify's default
key-value store, which is created fresh for every run. Proven on 2026-08-01 by two
consecutive runs of the same input minutes apart: store WZg65xVw4OZPksVqB, then
Za29zsSQpiiU0EJ7E — 60 of 60 rows tagged change_type: "new" both times. A
scheduled daily run was billing for a full dump every day, labelled as newly-entered
shortages. v1.1 uses a named store with a scope-keyed baseline.
"To Be Discontinued" was invisible — 432 records, 26.4% of the feed. v1.0 knew only
Current and Resolved. You could not select that status, it was excluded from every
rollup, and a product entering permanent discontinuation fell through to a generic
updated — or, when the update date had not moved, to unchanged, which monitor mode
drops. A permanent product withdrawal was reported as a routine edit, or not at all.
The delta baseline keyed on a non-unique id. shortage_id (generic + company +
dosage form) yields only 481 distinct values across the 1,636-record feed - 70.6% of
records share one, and 12 ids cover 97 records whose members disagree on status
(Sodium Bicarbonate Injection / Hospira spans 12 records at different package NDCs, some
Current and some To Be Discontinued). This never showed while the baseline was always
empty. The first working v1.1 delta run produced 4 fabricated
discontinuation_cancelled rows on data that had not changed in two minutes. The
baseline now keys on record_key, and shortage_id keeps its original meaning.
maxResults defaulted to 1,000 against 1,180 Current records. The default input
silently read 84.7% of the feed, the rollup counted the subset, and the 180 dropped
records would have churned in and out of any baseline as fabricated events. The default
is now 3,000 and every row carries feed_complete.
Example input
{"status": "Current","maxResults": 3000,"includeRollup": true}
| Goal | Input |
|---|---|
| Daily change feed | { "monitor": true, "status": "all", "maxResults": 3000 } on a schedule |
| What is being discontinued | { "status": "To Be Discontinued", "maxResults": 3000 } |
| One therapeutic class | { "status": "Current", "therapeuticCategory": "Oncology" } — 135 live records |
| Recently resolved | { "status": "Resolved" } — 24 live records |
| Recent activity only | { "sinceDate": "2026-07-01" } |
Valid therapeuticCategory values include Oncology (135), Psychiatry (281),
Analgesia/Addiction (185), Cardiovascular (161).
sinceDate accepts four forms, and rejects everything else
2026-07-01 (ISO, preferred) · 2026-07 · 2026 · 07/01/2026 (US month/day/year).
Anything else fails the run instead of being guessed at, and that is
deliberate. sinceDate is compared against ISO dates, so an unusable format does
not raise an error downstream — it silently returns the wrong thing, and which
wrong thing depends on the first character:
| You send | What you used to get |
|---|---|
07/01/2026 | every record in the feed, labelled as filtered — "0..." sorts below every "20xx-.." |
Jul 2026 | zero records, which reads as "nothing has been updated since July" — "J" sorts above every "2..." |
Neither is visible in the output, so a failed run is the only honest answer. The
failure message names the field, echoes what you sent, and lists the accepted
forms. Day-first dates (01/07/2026 meaning 1 July) are rejected rather than
assumed, because 05/06/2026 is genuinely ambiguous and guessing would produce a
plausible wrong window instead of a visible error.
Fixed 2026-08-20. The same defect was found and fixed on PBGC Pension Failure Monitor, where a US-format date returned all 5,176 rows against a true 55.
Output fields
record_type distinguishes a shortage detail row from a rollup summary row.
Identity
| Field | Description |
|---|---|
record_type | shortage or rollup. |
shortage_id | Product-level grouping id: generic name + company + dosage form. Groups every presentation of one product. Not unique - live it yields 481 distinct values across 1,636 records. |
record_key | Unique per FDA record: shortage_id + package NDC + presentation. This is what the delta baseline keys on. |
generic_name | Generic (non-proprietary) drug name. |
proprietary_name | Brand name, else the first openFDA brand name. |
brand_names | All openFDA brand names, not only the first. |
company_name | Manufacturer / labeler reporting the record. |
manufacturer_names | All openFDA manufacturer names. |
dosage_form | Injection, Tablet, Capsule, ... |
strength | Always empty - openFDA does not publish a strength field on this endpoint (0 of 1,636 live records). Retained so existing pipelines do not break; the strength is inside presentation. |
presentation | Full presentation string. |
contact_info | Company contact phone. |
therapeutic_category | Therapeutic class(es). |
Status — a shortage and a discontinuation are different things
| Field | Description |
|---|---|
status | Current, To Be Discontinued, or Resolved. |
is_shortage | True for Current/Resolved — an actual supply shortage. Null when the record carries no status. |
is_discontinuation | True for To Be Discontinued — a planned permanent withdrawal. |
discontinued_date | When the product goes away. Present on ~28% of records; the whole point of a discontinuation row, and thrown away by v1.0. |
availability | FDA availability note (e.g. Limited Availability). |
shortage_reason | FDA-reported cause, when given. |
resolved_note | Note on a resolved record. |
related_info | Additional FDA note (allocation / wholesaler guidance). |
related_info_link | FDA link accompanying that note. |
update_type | Reverified, New, or Revised. |
initial_posting_date / update_date / change_date | FDA dates, normalized to YYYY-MM-DD. |
Codes and classification
| Field | Description |
|---|---|
package_ndc | The package NDC exactly as FDA publishes it — the code a pharmacy orders by. On every record. |
product_ndc | First product NDC (else package NDC). |
product_ndcs / package_ndcs | All openFDA NDCs. |
route | Route(s) of administration. |
substance_name | Active substance name(s). |
unii | Unique Ingredient Identifier(s). |
rxcui | RxNorm concept id(s). |
product_type | e.g. HUMAN PRESCRIPTION DRUG. |
application_number | NDA / ANDA / BLA. |
spl_set_id | SPL set id, stable across label revisions. |
pharm_class_epc | Established pharmacologic class — what to reach for when substituting. |
pharm_class_moa / pharm_class_cs / pharm_class_pe | Mechanism, chemical structure, physiologic effect. |
openfda_enriched | False when the record carries no openFDA block at all (~11% of the feed) — so an empty route means "not published", not "none". |
Delta / monitor mode
| Field | Description |
|---|---|
change_type | new, resolved, reappeared, discontinuing, discontinuation_cancelled, delisted, updated, unchanged. |
previous_status / previous_update_date | What the baseline held. Null on a new record. |
baseline_first_seen | When this id first entered the baseline. |
delta_baseline_key | The scope-keyed baseline used — a narrower run cannot overwrite a broader one. |
delta_baseline_size | Entries in the prior baseline. 0 means this was the first monitored run for this scope, so every record is legitimately new. |
delta_baseline_source | Which store the baseline came from. |
delta_disappeared_count | Baseline records not in this run's result set. |
delta_disappearance_classified | True only when the fetch was complete and unfiltered, so a disappearance can be proven to be a delisting. False means disappearances were carried forward — never reported as resolved. |
Rollup rows
| Field | Description |
|---|---|
rollup_dimension / rollup_key | therapeutic_category or dosage_form, and the value. |
current_shortage_count | Records with status Current. Unchanged meaning from v1.0 — it does not include discontinuations. |
discontinuing_count | Records with status To Be Discontinued. Invisible in v1.0. |
resolved_count | Records with status Resolved. |
rollup_total_count | All three. |
Provenance and completeness
| Field | Description |
|---|---|
feed_status | ok or unavailable. A run where openFDA did not answer fails and emits nothing. |
feed_error | The openFDA error. Null here is good news, not a dead column. |
feed_complete | true when everything openFDA reported as matching was retrieved. false when maxResults truncated it — the rollup is then over a subset. null when no total was reported. Never silently true. |
feed_total_reported / records_retrieved | What openFDA said matched, vs what we hold. |
truncation_note | What limited the run. Null on a complete fetch. |
feed_last_updated | openFDA meta.last_updated. |
query_scope | The status / category / since filter this row was produced under. |
source_system | Attribution. |
null means "not checked". false means "checked, and negative".
Which fields are empty, and how to populate them
Audited live on 2026-08-01 across four real runs totalling 1,329 rows. Every declared field is populated by at least one of these inputs, except where noted.
| Field(s) | Populating input (live-verified) |
|---|---|
resolved_note, change_date | {"status": "Resolved"} - 23 and 32 live records respectively |
discontinued_date | {"status": "To Be Discontinued"} - 431 live records |
related_info_link | any broad run - 45 live records |
rollup_dimension, rollup_key, current_shortage_count, discontinuing_count, resolved_count, rollup_total_count | {"includeRollup": true} |
change_type, baseline_first_seen, delta_* | {"monitor": true} |
truncation_note, feed_complete: false | any run where maxResults is below feed_total_reported |
openfda_enriched: false, empty route/substance_name/rxcui | about 11% of records carry no openFDA block |
Three fields are null on every healthy row by design:
feed_error- a feed outage fails the run and emits nothing, so this is null on every emitted row. It is a contract marker, not data.previous_status,previous_update_date- populated only when a record actually changes between two monitored runs, which requires FDA to publish a change. Pinned by offline fixtures so the path cannot rot.
And strength is always empty because openFDA does not publish it (see above).
Live source-integrity checks
Before a single billable row is produced, the Actor runs 10 checks against the live openFDA feed and fails the run if any regresses. Drug-supply information is acted on by pharmacies and hospitals; a confident wrong answer here is worse than no answer.
| Check | What it protects against |
|---|---|
feed_total_in_band | The feed collapsing to a stub. |
status_vocabulary_closed | FDA adding a status this Actor would silently mis-bucket — exactly how 432 "To Be Discontinued" records were reported as ordinary updates for a year. An unknown status fails the run. |
status_vocabulary_complete | A known status disappearing. |
discontinuation_bucket_populated | The discontinuation bucket emptying or being renamed. |
update_type_vocabulary_closed | FDA adding an update_type. |
positive_canary_category | therapeutic_category no longer matching a known-populated value (Oncology, 135 records). |
negative_control_category_empty | The category filter being silently dropped so everything matches. |
required_fields_present | FDA renaming a field this Actor emits. |
feed_freshness_days | FDA stopping updates — a stale feed silently answers last month's question. |
pagination_reconciles | skip= being ignored so every page repeats page one. |
openFDA answers both a genuine no-match and a query on a renamed field with the
same HTTP 404 NOT_FOUND. v1.0 converted both into a clean, billable zero result — so a
renamed therapeutic_category would have told a buyer there are no oncology shortages.
That case is now flagged on the row and caught by the checks above.
Every measured value is logged on every run. skipDriftAssertions: true overrides.
Pricing
Pay per result: $0.006 per row ($6 per 1,000), with graduated discounts on paid Apify plans. Delta mode is the cheap way to run this on a schedule — after the first run you pay only for what changed, typically a few dozen rows a day rather than 1,600.
Use as an MCP tool
Available to AI agents via mcp.apify.com. Every field carries a description.
"Is there a current shortage of any oncology injectable, and is anything in that class being discontinued?" →
{ "status": "all", "therapeuticCategory": "Oncology", "includeRollup": true }
Agents should read feed_complete, feed_status and delta_baseline_size before
asserting that something is new or resolved.
FAQ
Every row on my first monitored run says new. That is correct — there was no prior
baseline for that query scope. Check delta_baseline_size: 0 confirms it. The second
scheduled run shows real deltas. (v1.0 was permanently stuck in this state.)
Why did my delta run fail with a truncation message? A delta computed against a
partial fetch invents events for records it never read. Raise maxResults to at least
feed_total_reported and re-run.
Why is delta_disappearance_classified false? Because your query was filtered (by
status, category or date) or truncated. A record leaving a filtered result set left the
filter, not the feed — reporting that as "resolved" would be a fabricated event.
Run with status: "all" and no other filter to get provable delisted rows.
Is "To Be Discontinued" a shortage? No, and that is the point of is_shortage /
is_discontinuation. It is a planned permanent withdrawal that FDA publishes in the
same feed. current_shortage_count deliberately excludes it; discontinuing_count
counts it.
Why do several rows share a shortage_id? Because FDA publishes one record per
presentation. Group on shortage_id for a product-level view; use record_key when you
need row identity.
Is this the ASHP shortage list? No. This is the FDA's own drug shortage database via openFDA. ASHP maintains a separate, differently-scoped list.
Related Actors
- NADAC Drug Pricing API — Weekly NDC Price Movers — CMS acquisition-cost deltas per NDC. A shortage and a price spike are usually the same event seen from two sides.
- Medicaid Exclusion Screener, License Verifier — the rest of the healthcare shelf.
Data: FDA drug shortages via openFDA, a public, keyless API. Not medical advice and not a substitute for the FDA Drug Shortages database itself. openFDA data is provided unvalidated; see https://open.fda.gov/terms/.