# openFDA — Drug Labels, Adverse Events, Recalls, Devices (`dalbian/openfda-drug-device-food-data`) Actor

Query the FDA's own open data: prescribing information from drug labels, adverse event reports from FAERS, drug, food and device recalls, and 510(k) device clearances. Field-scoped searches, date ranges, no API key.

- **URL**: https://apify.com/dalbian/openfda-drug-device-food-data.md
- **Developed by:** [Benjamin Jerez](https://apify.com/dalbian) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $30.00 / 1,000 search runs

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## openFDA — Drug Labels, Adverse Events, Recalls and Device Clearances

Six FDA datasets behind one input form. Ask for a drug by brand or ingredient, a firm by name, a hazard class or a date range, and get back the prescribing information, the adverse event reports, the recalls and the device clearances that match.

No API key. The data is US federal government work in the public domain, queried live on every run.

### What is in each dataset

| Dataset | Endpoint | Records | Date field used for filtering |
| --- | --- | --- | --- |
| Drug labels | `/drug/label.json` | 262 883 | `effective_time` |
| Adverse events (FAERS) | `/drug/event.json` | 20 692 627 | `receiptdate` |
| Drug recalls | `/drug/enforcement.json` | 17 965 | `recall_initiation_date` |
| Food recalls | `/food/enforcement.json` | 29 406 | `recall_initiation_date` |
| Device recalls | `/device/enforcement.json` | 39 949 | `recall_initiation_date` |
| Device clearances (510k) | `/device/510k.json` | 176 070 | `decision_date` |

The date field differs per dataset and so does the field a manufacturer lives in. You give one date range and one list of company names; the Actor translates them per endpoint, and the summary row prints the search string it actually sent.

### What a run returns

Every dataset produces a `summary` row first, then the records.

Searching `foodEnforcement` for `Listeria`, Class I, initiated between 2025-01-01 and 2025-06-30:

```
rowType         summary
dataset         foodEnforcement
searchString    (product_description:"Listeria" OR reason_for_recall:"Listeria")
                AND recall_initiation_date:[20250101 TO 20250630]
                AND classification:"Class I"
totalMatching   127
returned        4
capped          false
```

and the records:

| `recallInitiationDate` | `classification` | `recallingFirm` | `productDescription` | `reasonForRecall` |
| --- | --- | --- | --- | --- |
| 20250527 | Class I | AND GO CONCEPTS DBA SALAD AND GO | FG20004 Cucumber, Sliced, poly bag | Potential to be contaminated with Listeria monocytogenes |
| 20250510 | Class I | Fresh & Ready Foods LLC | Fresh Take Crave Away Club Supreme Sub Sandwich, 6oz | Listeria monocytogenes detected on food contact surfaces |

A drug label search for `Lipitor` returns one record: generic `ATORVASTATIN CALCIUM`, labeller `Viatris Specialty LLC`, application `NDA020702`, UNII `48A5M73Z4Q`, eight RxCUI codes, four NDCs, effective `20240415`, plus the indications, dosage, contraindications, warnings, adverse reactions, interactions, pregnancy, paediatric and geriatric sections as plain text.

A device clearance search for `insulin pump` decided in 2024 or 2025 returns five: `K253074` Tandem Mobi (Tandem Diabetes Care, decided 2025-11-06), `K251032` MiniMed 780G (Medtronic Minimed, 2025-07-01), and three more Tandem filings. All product code `QFG`, class 2, regulation 880.5730, Traditional 510(k), Substantially Equivalent.

### How the search is built

Every value you supply is quoted and prefixed with a field name. This is the difference between a search and a word count.

| Query | Hits | What comes back |
| --- | --- | --- |
| `openfda.brand_name:"Tylenol"` | 111 | Tylenol products |
| `Tylenol` | 3 142 | Any label mentioning the word, mostly other painkillers |
| `openfda.brand_name:"Advil PM"` | 6 | Advil PM |
| `openfda.brand_name:Advil PM` | 10 235 | An OR of two common words |

openFDA scores rather than filters when you leave the field off, so an unscoped term ranks the whole index and hands back the top of it. Unquoted multi-word terms are split into an OR. Neither failure raises an error; both just return plausible-looking rows about the wrong thing. The Actor never emits either form, and the `searchString` field on the summary row lets you check.

Within one filter, values are combined with OR, so two brand names widen the search. Between filters they are combined with AND, so a brand plus a date range narrows it.

### Reading FAERS adverse events correctly

A FAERS report lists every drug the patient was on, not only the one suspected of causing the reaction. openFDA's `patient.drug.*` fields cover all of them, so searching a drug returns reports where it was merely background medication.

This is not a small effect. Searching serious 2025 reports for `semaglutide` matches 10 866 reports; of the first twelve, eleven list semaglutide only as a concomitant drug and one as a suspect drug. The suspect drugs in the other eleven were Xolair, Xyrem, Actemra, Revlimid and others.

Each adverse event row therefore carries `queryDrugRole`:

- `suspect` — the reporter named the drug you searched for as a cause
- `concomitant` — the patient was also taking it
- empty — no drug on the report matched your term, which happens when a free-text term matched a reaction name instead; searching `insulin` returns report 10032297 because it lists the reaction "Insulin-like growth factor decreased"

Filter on `suspect` before counting anything. The row also carries `suspectDrugs` and `concomitantDrugs` separately, and `outcomes` as words rather than the FAERS digit codes — `death`, `hospitalisation`, `disabling`, `life-threatening`, `congenital anomaly`, `other serious`.

#### A brand name may not mean the brand

openFDA harmonises each reported drug against its product index and attaches every brand name that shares the ingredient. The aspirin entry carries more than 130 of them, from Bayer to Ecotrin to a supermarket own-label.

The consequence is blunt. `patient.drug.openfda.brand_name:"Ecotrin"` matches **515 357** reports; only **8 538** reports actually named Ecotrin. The rest reported plain aspirin and were harmonised into the same brand list.

So every adverse event row also carries `queryDrugReportedAs` — the product string the reporter actually wrote for the drug that matched your search. Searching Ecotrin and reading that column shows `ASPIRIN` on nine rows out of ten and `ECOTRIN` on one. If you need reports that named the brand, filter on it.

Distinctive brands do not suffer this. Lipitor matches 248 030 reports against 180 902 that named it, an expansion of atorvastatin generics rather than a different drug.

A report is a report. FAERS submissions are unvalidated, unverified and carry no denominator, so they do not establish that a drug caused anything.

### Label text is truncated

Drug label sections are arrays of long prose. The longest single section seen while building this Actor was 69 834 characters; several routinely pass 30 000. In one test run of forty labels, sixteen had at least one section over the limit.

Each section is joined, whitespace-collapsed and cut at **4 000 characters**, with ` […]` marking the cut. The row lists every section it shortened in `truncatedFields`, and `labelUrl` points at the complete label on DailyMed.

The sections returned are boxed warning, indications and usage, dosage and administration, contraindications, warnings and cautions, adverse reactions, drug interactions, pregnancy, paediatric use, geriatric use and mechanism of action. `hasBoxedWarning` is a boolean you can filter on directly.

### Empty results are not errors

openFDA answers a search that matches nothing with HTTP 404 and `{"error":{"code":"NOT_FOUND","message":"No matches found!"}}`. That is a valid answer: the query was understood, the index holds nothing for it.

The Actor treats it as such. You get a summary row with `totalMatching: 0` and a note saying so, not a failed run. Searching `Ozempic` across food recalls, device recalls and device clearances produces three such rows in a single run, alongside the 18 767 adverse events that do exist.

### The 26 000 record ceiling

openFDA refuses `limit` above 1 000 and `skip` above 25 000, which puts a hard ceiling of 26 000 records on any single search. Past that it returns HTTP 400 with `Skip value must 25000 or less.`

When a run hits the ceiling, the summary row sets `capped: true` and the note says how many records matched against how many were reachable. To get the rest, split the run into narrower date ranges — the `dateFrom` and `dateTo` inputs exist partly for this.

### Who uses this

Pharmacovigilance and drug safety teams pulling FAERS signals for a molecule without maintaining their own extract of a twenty-million-row dataset. Regulatory affairs staff tracking competitor 510(k) clearances by product code or applicant. Food safety and supply chain teams watching recalls by firm, state or hazard class. Clinical decision support and formulary tools that need the current prescribing information keyed to an NDC or RxCUI. Health journalists and researchers who need a defensible, citable query rather than a screenshot of a search page.

### Scope and limits

**Labels are current, not historical.** `/drug/label.json` holds the label in force now, one record per SPL set id. `effective_time` is when that version took effect. A date range on it selects labels whose current version took effect in that window, which is not the same as labels that were in force then. Ozempic has four label records with effective times in 2023 and 2026 and none in 2025, so a 2025 range returns nothing.

**FAERS is a passive reporting system.** No denominator, no verification, heavy reporting bias toward newly marketed and heavily litigated drugs, and duplicate reports are common enough that the raw records carry a `duplicate` flag.

**Recalls are what the firm and the FDA district office wrote down.** Product descriptions are free text, so a brand may be spelled several ways across a single event. Search the product description with more than one spelling if the brand matters.

**510(k) covers clearances, not approvals.** A cleared device was found substantially equivalent to a predicate. PMA approvals and De Novo grants are separate datasets, not covered here.

**Rate limits.** Without a key openFDA allows 240 requests per minute and 1 000 per day, shared across everyone calling from the same address. One dataset at the 26 000 ceiling costs 26 requests, so a normal run is nowhere near either limit.

### Source and licence

**openFDA** at `api.fda.gov`, operated by the US Food and Drug Administration. US federal government works in the public domain; terms at `open.fda.gov/terms`, licence at `open.fda.gov/license`.

The FDA's own disclaimer applies and is worth repeating: the data is not validated, and it is not a basis for decisions about medical care.

Index freshness at the time of writing: drug labels 2026-09-18, recalls 2026-09-09, device clearances 2026-09-07, adverse events 2026-07-30. Each response carries its own `last_updated`.

### Frequently asked

**Why did my brand name return nothing?**
Because `openfda.brand_name` is populated from the structured label, not from the text. Generic and repackaged products often carry the generic name only. Try the ingredient in Generic names, or put the term in Search terms, which covers brand, generic and substance fields at once.

**Why does one drug return several label records?**
One record per SPL set id. Repackagers and relabellers file their own, so Ozempic has four and a common generic can have hundreds. `manufacturerName` tells them apart; `setId` is the stable identifier.

**Can I get more than 26 000 records for one search?**
Not in one search. Split it by date range and run again. The Actor tells you when you have hit the ceiling rather than silently stopping.

**Are the adverse event counts a safety signal?**
No. They are report counts with no exposure denominator, and the total on the summary row counts every report the drug appears anywhere in. Use `queryDrugRole: suspect` as a floor, check `queryDrugReportedAs` for what was actually reported, treat the rest as context, and read the FDA's own guidance before drawing a conclusion.

**Why is a field empty?**
openFDA harmonises records against its own product index and leaves the `openfda` block out when it cannot match. Drug recalls in particular frequently have no `openfda` block at all, which is why recalls are searched on `product_description` and `recalling_firm` rather than on structured names.

**What counts as a charged record?**
One `search-run` event per dataset per run, including one that legitimately returns nothing, and one `record-returned` event per record pushed.

# Actor input Schema

## `datasets` (type: `array`):

Which openFDA endpoints to query. Every filter below is applied to each dataset you pick, mapped to that dataset's own field names — a manufacturer becomes openfda.manufacturer\_name on a drug label and recalling\_firm on a recall.

## `searchTerms` (type: `array`):

Free text, one term per line. Each term is matched against the fields that carry a name in the chosen dataset: brand, generic and substance names on labels, drug and reaction terms on adverse events, product description and recall reason on recalls, device name on clearances. Terms are combined with OR, so two lines widen the search.

## `brandNames` (type: `array`):

Exact brand names, one per line — Lipitor, Ozempic, Advil PM. More precise than a free-text term because it is matched only against the brand field.

## `genericNames` (type: `array`):

Active ingredient names, one per line — atorvastatin calcium, semaglutide.

## `manufacturers` (type: `array`):

Company names, one per line. Matched against the labeller on drug labels, the recalling firm on recalls and the applicant on device clearances.

## `dateFrom` (type: `string`):

Start of the date range, as YYYY-MM-DD or YYYYMMDD. Applied to the date field that dataset actually publishes: label effective date, adverse event receipt date, recall initiation date, or clearance decision date.

## `dateTo` (type: `string`):

End of the date range, as YYYY-MM-DD or YYYYMMDD. Inclusive.

## `seriousOnly` (type: `boolean`):

Restrict adverse event reports to those the reporter flagged as serious — death, hospitalisation, disability, life-threatening or congenital anomaly. Ignored by the other datasets.

## `classification` (type: `string`):

Restrict recalls to one FDA hazard class. Ignored by the other datasets.

## `maxResultsPerQuery` (type: `integer`):

Cap on records returned per dataset. openFDA pages 1000 at a time and refuses to skip past 25 000, so 26 000 is the hard ceiling for any one search. The summary row says when that ceiling was reached.

## Actor input object example

```json
{
  "datasets": [
    "drugLabel"
  ],
  "brandNames": [
    "Ozempic"
  ],
  "seriousOnly": false,
  "classification": "",
  "maxResultsPerQuery": 100
}
```

# Actor output Schema

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "brandNames": [
        "Ozempic"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("dalbian/openfda-drug-device-food-data").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "brandNames": ["Ozempic"] }

# Run the Actor and wait for it to finish
run = client.actor("dalbian/openfda-drug-device-food-data").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "brandNames": [
    "Ozempic"
  ]
}' |
apify call dalbian/openfda-drug-device-food-data --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,dalbian/openfda-drug-device-food-data"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/aQNpyLKhDEaGNWm6I/builds/QmG5jchH36gfYqY2W/openapi.json
