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

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.

Try the scraper

The actor referenced in this article. Pay only for results delivered.

View the scraper →

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, OR and NOT as words and write ranges as field:[20260101 TO 99991231]. 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.

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:

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, Class II, Class III, or Not Yet Classified
statusOngoing, Completed, or Terminated
recall_initiation_dateWhen the firm initiated the recall
report_dateWhen FDA published the enforcement report
voluntary_mandatedVoluntary: 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.

EndpointWhat it returnsRecords
drug/eventFAERS adverse event reports submitted for drugs20.7 million
drug/labelStructured product labelling, the approved prescribing information262,000
drug/enforcementDrug recall and market withdrawal reports18,000
device/510kDevice premarket clearance decisions176,000
device/eventMAUDE adverse event reports for medical devices26.1 million
device/enforcementDevice recall enforcement reports40,000
device/recallCDRH device recall and correction records59,000
food/enforcementFood and supplement recall reports29,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:

InputWhat it does
endpointOne of drug/event, drug/label, drug/enforcement, device/510k, device/recall, device/event, food/enforcement. One endpoint per run.
searchPassed through to openFDA unchanged, so the whole query grammar above is available. Write spaces, not +.
maxResults1 to 5,000. Defaults to 100.
fdaApiKeyOptional. 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:

EndpointDate fieldFormat
drug/eventreceivedate20260101
drug/labeleffective_time20260101
drug/enforcementrecall_initiation_date, report_date20260101
device/510kdecision_date, date_received2026-01-01
device/recallevent_date_initiated, event_date_posted2026-01-01
device/eventdate_received, date_of_event2026-01-01
food/enforcementrecall_initiation_date, report_date20260101

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.

FieldWhat it is
recall_numberFDA recall identifier, for example D-0123-2026
event_idFDA event ID grouping related recall records
statusOngoing, Completed or Terminated
classificationClass I, Class II, Class III or Not Yet Classified
product_typeDrugs, Devices or Food
recalling_firmName of the firm conducting the recall
reason_for_recallFree text reason
product_descriptionProduct name, strength, pack size and lot detail
product_quantityAmount in distribution, as free text
code_infoLot and expiry codes covered by the recall
more_code_infoOverflow lot codes
distribution_patternWhere the product went, as free text
voluntary_mandatedVoluntary: Firm initiated, FDA Mandated or N/A
initial_firm_notificationHow the firm told its customers, for example Letter, E-Mail
recall_initiation_dateDate the firm started the recall, YYYYMMDD
center_classification_dateDate the FDA centre assigned the class, YYYYMMDD
report_dateDate FDA published the report, YYYYMMDD
termination_dateDate the recall was closed, YYYYMMDD, absent while ongoing
city, state, countryLocation of the recalling firm, not of the affected product
address_1, address_2, postal_codeStreet address of the recalling firm
openfdaEnrichment 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.

FieldWhat it is
product_res_numberRecall identifier, for example Z-0001-04
res_event_numberEvent number grouping products in one recall
cfres_idInternal CDRH recall record ID
recall_statusRecall status, for example Terminated
recalling_firmName of the firm conducting the recall
firm_fei_numberFDA Establishment Identifier, numeric, for exact firm matching
product_codeThree letter CDRH device code, for example LWS
k_numbersArray of 510(k) numbers for the recalled device
product_descriptionDevice name, model and manufacturer, as free text
product_quantityUnits affected
reason_for_recallFree text reason
root_cause_descriptionFDA’s cause category, for example Device Design, Other
actionWhat the firm did, for example the text of the safety notice
code_infoSerial, lot or model numbers affected
distribution_patternWhere the device went
event_date_initiatedDate the firm started the recall, YYYY-MM-DD
event_date_postedDate FDA posted the record, YYYY-MM-DD
event_date_terminatedDate the recall was closed, YYYY-MM-DD
additional_info_contactContact named for enquiries
pma_numbersArray of PMA numbers, for devices approved rather than cleared
event_date_createdDate the record was created, YYYY-MM-DD
city, state, country, postal_code, address_1, address_2Recalling firm’s address

device/510k

FieldWhat it is
k_numberClearance number, for example K202166
applicantCompany that submitted the notification
contactNamed contact on the submission
device_nameTrade name as submitted
product_codeThree letter CDRH device code
decision_codeShort decision code, for example SESE
decision_descriptionDecision in words, for example Substantially Equivalent
decision_dateDate of the decision, YYYY-MM-DD
date_receivedDate FDA received the submission, YYYY-MM-DD
clearance_typeFor example Traditional, Special, Abbreviated
advisory_committeeTwo letter panel code
advisory_committee_descriptionPanel in words, for example Cardiovascular
review_advisory_committeePanel that actually reviewed it
statement_or_summaryWhether a summary or a statement was filed
third_party_flag, expedited_review_flagY or N
address_1, address_2, city, state, country_code, zip_code, postal_codeApplicant address
openfdadevice_class, device_name, regulation_number, medical_specialty_description, fei_number, registration_number

