openFDA Drug & Device Records Scraper
Pricing
from $1.00 / 1,000 record scrapeds
openFDA Drug & Device Records Scraper
One row per FDA record: drug, device and food recalls, marketed drug products from the NDC directory, or 510(k) device clearances. Pick one dataset per run. No API key, no account, no browser.
Pricing
from $1.00 / 1,000 record scrapeds
Rating
0.0
(0)
Developer
Dami's Studio
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
2 days ago
Last modified
Categories
Share
Pulls FDA records straight out of the agency's own public API and hands them back as a flat table: drug, device and food recalls, the marketed drug products in the NDC directory, or 510(k) device clearances. Pick one of those per run. No API key, no account, no browser, and nothing to sign up for.
- Five datasets, one per run, each with its own row shape. A recall is not a product and the two never land in the same table.
- The three recall datasets share one shape across drugs, devices and food, so three runs stack into a single spreadsheet without any reconciling.
- Pages past FDA's own 26,000-record offset ceiling by following the agency's paging cursor, so a single run can go deeper than FDA's own paging normally allows.
- A query that matches nothing returns a free row saying so, and tells you whether it found nothing or whether a field name in your query does not exist.
- Records FDA returned twice are dropped before you are billed. Over a 27,000-record test pull that was 334 rows you did not pay for.
Price
$1.00 per 1,000 records, plus a $0.0005 start fee per run.
It is the same rate on every plan, free or paid. There are no volume tiers, no minimum spend, no subscription and no add-on fees. What you read here is what you pay on day one and on day four hundred.
| Records | Total cost |
|---|---|
| 100 | $0.1005 |
| 1,000 | $1.0005 |
| 10,000 | $10.0005 |
| 30,000 | $30.0005 |
One run returns up to 30,000 records. Above that, split the work across runs. Each run carries its own start fee.
What is actually charged
- One
record-scrapedevent per record row written to the dataset. Nothing else is metered per row. - Free: the sample row an empty run returns, and every diagnostic row: a blocked target, a dead URL, a search that matched nothing. Those rows all carry
"charged": false. - Records FDA hands back more than once are dropped before the charge fires, so a repeat never bills twice.
- A run that finds nothing costs the start fee and nothing more.
- Rows never leave the dataset without a charge, and are never charged without a row.
Input
{"dataset": "drug-recalls","search": "classification:\"Class I\"","dateFrom": "2026-01-01","maxItems": 100}
| Field | What it does |
|---|---|
dataset | Which records you want, one per run: drug-recalls, device-recalls, food-recalls, drug-products (the NDC directory) or device-clearances (510(k)). Leave it empty and you get the free sample row instead. |
search | Optional. FDA's own query syntax, for example recalling_firm:"Abbott", classification:"Class I", brand_name:"Tylenol", device_name:"catheter". Join clauses with +AND+ or +OR+, put phrases in double quotes, and add .exact to a field name to match the whole value instead of single words. A bare "salmonella" with no field searches the entire record. Leave it empty for the most recent records. |
dateFrom | Optional. Earliest date to include, as 2026-01-01 or 20260101. It is applied to whichever date matters for the dataset you picked: report date for recalls, marketing start date for drug products, decision date for clearances. |
dateTo | Optional. Latest date to include, same format. Leave both empty for the whole history. |
maxItems | How many records to return. Newest first, so a small number gives you the most recent ones. Default 100, hard ceiling 30,000 per run. |
proxyConfiguration | Leave it alone. The run brings its own addresses and their cost is already inside the price. Set it only if you want traffic to leave through servers you already pay for. |
Run it with empty input and you get one clearly labelled sample row, free, so you can see the output shape before you spend anything.
Output
One row per record. A real row from a real run:
{"ok": true,"charged": true,"dataset": "drug-recalls","recordType": "enforcement","recordId": "D-0832-2026","recallingFirm": "B BRAUN MEDICAL INC","productDescription": "Lactated Ringer's Injection USP, 1000 mL EXCEL container, Rx only, L7500, B. Braun Medical, Inc., Bethlehem, PA 18018-3524 USA, NDC 0264-7750-00","classification": "Class I","status": "Ongoing","reasonForRecall": "Presence of Particulate matter.","productType": "Drugs","eventId": "99633","distributionPattern": "U.S. Nationwide","productQuantity": "23,688 bags","codeInfo": "Lot#: J4H077, Exp 30NOV2026.","voluntaryMandated": "Voluntary: Firm initiated","initialFirmNotification": "Letter","city": "Allentown","state": "PA","country": "United States","postalCode": "18109-9577","recallInitiationDate": "2026-08-12","centerClassificationDate": "2026-09-03","reportDate": "2026-09-02","terminationDate": null,"apiUrl": "https://api.fda.gov/drug/enforcement.json?search=recall_number:%22D-0832-2026%22&limit=1","scrapedAt": "2026-09-20T10:28:08.965Z"}
Field notes
dataset: echoes which dataset you asked for, so stacked runs stay tellable apart.recordType: the grain of the row, one ofenforcement,drug-productordevice-clearance. One value per run, never mixed.recordId: the record's own FDA identifier (a recall number, an NDC product code or a K number). Stable, and safe to use as a primary key when you re-run.apiUrl: the exact FDA API address that returns this one record. Paste it in a browser and you get the untouched original, every field, including the ones trimmed out here.classification: FDA's hazard class. Class I is the serious end: a reasonable probability of serious harm or death. Class II and III are progressively less severe.terminationDate: null while a recall is still open. See the limitations below before you read anything into that.activeIngredients: a single string,NAME strength; NAME strength. Kits have no single strength, so this is null on those rows.route: null on kit products, which have no single route of administration.packageNdcs: every package code under the product code, as an array.packageCountis its length.deviceClass: FDA's device risk class as a digit (1, 2 or 3). Blank on older records where FDA never assigned one.
Every real row carries "charged": true. Sample rows carry "_sample": true and diagnostic rows carry "_diagnostic": true with an errorCode you can filter on, and neither is ever billed.
How it works
- It reads FDA's own public API directly. There is no page to render, no login, no cookie and no key. The agency publishes these records openly and this just fetches them.
- Deep runs follow the paging cursor FDA hands back with each response rather than counting offsets, which is what gets past the agency's own 26,000-record offset ceiling.
- Requests are paced to the published rate allowance, so a long run does not run out of daily quota.
- Every record is checked against the ones already delivered in the run before it is charged, because FDA occasionally hands the same record back on two consecutive pages.
- Dates are normalised to ISO
YYYY-MM-DDon the way out, whichever of FDA's two formats the source field used.
What people use it for
- Watching for recalls that touch your own supply chain: run
drug-recallson a schedule withrecalling_firmor a product term, and diff onrecordIdto see only what is new. - Keeping a formulary or a product catalogue current:
drug-productsfiltered bylabeler_namegives you every NDC that labeler markets, with packaging and marketing dates. - Medtech market tracking:
device-clearancesfiltered byproduct_codeoradvisory_committeeshows who cleared what and when, month by month. - Food-safety monitoring for a distributor or a grocery buyer:
food-recallsnarrowed to a state or a distribution pattern. - Building a research dataset. The rows are flat, typed and deduplicated, so they load into a spreadsheet or a database without any cleaning first.
One kind of record per run
FDA publishes these as separate datasets and they genuinely are separate things. A recall action, a marketed product and a premarket clearance have almost no fields in common. So dataset picks exactly one, and a run returns exactly one recordType. You never get a table where two thirds of the columns are blank because two kinds of record got mixed together.
The dataset view in the Console has a tab per shape. Run Drug products and switch to the Drug products view; run any of the three recall datasets and use Recalls and enforcement. The default Overview tab shows a spread that is populated whichever one you ran, which is handy for a quick look but is not the table you want for real work.
dataset | One row is | FDA refreshes it |
|---|---|---|
drug-recalls | one drug enforcement action | weekly |
device-recalls | one device enforcement action | weekly |
food-recalls | one food enforcement action | weekly |
drug-products | one marketed drug product in the NDC directory | daily |
device-clearances | one 510(k) premarket clearance | monthly |
The three recall datasets share an identical field list, so you can run all three and concatenate the results into one table with nothing to reconcile.
What this does not do
It does not return adverse-event reports. FDA publishes those too, and they are deliberately left out. Those records describe individual patients (age, weight, sex, the reactions they had), and this Actor stays on records about products and companies. If adverse-event data is what you need, go to FDA directly.
It does not return full drug labelling text. The prescribing-information sections (indications, warnings, dosage) are a different shape and a different size entirely (one record runs to tens of kilobytes), so they are not folded in here.
It does not give medical advice, and it is not a safety alert service. It returns FDA's records as FDA published them. Nothing here tells anyone what to take, what to avoid, or what any record means for them. FDA's own guidance on these datasets is that they should not be relied on for decisions about medical care.
It does not enrich, score, cross-reference or interpret. No matching a recall to a product you sell, no severity ranking beyond FDA's own classification, no company resolution. The fields are the agency's fields.
Reading the output
Every run writes three kinds of row and they are easy to tell apart:
- Real rows carry
"charged": trueand arecordTypeofenforcement,drug-productordevice-clearance. One billed event each. - The sample row carries
"_sample": trueand"charged": false. There is exactly one, it only appears when no dataset was picked, and it exists so you can see the shape before spending anything. - Diagnostic rows carry
"_diagnostic": true,"charged": falseand anerrorCodeyou can switch on:NO_RESULTSwhen the query was valid but FDA holds nothing matching it,BAD_INPUTwhen a field name in your search does not exist on that dataset (it names the field),SERVER_ERRORwhen FDA rejected the request,NETWORKwhen it could not be reached,TIME_BUDGETwhen the run ran out of time.
If you only want data, filter on charged == true. The count of those rows always equals the number of events you were billed for, so the dataset is its own invoice.
Where the data comes from
Every record is served by openFDA, the U.S. Food and Drug Administration's public API. FDA places that content in the public domain under a CC0 1.0 dedication, which is why no key and no licence is needed to read it and why it can be handed to you as a plain table.
Data provided by the U.S. Food and Drug Administration (https://open.fda.gov). FDA does not endorse this Actor and has nothing to do with it.
Limitations
- FDA does not update a recall record after it has been classified. A recall shown as Ongoing may have finished months ago without the record changing, so
statusandterminationDateare the state at classification, not today. FDA says plainly that these datasets should not be used to track the lifecycle of a recall or to issue public alerts, and that is worth taking seriously before wiring this into anything that warns people. - Refresh is FDA's, not ours: recalls weekly, the NDC directory daily, 510(k) clearances monthly. A clearance decided yesterday may not appear for weeks.
- Adverse-event reports are not returned, by design. See the section above.
- Full drug labelling text is not returned.
- Searching is FDA's search, with FDA's quirks. A field name that does not exist matches nothing rather than raising an error, which is why the run spends one extra free request checking the field and telling you which of the two happened.
- A malformed search expression comes back from FDA as a server error rather than a helpful message. The run reports it as one uncharged row; it cannot tell you which bracket was wrong.
- The ceiling is 30,000 records per run, and a run that size takes roughly 50 minutes. The limit is how fast rows can be written out, not how fast FDA answers. A thousand records arrive in four requests and then take a minute and a half to land. Deeper pulls need more than one run. If a run does hit the clock it stops, writes a
TIME_BUDGETrow and charges nothing for the records it never reached. - Records occasionally repeat across FDA's page boundaries, about 1% on a deep run. They are dropped before billing, but it means a run can read slightly more records than it delivers.
- Enforcement records carry FDA's free-text
product_descriptionrather than a structured product code, so matching a recall to a specific SKU is work you will still have to do. - Device clearance records leave out the long establishment-registration arrays FDA attaches to them. The
apiUrlon every row fetches the untouched original if you need those. - Coverage is United States only. These are FDA datasets; no other regulator is included.
Questions
Do I need an FDA API key?
No. The run works against the open, keyless tier of FDA's API and never asks you for a credential of any kind.
Can I get more than 26,000 records?
Yes. FDA's own offset paging stops at 26,000 records, but the API also hands back a paging cursor with every response and the run follows that instead. The cap you hit here is the 30,000 per run in maxItems, not FDA's.
What happens if my search matches nothing?
You get one free, uncharged row. If every field name in your query is real, the code is NO_RESULTS and it means FDA genuinely holds nothing matching. If a field name does not exist on that dataset, the code is BAD_INPUT and the row names the field. FDA answers both cases identically, so the run checks which it is for you. Neither is billed.
Can I run all three recall datasets and combine them?
Yes, and that is the intended use. The three share an identical field list, so three runs concatenate into one table with nothing to reconcile. The dataset column tells you which run each row came from.
Is this suitable for issuing recall alerts to customers or patients?
No, and FDA says so about the source data itself: these records are not updated after classification and are not meant for public alerting. Use them for research, monitoring and analysis. For anything that warns people, go to FDA's own recall announcements.
Will the run fail if something goes wrong?
No. A rejected query, an unreachable API or a search that matched nothing all produce an uncharged row explaining what happened, and the run still finishes as succeeded. A failed run would still bill the start fee, which would mean paying to be told something went wrong.
Can I run this on a schedule?
Yes. Nothing is held between runs, so the same input is safe to repeat. Diff on recordId to see only what is new since last time.
Why is one run limited to a single dataset?
Because the records are genuinely different things. A recall action and a marketed drug product share almost no fields, and putting both in one table gives you a spreadsheet that is mostly blank. One dataset per run keeps every column meaningful.