# openFDA Scraper: Drug Events, Recalls & Device Reports (`arman-bd/openfda-adverse-events-scraper`) Actor

Query openFDA for adverse event reports, enforcement actions and recalls across drugs, devices, food and tobacco. Filter by product, date and free-text search.

- **URL**: https://apify.com/arman-bd/openfda-adverse-events-scraper.md
- **Developed by:** [Arman Hossain](https://apify.com/arman-bd) (community)
- **Categories:** Business, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.56 / 1,000 record scrapeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## openFDA Scraper: Drug Events, Recalls & Device Reports

![openFDA Scraper: Adverse event reports, recalls, device reports and drug labels, normalised into one flat row across drugs, devices, food and tobacco](https://api.apify.com/v2/key-value-stores/ZQOcNAOHrIgTacAmy/records/openfda-adverse-events-scraper.jpg)

Pulls FDA safety surveillance data into one flat, consistent dataset: adverse event reports, enforcement actions, product recalls and drug labels across drugs, medical devices, food and tobacco.

openFDA is the FDA's own public API, so there is no login and nothing to solve with a proxy. Nine dataset combinations are supported and every one normalises into the same record shape, so a drug adverse event and a food recall land in the same columns.

**Agent skill: [SKILL.md](https://api.apify.com/v2/key-value-stores/t7YoTxpZEJOWvw4Ug/records/openfda-adverse-events-scraper.md)**

```
https://api.apify.com/v2/key-value-stores/t7YoTxpZEJOWvw4Ug/records/openfda-adverse-events-scraper.md
```

### What you get

| Field | What it holds |
|---|---|
| `domain`, `endpoint` | Which openFDA dataset the record came from |
| `recordId`, `safetyReportId` | The dataset's own identifier: FAERS report number, recall number, MDR report number or label set ID |
| `productName`, `genericName`, `productDescription` | Brand name, generic or active ingredient, and the full product description where the dataset has one |
| `manufacturer`, `recallingFirm` | Manufacturer as openFDA resolves it, and the firm that issued a recall |
| `reactions` | MedDRA reaction terms for drug and food events; report-text types for device events |
| `seriousness` | Which seriousness criteria the reporter ticked (`Death`, `Hospitalization`, `Life-threatening` and so on), or the device event type |
| `patientAge`, `patientSex` | Decoded from the FAERS numeric codes into `"27 years"` and `"Male"` |
| `outcome` | Reaction outcomes, decoded from codes to text |
| `receiveDate`, `reportDate` | ISO dates, converted from openFDA's `YYYYMMDD` and `MM/DD/YYYY` forms |
| `recallReason`, `classification`, `recallNumber`, `status` | Recall detail: Class I, II or III, ongoing or terminated |
| `country`, `state`, `city` | Where the firm or reporter is located |
| `narrative` | The longest free-text field the dataset offers: MDR narrative, indication, distribution pattern or root cause |
| `productQuery`, `scrapedAt` | Which product name produced the record, and the run timestamp |

Every record carries the same key set whichever dataset you query. Fields that do not apply are `null`.

`RUN_SUMMARY` in the key-value store holds per-run counts, the filters you used, and any query that failed.

### Use cases

- **Pharmacovigilance.** Sweep FAERS nightly for new reports naming your molecules and page on a seriousness change.
- **Competitive recall monitoring.** Watch `drug/enforcement` and `device/recall` for a list of competitor firms.
- **Clinical risk research.** Pull every report pairing a drug with a reaction term and count by year.
- **Supply chain and food safety.** Track Class I food recalls by state and distribution pattern.
- **Regulatory intelligence.** Diff `drug/label` set IDs to spot label changes on a watched product.

### Quick start

Recent adverse event reports for one drug:

```json
{
 "domain": "drug",
 "endpoint": "event",
 "productNames": ["ibuprofen"],
 "maxResults": 100
}
```

Two drugs, a specific reaction and a date window:

```json
{
 "domain": "drug",
 "endpoint": "event",
 "productNames": ["metformin", "atorvastatin"],
 "searchQuery": "patient.reaction.reactionmeddrapt:\"nausea\"",
 "fromDate": "2024-01-01",
 "toDate": "2025-12-31",
 "maxResults": 2000
}
```

Every Class I food recall this year, no product filter:

```json
{
 "domain": "food",
 "endpoint": "enforcement",
 "searchQuery": "classification:\"Class I\"",
 "fromDate": "2025-01-01",
 "maxResults": 5000,
 "apiKey": "YOUR_OPENFDA_KEY"
}
```

### Input

| Field | Type | Default | Notes |
|---|---|---|---|
| `domain` | string | `drug` | `drug`, `device`, `food` or `tobacco`. |
| `endpoint` | string | `event` | `event`, `enforcement`, `label`, `recall` or `problem`. |
| `productNames` | array | `[]` | One query per name, run against the endpoint's own product field. Empty means the whole dataset. |
| `searchQuery` | string | `""` | Raw openFDA search expression, ANDed with the other filters. |
| `fromDate` | string | `""` | `YYYY-MM-DD` or `YYYYMMDD`, on the endpoint's own date field. |
| `toDate` | string | `""` | Same format, upper bound. |
| `maxResults` | integer | `100` | Cap per product name. Hard ceiling 25,100. |
| `apiKey` | string (secret) | `""` | Optional openFDA key. |

Only these nine domain and endpoint pairs exist. Anything else stops the run immediately with an error listing the valid ones:

| Dataset | Date field used for `fromDate`/`toDate` | Product field used for `productNames` |
|---|---|---|
| `drug/event` | `receivedate` | `patient.drug.medicinalproduct` |
| `drug/enforcement` | `report_date` | `product_description` |
| `drug/label` | `effective_time` | `openfda.brand_name` |
| `device/event` | `date_received` | `device.brand_name` |
| `device/enforcement` | `report_date` | `product_description` |
| `device/recall` | `event_date_initiated` | `product_description` |
| `food/event` | `date_created` | `products.name_brand` |
| `food/enforcement` | `report_date` | `product_description` |
| `tobacco/problem` | `date_submitted` | `tobacco_products` |

Results are always sorted newest first on the dataset's date field.

### Output example

```json
{
 "domain": "drug",
 "endpoint": "event",
 "recordId": "26940509",
 "safetyReportId": "26940509",
 "productName": "METFORMIN",
 "genericName": "METFORMIN",
 "manufacturer": "Mylan Pharmaceuticals Inc.",
 "reactions": [
 "Adrenal insufficiency",
 "Hepatocellular injury",
 "Tachycardia",
 "Abdominal pain",
 "Nausea",
 "Vomiting",
 "Diarrhoea",
 "Muscular weakness"
 ],
 "seriousness": "Hospitalization; Other serious",
 "patientAge": "27 years",
 "patientSex": "Male",
 "receiveDate": "2026-06-30",
 "reportDate": "2026-07-09",
 "outcome": ["Recovered/resolved", "Not recovered/not resolved"],
 "recallReason": null,
 "classification": null,
 "recallNumber": null,
 "status": null,
 "recallingFirm": null,
 "productDescription": null,
 "country": "US",
 "state": null,
 "city": null,
 "narrative": "Diabetes mellitus inadequate control",
 "productQuery": "metformin",
 "scrapedAt": "2026-08-06T11:49:45.177Z"
}
```

A `drug/enforcement` record fills the recall side of the same shape instead:

```json
{
 "domain": "drug",
 "endpoint": "enforcement",
 "recordId": "D-0390-2026",
 "productName": "IBUPROFEN",
 "manufacturer": "Sun Pharmaceutical Industries, Inc.",
 "receiveDate": "2026-03-02",
 "reportDate": "2026-03-25",
 "recallReason": "Presence of foreign substance: the firm received complaints for a gel-like mass and black particles in the product.",
 "classification": "Class II",
 "recallNumber": "D-0390-2026",
 "status": "Ongoing",
 "recallingFirm": "STRIDES PHARMA INC",
 "productDescription": "Children's Ibuprofen Oral Suspension, USP, 100 mg per 5mL, 4 FL OZ (120 mL) bottle .",
 "country": "United States",
 "state": "NJ",
 "city": "Bridgewater",
 "narrative": "U.S.A. Nationwide"
}
```

### Writing a search expression

`searchQuery` is passed through to openFDA's Lucene-style syntax and ANDed with the product and date filters. Useful patterns:

| Goal | Expression |
|---|---|
| One reaction term | `patient.reaction.reactionmeddrapt:"nausea"` |
| Serious reports only | `serious:1` |
| Reports where the patient died | `patient.reaction.reactionoutcome:5` |
| One manufacturer | `patient.drug.openfda.manufacturer_name:"Pfizer"` |
| Class I recalls | `classification:"Class I"` |
| Recalls in one state | `state:"CA"` |
| Two terms as an OR | `patient.reaction.reactionmeddrapt:("nausea" OR "vomiting")` |

Field names differ between datasets. The [openFDA field reference](https://open.fda.gov/apis/) lists every searchable field per endpoint.

### API example

```bash
curl -X POST "https://api.apify.com/v2/acts/arman-bd~openfda-adverse-events-scraper/run-sync-get-dataset-items?token=YOUR_TOKEN" \
 -H "Content-Type: application/json" \
 -d '{
 "domain": "drug",
 "endpoint": "event",
 "productNames": ["ibuprofen"],
 "fromDate": "2025-01-01",
 "maxResults": 500
 }'
```

### JavaScript example

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

const client = new ApifyClient({ token: 'YOUR_TOKEN' });
const run = await client.actor('arman-bd/openfda-adverse-events-scraper').call({
 domain: 'drug',
 endpoint: 'enforcement',
 searchQuery: 'classification:"Class I"',
 fromDate: '2025-01-01',
 maxResults: 1000,
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
const byFirm = {};
for (const r of items) byFirm[r.recallingFirm] = (byFirm[r.recallingFirm] ?? 0) + 1;
console.log(Object.entries(byFirm).sort((a, b) => b[1] - a[1]).slice(0, 10));
```

### Notes

- 25,000 records per query is the ceiling. openFDA refuses any `skip` above 25,000, so split a bigger pull into date windows, or into several product names, to get past it.
- A query that matches nothing comes back as a 404. openFDA returns `NOT_FOUND` rather than an empty list, and the Actor treats that as zero results rather than an error and moves on.
- Codes are decoded, not passed through. FAERS ships sex, age unit, reaction outcome and seriousness as numeric codes, including a seriousness flag where `"2"` means no, which is easy to misread as a set flag. All four are decoded to text.
- The searched product is the one reported. A FAERS report lists every drug the patient was taking, so the record picks the drug matching your `productNames` entry and falls back to the reporter's designated suspect drug.
- Rate limits are per IP. Unauthenticated you get 240 requests per minute and 1,000 per day. With a free key it is 240 per minute and 120,000 per day. One page is 100 records, so a 5,000-record pull is 50 requests.
- openFDA is slow. Some device endpoints take four seconds per request, so the request timeout is 60 seconds and the default run timeout is 30 minutes.
- Transient errors get retried. 429 and 5xx responses get three attempts with linear backoff, and one failing product name does not stop the others.

### FAQ

**Do I need an API key?** No, but a free key from open.fda.gov raises the daily cap from 1,000 to 120,000 requests. Paste it into `apiKey`, which is stored as a secret.

**Do I need a proxy?** No. Proxy configuration is not required to run this Actor.

**Is this data validated?** No, and the FDA says so explicitly. Adverse event reports are voluntary submissions, and a report does not establish that the product caused the effect. Duplicate reports of the same case are common, so dedupe on `safetyReportId` before counting.

**Why did a search return nothing?** Field names are dataset-specific. `patient.drug.medicinalproduct` exists in `drug/event` and nowhere else. Check the endpoint table above and the openFDA field reference.

**How current is it?** FAERS is refreshed quarterly with rolling additions, and enforcement reports update weekly. The `meta.last_updated` date openFDA reports is typically within a week or two of today.

**Can I schedule it?** Yes. Run daily with a `fromDate` a few days back and dedupe on `recordId` to build an incremental store.

# Actor input Schema

## `domain` (type: `string`):

Which FDA centre's data to read. Not every endpoint exists in every domain, so check the endpoint field below.

## `endpoint` (type: `string`):

Valid pairs are drug/event, drug/enforcement, drug/label, device/event, device/enforcement, device/recall, food/event, food/enforcement and tobacco/problem. Any other combination stops the run with an error naming the valid ones.

## `productNames` (type: `array`):

Brand or generic names. Each name is queried separately against the endpoint's own product field and tagged as productQuery in the output. Leave empty to search the whole dataset.

## `searchQuery` (type: `string`):

A raw openFDA search expression, for example patient.reaction.reactionmeddrapt:"nausea". It is ANDed with the product name and date filters. Leave empty to filter on those alone.

## `fromDate` (type: `string`):

Lower bound on the endpoint's own date field (receivedate for drug events, report\_date for enforcement, and so on). YYYY-MM-DD or YYYYMMDD. Empty means no lower bound.

## `toDate` (type: `string`):

Upper bound on the same date field. YYYY-MM-DD or YYYYMMDD. Empty means no upper bound.

## `maxResults` (type: `integer`):

Cap on records saved for each product name, or for the whole run when no product is given. openFDA refuses to page past 25,000 records for one query, which is the hard ceiling.

## `apiKey` (type: `string`):

Optional. Without a key openFDA allows 240 requests per minute and 1,000 per day per IP; a free key raises that to 240 per minute and 120,000 per day. Get one at open.fda.gov/apis/authentication.

## Actor input object example

```json
{
  "domain": "drug",
  "endpoint": "event",
  "productNames": [
    "metformin",
    "atorvastatin"
  ],
  "searchQuery": "patient.reaction.reactionmeddrapt:\"nausea\"",
  "fromDate": "2024-01-01",
  "toDate": "2025-12-31",
  "maxResults": 100
}
```

# Actor output Schema

## `items` (type: `string`):

Every record the run produced.

## `runsummary` (type: `string`):

The RUN\_SUMMARY record from the run's key-value store.

# 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 = {
    "domain": "drug",
    "endpoint": "event",
    "productNames": [
        "ibuprofen"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("arman-bd/openfda-adverse-events-scraper").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 = {
    "domain": "drug",
    "endpoint": "event",
    "productNames": ["ibuprofen"],
}

# Run the Actor and wait for it to finish
run = client.actor("arman-bd/openfda-adverse-events-scraper").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 '{
  "domain": "drug",
  "endpoint": "event",
  "productNames": [
    "ibuprofen"
  ]
}' |
apify call arman-bd/openfda-adverse-events-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,arman-bd/openfda-adverse-events-scraper"
        }
    }
}

```

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/J08SVq04ji66Xkdos/builds/pFsMSE3g8rbZWqbzm/openapi.json
