openFDA API: FDA Recalls, Drug Labels & Device Reports avatar

openFDA API: FDA Recalls, Drug Labels & Device Reports

Pricing

from $1.40 / 1,000 fda record returneds

Go to Apify Store
openFDA API: FDA Recalls, Drug Labels & Device Reports

openFDA API: FDA Recalls, Drug Labels & Device Reports

Query the official openFDA API in one actor: drug, device and food recall enforcement reports, SPL drug labels by generic or brand name, and device adverse-event (MAUDE) reports with detail links that open. Filter by keyword, recall class, status, state, event type and date. No API key.

Pricing

from $1.40 / 1,000 fda record returneds

Rating

0.0

(0)

Developer

Samat Makatov

Samat Makatov

Maintained by Community

Actor stats

0

Bookmarked

1

Total users

0

Monthly active users

2 hours ago

Last modified

Share

Pull four kinds of FDA record straight from the official openFDA API (api.fda.gov) as flat, ready-to-use rows: recall enforcement reports for drugs, medical devices and food; structured product labels (SPL) for any brand or generic drug name; medical device adverse event reports from MAUDE; and the export status of every openFDA dataset. Every row carries the same handful of shared columns (recordId, title, organization, category, date, url), every date is UTC ISO-8601, and every report links back to the exact openFDA record it came from. No API key needed (your own free key is optional and raises openFDA's daily limit), no proxy, no browser.

The data is US public-domain FDA data. It is not medical advice: a recall or an adverse event record is a report, not proof that a product caused harm, and the datasets can be incomplete or several weeks behind.

Need it tailored to your workflow? Missing a field, a filter, another source or a different output format? Open an issue and describe what you need — we read every request and adapt the actor.

Use cases

  • Pharmacovigilance and pharmacy compliance: a standing list of Class I drug recalls of the last 12 months, with the reason, the recalling firm and the distribution pattern, to check against your own stock lists.
  • Food safety and retail QA: every food recall whose stated reason mentions an undeclared allergen or ingredient, so a grocery or private-label buyer can react the same week the recall is published.
  • Hospital biomedical engineering and device distributors: the running list of device recall enforcement reports, filtered to a device family or a manufacturer, to match against installed equipment.
  • Clinical and pharmacy app developers: the newest SPL label per generic name (set id, version, NDC codes, route, labeler, application number), which answers "which label do I show for this drug" without parsing SPL XML.
  • Device safety, quality and product-liability research: MAUDE reports for one device type or product code, with device and patient problem codes and a detail link that actually opens the report on the FDA site.
  • Data engineering: mode: "datasets" answers "how fresh is FDA data right now" — export date and record count for each dataset openFDA publishes (31 on 2026-10-02) — before a pipeline run trusts them.
  • Monitoring: sinceDays plus onlyNew turns any of the above into a scheduled watchlist that emits only records it has not delivered before — the memory belongs to the input, so it carries over from one day to the next.

Input

Nothing is required. The defaults run a drug-recall search; query is the only field with a prefilled example.

FieldTypeDefaultAllowed values / notes
modestringrecallsrecalls, drugLabels, deviceEvents, datasets — see Modes
productAreastringdrugdrug, device, food. Which enforcement dataset recalls reads; ignored by other modes
querystring(prefill insulin)Search words. Several words are searched as a phrase. Empty = everything matching the filters
searchFieldstringautoWhich field query is matched in — see Search fields
classificationsstring[]allClass I, Class II, Class III. recalls only — see Recall classes
recallStatusesstring[]allOngoing, Completed, Terminated, Pending. recalls only
eventTypesstring[]allDeath, Injury, Malfunction, Other, No answer provided. deviceEvents only
productCodestring—Three-character FDA device product code, e.g. QFG. deviceEvents only — see Device product codes
statestring—Two-letter US state of the recalling firm, e.g. NJ. recalls only
countrystring—Country of the recalling firm as FDA spells it, e.g. United States. recalls only
sinceDaysinteger—1–3650. Keep only records whose main date falls in the last N days (UTC). Wins over fromDate/toDate
fromDatestring—Earliest date, YYYY-MM-DD, inclusive
toDatestring—Latest date, YYYY-MM-DD, inclusive
onlyNewbooleanfalseEmit only records this input has not delivered in an earlier run — see onlyNew memory
sortOrderstringnewestnewest or oldest, by the mode's main date
maxItemsinteger251–5000 rows
includeTextbooleanfalseAdd SPL section text (drugLabels) or the report narrative (deviceEvents)
labelSectionsstring[]purpose, indications and usage, warningsWhich SPL sections includeText adds — see Label sections
fieldsstring[]allKeep only these output fields, in this order; mode and recordId are always kept
customSearchstring—A raw openFDA expression ANDed to everything above — see Raw search expressions
apiKeystring (secret)—Your own free openFDA API key: 120,000 requests a day instead of 1,000 per IP address — see Limits

Choice fields accept only the listed values (Apify rejects anything else before the run starts). Free-text fields are normalised — query is trimmed, state and productCode are upper-cased — and an impossible combination (e.g. productCode with mode: "recalls") fails the run with a clear message instead of quietly returning something wider. In fields, a name in another letter case or one or two letters off (recallnumber, reasonForRecal) is corrected and a name the mode does not have is left out; the run status says both. If not one name is a column of the mode, the run stops before any request instead of writing (and billing) rows that hold only mode and recordId.

Reference

Modes

modeopenFDA endpointWhat one row isFilters that apply
recalls/{drug|device|food}/enforcement.jsonOne recall enforcement reportquery, searchField, classifications, recallStatuses, state, country, dates
drugLabels/drug/label.jsonOne SPL label versionquery, searchField, dates (effective_time), includeText, labelSections
deviceEvents/device/event.jsonOne MAUDE adverse event report (MDR)query, searchField, eventTypes, productCode, dates, includeText
datasets/download.jsonOne openFDA dataset (31 on 2026-10-02)none — the date and search filters are ignored and reported as such

The main date differs per mode: report_date for recalls, effective_time for labels, date_received for device reports. sinceDays, fromDate, toDate and sortOrder all work on that field.

One manufacturer report number can cover several MDRs — one per device (report 0001222315-2026-002643 lists ten dental implants). Each MDR is its own row with its own mdrReportKey and detail page; they share recordId and reportNumber. Use mdrReportKey when you need one unique key per device-report row.

Recall classes

ValueWhat FDA means by it
Class IReasonable probability that the product causes serious harm or death
Class IIMay cause temporary or medically reversible harm; serious harm unlikely
Class IIIUnlikely to cause harm, but the product violates FDA regulations

Recall statuses are Ongoing (the firm is still recalling), Completed, Terminated (FDA closed the recall) and Pending (not yet classified).

Search fields

searchField picks the field query is matched in. A field an endpoint does not have falls back to auto, with a warning in the log — the search is never silently widened.

searchFieldrecallsdrugLabelsdeviceEvents
autoevery field of the recordevery fieldevery field
productproduct_descriptionpackage label textdevice.generic_name
reasonreason_for_recall→ auto→ auto
firmrecalling_firm→ automanufacturer_name
brandNameopenfda.brand_nameopenfda.brand_namedevice.brand_name
genericNameopenfda.generic_nameopenfda.generic_namedevice.generic_name
manufactureropenfda.manufacturer_nameopenfda.manufacturer_namedevice.manufacturer_d_name
productCodenot possible (see below)→ autodevice.device_report_product_code
identifierrecall_numberset_id, SPL id or NDCMAUDE report number or report key

identifier looks up one exact record. When the id does not exist, the run writes a single row with found: false instead of returning nothing.

Device recall records carry no product code — measured on 2026-09-19, 0 of 39,949 device enforcement records have one and there is no field to fall back to. productCode (or searchField: "productCode") combined with mode: "recalls" therefore fails with a message instead of returning an unfiltered list. Search device recalls by product description or recalling firm instead.

Device product codes

A product code is FDA's three-letter key for a device type. Examples verified in the device classification data:

CodeDevice typeClass
QFGAlternate controller enabled insulin infusion pump2
QJYInfusion pump, drug specific, pharmacy-filled2
DTEPulse generator, pacemaker, external2
DXYImplantable pacemaker pulse generator3
PQFSensor, glucose, invasive, non-adjunctive3
DQOCatheter, intravascular, diagnostic2
QOSContinuous ventilator—
JDGProsthesis, hip, femoral component, cemented, metal2

To find the code for any other device, search FDA's classification list by name in a browser: https://api.fda.gov/device/classification.json?search=device_name:"insulin pump"&limit=10. Each device event row also carries the code that FDA filed it under, so one broad genericName run tells you which code to narrow to.

Label sections

includeText adds a sections object to drug label rows. labelSections picks which ones: purpose, indicationsAndUsage, dosageAndAdministration, warnings, adverseReactions, contraindications, activeIngredient, inactiveIngredient, description, howSupplied. Sections a particular label does not have are left out. These fields can be tens of kilobytes each, which is why they are off by default.

Raw search expressions

customSearch is ANDed to everything the input already built, for people who know the openFDA field names. Useful starting points:

  • drug labels: openfda.pharm_class_epc:"Nonsteroidal Anti-inflammatory Drug [EPC]", openfda.route:"ORAL", openfda.is_original_packager:true
  • enforcement: voluntary_mandated:"Voluntary: Firm initiated", distribution_pattern:"Nationwide"
  • device events: patient.patient_problems:"Death", device.device_class:3, source_type:"Consumer"

Multi-word values must be quoted. An expression openFDA cannot parse fails the run with the API's own message, naming customSearch as the culprit.

onlyNew memory

With onlyNew: true the actor remembers the record ids each input has delivered — recall number (or event-<id>), SPL set id, MAUDE report number or dataset key — in the key-value store openfda-records-state of your account, one record per input (mode, product area, search, filters, sort and sinceDays as given), keeping the newest 20,000 ids. Records in that memory are skipped; the rest are written and remembered. sinceDays: 30 is part of the memory's name, the dates it means on a given day are not, so a daily schedule keeps one memory. (Builds before October 2026 named the memory after those dates and started an empty one every day; the first run of this build takes over what those daily records hold for the same input, so the upgrade does not deliver the window again.) Rows cut off by maxItems or by your maximum cost per run are not remembered and come in the next run. An identifier lookup that found nothing writes its found: false row once.

Requests: FDA adds records late, so a new one can sit anywhere in the date window, and finding it means reading the window. A run whose memory already holds ids asks for full pages (1,000 recalls, 500 device reports, up to 100 labels per request, after a first one of at most 50 recalls or labels) and stops as soon as it has maxItems new rows or reaches the end of the window — a daily watchlist of device recalls over 200 days (about 1,700 matches) costs 3 requests. Without apiKey a run also stops after 10 requests in a row that found nothing new, to spare openFDA's keyless allowance of 1,000 requests a day per IP address (shared with other Apify users); the status then says how many older matches were not checked — rows maxItems cut off further down than that come only with a key or a shorter sinceDays. New records sit at the newest end, so keep sortOrder: "newest" for monitoring, and add your own key or shorten sinceDays to have the whole window checked on every run.

These are the ids earlier builds remembered, so an existing memory carries over unchanged. Where several rows share one id — the MDRs of one report number, the products of a food recall event that has no recall numbers yet — the memory holds the id once. So maxItems leaves a group of one page for the next run rather than splitting it (unless the group alone is bigger than the rows left); when your maximum cost per run stops the run inside a group, the next run delivers the whole group again, including the rows already delivered; and a row that arrives later under an id already delivered (an MDR filed later under the same report number, one on the next page when the run stopped between two pages, or the rest of a group that maxItems had to split) is not delivered.

Examples

Class I drug recalls of the last 12 months — pharmacovigilance and pharmacy compliance.

{ "mode": "recalls", "productArea": "drug", "classifications": ["Class I"], "sinceDays": 365, "sortOrder": "newest", "maxItems": 20 }

Food recalls for undeclared allergens — food safety and retail QA.

{ "mode": "recalls", "productArea": "food", "query": "undeclared", "searchField": "reason", "sinceDays": 180, "maxItems": 20 }

Device recalls of the last 6 months from one firm — hospital biomedical engineering.

{ "mode": "recalls", "productArea": "device", "query": "Medtronic", "searchField": "firm", "sinceDays": 180, "maxItems": 25 }

Newest drug labels for a generic name — clinical and pharmacy apps.

{ "mode": "drugLabels", "query": "ibuprofen", "searchField": "genericName", "sortOrder": "newest", "maxItems": 15 }

Adverse event reports for one device family — device safety and liability research.

{ "mode": "deviceEvents", "query": "infusion pump", "searchField": "genericName", "sinceDays": 180, "maxItems": 20 }

Device reports with a death outcome, as a daily watchlist — safety monitoring.

{ "mode": "deviceEvents", "eventTypes": ["Death"], "sinceDays": 180, "onlyNew": true, "maxItems": 15 }

How fresh is each FDA dataset — data engineering.

{ "mode": "datasets", "maxItems": 40 }

Output

One real row from a cloud run with the default input (mode: "recalls", productArea: "drug", query: "insulin"), with the long codeInfo trimmed:

{
"mode": "recalls",
"recordId": "D-0806-2026",
"title": "MYXREDLIN, Insulin Human in 0.9% Sodium Chloride Injection, 100 units/100 mL (1 unit/mL), Rx only, Baxter Healthcare Corporation, Deerfield, IL 60015 USA, NDC 0338-0126-12.",
"organization": "Baxter Healthcare Corporation",
"category": "Class II",
"date": "2026-09-09",
"status": "Ongoing",
"found": true,
"recallNumber": "D-0806-2026",
"eventId": "99500",
"productArea": "drug",
"classification": "Class II",
"recallStatus": "Ongoing",
"productType": "Drugs",
"productDescription": "MYXREDLIN, Insulin Human in 0.9% Sodium Chloride Injection, 100 units/100 mL (1 unit/mL), Rx only, Baxter Healthcare Corporation, Deerfield, IL 60015 USA, NDC 0338-0126-12.",
"reasonForRecall": "CGMP Deviations",
"productQuantity": null,
"codeInfo": "Lot # NC188630, NC188708, NC188791, NC188845, NC188876, NC188968, Exp Date: 12/31/2027; Lot # NC193719, NC193849, …",
"moreCodeInfo": null,
"distributionPattern": "US Nationwide; Puerto Rico; and Chile.",
"recallingFirm": "Baxter Healthcare Corporation",
"firmAddress": "1 Baxter Pkwy",
"firmCity": "Deerfield",
"firmState": "IL",
"firmPostalCode": "60015-4625",
"firmCountry": "United States",
"voluntaryMandated": "Voluntary: Firm initiated",
"initialFirmNotification": "Letter",
"recallInitiationDate": "2026-08-06",
"centerClassificationDate": "2026-08-28",
"reportDate": "2026-09-09",
"brandNames": [
"MYXREDLIN"
],
"genericNames": [
"INSULIN HUMAN"
],
"manufacturerNames": [
"Baxter Healthcare Corporation"
],
"productNdcs": [
"0338-0126"
],
"url": "https://api.fda.gov/drug/enforcement.json?search=recall_number%3A%22D-0806-2026%22&limit=1",
"detailUrl": null,
"fetchedAt": "2026-09-19T20:14:59.688Z"
}

Shared fields, present in every mode:

FieldTypeMeaning
modestringWhich job produced the row
recordIdstringRecall number, SPL set id, MAUDE report number or dataset key (device reports: one report number can cover several MDR rows)
titlestringProduct description, brand (generic) name, device brand or dataset name
organizationstringRecalling firm, labeler or device manufacturer
categorystringRecall class, product type, event type or dataset category
datestringThe mode's main date, YYYY-MM-DD UTC
statusstringRecall status or MAUDE report type; empty for labels and datasets
foundbooleanfalse only for an identifier lookup that matched nothing
urlstringopenFDA URL that returns exactly this record
detailUrlstringFDA or label page for the record, where one exists
fetchedAtstringFetch time, ISO 8601 UTC

recalls adds: recallNumber, eventId, productArea, classification, recallStatus, productType, productDescription, reasonForRecall, productQuantity, codeInfo, moreCodeInfo, distributionPattern, recallingFirm, firmAddress, firmCity, firmState, firmPostalCode, firmCountry, voluntaryMandated, initialFirmNotification, recallInitiationDate, centerClassificationDate, reportDate, brandNames[], genericNames[], manufacturerNames[], productNdcs[].

drugLabels adds: setId, splId, version, effectiveDate, brandName, brandNames[], genericName, genericNames[], manufacturerName, substanceNames[], productNdcs[], packageNdcs[], routes[], productType, rxcuis[], pharmClasses[], applicationNumbers[], isOriginalPackager, labelUrl, and sections{} with includeText. The scalar brandName/genericName are the first entry of the matching array, so filters and spreadsheets work on a string.

deviceEvents adds: reportNumber, mdrReportKey, eventType, dateReceived, dateOfEvent, dateReport, sourceType, deviceBrandName, deviceGenericName, deviceManufacturer, productCode, modelNumber, catalogNumber, lotNumber, udiDi, deviceName, deviceClass, medicalSpecialty, regulationNumber, productProblems[], patientProblems[], deviceCount, patientCount, remedialAction, summaryReportFlag, and narrative with includeText.

datasets adds: datasetKey, endpointCategory, endpointName, exportDate, totalRecords, partitionCount, downloadSizeMb, downloadUrl.

Link fields: url is always an api.fda.gov query that returns this single record. For device reports detailUrl is the MAUDE detail page built from mdr_report_key plus the product code — building it from the report number instead is the usual reason such links do not open. For drug labels detailUrl and labelUrl point at the DailyMed page of the SPL set; for datasets detailUrl is the openFDA documentation page. Recall enforcement records have no per-record page on the FDA site, so detailUrl is empty there.

Dataset views: Overview (works for every mode), Recall enforcement reports, Drug labels (SPL), Device adverse events.

A SUMMARY record in the run's key-value store holds the endpoint and the exact search expression that was sent, the dataset's last_updated date, the total number of matches, rows written (delivered), API requests used, whether your API key was used (never the key), how many rows were dropped by a filter or as already seen, why the run stopped early if it did (stoppedBy, stoppedEarly), every input warning, and the FDA disclaimer. The run's status message says the same in one line.

Use it from code / agents

curl -X POST "https://api.apify.com/v2/acts/yadroo~openfda-records/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"mode":"recalls","productArea":"drug","classifications":["Class I"],"sinceDays":365,"maxItems":20}'
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('yadroo/openfda-records').call({
mode: 'deviceEvents', query: 'infusion pump', searchField: 'genericName', sinceDays: 180, maxItems: 20,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("yadroo/openfda-records").call(run_input={
"mode": "drugLabels", "query": "ibuprofen", "searchField": "genericName", "maxItems": 15,
})
items = client.dataset(run["defaultDatasetId"]).list_items().items

For LLM agents, fields keeps rows small enough to paste into a prompt: {"mode":"recalls","classifications":["Class I"],"sinceDays":90,"fields":["recallNumber","productDescription","reasonForRecall","recallingFirm","reportDate"]}.

MCP: add https://mcp.apify.com to Claude / Cursor / any MCP client and call the yadroo/openfda-records tool with the same JSON input.

Pricing

Pay per event: $0.001 per run start + $0.002 per dataset row. Nothing else is charged; there is no proxy and no browser in the run.

RunCost
The default 25-row recall search$0.051
50 rows (a typical monitoring run, ~40 s)$0.101
500 rows$1.001

Store discount tiers apply to the row price: Bronze −10 %, Silver −20 %, Gold and above −30 %. The start event is the same on every plan, and platform usage is included. A run never costs more than the maximum cost per run you set: it stops at that limit with the status "Stopped at your spending limit: N rows delivered", and every row it wrote is one billed row.

Limits & FAQ

  • Rate limit. Without an API key openFDA allows 240 requests per minute and 1,000 per day per IP address, and Apify's IP addresses are shared with other users, so that daily allowance can be used up by someone else's runs. A run needs one request per 1,000 recall rows, 500 device reports or 100 drug labels (the first recall or label request reads at most 50, to measure how big the records are: a pacemaker recall can list 1.8 MB of serial numbers), so a normal run costs 1–2 requests and the 5,000-row maximum costs 6, 10 or 51. An onlyNew run reads its date window to find what is new — one request per 1,000 recalls, 500 device reports or 100 labels in it — and without a key stops after 10 requests in a row with nothing new (see onlyNew memory). "Too many requests" (HTTP 429) is retried with backoff; if openFDA keeps refusing, the run ends with a message that says so. Put your own free key from open.fda.gov/apis/authentication in apiKey and the limit becomes 120,000 requests a day. The key travels in a request header only — it is never written to the dataset, the log or SUMMARY, and a key openFDA refuses ends the run with a message that names apiKey.
  • Capacity. One run writes up to 5,000 rows, inside the default 15-minute timeout and 256 MB of memory. Measured on 2026-10-03: 1,500 device reports in 24 s; 1,000 drug labels in about 1 minute (ibuprofen) to 2.5 minutes (prescription labels with includeText); 5,000 drug labels in about 7 minutes. A label is 10–200 KB of JSON, which is why labels are read 100 at a time (fewer when they are unusually big) — a page of 500 did not fit in memory.
  • Partial runs. Rows are saved page by page. If a later page fails (or the run reaches its timeout or your maximum cost per run), the pages already read stay in the dataset, and the status message says why the run stopped. A run that is restarted by the platform does not write or bill a row twice.
  • Paging depth. openFDA refuses to page past 25,000 records of one query. maxItems is capped at 5,000, which stays well inside that, but a query that would need to skip further stops with a message asking you to narrow the filters.
  • Freshness. FDA publishes in batches, roughly weekly. On 2026-09-19 the enforcement datasets were last updated 2026-09-09 and device events 2026-09-08, with the newest report received 2026-08-31. A sinceDays window shorter than about 30 days can legitimately return nothing — that is the source, not the actor.
  • Empty results are not errors. openFDA answers "no matches" with HTTP 404; the actor reports zero rows with a message that says what to widen. Only an identifier lookup writes a row (found: false), so you can tell "id does not exist" from "run failed".
  • openfda is often empty. Device and food recall records usually have no openfda block, so brandNames, genericNames and productNdcs are empty arrays on those rows. Drug recalls and labels do have it.
  • Recall numbers can be missing. Food enforcement records are published before FDA assigns a recall number; those rows fall back to event-<eventId> as recordId and leave recallNumber empty.
  • No personal data. MAUDE records contain patient age, sex, weight, race and ethnicity and a manufacturer contact person with phone and address. None of those are ever written to the dataset. Patient problem codes and the device and patient counts are kept, because that is what safety analysis needs. Company names and addresses in recall records stay — they are the subject of the record.
  • What is not here. This actor covers four endpoints. openFDA has more (drug adverse events/FAERS, NDC directory, 510(k), UDI, Drugs@FDA, Orange Book, drug shortages, food and cosmetic events, tobacco reports); mode: "datasets" lists them all with their freshness, but reading their records is not implemented. FDA guidance documents are not part of openFDA at all.
  • Is this medical advice? No. openFDA data is provided as-is. Adverse event and recall records are reports and do not establish that a product caused a problem. Do not use this data to make decisions about medical care.
  • Licence. FDA data is US public domain and may be used commercially; FDA asks for credit and does not endorse any product built on it. The actor calls only api.fda.gov.

Made by Yadroo. Sibling actors: sec-edgar-filings, sanctions-screen, world-bank-indicators, openalex-works, public-holidays.