The openFDA API is one of the most valuable free resources available to healthcare technology developers. Launched in 2014, it provides programmatic access to millions of records across drug labels, adverse event reports, product recalls, device complaints, and food enforcement actions. For teams building formulary management systems, clinical decision support tools, or drug safety monitoring platforms, it is an essential data source.

This guide covers the practical details: what endpoints are available, how the query syntax works, what the rate limits are, and the patterns we have found most effective when building production systems on top of it.

Available Endpoints

The openFDA API is organized around five major domains, each with multiple endpoints:

Query Syntax

All openFDA endpoints use a common query syntax based on Elasticsearch. The base URL pattern is:

https://api.fda.gov/drug/label.json?search=QUERY&limit=N

The search parameter accepts field-level queries using the format field:value. Here is an example that fetches drug label data for atorvastatin:

# Fetch labels containing "atorvastatin" in the generic name field
curl "https://api.fda.gov/drug/label.json?\
search=openfda.generic_name:atorvastatin&\
limit=5"

You can combine conditions with AND and OR operators:

# Find adverse events for metformin with outcome "Death"
curl "https://api.fda.gov/drug/event.json?\
search=patient.drug.medicinalproduct:metformin+AND+\
serious:1&\
limit=10"

Counting and Aggregation

The count parameter lets you aggregate results without fetching individual records. This is extremely useful for analytics dashboards:

# Count adverse events by reaction for lisinopril
curl "https://api.fda.gov/drug/event.json?\
search=patient.drug.medicinalproduct:lisinopril&\
count=patient.reaction.reactionmeddrapt.exact"

This returns a ranked list of reaction terms and their frequencies, which is exactly the kind of data a formulary team needs when evaluating drug safety profiles.

Rate Limits and API Keys

Without an API key, openFDA allows 40 requests per minute and 1,000 requests per day. With a free API key (available at open.fda.gov), those limits increase to 240 requests per minute and no daily cap. For production systems, the API key is mandatory.

Register your key and include it as a query parameter:

https://api.fda.gov/drug/label.json?api_key=YOUR_KEY&search=...

Practical Patterns for Production Use

Pattern 1: Local Caching with TTL

Drug label data does not change frequently. Most labels are updated quarterly or less. Caching API responses locally with a 24-hour TTL dramatically reduces API calls while keeping data reasonably current:

import hashlib, json, time, os

CACHE_DIR = "./openfda_cache"
CACHE_TTL = 86400  # 24 hours

def get_cached(url):
    key = hashlib.md5(url.encode()).hexdigest()
    cache_path = f"{CACHE_DIR}/{key}.json"

    if os.path.exists(cache_path):
        age = time.time() - os.path.getmtime(cache_path)
        if age < CACHE_TTL:
            with open(cache_path) as f:
                return json.load(f)

    response = requests.get(url)
    data = response.json()

    os.makedirs(CACHE_DIR, exist_ok=True)
    with open(cache_path, 'w') as f:
        json.dump(data, f)

    return data

Pattern 2: Batch Lookups with Retry

When processing a full formulary against openFDA (often thousands of NDCs), you need a robust batch pipeline with exponential backoff:

import time

def batch_lookup(ndc_list, batch_size=10):
    results = {}
    for i in range(0, len(ndc_list), batch_size):
        batch = ndc_list[i:i+batch_size]
        query = "+OR+".join(
            f'openfda.package_ndc:"{ndc}"' for ndc in batch
        )
        url = f"https://api.fda.gov/drug/label.json?search={query}&limit=100"

        for attempt in range(3):
            try:
                data = get_cached(url)
                for result in data.get("results", []):
                    for ndc in result.get("openfda", {}).get("package_ndc", []):
                        results[ndc] = result
                break
            except Exception:
                time.sleep(2 ** attempt)

    return results

Pattern 3: Adverse Event Signal Detection

For formulary teams evaluating drug safety, the adverse event endpoint can surface signals that inform tier placement and coverage decisions:

# Compare adverse event profiles for two drugs in the same class
def compare_safety(drug_a, drug_b, top_n=20):
    """Fetch top adverse reactions for two drugs and compare."""
    base = "https://api.fda.gov/drug/event.json"
    results = {}

    for drug in [drug_a, drug_b]:
        url = (f"{base}?search=patient.drug.medicinalproduct:{drug}"
               f"&count=patient.reaction.reactionmeddrapt.exact"
               f"&limit={top_n}")
        data = get_cached(url)
        results[drug] = {
            r["term"]: r["count"]
            for r in data.get("results", [])
        }

    return results

Limitations to Know About

OpenFDA is powerful but has specific limitations that affect system design:

Despite these limitations, openFDA is a foundation of any modern healthcare data stack. Combined with RxNorm for drug relationships and DailyMed for prescribing information, it provides a remarkably comprehensive free data layer for pharmaceutical technology applications.