drug/event (FAERS)

FieldWhat it is
safetyreportidReport identifier
safetyreportversionVersion of the report
serious1 if the report is serious, 2 if not
seriousnessdeath1 when the report involves death
seriousnesshospitalization1 when it involves hospitalisation
seriousnesslifethreatening1 when life-threatening
seriousnessdisabling1 when disabling
seriousnessother1 for other serious outcomes
receivedateDate FDA first received the report, YYYYMMDD
receiptdateDate of the most recent version, YYYYMMDD
transmissiondateDate FDA released the record, YYYYMMDD
reporttypeFor example expedited or periodic
occurcountry, primarysourcecountryWhere the event happened and where it was reported from
primarysource.qualificationReporter type, for example physician or consumer
companynumbManufacturer’s own report number
sender.senderorganizationWho sent the report to FDA
duplicate, reportduplicateFlags for known duplicate submissions
patient.patientsex1 male, 2 female
patient.patientonsetage, patient.patientonsetageunitAge at onset and its unit
patient.patientagegroupBanded age group
patient.patientweightWeight in kilograms
patient.patientdeathPresent when the patient died
patient.reaction[].reactionmeddraptReaction as a MedDRA preferred term
patient.reaction[].reactionoutcomeCoded outcome, for example recovered or fatal
patient.drug[].medicinalproductDrug name as reported
patient.drug[].activesubstanceActive substance name
patient.drug[].drugcharacterization1 suspect, 2 concomitant, 3 interacting
patient.drug[].drugindicationWhy the drug was taken
patient.drug[].drugdosagetext, drugdosageformDose as free text and dosage form
patient.drug[].drugadministrationrouteCoded route
patient.drug[].drugstartdate, drugenddateTreatment dates
patient.drug[].actiondrugWhat was done with the drug, for example withdrawn
patient.drug[].drugbatchnumbLot number when reported

device/event (MAUDE)

The widest record in openFDA, with over eighty top-level fields. The ones you will actually query:

FieldWhat it is
report_number, mdr_report_keyReport identifiers
event_typeMalfunction, Injury, Death or No answer provided
date_receivedDate FDA received the report, YYYY-MM-DD, the field to filter on
date_of_eventDate the event happened
date_reportDate the report was written
event_locationWhere it happened, for example Hospital
adverse_event_flag, product_problem_flagY or N
manufacturer_name, manufacturer_city, manufacturer_state, manufacturer_countryManufacturer of record
number_devices_in_event, number_patients_in_eventCounts
remedial_actionWhat was done, for example Recall, Repair
report_source_codeWho reported, for example manufacturer or voluntary
reporter_occupation_codeReporter’s role
device[].brand_name, device[].generic_nameDevice names
device[].manufacturer_d_nameManufacturer as given on the device
device[].device_report_product_codeThree letter CDRH device code
device[].model_number, catalog_number, lot_numberDevice identifiers
device[].implant_flag, device[].single_use_flagY or N
device[].device_evaluated_by_manufacturerWhether the device was returned and examined
mdr_text[].textThe narrative. text_type_code separates the event description from the manufacturer’s evaluation
patient[].sequence_number_outcomePatient outcome codes
patient[].patient_sex, patient_age, patient_weightPatient detail when reported

drug/label

drug/label fields are label sections, and around eighty of them exist. The commonly used ones:

FieldWhat it is
id, set_id, versionSPL document identifiers. set_id is stable across versions
effective_timeVersion date, YYYYMMDD
indications_and_usageWhat the drug is approved to treat
dosage_and_administrationDosing
boxed_warningThe black box warning, present only when the drug has one
warnings, warnings_and_cautionsWarning text, old and new label formats
contraindicationsWhen not to use it
adverse_reactionsReported adverse reactions
drug_interactionsInteraction text
clinical_pharmacology, mechanism_of_action, pharmacokineticsPharmacology sections
use_in_specific_populations, pregnancy, pediatric_use, geriatric_usePopulation sections
active_ingredient, inactive_ingredientIngredients, mostly on OTC labels
purpose, do_not_use, ask_doctor, when_using, stop_useOTC Drug Facts sections
how_supplied, storage_and_handlingPackaging and storage
spl_product_data_elementsFlat list of product names and ingredients
openfda.brand_name, generic_name, manufacturer_nameNames, as arrays
openfda.product_ndc, package_ndc, application_numberProduct identifiers
openfda.rxcui, unii, substance_nameRxNorm and UNII codes, for joining to other datasets
openfda.pharm_class_epc, pharm_class_moa, pharm_class_cs, pharm_class_pePharmacologic 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.

Related Actor

Explore the scraper referenced in this article: inputs, outputs, and pricing, then run it on Apify.

Apify Store

Find a ready-made scraper for your job

Apify has over 80,000 scrapers and automations, ours included. Start free with $5 of platform credit every month.