openfda-api

Query the openFDA REST API (api.fda.gov) for public FDA data on drugs, devices, food, and tobacco. Covers 17 endpoints including drug adverse events (FAERS), drug labeling, Drugs@FDA approvals, NDC directory, device adverse events (MAUDE), 510(k) clearances, PMA approvals, device classification, UDI/GUDID, recall enforcement reports, food adverse events (CAERS), tobacco problems, NSDE, and substance data. Trigger for any mention of openFDA, FAERS, MAUDE, CAERS, adverse events, drug reactions, recall data, 510(k), PMA, NDC, UDI, drug labeling, safety signals, or FDA product data. Also trigger for pharmacovigilance, post-market surveillance, adverse event profiling, predicate device research, or recall intelligence across any FDA-regulated product category.

openFDA API Skill v1.1

Changelog

  • v1.1: Refactored for efficiency; moved endpoint field details, composite workflows, pagination, response schemas to REFERENCE.md
  • v1.0: Initial release covering 17 endpoints

Overview

The openFDA API (https://api.fda.gov) provides free, public access to FDA datasets on drugs, devices, food, tobacco, and other regulated products. Elasticsearch-based, returns JSON.

Base URL: https://api.fda.gov Docs: https://open.fda.gov

For endpoint field details, composite workflows (safety signal scan, drug comparison, predicate finder, recall intelligence), pagination, and response schemas: view the REFERENCE.md file in this skill directory.

Authentication and Rate Limits

Query Syntax

https://api.fda.gov/{noun}/{endpoint}.json?search={field}:{term}&limit={n}
ParameterDescriptionNotes
searchField:term search+AND+ for AND, space for OR
countCount unique valuesReturns aggregation, not records
limitMax recordsDefault 1, max 1000
skipPagination offsetMax skip+limit = 25,000
sortSort by fieldAppend :asc or :desc

Search Patterns

search=field:term                          # Single field
search=field:term+AND+field:term           # AND
search=field:term+field:term               # OR (space)
search=field:[20240101+TO+20241231]        # Date range
search=field:"exact phrase"                # Exact phrase
search=field:term*                         # Wildcard
search=_exists_:field                      # Field exists

Critical: Use .exact on text fields for count (e.g., count=patient.reaction.reactionmeddrapt.exact). Without it, multi-word values tokenize and the API returns SERVER_ERROR. Do NOT use .exact on numeric fields (returns 404).


Core Helper

import urllib.request, urllib.parse, json

BASE = "https://api.fda.gov"

def openfda_query(endpoint, search=None, count=None, limit=None, skip=None, sort=None, api_key=None):
    """Generic openFDA query."""
    params = {}
    if search: params["search"] = search
    if count: params["count"] = count
    if limit: params["limit"] = str(limit)
    if skip: params["skip"] = str(skip)
    if sort: params["sort"] = sort
    if api_key: params["api_key"] = api_key
    url = f"{BASE}/{endpoint}.json"
    if params:
        url += "?" + urllib.parse.urlencode(params, safe=':+[]"*')
    req = urllib.request.Request(url, headers={"Accept": "application/json"})
    with urllib.request.urlopen(req, timeout=15) as resp:
        return json.loads(resp.read().decode())

def safe_query(endpoint, **kwargs):
    """Wrapper that handles 404 (no results) and 500 (server errors) gracefully."""
    try:
        return openfda_query(endpoint, **kwargs)
    except urllib.error.HTTPError as e:
        if e.code == 404:
            return {"meta": {"results": {"total": 0}}, "results": []}
        if e.code == 500:
            body = e.read().decode() if hasattr(e, 'read') else ""
            return {"meta": {"results": {"total": 0}}, "results": [], "error": body}
        raise

Endpoint Map

Drug Endpoints

EndpointDescriptionKey Use
drug/eventFAERS adverse events (18M+)Safety signals, reaction lookups
drug/labelSPL labeling (130K+)Prescribing info, boxed warnings
drug/drugsfdaDrugs@FDA approvals (45K+)Approval history, sponsors
drug/ndcNDC Directory (300K+)Product/packaging lookup
drug/enforcementRecall enforcement (17K+)Drug recalls

Device Endpoints

EndpointDescriptionKey Use
device/eventMAUDE adverse events (15M+)Device safety, MDR reports
device/510k510(k) clearances (230K+)Predicate research
device/pmaPMA approvals (55K+)Class III device approvals
device/classificationProduct classifications (7K+)Device class, regulation numbers
device/udiGUDID identifiers (3M+)UDI lookups
device/enforcementRecall enforcement (38K+)Device recalls
device/recallDevice recalls (100K+)Separate from enforcement

Food, Tobacco, Other

EndpointDescriptionKey Use
food/eventCAERS adverse events (148K+)Food/supplement safety
food/enforcementRecall enforcement (28K+)Food recalls
tobacco/problemProblem reports (1K+)Tobacco complaints
other/nsdeNSDE product data (652K+)NDC-level comprehensive data
other/substanceUNII substance data (167K+)Ingredient/UNII lookup

Quick Reference: Common Queries

# Drug: Top adverse reactions
openfda_query("drug/event",
    search="patient.drug.openfda.generic_name:metformin",
    count="patient.reaction.reactionmeddrapt.exact", limit=10)

# Drug: Labeling by brand name
openfda_query("drug/label",
    search='openfda.brand_name:"LIPITOR"', limit=1)

# Drug: Approval by sponsor
openfda_query("drug/drugsfda",
    search='sponsor_name:"PFIZER"', limit=10)

# Drug: Recent Class I recalls
openfda_query("drug/enforcement",
    search='classification:"Class I"+AND+report_date:[20250101+TO+20261231]', limit=10)

# Device: MAUDE events by generic name
openfda_query("device/event",
    search='device.generic_name:"INFUSION PUMP"', limit=10)

# Device: 510(k) by product code
openfda_query("device/510k",
    search="product_code:DXN", limit=10, sort="decision_date:desc")

# Device: Classification lookup
openfda_query("device/classification",
    search="product_code:DXN", limit=5)

# Food: Dietary supplement adverse events
openfda_query("food/event",
    search='products.industry_name:"Dietary Supplements"',
    count="reactions.exact", limit=20)

# NDC lookup
openfda_query("drug/ndc", search='product_ndc:"0071-0155"', limit=1)

# Substance/UNII lookup (names are case-sensitive, use uppercase)
openfda_query("other/substance", search="names.name:CAFFEINE", limit=1)

Critical Rules

  1. .exact on text fields for count. Without it: SERVER_ERROR. With it: counts whole phrases. Do NOT use on numeric fields (returns 404).
  2. 404 = no results, not an error. Wrap in safe_query.
  3. FAERS ≠ incidence rates. Spontaneous reporting with known biases (stimulated reporting, Weber effect, underreporting). Never say a drug "causes" a reaction from FAERS alone.
  4. Not validated for clinical use. Always caveat.
  5. Nested fields use dot notation. E.g., patient.drug.openfda.brand_name.
  6. openfda.* subfields are harmonized and more reliable than raw reported fields for structured lookups and counting.
  7. Quoted phrases + AND can cause 404s. Drop quotes and use single keyword, or split into separate queries.
  8. Skip + limit max = 25,000. Use bulk downloads for larger datasets: https://open.fda.gov/data/downloads/
  9. Substance endpoint: No top-level substance_name. Search via names.name:TERM (case-sensitive, uppercase).
  10. NSDE proprietary_name: Case-sensitive. Use proper case.
  11. sponsor_name.exact doesn't work for count on drug/drugsfda. Use openfda.manufacturer_name.exact instead.

Rate Limiting

  1. Add time.sleep(0.3) between batch requests
  2. With API key: 240/min, 120K/day. Without: 40/min, 1K/day
  3. 409 = rate limit exceeded; wait and retry

Additional Resources

For full endpoint field lists, composite workflows (safety signal scan, drug profile comparison, predicate device finder, recall intelligence), pagination helper, response schemas, and troubleshooting: view the REFERENCE.md file in this skill directory.