FDA Recall Monitor API — Drug, Device & Food Recalls (JSON)
Pricing
from $4.00 / 1,000 recall records
FDA Recall Monitor API — Drug, Device & Food Recalls (JSON)
Monitor FDA drug, device & food recalls for internal QA/compliance workflows via the official openFDA API. Not for public recall alerts — see openFDA terms.
Pricing
from $4.00 / 1,000 recall records
Rating
0.0
(0)
Developer
Moose & Raven
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
0
Monthly active users
an hour ago
Last modified
Categories
Share
An internal monitoring, QA, and compliance tool for tracking FDA drug, device, and food recalls against the official openFDA API — no scraping, no account required. Filter by company, severity classification, keyword, date range, and status; run once or on a schedule with incremental monitor mode.
This is not a public-facing recall alert service. openFDA's terms of use prohibit using its data to issue public recall alerts to consumers; this actor is built and intended for internal use — feeding a company's own QA, compliance, or supply-chain monitoring workflows — not for redistribution as a consumer notification product. See openFDA's terms for the authoritative restriction.
1. What it does
Queries three separate openFDA/FDA data sources — drug enforcement, food enforcement, and the CDRH device recall database — and normalizes them into one consistent JSON schema. You choose which categories to check and how to filter (company, severity, keyword, date range, status); the actor fetches matching recalls, flags anything it couldn't verify (a failed request, a rate-limit hit, a capped/truncated pull), and — in monitor mode — remembers what it has already reported so scheduled runs only surface genuinely new recalls.
2. How it works
- Choose which
categoriesto check (drug,device,food— default all three). - Optionally filter by
companies(recalling-firm name),classification(Class I/II/III severity — drug/food only),keywords(matchesproduct_descriptionorreason_for_recall),dateFrom/dateTo(recall-initiation date range), and/orstatus(the recall's current status — field name and value vocabulary differ by category, see Output fields below). - Every filter is applied server-side, against openFDA's own search API — this actor never fetches an entire category and filters it locally, which would over-fetch and slow down a narrow query.
- Optional incremental monitor mode: remembers which recalls it has already delivered per category (by the source's own stable ID, in a persistent named key-value store) and only reports genuinely NEW recalls on later runs — schedule it daily/weekly and get exactly the delta.
- A confirmed zero-match result is treated as a real, good outcome, not an error. openFDA
returns a distinguishable
404with a specificNOT_FOUNDerror code for a well-formed query with no matches (e.g. "this company has a clean recall record") — separate from a genuinely broken request (500). This actor tells these apart explicitly. - A failed fetch is never silently treated as "nothing new." Each category's failure pushes
an explicit
fetch-failed(orrate-limited) status record — one category's problem doesn't hide results from the others. maxRecordsPerCategory(default 500) caps each category independently; an optionalmaxRecordscaps the total across every category combined. If a category's real total exceeds what was fetched, an explicitcategory-truncatedrecord surfaces the true count — a capped pull is never silently reported as a complete one.
3. Input
| Field | Type | Notes |
|---|---|---|
categories | array | drug, device, food — default all three |
companies | array | Recalling-firm name(s) — tokenized phrase match, not exact-string |
classification | array | Class I / Class II / Class III — drug/food only, no effect on device |
keywords | array | Server-side text search against product_description/reason_for_recall |
dateFrom / dateTo | string | YYYYMMDD — filters by recall-initiation date |
status | array | Current recall status — field name AND value vocabulary differ by category |
monitorMode | boolean | Persistent incremental "new since last run" tracking |
maxRecordsPerCategory | integer | Per-category cap, default 500 |
maxRecords | integer | Optional aggregate cap across ALL categories combined, in addition to the per-category cap |
apiKey | string (secret) | Optional free openFDA key — raises the daily rate limit, not required |
See the Input tab for the full form with inline descriptions.
4. Output fields
Each dataset item has a recordType of "recall", "category-truncated",
"category-skipped", "fetch-failed", or "rate-limited". A "recall" record:
| Field | Type | Notes |
|---|---|---|
category | string | drug, device, or food |
recordId | string | null | recall_number (drug/food) or cfres_id (device) |
recallingFirm, productDescription, reasonForRecall | string | null | |
classification | string | null | Class I/II/III — always null for device (no such field exists) |
status | string | null | drug/food: status field; device: recall_status field — different field names for the same concept, mapped automatically |
city, state, country, address1, address2, postalCode | string | null | Firm location — country not present in device records |
distributionPattern | string | null | |
voluntaryMandated | string | null | drug/food only — always null for device (no comparable field) |
initiationDate, reportDate, terminationDate | string | null | Cross-category field-name mapping applied automatically (e.g. device's event_date_terminated vs. drug/food's termination_date) |
classificationDate | string | null | drug/food only (center_classification_date) — null for device |
productQuantity | string | null | |
codeInfo | string | null | Free text, exactly as FDA publishes it (e.g. "Lot #: 072915, Exp 10/29/2015") — not a structured field, do not parse assuming a fixed format |
ndcCodes, packageNdc | array of string | null | National Drug Code(s), from openFDA's nested openfda enrichment |
brandName, genericName, substanceName, manufacturerName | array of string | null | From openfda |
applicationNumber, route, rxcui, unii, openfdaProductType | array of string | null | From openfda |
fetchedAt | string (ISO datetime) | When this actor fetched the record |
sourceApi | string | Which openFDA endpoint this record came from |
Important, honest limitation on the openfda-derived fields: openFDA's nested openfda
enrichment object is not always populated — confirmed live, only ~18% of drug enforcement
records (3,206 of 17,793 at last check) have a non-empty openfda object; the rest return an
empty {}. When empty, every openfda-derived field above is set to an explicit null —
never a guess, never an empty array standing in for "unknown." When populated, every subfield is
a real array in the source data (even single-value fields), mapped as arrays here to avoid
silently dropping a recall with multiple NDC codes.
Status records (category-truncated, category-skipped, fetch-failed, rate-limited)
each carry category, message, and detectedAt — read the message, it explains exactly
what happened and what it means for that category's completeness this run.
Real sample output
Live-fetched from api.fda.gov/food/enforcement.json (input:
{"categories":["food"],"keywords":["listeria"],"status":["Terminated"],"dateFrom":"20200101","dateTo":"20241231"})
— not a fabricated example:
{"recordType": "recall","category": "food","recordId": "F-0757-2022","recallingFirm": "Dole Fresh Vegetables Inc","productDescription": "Marketside 12oz Classic Salad UPC:6-81131-32894-4 SKU: 3107","reasonForRecall": "Harvest equipment used in harvesting raw iceberg lettuce was tested by Dole and found to contain Listeria monocytogenes.","classification": "Class I","status": "Terminated","city": "Monterey","state": "CA","country": "United States","address1": "2959 Salinas Hwy","address2": "","postalCode": "93940-6400","distributionPattern": "Canada: AB, BC, NB, ON, QC, Sk; United States: AL, AZ, CA, CO, CT, FL, GA, HI, IA, ID, IL, IN, KS, KY, LA, MA, MD, MI, MN, MO, MS, NC, ND, NE, NV, NY, OH, OK, OR, PA, SC, TN, TX, UT, VA, WA, WI","voluntaryMandated": "Voluntary: Firm initiated","initiationDate": "20220106","reportDate": "20220302","terminationDate": "20220322","classificationDate": "20220222","productQuantity": "","codeInfo": "Product codes beginning with B - Best by Dates of December 23, 2021 through January 8, 2022.","ndcCodes": null,"packageNdc": null,"brandName": null,"genericName": null,"substanceName": null,"applicationNumber": null,"manufacturerName": null,"route": null,"rxcui": null,"unii": null,"openfdaProductType": null,"fetchedAt": "2026-07-17T22:22:57.008Z","sourceApi": "api.fda.gov/food/enforcement"}
Note the openfda-derived fields are all null here — food enforcement records don't carry
the NDC/brand/generic drug-identity enrichment (that's a drug-specific dataset feature); this
is the honest, expected shape for a food record, not a missing-data bug.
5. Use cases
- Pharma compliance — track recalls affecting your own NDC codes, or a competitor's/ supplier's product line, by company or keyword.
- Hospital/health-system procurement — monitor device and drug recalls for everything on formulary or in the supply chain, bundled in one feed instead of three separate lookups.
- Insurance & underwriting — screen a manufacturer's or distributor's recall history
(severity mix, frequency, resolution time via
initiationDate→terminationDate) as part of risk assessment. - Legal / M&A due diligence — pull a target company's full FDA recall history by firm name, with real classification and status, before closing.
- Healthcare software / EHR & supply-chain platforms — feed a normalized, deduplicated recall stream into an existing alerting or dashboard system via monitor mode.
6. Setting up recall alerts (Apify Schedules + your own notification channel)
This actor produces structured data; wiring that into an email/Slack/Teams alert is done with Apify's own platform features, not actor-specific code:
- Create an Apify Schedule pointing at this actor, with
monitorMode: trueand your filters set (e.g.companiesorkeywordsfor what you care about). Daily or weekly is typical for compliance monitoring. - Attach a Webhook to the schedule/actor for the
ACTOR.RUN.SUCCEEDEDevent. Apify's webhook payload includes the run's dataset ID, so your webhook target can fetch the new items. - Route the webhook to Slack/Teams via their native "incoming webhook" URL, or to email via
a lightweight relay (e.g. a Zapier/Make webhook-to-email step, or your own small function) —
since
monitorModeonly delivers genuinely new recalls, every triggered notification represents real new information, not a repeat. - Because the dataset always distinguishes
recallrecords fromcategory-skipped/fetch-failed/rate-limitedstatus records, your alert logic can (and should) treat the latter as "monitoring degraded this run" rather than silently trusting an empty result.
7. Classification guide (real examples from live FDA data)
FDA assigns one of three severity classes to drug and food recalls (device recalls have no classification field — see Limitations):
- Class I — reasonable probability that use of the product will cause serious adverse health consequences or death. Real example: CareFusion 213, LLC's recall of BD ChloraPrep Clear antiseptic applicators — reason: "Non-Sterility: Due to presence of Aspergillus penicillioides."
- Class II — use of the product may cause temporary or medically reversible adverse health consequences, with remote probability of serious harm. Real example: Mars Snacking's recall of MorningStar Farms Buffalo Chik'n Nuggets — reason: "Potential contamination with plastic material."
- Class III — use of the product is not likely to cause any adverse health consequence. Real example: Produce Innovations' recall of a roast beef & cheddar sandwich — reason: "Incorrect Product Label; Roast Beef and Swiss closed face sub sandwich labeled as Turkey and Cheddar closed face sub sandwich."
8. Pricing
Pay per event (current live pricing — confirmed via the Apify API at time of writing):
recall-record— $0.004 per new recall deliveredmonitor-run— $0.05 per scheduled monitor-mode run, in addition to per-record charges
monitor-run is billed only when nothing was compromised (no fetch failures, no rate-limit
hits) — a disclosed truncation/skip still bills normally (it's a known, surfaced cap, not a
blind spot), but a run that couldn't reliably confirm what it checked does not.
See the actor's Pricing tab for the authoritative, current numbers.
9. Use from Claude, Cursor, and other AI agents (MCP)
This actor can be called as a tool by any MCP-compatible AI client (Claude Desktop, Cursor, VS Code, etc.) via Apify's hosted MCP server, without any actor-specific integration code:
- Point your MCP client at
https://mcp.apify.com, authenticated with your Apify API token (as a bearer header or via OAuth — see Apify's MCP docs for client-specific config). - Expose this actor specifically with the
toolsquery parameter:https://mcp.apify.com?tools=mooseandraven/openfda-recall-monitor - Your agent can then call it directly — e.g. "check for any new Class I drug recalls from
Pfizer in the last 30 days" maps naturally onto
companies,classification,dateFrom. The actor's own input schema (this page's Input tab) is what the MCP tool schema is built from, so any filter documented here is available to the agent.
10. FAQ
How fresh is the data? openFDA's enforcement and device-recall datasets are refreshed weekly (per openFDA's own documentation) — this actor queries live at run time, so results are as current as openFDA's own weekly refresh, not cached or delayed further by this actor.
How far back does the data go? Per openFDA's own documentation: drug enforcement and food enforcement both cover 2004–present; device recalls cover 2002–present. In practice, the earliest genuinely dense, non-anomalous data in each dataset starts a few years into that window — and a small number of records carry visibly corrupted dates (e.g. a handful of device records with a garbled 4-digit year) that are a real, openFDA-side data-quality artifact, not something this actor introduces or can clean up.
Do I need an openFDA API key? No — openFDA works with no key at all (240 requests/minute,
1,000/day per IP). An optional free key (apiKey input) only raises the daily cap to 120,000,
useful only for high-volume scheduled monitoring across many filters.
What's the pricing/subscription model? Pay-per-event, not a flat subscription — you're charged per recall record actually delivered plus a per-monitor-run fee, so cost scales with how much genuinely new data the actor finds, not a flat rate regardless of activity. See Pricing above.
Why do I sometimes get a category-skipped or category-truncated record instead of just
recalls? This actor deliberately surfaces its own limits instead of hiding them — see Output
fields and How it works above. A flat "just the recalls" actor that never emits these is not
necessarily more reliable; it may simply not be telling you when a pull was capped or a category
couldn't be checked.
11. What it does NOT do / Limitations
- Not a public consumer alert service — see the ToS note at the top. Build internal workflows on top of this, not a public-facing notification product.
devicerecalls come from a genuinely different openFDA dataset thandrug/food.drugandfoodboth use openFDA's "enforcement" endpoints — the same underlying data model.deviceuses the separate CDRH recall database, with its own field names (confirmed live) and no classification concept. This actor's field mapping is fully consistent with that distinct source, but "device" isn't just "drug/food's data filtered to devices."- Device recalls have no severity classification field at all. The
classificationfilter has no effect on the device category, and every devicerecallrecord'sclassificationfield isnull— not "unknown," genuinely nonexistent in FDA's device dataset. statusfield name AND value vocabulary differ by category — drug/food's confirmed real values are"Ongoing"/"Completed"/"Terminated"; device's confirmed real values are"Open, Classified"/"Completed"/"Terminated"(not"Ongoing"). A status value drawn from one category's vocabulary has no effect on another category's results.companies/classification/statusfilters use TOKENIZED PHRASE matching, not a literal exact-string match and not fuzzy matching — a misspelled or very differently-phrased name will not match.- The
openfdaenrichment fields (NDC codes, brand/generic/substance names, etc.) are genuinely absent on most records, not an integration gap — see Output fields above. codeInfo(lot/expiry info) is free text, not a structured field — do not assume a fixed format when parsing it downstream.- No fuzzy/phonetic company-name matching, no cross-category correlation (e.g. flagging that one firm has recalls in multiple categories) — each category is filtered and reported independently.
Local development
npm installnpm test # recorded-fixture unit tests, no network needednpx apify-cli run # local end-to-end run — works immediately, no key/account needed
Support
Built and maintained by Moose & Raven. Questions or issues: support@mooseandraven.com.