openFDA API: FDA Recalls, Drug Labels & Device Reports
Pricing
from $1.40 / 1,000 fda record returneds
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
Maintained by CommunityActor stats
0
Bookmarked
1
Total users
0
Monthly active users
2 hours ago
Last modified
Categories
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:
sinceDaysplusonlyNewturns 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.
| Field | Type | Default | Allowed values / notes |
|---|---|---|---|
mode | string | recalls | recalls, drugLabels, deviceEvents, datasets — see Modes |
productArea | string | drug | drug, device, food. Which enforcement dataset recalls reads; ignored by other modes |
query | string | (prefill insulin) | Search words. Several words are searched as a phrase. Empty = everything matching the filters |
searchField | string | auto | Which field query is matched in — see Search fields |
classifications | string[] | all | Class I, Class II, Class III. recalls only — see Recall classes |
recallStatuses | string[] | all | Ongoing, Completed, Terminated, Pending. recalls only |
eventTypes | string[] | all | Death, Injury, Malfunction, Other, No answer provided. deviceEvents only |
productCode | string | — | Three-character FDA device product code, e.g. QFG. deviceEvents only — see Device product codes |
state | string | — | Two-letter US state of the recalling firm, e.g. NJ. recalls only |
country | string | — | Country of the recalling firm as FDA spells it, e.g. United States. recalls only |
sinceDays | integer | — | 1–3650. Keep only records whose main date falls in the last N days (UTC). Wins over fromDate/toDate |
fromDate | string | — | Earliest date, YYYY-MM-DD, inclusive |
toDate | string | — | Latest date, YYYY-MM-DD, inclusive |
onlyNew | boolean | false | Emit only records this input has not delivered in an earlier run — see onlyNew memory |
sortOrder | string | newest | newest or oldest, by the mode's main date |
maxItems | integer | 25 | 1–5000 rows |
includeText | boolean | false | Add SPL section text (drugLabels) or the report narrative (deviceEvents) |
labelSections | string[] | purpose, indications and usage, warnings | Which SPL sections includeText adds — see Label sections |
fields | string[] | all | Keep only these output fields, in this order; mode and recordId are always kept |
customSearch | string | — | A raw openFDA expression ANDed to everything above — see Raw search expressions |
apiKey | string (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
mode | openFDA endpoint | What one row is | Filters that apply |
|---|---|---|---|
recalls | /{drug|device|food}/enforcement.json | One recall enforcement report | query, searchField, classifications, recallStatuses, state, country, dates |
drugLabels | /drug/label.json | One SPL label version | query, searchField, dates (effective_time), includeText, labelSections |
deviceEvents | /device/event.json | One MAUDE adverse event report (MDR) | query, searchField, eventTypes, productCode, dates, includeText |
datasets | /download.json | One 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
| Value | What FDA means by it |
|---|---|
Class I | Reasonable probability that the product causes serious harm or death |
Class II | May cause temporary or medically reversible harm; serious harm unlikely |
Class III | Unlikely 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.
searchField | recalls | drugLabels | deviceEvents |
|---|---|---|---|
auto | every field of the record | every field | every field |
product | product_description | package label text | device.generic_name |
reason | reason_for_recall | → auto | → auto |
firm | recalling_firm | → auto | manufacturer_name |
brandName | openfda.brand_name | openfda.brand_name | device.brand_name |
genericName | openfda.generic_name | openfda.generic_name | device.generic_name |
manufacturer | openfda.manufacturer_name | openfda.manufacturer_name | device.manufacturer_d_name |
productCode | not possible (see below) | → auto | device.device_report_product_code |
identifier | recall_number | set_id, SPL id or NDC | MAUDE 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:
| Code | Device type | Class |
|---|---|---|
QFG | Alternate controller enabled insulin infusion pump | 2 |
QJY | Infusion pump, drug specific, pharmacy-filled | 2 |
DTE | Pulse generator, pacemaker, external | 2 |
DXY | Implantable pacemaker pulse generator | 3 |
PQF | Sensor, glucose, invasive, non-adjunctive | 3 |
DQO | Catheter, intravascular, diagnostic | 2 |
QOS | Continuous ventilator | — |
JDG | Prosthesis, hip, femoral component, cemented, metal | 2 |
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:
| Field | Type | Meaning |
|---|---|---|
mode | string | Which job produced the row |
recordId | string | Recall number, SPL set id, MAUDE report number or dataset key (device reports: one report number can cover several MDR rows) |
title | string | Product description, brand (generic) name, device brand or dataset name |
organization | string | Recalling firm, labeler or device manufacturer |
category | string | Recall class, product type, event type or dataset category |
date | string | The mode's main date, YYYY-MM-DD UTC |
status | string | Recall status or MAUDE report type; empty for labels and datasets |
found | boolean | false only for an identifier lookup that matched nothing |
url | string | openFDA URL that returns exactly this record |
detailUrl | string | FDA or label page for the record, where one exists |
fetchedAt | string | Fetch 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 ApifyClientclient = 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.
| Run | Cost |
|---|---|
| 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
onlyNewrun 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 inapiKeyand 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 namesapiKey. - 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.
maxItemsis 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
sinceDayswindow 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
identifierlookup writes a row (found: false), so you can tell "id does not exist" from "run failed". openfdais often empty. Device and food recall records usually have noopenfdablock, sobrandNames,genericNamesandproductNdcsare 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>asrecordIdand leaverecallNumberempty. - 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.