openFDA Drug & Device Records Scraper
Pull FDA recalls, NDC drug products and 510(k) device clearances into a flat table. One dataset per run. No API key, no account, no browser.
How it works
- 1Open it on Apify
Hit Run on Apify — it opens the tool in the cloud, no install.
- 2Set the inputs
Adjust
dataset,search,dateFrom(sensible defaults are pre-filled). - 3Click Run
The tool runs on Apify’s cloud and collects the data for you.
- 4Export the results
Download as JSON, CSV or Excel, or pipe straight into your app, Google Sheets, or an AI agent.
Pricing
$0.001 per record = $1 per 1,000
| You are charged for | When | Price |
|---|---|---|
| Record scraped | One FDA record — a recall, a drug product or a device clearance. Queries that match nothing are never charged. | $0.001 |
Pay-per-event pricing: you are billed per result, not per subscription. Billing is handled by Apify on your own account. These are the live Apify store prices, in effect since 2026-09-20, and they are what you are actually charged.
Inputs
| Field | What it does | Type |
|---|---|---|
dataset | One dataset per run. Each one has its own row shape, and the run returns only that shape. Recall datasets share a single shape across drugs, devices and food, so you can stack three runs into one table. | string |
search | FDA's own query syntax. Leave empty for the most recent records. Examples: recalling_firm:"Abbott" · classification:"Class I" · state:"CA"+AND+status:"Ongoing" · brand_name:"Tylenol" · device_name:"catheter" · applicant.exact:"Medtronic Inc." · "salmonella" on its own searches the whole record. Join clauses with +AND+ or +OR+, wrap phrases in double quotes, and add .exact to a field name to match the whole value instead of single words. | string |
dateFrom | Earliest date to include, as 2025-01-01 or 20250101. Applied to the date that matters for the dataset you picked: report date for recalls, marketing start date for drug products, decision date for device clearances. | string |
dateTo | Latest date to include, same format. Leave both dates empty to search the whole history. | string |
maxItems | How many records to return. Records come back newest first, so a small number gives you the most recent ones. Hard ceiling 30,000 per run. In a test run, 1,000 device recalls came back in about five seconds. Keep it low while you are testing - you pay per record. | integer |
What you get
A structured dataset — each result includes fields like:
datasetrecordTyperecordIdrecallingFirmproductDescriptionclassificationreasonForRecallstatusreportDatebrandNamelabelerNamemarketingStartDatekNumberdeviceNameproductQuantitydistributionPatterncitystatecountryrecallInitiationDateterminationDategenericNamedosageFormrouteactiveIngredientsmarketingCategoryapplicationNumberdeaSchedulepackageCountmarketingEndDatelistingExpirationDateapplicantdeviceClassproductCoderegulationNumbermedicalSpecialtyadvisoryCommitteeclearanceTypedecisionCodedecisionDescriptiondecisionDateExport every run as JSON, CSV or Excel, or send it to your app, a database, Google Sheets, or an AI agent.
Related tools in Developer & Research Tools
Other ready-to-run tools in the same category — all pay-per-use on the Apify cloud.
GitHub Scraper
Search GitHub repos and users: stars, forks, language, topics, licence, plus user bio, company and followers. No token needed. $0.90 per 1,000 rows.
Stack Overflow / Stack Exchange Scraper
Search Stack Overflow and Stack Exchange by keyword or tag. Score, answer count, views, reputation and body text. $2 per 1,000 questions.
Package Registry Scraper (npm + PyPI)
Get npm and PyPI package metadata as JSON. Version, license, author, repo, keywords and npm monthly downloads. $2 per 1,000 packages.
arXiv Scraper
Search arXiv papers by title, author, abstract or category. Get full abstracts, authors, categories, DOI, dates and PDF links. $2 per 1,000 papers.
OpenAlex Scholarly Works Scraper
Search 250M+ OpenAlex papers with no API key. Get titles, authors, venue, year, citations, DOI, OA links and full abstracts. $2.00 per 1,000 papers.
Crossref Scholarly Works Scraper
Search 150M+ papers on Crossref: DOI, title, authors, journal, publisher, date, citations and abstract. No API key. $1.00 per 1,000 works.
Where this tool sits
- Categories
- Developer & Research Tools
openFDA Drug & Device Records Scraper: FDA recalls, NDC drug products and 510(k) clearances as rows
Pick one FDA dataset (drug, device or food recalls, the NDC drug directory or 510(k) device clearances), narrow it with FDA's own search syntax and a date range, and it reads the records from openFDA. You get one flat row per record with FDA's own id, the fields that dataset carries, and a link that returns the untouched original. The part to know first: FDA does not update a recall after it is classified, so status and terminationDate show the state on that day, not today, and FDA itself says these records should not drive public alerts.
| Input | One dataset, an optional FDA search expression and a date range |
| Output | One row per FDA record: a recall, a drug product or a device clearance |
| Ceiling | 30,000 records per run, one dataset per run |
| Account needed | None. No FDA API key |
| Price | $1.00 per 1,000 records, flat on every plan |
🔍 What openFDA Drug & Device Records Scraper does
Each run reads one dataset and returns one row shape. A recall, a marketed product and a premarket clearance share almost no fields, so they never land in the same table. The three recall datasets do share one field list, and three runs stack into a single spreadsheet, with the dataset column saying which run each row came from.
dataset | One row is | The date range applies to | FDA refreshes it |
|---|---|---|---|
drug-recalls | one drug enforcement action | report date | weekly |
device-recalls | one device enforcement action | report date | weekly |
food-recalls | one food enforcement action | report date | weekly |
drug-products | one marketed product in the NDC directory | marketing start date | daily |
device-clearances | one 510(k) premarket clearance | decision date | monthly |
Records come newest first. FDA hands back roughly 1% of records twice on a deep run, and those repeats are dropped before you are billed.
When a search matches nothing, FDA gives the same answer whether nothing matched or a field name in your query does not exist. The run checks which one it was and tells you, on a free row.
📥 What you give it
| Field | If you leave it out | What it does |
|---|---|---|
dataset | the free sample row | drug-recalls, device-recalls, food-recalls, drug-products or device-clearances. Empty, or any other value, returns the free sample row instead of records. |
search | the most recent records | FDA's query syntax, such as recalling_firm:"Abbott", classification:"Class I", brand_name:"Tylenol" or device_name:"catheter". Join clauses with +AND+ or +OR+, quote phrases, and add .exact to a field name to match the whole value. A bare "salmonella" searches the whole record. |
dateFrom | no start date | Year first: 2026-01-01, 20260101, 2026/1/5 or 2026.01.05. Anything else stops the run with a free BAD_INPUT row before FDA is asked. |
dateTo | no end date | Same formats. A range that runs backwards stops the run the same way. |
maxItems | 100 | 1 to 30,000 records, newest first. |
proxyConfiguration | not used | Leave it alone unless you want the traffic to go through servers of your own. |
{
"dataset": "drug-recalls",
"search": "classification:\"Class I\"",
"dateFrom": "2026-01-01",
"maxItems": 100
}
📤 What you get back
A real row from a real run:
{
"ok": true,
"charged": true,
"dataset": "drug-recalls",
"recordType": "enforcement",
"recordId": "D-0866-2026",
"recallingFirm": "VITRUVIAS THERAPEUTICS INC",
"productDescription": "Thyroid Tablets, USP, 1/2 Grain (30 mg), Each tablet contains: levothyroxine (T4) 19 mcg, liothyronine (T3) 4.5 mcg, 100 Tablets, Rx only, Distributed by: Vitruvias Therapeutics, Auburn, AL 36830, Product of USA, NDC 69680-166-00.",
"classification": "Class I",
"status": "Ongoing",
"reasonForRecall": "Superpotent Drug",
"productType": "Drugs",
"eventId": "99709",
"distributionPattern": "US Nationwide.",
"productQuantity": "1955 bottles",
"codeInfo": "Lot # 504950, Exp Date: 09/30/2026.",
"voluntaryMandated": "Voluntary: Firm initiated",
"initialFirmNotification": "Letter",
"city": "Auburn",
"state": "AL",
"country": "United States",
"postalCode": "36830-4318",
"recallInitiationDate": "2026-08-21",
"centerClassificationDate": "2026-09-22",
"reportDate": "2026-09-23",
"terminationDate": null,
"apiUrl": "https://api.fda.gov/drug/enforcement.json?search=recall_number:%22D-0866-2026%22&limit=1",
"scrapedAt": "2026-09-30T02:31:36.343Z"
}
| Field | What it is |
|---|---|
dataset, recordType | The dataset you asked for, and the grain of the row: enforcement, drug-product or device-clearance. |
recordId | FDA's own id: a recall number, an NDC product code or a K number. Stable, so use it as the key across runs. |
apiUrl | The FDA address that returns this one record untouched, including the fields left out here. |
classification | FDA's hazard class. Class I is the serious end: a reasonable chance of serious harm or death. |
status, terminationDate | The state when FDA classified the recall. terminationDate is null while it counts as open. |
activeIngredients, route | Drug products only. Kits have no single strength or route, so both are null on kits. |
packageNdcs, packageCount | Drug products only. Every package code under the product, and how many there are. |
deviceClass | Device clearances only. FDA's risk class, 1, 2 or 3, blank on older records. |
🧾 Reading the output
Three kinds of row can share a dataset, and charged is the field that separates them.
| Row | How to spot it | Charged |
|---|---|---|
| A record | charged: true | yes |
| The sample | _sample: true, charged: false | no |
| A diagnostic | _diagnostic: true, charged: false, an errorCode | no |
The sample row also says recordType: "enforcement", so filter on charged == true, not on recordType.
| Code | What it means |
|---|---|
NO_RESULTS | The query was valid and FDA holds nothing matching it. |
BAD_INPUT | A field name in your search does not exist on that dataset, and the row names it. Also a date that could not be read, a backwards range, or a request FDA rejected. |
SERVER_ERROR | FDA refused the request several times over, which nearly always means the search expression could not be parsed. Check the brackets and quotes. |
NETWORK | FDA could not be reached. |
TIME_BUDGET | The run hit its time limit. Records it never reached are not charged. |
In the Console, switch the dataset to the view that matches your run: Recalls and enforcement, Drug products or Device clearances. The default Overview is only for a quick look.
▶️ How to run it
1. Open openFDA Drug & Device Records Scraper and click Try for free. 2. Pick a dataset in Which FDA records. The form starts on drug recalls. 3. Type a query into Search expression (optional), or clear it for the newest records. It arrives set to classification:"Class I". 4. Set From date (optional), To date (optional) and Maximum records. 5. Click Start, then open the dataset view for your run and download it as CSV, JSON or Excel.
💰 How much does it cost?
$1.00 per 1,000 records, which is $0.001 a record, flat on every Apify plan. The 100-record example above comes to ten cents.
You pay for record rows and nothing else per row. The sample row and every diagnostic row are free, and so are the repeats FDA hands back twice. A run stopped on an unreadable date asks FDA for nothing. If you set a maximum charge on the run, it stops at that figure instead of going past it.
💡 What people use it for
- Watching for recalls that touch your own supply chain:
drug-recallson a schedule with arecalling_firmor product term, compared onrecordId. - Keeping a formulary or product catalogue current with
drug-productsfiltered bylabeler_name. - Tracking who cleared which medical device and when, with
device-clearancesfiltered byproduct_codeoradvisory_committee. - Food-safety monitoring for a distributor or a grocery buyer, with
food-recallsnarrowed to a state.
🚧 What it does not do
- No adverse-event reports. Those describe individual patients, so they are left out on purpose.
- No drug labelling text. The prescribing sections are a different shape and far larger per record.
- No medical advice, and no alert service. FDA says these records should not be relied on for medical decisions or public alerting.
- Recalls are not kept current by FDA. One shown as Ongoing may have ended months ago without the record changing.
- FDA's refresh schedule, not ours. A clearance decided yesterday may not show up for weeks.
- No product matching. A recall carries FDA's free-text
productDescription, so tying it to your own SKU is still your job. - US FDA only. No other regulator is included.
🧭 Which public records scraper do you need?
| If you want | Use |
|---|---|
| FDA recalls, NDC drug products and 510(k) clearances | This one |
| Safety recalls and specs for a vehicle, by VIN | VIN Decoder & Recalls |
| Patents by keyword, inventor or assignee | Google Patents Search Scraper |
| Federal court cases, such as product liability suits | US Federal Court Cases Scraper |
❓ Questions people ask
Do I need an FDA API key? No. Nothing to sign up for, and the run never asks for a credential.
How many records can one run return? Up to 30,000. For more, split the date range across runs.
What happens if my search matches nothing? One free row. NO_RESULTS means FDA genuinely holds nothing. BAD_INPUT means a field name in your query does not exist on that dataset, and the row names it.
Can I combine the three recall datasets? Yes. They share one field list, so three runs concatenate into one table.
Can I use this to send recall alerts to customers or patients? No. FDA says so about the source itself: the records are not updated after classification. Use them for research and monitoring, and FDA's own announcements for anything that warns people.
Where does the data come from? From openFDA, the FDA's public API, which places these records in the public domain. FDA does not endorse this actor.
🆘 If something breaks
Open the Issues tab on the actor page and send the run ID with the input you used. The errorCode and details on a diagnostic row usually name the reason already.