Drug Shortage API — Delta Monitor & FDA Shortage Tracker avatar

Drug Shortage API — Delta Monitor & FDA Shortage Tracker

Pricing

from $3.30 / 1,000 results

Go to Apify Store
Drug Shortage API — Delta Monitor & FDA Shortage Tracker

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

Kyle Maloney

Maintained by Community

Actor stats

0

Bookmarked

1

Total users

1

Monthly active users

19 hours ago

Last modified

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 wholesalersdiscontinued_date is 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 Za29zsSQpiiU0EJ7E60 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
}
GoalInput
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 sendWhat you used to get
07/01/2026every record in the feed, labelled as filtered — "0..." sorts below every "20xx-.."
Jul 2026zero 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

FieldDescription
record_typeshortage or rollup.
shortage_idProduct-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_keyUnique per FDA record: shortage_id + package NDC + presentation. This is what the delta baseline keys on.
generic_nameGeneric (non-proprietary) drug name.
proprietary_nameBrand name, else the first openFDA brand name.
brand_namesAll openFDA brand names, not only the first.
company_nameManufacturer / labeler reporting the record.
manufacturer_namesAll openFDA manufacturer names.
dosage_formInjection, Tablet, Capsule, ...
strengthAlways 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.
presentationFull presentation string.
contact_infoCompany contact phone.
therapeutic_categoryTherapeutic class(es).

Status — a shortage and a discontinuation are different things

FieldDescription
statusCurrent, To Be Discontinued, or Resolved.
is_shortageTrue for Current/Resolved — an actual supply shortage. Null when the record carries no status.
is_discontinuationTrue for To Be Discontinued — a planned permanent withdrawal.
discontinued_dateWhen the product goes away. Present on ~28% of records; the whole point of a discontinuation row, and thrown away by v1.0.
availabilityFDA availability note (e.g. Limited Availability).
shortage_reasonFDA-reported cause, when given.
resolved_noteNote on a resolved record.
related_infoAdditional FDA note (allocation / wholesaler guidance).
related_info_linkFDA link accompanying that note.
update_typeReverified, New, or Revised.
initial_posting_date / update_date / change_dateFDA dates, normalized to YYYY-MM-DD.

Codes and classification

FieldDescription
package_ndcThe package NDC exactly as FDA publishes it — the code a pharmacy orders by. On every record.
product_ndcFirst product NDC (else package NDC).
product_ndcs / package_ndcsAll openFDA NDCs.
routeRoute(s) of administration.
substance_nameActive substance name(s).
uniiUnique Ingredient Identifier(s).
rxcuiRxNorm concept id(s).
product_typee.g. HUMAN PRESCRIPTION DRUG.
application_numberNDA / ANDA / BLA.
spl_set_idSPL set id, stable across label revisions.
pharm_class_epcEstablished pharmacologic class — what to reach for when substituting.
pharm_class_moa / pharm_class_cs / pharm_class_peMechanism, chemical structure, physiologic effect.
openfda_enrichedFalse 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

FieldDescription
change_typenew, resolved, reappeared, discontinuing, discontinuation_cancelled, delisted, updated, unchanged.
previous_status / previous_update_dateWhat the baseline held. Null on a new record.
baseline_first_seenWhen this id first entered the baseline.
delta_baseline_keyThe scope-keyed baseline used — a narrower run cannot overwrite a broader one.
delta_baseline_sizeEntries in the prior baseline. 0 means this was the first monitored run for this scope, so every record is legitimately new.
delta_baseline_sourceWhich store the baseline came from.
delta_disappeared_countBaseline records not in this run's result set.
delta_disappearance_classifiedTrue 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

FieldDescription
rollup_dimension / rollup_keytherapeutic_category or dosage_form, and the value.
current_shortage_countRecords with status Current. Unchanged meaning from v1.0 — it does not include discontinuations.
discontinuing_countRecords with status To Be Discontinued. Invisible in v1.0.
resolved_countRecords with status Resolved.
rollup_total_countAll three.

Provenance and completeness

FieldDescription
feed_statusok or unavailable. A run where openFDA did not answer fails and emits nothing.
feed_errorThe openFDA error. Null here is good news, not a dead column.
feed_completetrue 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_retrievedWhat openFDA said matched, vs what we hold.
truncation_noteWhat limited the run. Null on a complete fetch.
feed_last_updatedopenFDA meta.last_updated.
query_scopeThe status / category / since filter this row was produced under.
source_systemAttribution.

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_linkany 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: falseany run where maxResults is below feed_total_reported
openfda_enriched: false, empty route/substance_name/rxcuiabout 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.

CheckWhat it protects against
feed_total_in_bandThe feed collapsing to a stub.
status_vocabulary_closedFDA 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_completeA known status disappearing.
discontinuation_bucket_populatedThe discontinuation bucket emptying or being renamed.
update_type_vocabulary_closedFDA adding an update_type.
positive_canary_categorytherapeutic_category no longer matching a known-populated value (Oncology, 135 records).
negative_control_category_emptyThe category filter being silently dropped so everything matches.
required_fields_presentFDA renaming a field this Actor emits.
feed_freshness_daysFDA stopping updates — a stale feed silently answers last month's question.
pagination_reconcilesskip= 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.


  • 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/.