FDA Recall Data API: How to Monitor Drug, Device, and Food Recalls Programmatically
openFDA serves drug, device and food recalls plus adverse events and 510(k) clearances over a free REST API. How the endpoints and fields work.
The actor referenced in this article. Pay only for results delivered.
The FDA issues hundreds of recall notices per year covering prescription drugs, medical devices, and food products. For supply chain teams, compliance officers, and healthcare analysts, staying current with this data manually is error-prone. There is an API for it.
The openFDA API is a public REST API that exposes drug enforcement reports, medical device recalls, and food enforcement actions. It is free, well-documented, and uses a query syntax that is powerful once you understand it. The syntax is non-obvious because it is Elasticsearch-based rather than standard SQL-style filtering.
TL;DR: openFDA provides recall data for drugs, devices, and food via a free REST API, plus drug adverse events, drug labels and device 510(k) clearances on separate endpoints. No API key is required: keyless callers get 240 requests per minute and 1,000 per day, and a free key keeps the same 240 per minute but raises the daily cap to 120,000. The query language is Lucene, so write
AND,ORandNOTas words and write ranges asfield:[20260101 TO 99991231]. The most important recall fields arerecall_number,reason_for_recall,recalling_firm,classification, andstatus.
What openFDA Covers
The openFDA project (api.fda.gov) exposes several distinct datasets. For recalls and enforcement actions, the relevant endpoints are:
Drug enforcement (/drug/enforcement.json) covers drug recalls and market withdrawals. Each record is an enforcement report filed with FDA’s CDER (Center for Drug Evaluation and Research). This includes prescription drugs, OTC medications, and biologics.
Food enforcement (/food/enforcement.json) covers food safety recalls including contamination events, labeling issues, and undeclared allergens. This dataset is maintained by CFSAN.
Device recalls, which come in two forms. /device/enforcement.json holds device enforcement reports and returns exactly the same 25 fields as the drug and food endpoints, which makes it the easy one to work with. /device/recall.json holds CDRH’s own recall records (Center for Devices and Radiological Health) and uses a completely different set of field names, but carries detail the enforcement report does not: the device product code, the 510(k) numbers for the recalled device, and FDA’s root cause category. Both cover medical devices and both are worth knowing about. Pick the enforcement one if you want devices to sit in the same table as drugs and food.
One important distinction: the enforcement endpoints return the recall notice itself. The enforcement notice tells you the firm, the product, the reason, and the classification. It does not return the full text of every FDA safety communication or press release, which live on FDA’s website rather than in the structured database.
Recalls are four of the eight openFDA endpoints. The rest cover adverse events, labels and device clearances, and they are described further down under the rest of openFDA.
API Key and Rate Limits
The openFDA API has a two-tier rate limit:
Without an API key: 240 requests per minute, 1,000 requests per day, counted per IP address. Sufficient for development and small-scale monitoring.
With a free API key: still 240 requests per minute, but 120,000 requests per day, counted per key. The per-minute ceiling does not move. What a key buys you is the daily volume, which is what any production monitoring workflow runs out of first.
Get a key from the openFDA authentication page. The key is appended as a query parameter: ?api_key=YOUR_KEY. No authentication headers, no OAuth. It is simple.
BASE_URL = "https://api.fda.gov"
API_KEY = "your_api_key_here"
def fda_get(endpoint, search, limit=100, skip=0):
"""Make a GET request to the openFDA API."""
params = {
"search": search,
"limit": limit,
"skip": skip,
}
if API_KEY:
params["api_key"] = API_KEY
resp = requests.get(f"{BASE_URL}{endpoint}", params=params)
resp.raise_for_status()
return resp.json()
The Search Syntax
The openFDA search syntax is Lucene query string syntax. It is the part that most people find confusing, mostly because of one encoding trap covered at the end of this section.
Basic field search:
field:value
AND, OR and NOT, spelled out as words:
classification:"Class I" AND status:"Ongoing"
classification:"Class I" OR classification:"Class II"
classification:"Class I" AND NOT status:"Terminated"
Two clauses with nothing between them is OR, not AND. classification:"Class I" status:"Ongoing" returns every Ongoing recall, because the default operator is OR. This catches people out constantly. If you mean AND, write AND.
Date range, with the word TO and spaces around it:
recall_initiation_date:[20250101 TO 20260101]
Dates work in either 20250101 or 2025-01-01 form on the enforcement endpoints. An open-ended upper bound is *, so recall_initiation_date:[20260101 TO *] means “since the start of 2026”.
Exact phrase, using quotes:
reason_for_recall:"undeclared allergen"
Wildcard, for partial values:
recalling_firm:Pfizer*
Now the encoding trap. In a URL, + decodes to a space, which is why openFDA’s own docs write search=a:1+AND+b:2. That form is correct when you hand-write a URL. It is wrong when you pass the string through an HTTP client’s parameter encoder, because the client encodes your + as %2B and openFDA then reads it as a literal plus sign. A range written [20260101+TO+99991231] through requests or got comes back as an HTTP 500 with parse_exception. Write plain spaces in your query string and let the client encode them. Everything in this post does that.
Classification codes for drug and food recalls:
- Class I: Most serious. Reasonable probability of serious adverse health consequence or death.
- Class II: Product may cause temporary adverse health consequences.
- Class III: Unlikely to cause adverse health consequences.
A handful of records carry Not Yet Classified, so do not assume the field is one of exactly three values. The device/recall endpoint has no classification field at all.
Python: Class I Drug Recalls in the Last 90 Days
import requests
from datetime import datetime, timedelta
BASE_URL = "https://api.fda.gov"
API_KEY = "your_api_key_here"
def get_class_i_drug_recalls(days_back=90):
"""Fetch Class I drug recalls from the last N days."""
cutoff = datetime.now() - timedelta(days=days_back)
date_str = cutoff.strftime("%Y-%m-%d")
# Plain spaces: requests encodes them, openFDA reads them as spaces.
search = (
f'classification:"Class I"'
f' AND recall_initiation_date:[{date_str} TO *]'
)
params = {
"search": search,
"limit": 100,
"sort": "recall_initiation_date:desc",
"api_key": API_KEY,
}
resp = requests.get(f"{BASE_URL}/drug/enforcement.json", params=params)
resp.raise_for_status()
data = resp.json()
total = data["meta"]["results"]["total"]
print(f"Total Class I drug recalls since {date_str}: {total}")
return data["results"]
recalls = get_class_i_drug_recalls(days_back=90)
for r in recalls[:5]:
print(f"\nRecall #: {r['recall_number']}")
print(f"Firm: {r['recalling_firm']}")
print(f"Product: {r['product_description'][:100]}")
print(f"Reason: {r['reason_for_recall'][:100]}")
print(f"Date: {r['recall_initiation_date']}")
print(f"Status: {r['status']}")
print(f"Quantity: {r.get('product_quantity', 'N/A')}")
The fields you will use most on drug/enforcement and food/enforcement:
| Field | Description |
|---|---|
recall_number | FDA-assigned unique recall identifier |
reason_for_recall | Free text description of why the recall was initiated |
recalling_firm | Company initiating the recall |
product_description | What product was recalled, including lot numbers |
product_quantity | How much product is affected |
distribution_pattern | Geographic scope of distribution |
classification | Class I, Class II, Class III, or Not Yet Classified |
status | Ongoing, Completed, or Terminated |
recall_initiation_date | When the firm initiated the recall |
report_date | When FDA published the enforcement report |
voluntary_mandated | Voluntary: Firm initiated, FDA Mandated, or N/A |
The complete list for every endpoint is in the field reference below.
Python: Streaming All Device Recalls with Pagination
The skip parameter handles pagination. limit can go up to 1,000 per request; 100 keeps responses small enough to parse without buffering problems, and skip offsets into the result set:
def stream_device_recalls(search_query, batch_size=100):
"""Generator that yields all device recall records matching a search query."""
skip = 0
while True:
params = {
"search": search_query,
"limit": batch_size,
"skip": skip,
"api_key": API_KEY,
}
resp = requests.get(f"{BASE_URL}/device/recall.json", params=params)
# 404 means no more results
if resp.status_code == 404:
break
resp.raise_for_status()
data = resp.json()
results = data.get("results", [])
if not results:
break
yield from results
total = data["meta"]["results"]["total"]
skip += batch_size
if skip >= total:
break
if skip >= 25000:
# openFDA rejects skip above 25,000 with a 400. For larger sets,
# split by date range, add filters, or switch to search_after.
print("Warning: hit the skip ceiling. Refine your query to get more.")
break
# Pull all implantable cardioverter defibrillator recalls
icd_search = "product_code:LWS" # LWS = Implantable Cardioverter Defibrillator (Non-Crt)
all_icd_recalls = list(stream_device_recalls(icd_search))
print(f"Total ICD recalls collected: {len(all_icd_recalls)}")
# Filter by company. recalling_firm is the name; firm_fei_number is a numeric
# FDA establishment ID, so match names against recalling_firm only.
company_recalls = [
r for r in all_icd_recalls
if "medtronic" in str(r.get("recalling_firm", "")).lower()
]
print(f"Medtronic ICD recalls: {len(company_recalls)}")
Two things to watch on this endpoint. skip is capped at 25,000; go past it and openFDA returns a 400 telling you to use search_after instead. And device/recall does not share field names with the enforcement endpoints: there is no recall_number, status, classification or recall_initiation_date. The equivalents are product_res_number, recall_status, root_cause_description and event_date_initiated. Writing an enforcement-style query against device/recall returns a clean NOT_FOUND rather than an error, which is the failure mode most likely to waste an afternoon. If you want device recalls with the same field names as drugs and food, query /device/enforcement.json instead and keep this endpoint for the product codes and root causes.
Filtering by Company
Filtering recalls to a specific company requires using recalling_firm or firm_fei_number:
def get_company_drug_recalls(company_name, years_back=5):
"""Get all drug recalls for a specific company."""
cutoff = datetime.now() - timedelta(days=365 * years_back)
date_str = cutoff.strftime("%Y-%m-%d")
search = (
f'recalling_firm:"{company_name}"'
f' AND recall_initiation_date:[{date_str} TO *]'
)
all_recalls = []
skip = 0
while True:
params = {
"search": search,
"limit": 100,
"skip": skip,
"api_key": API_KEY,
}
resp = requests.get(f"{BASE_URL}/drug/enforcement.json", params=params)
if resp.status_code == 404:
break
resp.raise_for_status()
data = resp.json()
results = data.get("results", [])
if not results:
break
all_recalls.extend(results)
total = data["meta"]["results"]["total"]
skip += 100
if skip >= total:
break
return all_recalls
# Company name must match exactly as it appears in FDA data
pfizer_recalls = get_company_drug_recalls("Pfizer Inc.", years_back=5)
print(f"Pfizer drug recalls (last 5 years): {len(pfizer_recalls)}")
by_class = {}
for r in pfizer_recalls:
c = r.get("classification", "Unknown")
by_class[c] = by_class.get(c, 0) + 1
print("By classification:", by_class)
Note that company names in the FDA database are not standardized. recalling_firm:"Pfizer Inc" matches 120 drug enforcement records while recalling_firm:Pfizer* matches 156, so the exact phrase quietly drops a quarter of them. Use the wildcard when you are not sure of the exact form.
Data Freshness
FDA updates the enforcement database weekly, typically on Wednesdays or Thursdays. The report_date field shows when FDA published the record to the database. The recall_initiation_date shows when the company initiated the recall. The gap between these two dates is typically 1 to 3 weeks for routine recalls. Class I events are often published faster.
This weekly cadence matters for monitoring workflows: running a daily check will not surface new records until after FDA’s weekly update cycle. A weekly scheduled pull on Friday mornings captures the full prior week.
Building a Recall Watch Without Writing Queries
The FDA Recalls scraper takes plain filters instead of the search syntax above: productType (drug, device or food), classification, status, recallingFirm, state, searchTerm (matched against the product description), dateFrom, dateTo and maxResults. It builds the openFDA query, pages through results and returns flat records using the same field names as the API (recall_number, recalling_firm, reason_for_recall and so on). You don’t need an openFDA key, and a search that matches nothing is not charged.
Two inputs cover most monitoring jobs. The first watches the most serious recalls in a category, the kind a grocer or distributor has to act on within hours:
{
"productType": "food",
"classification": "Class I",
"status": "Ongoing",
"dateFrom": "2026-01-01",
"maxResults": 200
}
The second watches one supplier. If a firm you buy from is named in a recall, you want to know before your customers do, and each record carries the product description, lot details, reason and distribution pattern you need to judge exposure:
{
"productType": "drug",
"recallingFirm": "Glenmark",
"maxResults": 100
}
Note that the scraper’s dateFrom and dateTo filter on report_date, the day FDA published the record, which is what you want for “what is new since my last run”. maxResults accepts up to 25,000, which is the openFDA ceiling for a single query.
Three fields come back that are not in openFDA’s own recall record. The scraper reads openFDA’s enrichment block and flattens brand_name, generic_name and manufacturer_name into single strings, joining multiple values with ; . openFDA sends those as arrays, so if you query the API directly you will find them under openfda, not at the top level.
Turning it into alerts
Schedule the run for the day after FDA’s weekly update, look back a little over a week, and compare recall_number against the ones you have already alerted on. Anything new goes to Slack:
import json
import os
import pathlib
from datetime import date, timedelta
import requests
from apify_client import ApifyClient
apify = ApifyClient(os.environ["APIFY_TOKEN"])
SEEN = pathlib.Path("seen_recalls.json")
seen = set(json.loads(SEEN.read_text())) if SEEN.exists() else set()
def new_recalls(product_type, **filters):
"""Pull the last 8 days of recalls and return only ones not seen before."""
since = (date.today() - timedelta(days=8)).isoformat()
run = apify.actor("themineworks/fda-recalls-scraper").call(run_input={
"productType": product_type,
"dateFrom": since,
"maxResults": 200,
**filters,
})
rows = apify.dataset(run["defaultDatasetId"]).iterate_items()
rows = [r for r in rows if "_type" not in r] # drop the run summary row
fresh = [r for r in rows if r.get("recall_number") not in seen]
seen.update(r.get("recall_number") for r in rows)
return fresh
alerts = new_recalls("food", classification="Class I")
alerts += new_recalls("drug", recallingFirm="Glenmark")
SEEN.write_text(json.dumps(sorted(x for x in seen if x)))
for r in alerts:
requests.post(os.environ["SLACK_WEBHOOK_URL"], json={
"text": (
f"{r.get('classification')} recall {r.get('recall_number')}: "
f"{r.get('recalling_firm')}. {(r.get('reason_for_recall') or '')[:200]}"
)
})
The one day of overlap means nothing slips between runs, and the seen set stops it from alerting twice. On a week when FDA posts nothing that matches, the run comes back empty and costs only the small start fee. Pricing per record is on the actor page.
Beyond Recalls: The Rest of openFDA
The four recall and enforcement endpoints above are not the whole API. The other four are where most of the volume sits, and they answer different questions: what went wrong with a drug after it shipped, what the approved label says, and what devices a competitor has cleared.
| Endpoint | What it returns | Records |
|---|---|---|
drug/event | FAERS adverse event reports submitted for drugs | 20.7 million |
drug/label | Structured product labelling, the approved prescribing information | 262,000 |
drug/enforcement | Drug recall and market withdrawal reports | 18,000 |
device/510k | Device premarket clearance decisions | 176,000 |
device/event | MAUDE adverse event reports for medical devices | 26.1 million |
device/enforcement | Device recall enforcement reports | 40,000 |
device/recall | CDRH device recall and correction records | 59,000 |
food/enforcement | Food and supplement recall reports | 29,000 |
Counts are from the meta.results.total on each endpoint in October 2026.
Drug adverse events (FAERS)
drug/event is the FDA Adverse Event Reporting System. Each record is one report submitted by a manufacturer, a clinician or a member of the public, describing one patient, the drugs they were taking and the reactions observed. The useful query fields are nested: the drug name is patient.drug.medicinalproduct and the reaction is patient.reaction.reactionmeddrapt, a MedDRA preferred term.
patient.drug.medicinalproduct:aspirin AND serious:1
That returns about 435,000 reports. serious:1 is FDA’s own flag for a report involving death, hospitalisation, a life-threatening event, disability or a congenital anomaly, broken out further into seriousnessdeath, seriousnesshospitalization, seriousnesslifethreatening, seriousnessdisabling and seriousnessother.
FAERS reports are voluntary submissions and are explicitly not causality findings. A high report count for a drug reflects how much it is prescribed and how closely it is watched as much as anything about the drug. Treat the data as a signal to investigate, not as evidence.
Device adverse events (MAUDE)
device/event is the device equivalent and the largest openFDA dataset at 26 million records. A report carries the device under device, the narrative text under mdr_text, and patient outcome codes under patient. event_type separates malfunctions from injuries and deaths, and date_received is the date to filter on.
Device 510(k) clearances
device/510k covers premarket notification clearances, which is how most devices reach the US market. Searching by applicant gives you a competitor’s regulatory pipeline:
applicant:Medtronic
That matches about 1,587 clearances. Each record carries k_number (the clearance ID, for example K202166), applicant, device_name, decision_date, decision_description, product_code and advisory_committee_description. The regulation number lives one level down, at openfda.regulation_number, alongside openfda.device_class.
Drug labels
drug/label holds Structured Product Labelling, which is the approved label broken into named sections: indications_and_usage, dosage_and_administration, warnings, boxed_warning, contraindications, adverse_reactions, drug_interactions and around seventy more. Not every label has every section, so code defensively. effective_time is the version date and openfda carries the identifiers you would join on, including brand_name, generic_name, product_ndc, rxcui and unii.
Pulling Any Endpoint With the openFDA Unified Crawler
Covering these endpoints yourself means a different record shape and a different date field for each one, plus the encoding trap above. The openFDA Unified Crawler is one actor over seven of them. You name the endpoint, write the search, and it handles paging and retries. The eighth, device/enforcement, is the one the recalls scraper covers under productType: "device".
The inputs are short:
| Input | What it does |
|---|---|
endpoint | One of drug/event, drug/label, drug/enforcement, device/510k, device/recall, device/event, food/enforcement. One endpoint per run. |
search | Passed through to openFDA unchanged, so the whole query grammar above is available. Write spaces, not +. |
maxResults | 1 to 5,000. Defaults to 100. |
fdaApiKey | Optional. Sent as api_key, which raises your daily cap to 120,000 requests. |
Adverse event reports for one drug:
{
"endpoint": "drug/event",
"search": "patient.drug.medicinalproduct:aspirin AND serious:1",
"maxResults": 500
}
A competitor’s device clearances:
{
"endpoint": "device/510k",
"search": "applicant:Medtronic AND decision_date:[20260101 TO *]",
"maxResults": 300
}
Food recalls for undeclared allergens, which is the single most common food recall reason:
{
"endpoint": "food/enforcement",
"search": "reason_for_recall:\"undeclared allergen\"",
"maxResults": 200
}
Date filtering goes in search, using the date field that belongs to the endpoint you picked. They are all different:
| Endpoint | Date field | Format |
|---|---|---|
drug/event | receivedate | 20260101 |
drug/label | effective_time | 20260101 |
drug/enforcement | recall_initiation_date, report_date | 20260101 |
device/510k | decision_date, date_received | 2026-01-01 |
device/recall | event_date_initiated, event_date_posted | 2026-01-01 |
device/event | date_received, date_of_event | 2026-01-01 |
food/enforcement | recall_initiation_date, report_date | 20260101 |
Both formats parse in a range clause on every endpoint, so [20260101 TO *] is safe everywhere. The format column is what you get back in the record, which matters when you compare dates downstream.
One behaviour worth knowing: leaving search empty does not give you the newest records. openFDA has no default sort, so you get an arbitrary slice of the dataset. A keyless limit=1 call on drug/event returns a 2008 report. If you want recent records, put a date range in search.
Every row comes back as openFDA published it, with nothing flattened or trimmed, plus two fields the actor adds: endpoint, telling you which record shape you are looking at, and scraped_at, an ISO timestamp. The last row of the dataset is a run summary carrying _type: "summary", the endpoint and the record count, so filter on _type before you process the rest.
Billing is one pay-per-event charge, record-scraped, per record that reaches your dataset. At the Apify FREE tier that is $0.002 per record, falling to $0.001 at GOLD and above. Empty searches, failed requests and blocked pages are never charged, so a run that finds nothing costs nothing. Current pricing is on the Apify listing.
Which of the Two Actors to Use
They overlap on recalls and the choice is straightforward.
Use the FDA Recalls scraper when recalls are the job. It takes plain dropdown filters instead of query syntax, flattens every record to the same 22 fields plus a scraped_at timestamp across drugs, devices and food, sorts newest first by report_date, and pulls the brand and manufacturer names out of the enrichment block for you. For a weekly supplier watch or a Class I alert feed, it is less code and fewer ways to get the query wrong.
Use the openFDA Unified Crawler when you need something the recalls endpoints do not hold: adverse event reports, label text, 510(k) clearances, or the device recall fields that the flattened view drops, such as root_cause_description and k_numbers. It is also the right tool when you want the raw openFDA record rather than a normalised one, for instance when you are archiving to a warehouse and would rather flatten on your own terms.
If you want recalls across all three categories plus adverse events for the same drugs, run both and join on recalling_firm or the NDC codes in openfda.
openFDA Field Reference by Endpoint
Field names are the thing people search for and the thing openFDA documents least conveniently. These are the fields returned in October 2026, read off live responses. openFDA omits keys it has no value for rather than sending nulls, so use .get() and expect gaps.
drug/enforcement, device/enforcement and food/enforcement
All three return the same 25 fields. Only product_type tells them apart, and it reads Drugs, Devices or Food.
| Field | What it is |
|---|---|
recall_number | FDA recall identifier, for example D-0123-2026 |
event_id | FDA event ID grouping related recall records |
status | Ongoing, Completed or Terminated |
classification | Class I, Class II, Class III or Not Yet Classified |
product_type | Drugs, Devices or Food |
recalling_firm | Name of the firm conducting the recall |
reason_for_recall | Free text reason |
product_description | Product name, strength, pack size and lot detail |
product_quantity | Amount in distribution, as free text |
code_info | Lot and expiry codes covered by the recall |
more_code_info | Overflow lot codes |
distribution_pattern | Where the product went, as free text |
voluntary_mandated | Voluntary: Firm initiated, FDA Mandated or N/A |
initial_firm_notification | How the firm told its customers, for example Letter, E-Mail |
recall_initiation_date | Date the firm started the recall, YYYYMMDD |
center_classification_date | Date the FDA centre assigned the class, YYYYMMDD |
report_date | Date FDA published the report, YYYYMMDD |
termination_date | Date the recall was closed, YYYYMMDD, absent while ongoing |
city, state, country | Location of the recalling firm, not of the affected product |
address_1, address_2, postal_code | Street address of the recalling firm |
openfda | Enrichment block. Carries brand_name, generic_name, manufacturer_name and NDC codes as arrays when FDA could match the product, and is an empty object when it could not |
device/recall
Different dataset, different names. Nothing here is called recall_number or status.
| Field | What it is |
|---|---|
product_res_number | Recall identifier, for example Z-0001-04 |
res_event_number | Event number grouping products in one recall |
cfres_id | Internal CDRH recall record ID |
recall_status | Recall status, for example Terminated |
recalling_firm | Name of the firm conducting the recall |
firm_fei_number | FDA Establishment Identifier, numeric, for exact firm matching |
product_code | Three letter CDRH device code, for example LWS |
k_numbers | Array of 510(k) numbers for the recalled device |
product_description | Device name, model and manufacturer, as free text |
product_quantity | Units affected |
reason_for_recall | Free text reason |
root_cause_description | FDA’s cause category, for example Device Design, Other |
action | What the firm did, for example the text of the safety notice |
code_info | Serial, lot or model numbers affected |
distribution_pattern | Where the device went |
event_date_initiated | Date the firm started the recall, YYYY-MM-DD |
event_date_posted | Date FDA posted the record, YYYY-MM-DD |
event_date_terminated | Date the recall was closed, YYYY-MM-DD |
additional_info_contact | Contact named for enquiries |
pma_numbers | Array of PMA numbers, for devices approved rather than cleared |
event_date_created | Date the record was created, YYYY-MM-DD |
city, state, country, postal_code, address_1, address_2 | Recalling firm’s address |
device/510k
| Field | What it is |
|---|---|
k_number | Clearance number, for example K202166 |
applicant | Company that submitted the notification |
contact | Named contact on the submission |
device_name | Trade name as submitted |
product_code | Three letter CDRH device code |
decision_code | Short decision code, for example SESE |
decision_description | Decision in words, for example Substantially Equivalent |
decision_date | Date of the decision, YYYY-MM-DD |
date_received | Date FDA received the submission, YYYY-MM-DD |
clearance_type | For example Traditional, Special, Abbreviated |
advisory_committee | Two letter panel code |
advisory_committee_description | Panel in words, for example Cardiovascular |
review_advisory_committee | Panel that actually reviewed it |
statement_or_summary | Whether a summary or a statement was filed |
third_party_flag, expedited_review_flag | Y or N |
address_1, address_2, city, state, country_code, zip_code, postal_code | Applicant address |
openfda | device_class, device_name, regulation_number, medical_specialty_description, fei_number, registration_number |
drug/event (FAERS)
| Field | What it is |
|---|---|
safetyreportid | Report identifier |
safetyreportversion | Version of the report |
serious | 1 if the report is serious, 2 if not |
seriousnessdeath | 1 when the report involves death |
seriousnesshospitalization | 1 when it involves hospitalisation |
seriousnesslifethreatening | 1 when life-threatening |
seriousnessdisabling | 1 when disabling |
seriousnessother | 1 for other serious outcomes |
receivedate | Date FDA first received the report, YYYYMMDD |
receiptdate | Date of the most recent version, YYYYMMDD |
transmissiondate | Date FDA released the record, YYYYMMDD |
reporttype | For example expedited or periodic |
occurcountry, primarysourcecountry | Where the event happened and where it was reported from |
primarysource.qualification | Reporter type, for example physician or consumer |
companynumb | Manufacturer’s own report number |
sender.senderorganization | Who sent the report to FDA |
duplicate, reportduplicate | Flags for known duplicate submissions |
patient.patientsex | 1 male, 2 female |
patient.patientonsetage, patient.patientonsetageunit | Age at onset and its unit |
patient.patientagegroup | Banded age group |
patient.patientweight | Weight in kilograms |
patient.patientdeath | Present when the patient died |
patient.reaction[].reactionmeddrapt | Reaction as a MedDRA preferred term |
patient.reaction[].reactionoutcome | Coded outcome, for example recovered or fatal |
patient.drug[].medicinalproduct | Drug name as reported |
patient.drug[].activesubstance | Active substance name |
patient.drug[].drugcharacterization | 1 suspect, 2 concomitant, 3 interacting |
patient.drug[].drugindication | Why the drug was taken |
patient.drug[].drugdosagetext, drugdosageform | Dose as free text and dosage form |
patient.drug[].drugadministrationroute | Coded route |
patient.drug[].drugstartdate, drugenddate | Treatment dates |
patient.drug[].actiondrug | What was done with the drug, for example withdrawn |
patient.drug[].drugbatchnumb | Lot number when reported |
device/event (MAUDE)
The widest record in openFDA, with over eighty top-level fields. The ones you will actually query:
| Field | What it is |
|---|---|
report_number, mdr_report_key | Report identifiers |
event_type | Malfunction, Injury, Death or No answer provided |
date_received | Date FDA received the report, YYYY-MM-DD, the field to filter on |
date_of_event | Date the event happened |
date_report | Date the report was written |
event_location | Where it happened, for example Hospital |
adverse_event_flag, product_problem_flag | Y or N |
manufacturer_name, manufacturer_city, manufacturer_state, manufacturer_country | Manufacturer of record |
number_devices_in_event, number_patients_in_event | Counts |
remedial_action | What was done, for example Recall, Repair |
report_source_code | Who reported, for example manufacturer or voluntary |
reporter_occupation_code | Reporter’s role |
device[].brand_name, device[].generic_name | Device names |
device[].manufacturer_d_name | Manufacturer as given on the device |
device[].device_report_product_code | Three letter CDRH device code |
device[].model_number, catalog_number, lot_number | Device identifiers |
device[].implant_flag, device[].single_use_flag | Y or N |
device[].device_evaluated_by_manufacturer | Whether the device was returned and examined |
mdr_text[].text | The narrative. text_type_code separates the event description from the manufacturer’s evaluation |
patient[].sequence_number_outcome | Patient outcome codes |
patient[].patient_sex, patient_age, patient_weight | Patient detail when reported |
drug/label
drug/label fields are label sections, and around eighty of them exist. The commonly used ones:
| Field | What it is |
|---|---|
id, set_id, version | SPL document identifiers. set_id is stable across versions |
effective_time | Version date, YYYYMMDD |
indications_and_usage | What the drug is approved to treat |
dosage_and_administration | Dosing |
boxed_warning | The black box warning, present only when the drug has one |
warnings, warnings_and_cautions | Warning text, old and new label formats |
contraindications | When not to use it |
adverse_reactions | Reported adverse reactions |
drug_interactions | Interaction text |
clinical_pharmacology, mechanism_of_action, pharmacokinetics | Pharmacology sections |
use_in_specific_populations, pregnancy, pediatric_use, geriatric_use | Population sections |
active_ingredient, inactive_ingredient | Ingredients, mostly on OTC labels |
purpose, do_not_use, ask_doctor, when_using, stop_use | OTC Drug Facts sections |
how_supplied, storage_and_handling | Packaging and storage |
spl_product_data_elements | Flat list of product names and ingredients |
openfda.brand_name, generic_name, manufacturer_name | Names, as arrays |
openfda.product_ndc, package_ndc, application_number | Product identifiers |
openfda.rxcui, unii, substance_name | RxNorm and UNII codes, for joining to other datasets |
openfda.pharm_class_epc, pharm_class_moa, pharm_class_cs, pharm_class_pe | Pharmacologic class codes |
Several sections also come in a _table variant, such as adverse_reactions_table, holding the same content as HTML tables.
Use Cases
Pharma compliance monitoring. QA teams at pharmaceutical manufacturers track competitor recalls to identify industry-wide contamination patterns (e.g., a specific excipient supplier affecting multiple firms) and to benchmark their own quality processes. Class I recalls in a product category are a leading indicator of FDA scrutiny.
Supply chain risk management. If you source components or ingredients from a specific company, monitoring that company’s recall history via recalling_firm gives you an early warning system. A device manufacturer sourcing components from a contract manufacturer can watch for that manufacturer’s FEI number appearing in recall records.
Pharmacovigilance. Standing drug/event queries on a drug or an active substance, refreshed on a schedule, give you a report count you can trend. Pair the reaction term with serious:1 to separate the signal from the noise, and remember the data cannot tell you about causation.
Healthcare competitive intelligence. Medical device companies track competitor recalls to identify product weaknesses. A Class I recall for a competing device is both a market opportunity and useful data for regulatory strategy. device/510k by applicant adds the other half of the picture: what the competitor is clearing and how long FDA is taking over it.
Food safety compliance. Food distributors and retailers use the food enforcement endpoint to proactively check whether products in their inventory have been recalled. Scraping this into a daily feed and cross-referencing against current inventory is more reliable than waiting for a vendor to notify you.
Academic public health research. Researchers studying food safety patterns, drug quality trends, or device failure modes use the openFDA recall data as a structured primary source. The data goes back to the 1990s for some categories, enabling longitudinal analysis.
For one-off queries, the raw openFDA API is approachable with the query syntax examples above. For production monitoring, the managed FDA Recalls scraper handles the query construction, pagination, rate limiting and weekly scheduling for recalls, and the openFDA Unified Crawler does the same across all seven endpoints when you need adverse events, labels or device clearances alongside them.
Explore the scraper referenced in this article: inputs, outputs, and pricing, then run it on Apify.
CourtListener API: How to Search US Court Records and Case Law Programmatically
CourtListener exposes 10M+ court opinions and dockets via a free REST API. Here is how to query it, what the rate limits actually are, and when a scraper is faster.
Federal Register API: How to Track US Rules, Proposed Rules, and Executive Orders
The Federal Register publishes every US executive action, proposed rule, and final rule via a REST API. Here is how to query it and what the data contains.
NPI Registry API: How to Look Up Any US Healthcare Provider Programmatically
CMS publishes the National Provider Identifier registry as a free API. Here is how to search by provider name, specialty, location, and NPI number, and what the data contains.
India Government Data API: How to Pull Any data.gov.in Dataset Without the Documentation Confusion
data.gov.in has 10,000+ datasets including mandi prices, foreign trade, and census data. The OGD API works but has quirks that are not documented anywhere.