The Mine Works
Browse on Apify
FDA Recall Data API: How to Monitor Drug, Device, and Food Recalls Programmatically
← All posts
tutorial June 22, 2026 · 8 min read Updated June 22, 2026

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.

Try the scraper

The actor referenced in this article is live on Apify. Pay only for results delivered.

Open on Apify →

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:value for AND logic and parentheses for grouping. The most important recall fields are recall_number, reason_for_recall, recalling_firm, classification, and status.

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:

FieldDescription
recall_numberFDA-assigned unique recall identifier
reason_for_recallFree text description of why the recall was initiated
recalling_firmCompany initiating the recall
product_descriptionWhat product was recalled, including lot numbers
product_quantityHow much product is affected
distribution_patternGeographic scope of distribution
classificationClass I, II, or III
statusOngoing, Completed, Terminated
recall_initiation_dateWhen the firm initiated the recall
report_dateWhen FDA published the enforcement report
voluntary_mandatedWhether 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.

Related Actor

Try the scraper referenced in this article — live on Apify, pay only for results.

Open fda-recalls-scraper on Apify →