FDA Recall Data API: How to Monitor Drug, Device, and Food Recalls Programmatically
openFDA exposes drug recalls, device recalls, and food safety enforcement actions via a REST API. Here is how the endpoints work and what the data actually contains.
The actor referenced in this article is live on Apify. 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. No API key is required for up to 240 requests per minute; a free key bumps this to 1,000 per minute. The query language uses Elasticsearch syntax with
+field:valuefor AND logic and parentheses for grouping. 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.
Device recalls (/device/recall.json) covers medical device recalls from CDRH (Center for Devices and Radiological Health). The device dataset is more complex because it includes both safety-related recalls and routine corrections.
Food enforcement (/food/enforcement.json) covers food safety recalls including contamination events, labeling issues, and undeclared allergens. This dataset is maintained by CFSAN.
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.
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. Sufficient for development and small-scale monitoring.
With a free API key: 1,000 requests per minute, 120,000 requests per day. Required for any production monitoring workflow.
Get a key at api.fda.gov/signup. 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 based on Elasticsearch’s query string syntax. It is the part that most people find confusing because it looks like a mix of different conventions.
Basic field search:
field:value
AND logic (using +):
+field1:value1+field2:value2
OR logic (using space or explicit OR):
field:value1 field:value2
Date range:
recall_initiation_date:[2025-01-01+TO+2026-01-01]
NOT:
+field1:value1+-field2:value2
Exact phrase (using quotes):
reason_for_recall:"contamination"
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.
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")
search = (
f"+classification:\"Class I\""
f"+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')}")
Key fields returned:
| 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, II, or III |
status | Ongoing, Completed, Terminated |
recall_initiation_date | When the firm initiated the recall |
report_date | When FDA published the enforcement report |
voluntary_mandated | Whether the recall was voluntary or mandated by FDA |
Python: Streaming All Device Recalls with Pagination
The skip parameter handles pagination. The maximum limit per request is 100 records; 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 hard cap: 25,001 records per query
# For larger sets, split by date range or add more filters
print(f"Warning: Hit 25,000 record limit. Refine your query to get more.")
break
# Pull all cardiovascular device recalls
cv_search = "+product_code:DQY" # DQY = implantable cardioverter defibrillators
all_cv_recalls = list(stream_device_recalls(cv_search))
print(f"Total ICD recalls collected: {len(all_cv_recalls)}")
# Filter by company
company_recalls = [
r for r in all_cv_recalls
if "medtronic" in r.get("firm_fei_number", "").lower()
or "medtronic" in str(r.get("recalling_firm", "")).lower()
]
print(f"Medtronic ICD recalls: {len(company_recalls)}")
The 25,001 record cap per query is an openFDA platform constraint. If your query returns more than 25,001 records, you need to split it into smaller date ranges and combine the results.
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")
# Use a wildcard for partial name matches
search = (
f"+recalling_firm:\"{company_name}\""
f"+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. “Pfizer Inc.” and “Pfizer Inc” (no period) may be different records. Use partial matching with * wildcards when you are not sure of the exact form: recalling_firm:Pfizer*.
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.
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.
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.
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.
The managed FDA Recalls scraper handles the query construction, pagination, rate limiting, and weekly update scheduling. For one-off queries, the raw openFDA API is approachable with the query syntax examples above. For production monitoring across multiple categories and companies, a scheduled scraper run avoids maintaining the retry and pagination logic yourself.
Try the scraper referenced in this article — live on Apify, pay only for results.
Open fda-recalls-scraper on Apify →How to Scrape AmbitionBox Company Reviews and Ratings
AmbitionBox is India largest employer review platform with 300,000 companies. Learn how to pull ratings, review counts, salary data, and dimension scores as structured JSON without any official API.
AliExpress Product Data API: Prices, Ratings, and Orders in Python
AliExpress affiliate API has restricted coverage. Learn how to scrape AliExpress product listings for prices, ratings, order counts, and seller data as structured JSON — no affiliate approval needed.
ClinicalTrials.gov API v2: How to Search 500,000 Studies and Track Trial Status
ClinicalTrials.gov upgraded to a v2 REST API in 2024. Here is how to use it, what changed from v1, and how to build automated trial monitoring pipelines in Python.