FDA Recall Database API, Food, Drug, Device Enforcement Reports
Pricing
from $2.40 / 1,000 result items
FDA Recall Database API, Food, Drug, Device Enforcement Reports
A recall scraper for the FDA database — food, drug and device — from openFDA, plus an optional press-release feed up to 9 days faster. 37 fields incl. severity, a documented 0-100 riskScore (not AI), recalling firm, ISO dates, NDC/UPC/brand on drug recalls. From $0.0024/result, no start fee. FDA API
Pricing
from $2.40 / 1,000 result items
Rating
0.0
(0)
Developer
Fetch Smith
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
19 hours ago
Last modified
Categories
Share
FDA Recall Scraper API — Food, Drug, Device Enforcement Reports
Search every US FDA food recall, drug recall and device recall from the official openFDA enforcement API, and get it back as flat, typed JSON/CSV/Excel rows.
This Actor covers all three FDA recall types in a single run and a single schema, interleaved and tagged with productType, so a compliance sweep is one job instead of three. Drug recalls additionally come with the barcode identifiers you need to match a recall against your own catalogue: NDC, package NDC, UPC, brand and generic name, manufacturer and substance. Every row can also carry a riskScore (0-100) — a documented, deterministic severity/recency/scope formula, not a black-box "AI" claim. Name a watchLabel and every later run on the same saved search returns only recalls new since the last run, so a scheduled job never re-delivers or re-charges for the same recall twice; add watchChanges and it also catches a recall's status changing (e.g. Ongoing → Terminated) or FDA reclassifying its severity (e.g. Class II → Class I). It costs $0.0035/result on the free plan and $0.0024 on Gold and above, with no start fee — 10-20x under the niche's highest-user listing, which charges $0.05/result for the same raw openFDA data (their live pricing re-verified 2026-10-03). It is not the cheapest FDA recall Actor on the Store, and we do not claim to be: several rivals are free or charge a fraction of a cent per row, and the Pricing section below names every one we have found and priced, with the volumes at which each one beats us.
The openFDA enforcement API above is complete but slow — its newest reports typically lag the real recall announcement by over a week (measured 11 days on 2026-09-20), a gap every FDA-recall Actor on the Store shares, us included, because everyone reads the same lagging API. Set includePressReleases: true and this Actor also fetches FDA's own recall press-release feed, which had an item 9 days ahead of the enforcement API in that same measurement — the recall the day FDA announces it, not the day the enforcement paperwork catches up.
No API key, no login, no browser. Public US government open data (openFDA licence).
What you can do with it
- Retailers and distributors — pull every Class I recall for your state or category and check it against the products you carry.
- Compliance and QA teams — monitor recalls in your product category on a schedule, using
watchLabelso each scheduled run returns only what's new; addwatchChangesto also get alerted when a recall you already logged is upgraded to a more serious classification or is finally terminated. One catch worth knowing up front: awatchChangesrun must not filter onstatusorclassifications, because those are exactly the fields it watches — see the "don't filter on the field you're watching" FAQ below. - Product-liability and personal-injury firms — search recall reasons and firm names across the full history.
- Pharmacy and healthcare — match drug recalls to your inventory by NDC or UPC, not by fuzzy product-name matching.
- Insurers and risk analysts — build a recall time series by firm, classification or distribution pattern.
- Newsrooms and researchers — track "what got recalled this week" across all three FDA centres at once.
Input
All fields are optional; with an empty input you get the last year of food, drug and device recalls, newest first.
| Field | Type | Description |
|---|---|---|
productTypes | array | food, drug, device. Default: all three. Results are interleaved, not one type after another. |
dateField | string | Which date reportDateFrom/reportDateTo filter on: report_date (default, when FDA published the report), recall_initiation_date (when the recall actually started), center_classification_date (when FDA assigned the severity class), or termination_date (when it closed out). The first three are present on essentially every record; termination_date is missing on ~5% of food, ~18% of drug and ~36% of device recalls, which can never be returned when you filter on it (see FAQ). |
reportDateFrom | string | Earliest date for the field above, YYYY-MM-DD or YYYYMMDD. Default: one year ago. |
reportDateTo | string | Latest date for the field above. Default: today. |
classifications | array | Class I (reasonable probability of serious harm or death), Class II (temporary/reversible), Class III (unlikely to cause harm), Not Yet Classified (real but very rare — 5 rows across all three endpoints as of 2026-10-06). Empty = all, which is not the same as selecting all four minus nothing: picking only Class I/II/III excludes the unclassified rows. |
states | array | Two-letter state codes of the recalling firm. For Canadian firms FDA stores the province spelled out in full, so use British Columbia, not BC (see FAQ). Multiple values are ORed; combined with countries the two ANDed. Empty = all. |
countries | array | Country names of the recalling firm, exactly as FDA writes them (United States, Canada, Israel, ...). Most recalls are US firms; foreign firms whose products entered the US market show up too. Empty = all. |
recallNumber | string | Look up one recall by its exact FDA recall number, e.g. F-1233-2022. Overrides every filter above except productTypes, and automatically searches full history regardless of reportDateFrom/reportDateTo. |
eventId | string | Look up every product recalled under one FDA event ID, e.g. 90105 (one event can cover several products, each its own row). Same override/full-history behaviour as recallNumber. |
status | string | Ongoing, Completed, Terminated, Pending. Empty = all. Pending is in FDA's own vocabulary but has never appeared in openFDA's enforcement data (verified 2026-09-25) — expect zero rows if you select it. |
recallingFirm | string | Filter to one firm, e.g. Tyson Foods. Narrower than searchQuery, which also matches product description and recall reason. |
city | string | Filter to recalls whose recalling firm is in this city, e.g. Chicago. |
brandName | string | Drug recalls only. Filter by drug brand name, e.g. Nurtec. openFDA only cross-references brand/generic/manufacturer name for drug recalls — food and device recalls never match these three filters. |
genericName | string | Drug recalls only. Filter by generic/active-ingredient name, e.g. ibuprofen. |
manufacturerName | string | Drug recalls only. Filter by manufacturer name, e.g. Pfizer. Not the same as recallingFirm (the firm that issued the recall, any product type) — a drug's manufacturer and the firm recalling it can differ. |
voluntaryMandated | string | Voluntary: Firm initiated, FDA Mandated (rare, under 2% of recalls), or N/A (FDA's own value when the initiating party was never captured — 37 rows as of 2026-10-06). Empty = no filter, the only way to also see the 23 rows that carry an empty value for this field. |
searchQuery | string | Free-text phrase matched against product description, reason for recall and recalling firm. |
order | string | desc (newest first, default) or asc — sorts by whichever field dateField selects. |
maxResults | integer | Total rows across all selected product types. Default 100. |
includeRiskScore | boolean | Add the riskScore field (see Output/FAQ). Default true. |
includePressReleases | boolean | Also fetch FDA's recall press-release feed — up to 9 days ahead of the enforcement API, but only a rolling ~20-item/few-weeks window (see FAQ). Default false. Automatically skipped, with a log warning, if you also set a filter the feed can't support (see FAQ). |
watchLabel | string | Name a saved search to get only recalls new since this label's last run (see FAQ). Leave empty for the normal full-match-set behaviour. |
watchChanges | boolean | Optional, requires watchLabel. Also re-deliver an already-seen recall if its status or classification changed (default false) — see FAQ. |
webhookUrl | string | Optional. POST a small JSON completion summary (recalls pushed, rows scanned, dataset ID, watch new/changed counts) here when the run finishes — see FAQ. |
Filters are ANDed. A search query plus a state plus a classification over a short date window often has zero real matches — drop one filter and retry.
Example: Class I food and drug recalls in California this year
{"productTypes": ["food", "drug"],"classifications": ["Class I"],"states": ["CA"],"reportDateFrom": "2026-01-01","maxResults": 500}
Output
37 fields per row. Every date is converted from openFDA's YYYYMMDD strings (or the press-release feed's RFC-822 timestamp) to ISO YYYY-MM-DD.
All product types: source ("enforcement" or "press_release", see below), productType, recallNumber, eventId, status, classification, voluntaryMandated, initialFirmNotification, recallingFirm, city, state, country, productDescription, productQuantity, reasonForRecall, distributionPattern, codeInfo, moreCodeInfo, reportDate, recallInitiationDate, centerClassificationDate, terminationDate, riskScore.
Drug recalls only (see the FAQ): brandName, genericName, manufacturerName, substanceName, productNdc, packageNdc, upc, applicationNumber, drugRoute, rxcui, unii, splSetId.
includePressReleases rows only (source: "press_release", see FAQ): pressReleaseTitle (the FDA headline, also duplicated into productDescription), sourceUrl (link to the FDA press release). Every field above that the feed doesn't carry — recallNumber, classification, eventId, status, distributionPattern, all drug-only fields, riskScore — is explicit null on these rows, never guessed.
Watch-mode change fields, only on a watchChanges re-delivery: _watchChangeType (array, one or both of status/classification), _watchPrevious (object with the previous value(s) for each changed field).
Sample row (drug recall, trimmed)
{"source": "enforcement","productType": "drug","recallNumber": "D-0785-2026","eventId": "99584","status": "Ongoing","classification": "Class II","voluntaryMandated": "Voluntary: Firm initiated","initialFirmNotification": "Letter","recallingFirm": "ACCORD HEALTHCARE, INC.","city": "Raleigh","state": "NC","country": "United States","productDescription": "Levothyroxine Sodium Tablets, USP, 200 mcg (0.2 mg), packaged in a) 90-count bottles (NDC 16729-457-15) and b) 1000-count bottles (NDC 16729-457-17), ...","productQuantity": "N/A","reasonForRecall": "Subpotent Drug","distributionPattern": "Nationwide within the United States","codeInfo": "a) Lot # D2402430, D2402431, Exp Date: 10/31/2026. ...","reportDate": "2026-09-02","recallInitiationDate": "2026-08-06","centerClassificationDate": "2026-08-21","terminationDate": null,"riskScore": 80,"brandName": "LEVOTHYROXINE SODIUM","genericName": "LEVOTHYROXINE SODIUM","manufacturerName": "Accord Healthcare Inc.","substanceName": ["LEVOTHYROXINE SODIUM"],"productNdc": ["16729-447", "16729-458", "16729-448", "16729-449", "16729-451", "16729-450", "16729-452", "16729-453", "16729-454", "16729-455", "16729-456", "16729-457"],"packageNdc": ["16729-458-15", "16729-458-17", "16729-447-15", "16729-447-17", "16729-448-15", "16729-448-17", "16729-449-15", "16729-449-17", "16729-451-15", "16729-451-17", "16729-450-15", "16729-450-17", "16729-452-15", "16729-452-17", "16729-453-15", "16729-453-17", "16729-454-15", "16729-454-17", "16729-455-15", "16729-455-17", "16729-456-15", "16729-456-17", "16729-457-15", "16729-457-17"],"upc": ["0316729447157", "0316729449151", "0316729452151", "0316729454155", "0316729453158", "0316729448154", "0316729451154", "0316729457156", "0316729450157", "0316729456159", "0316729458153", "0316729455152"],"applicationNumber": "ANDA212399","drugRoute": ["ORAL"],"rxcui": ["892246", "892251", "892255", "966220", "966221", "966222", "966224", "966225", "966248", "966249", "966253", "966270"],"unii": ["9J765S329G"],"splSetId": "a6233381-3043-4e9a-aaa6-a6b105e5142b","pressReleaseTitle": null,"sourceUrl": null}
Sample row (includePressReleases: true, trimmed)
{"source": "press_release","productType": null,"recallNumber": null,"classification": null,"status": null,"recallingFirm": null,"productDescription": "GF Blends Recalls Truly AIP All Purpose Flour and Bread Mix, and EAT G.A.N.G.S.T.E.R. Flat Bread Pizza Mix Due to Potential Undeclared Wheat Gluten","reasonForRecall": "GF Blends is recalling Truly AIP All Purpose Flour and Bread Mix and EAT G.A.N.G.S.T.E.R. Flat Bread Pizza Mix listed below due to potential undeclared wheat gluten. ...","distributionPattern": null,"reportDate": "2026-09-18","riskScore": null,"pressReleaseTitle": "GF Blends Recalls Truly AIP All Purpose Flour and Bread Mix, and EAT G.A.N.G.S.T.E.R. Flat Bread Pizza Mix Due to Potential Undeclared Wheat Gluten","sourceUrl": "http://www.fda.gov/safety/recalls-market-withdrawals-safety-alerts/gf-blends-recalls-truly-aip-all-purpose-flour-and-bread-mix-and-eat-gangster-flat-bread-pizza-mix"}
This press release was live in the feed on 2026-09-18 — 2 days before this Actor's enforcement-API rows for the same window were captured, and well before the recall would typically clear openFDA's own pipeline.
Pricing
Pay per result: $0.0035 per recall on the free plan, dropping to $0.0024 on Gold and above (Bronze $0.003, Silver $0.0027), with no Actor-start fee. 1,000 recalls costs $3.50 on the free plan, $2.40 on Gold. You are charged only for rows actually delivered to your dataset.
FAQ
Why are the NDC/UPC/brand-name fields empty on my food and device rows?
Because FDA only publishes them for drugs. The openfda block that carries those identifiers is populated on drug enforcement reports and empty on food and device ones — verified on live samples, not an assumption. The fields are always present in the schema so your CSV columns stay stable; they are simply null or [] outside drug rows.
Even within drug rows, upc is much sparser than its sibling identifiers — why? Measured live against openFDA (two independent 100-row drug samples, 2026-10-12): brandName, productNdc, packageNdc, splSetId and unii all filled on ~30% and ~73% of rows in the respective samples (moving together, since they come from the same manufacturer submission), while upc filled on only 15% and 34% in the same two samples — roughly half the rate of everything else in the openfda block. Barcodes are the one identifier FDA leaves to the manufacturer to submit separately from the rest of the NDC/SPL paperwork, so plenty of drug rows carry a full set of drug identifiers with no upc at all. Don't treat a missing upc next to a filled brandName/productNdc as a parsing bug — it is real upstream sparsity, worse than the rest of the drug-only block.
In what order do results come back?
Interleaved across the product types you selected — one row per type per round — so a small maxResults gives you a mix rather than filling the whole quota from food. Within each product type, rows are ordered by reportDate.
What is the difference between reportDate and recallInitiationDate?
recallInitiationDate is when the firm started the recall; reportDate is when FDA published the enforcement report, which is often weeks or months later. The date filters apply to reportDate by default — set dateField: "recall_initiation_date" to filter on when the recall actually began instead, "center_classification_date" for when FDA's center assigned the severity class (it sits between the other two), or "termination_date" to find recalls that closed out in a window.
Which dateField should I use, and can one of them lose rows?
Yes — one of them can, and it is worth knowing before you trust a count. A recall with no value in the field you filter on can never be returned, however wide you make the window. Measured live on 2026-10-06 with openFDA's _exists_ operator, per product type:
dateField | food | drug | device |
|---|---|---|---|
report_date (default) | 29,471 / 29,471 | 18,002 / 18,002 | 40,113 / 40,113 |
recall_initiation_date | 29,471 / 29,471 | 18,002 / 18,002 | 40,113 / 40,113 |
center_classification_date | 29,469 / 29,471 | 18,000 / 18,002 | 40,112 / 40,113 |
termination_date | 27,958 / 29,471 | 14,810 / 18,002 | 25,491 / 40,113 |
So the first three are effectively lossless, but termination_date silently drops ~5% of food, ~18% of drug and ~36% of device recalls — the ones FDA has never closed out, which are mostly still Ongoing. That is usually what you want when you ask "what terminated in Q3", but it makes termination_date the wrong field for any "how many recalls happened" total. Use report_date or recall_initiation_date for counts, and termination_date only for close-out questions.
Why is terminationDate almost always null, and moreCodeInfo too?
Both are real openFDA fields, not bugs, and both are genuinely sparse — measured live, not assumed. terminationDate only gets a value once a recall's status flips to Terminated: sampled 30/30 Terminated-status rows filled across drug, food, and device, versus 0/30 for Ongoing and ~0-3% for Completed. Since the default sort returns the newest recalls first and most freshly-reported recalls are still Ongoing, terminationDate reads as almost-always-empty in a default query — set status: "Terminated" if you specifically want closed-out recalls with a termination date. moreCodeInfo is a free-text overflow field FDA rarely uses at all: sampled hundreds of rows with the field technically present in the API response, and it was an empty string in every drug and food row checked, non-empty in only a small fraction of device rows. Both fields are kept (not removed) because they are real, occasionally-populated upstream data, not an artifact of any input filter you chose.
Why do classifications and voluntaryMandated have an odd rare option each?
Because FDA's real vocabulary for both fields is one value longer than the obvious one, and without the extra option those rows were simply unreachable through the filter. Pulled straight from openFDA's own count=<field>.exact facets on 2026-10-06 (food / drug / device):
classification—Class I,Class II,Class III, plusNot Yet Classified(2 / 2 / 1 rows): recalls whose severity the relevant FDA center has not assigned yet. The practical consequence is that selecting["Class I","Class II","Class III"]is not the same as leavingclassificationsempty. These five rows are genuinely half-filled upstream — verified live, theirrecallNumberis blank orN/A— so treat them as early/incomplete records, and noteriskScorescores them with a neutral severity component rather than guessing a class.voluntary_mandated—Voluntary: Firm initiated,FDA Mandated, plusN/A(6 / 23 / 8 rows): FDA's own marker for recalls where the initiating party was never captured. A further 23 rows (1 / 12 / 10) carry an empty value for this field; those cannot be selected at all, since the empty option means "no filter" — leavevoluntaryMandatedblank if you need them.
status is the other direction and is unchanged: the same facet sweep confirms openFDA still only ever emits Ongoing, Completed and Terminated, so FDA's documented Pending remains a real-vocabulary-but-zero-data value (the Actor logs a warning if you pick it). The facet counts reconcile exactly to each endpoint's grand total, so no recall is missing a status or a classification either.
Can I get more than 25,000 rows from one filter?
Yes. openFDA refuses to page past row 25,000 (skip is hard-capped), so when a query matches more than that the Actor automatically splits it into narrower reportDate windows and pages each one — the year-boundary split was verified to sum exactly to the unsplit total, with no duplicated or dropped rows. If one single window still exceeds the cap, the run logs a warning telling you to narrow the date range.
Does states mean where the product was sold?
No — it is the recalling firm's own state. For distribution, read the distributionPattern field (e.g. "Nationwide within the United States", or a list of states).
Why does states: ["BC"] return nothing for a Canadian recall?
Because FDA does not use two-letter codes outside the US. Two-letter codes are correct for US firms, but for Canadian firms the state field holds the province name spelled out in full — British Columbia, Ontario, Quebec, Nova Scotia, Alberta, Manitoba, New Brunswick. states: ["BC"] matches zero rows; states: ["British Columbia"] matches them (11 device + 6 food recalls as of 2026-09-29). Matching is case-insensitive, so british columbia works too. Canada is the only country this affects: a sweep of all three enforcement endpoints on 2026-09-29 found that every state value that is not a two-letter US code is either empty, N/A, or one of those seven Canadian provinces.
Can I combine states and countries?
Yes, and they are ANDed, not ORed — a recall must match both. Values within each field are ORed, so states: ["CA","NY"] means "California or New York". Setting countries: ["Canada"] together with states: ["CA"] therefore returns zero rows (no Canadian firm is in California), which is the correct intersection, not a bug. Both fields describe the recalling firm's location, so the useful combination is a country plus one of that country's own subdivisions — e.g. countries: ["Canada"] + states: ["British Columbia"].
Are FDA-mandated recalls more or less severe than voluntary ones?
Combine voluntaryMandated with classifications to check this directly — a combination never tested together before 2026-10-01. Verified live across the full openFDA history: of the openFDA enforcement API's 1,750 Class I (most serious) drug recalls, zero are FDA Mandated — every single one is Voluntary: Firm initiated. FDA Mandated recalls do happen for Class I food (27) and Class I device (1) recalls, just never for drugs — of the 29 FDA-Mandated drug recalls in openFDA's entire history, none reached Class I. Confirmed with two live runs: voluntaryMandated: "FDA Mandated" alone declared 715 matches (396 food + 29 drug + 290 device); adding classifications: ["Class I"] narrowed that to exactly 28 (27 food + 0 drug + 1 device) — both totals matched independent direct openFDA API counts exactly, proving the two filters genuinely AND together rather than one silently overriding the other.
I have a recall number or event ID from another source — can I just look it up directly?
Yes. Set recallNumber (e.g. F-1233-2022) or eventId (e.g. 90105) and every other filter is ignored except productTypes — set that too if you know which endpoint the recall lives on, since recall numbers aren't unique across food/drug/device. The date window is also automatically widened to full history so an old recall isn't silently missed by the default one-year lookback; this costs one extra lookup request internally, not a full history scan, since the exact-match query itself stays cheap however wide the window is.
Is this legal / does it need an API key? It reads openFDA, the FDA's own public open-data API, which requires no key and is explicitly published for reuse. Rows are firm and product safety data, not personal data.
I got a SUCCEEDED run with 0 rows and no explanation — what happened? As of v0.1.26 this always shows a reason in the run's status message (and in the run log), not just a blank green run. Three cases: (1) your filters are ANDed — a searchQuery plus a state plus a classification over a short reportDate window often genuinely matches nothing; drop one and retry, or try one distinctive word in searchQuery rather than a long phrase. (2) a watchLabel run found nothing new since its last check — the expected, charge-nothing outcome most of the time. (3) recallNumber/eventId was set (an exact-identity lookup across full history, every other filter ignored) and the value itself didn't match anything, or matched on a productTypes endpoint not selected here — recall numbers aren't unique across food/drug/device. As of v0.1.27, the plain no-match status message is also a separate, shorter rendering of the same causes — Apify silently cuts a run's status message at 500 characters, and the original full 5-cause wording measured ~980 (verified cut mid-word live before this fix); the full text still appears in the run log either way.
Does searchQuery match partial words, or whole words only?
Whole words (and, for a multi-word phrase, consecutive whole words), on both sources it can match against. The main enforcement-API results use openFDA's own server-side phrase search, which we verified live is whole-word only — "simvastatin" matches 41 rows, but the mid-word fragment "vastat" (or the prefix "simvastat") of that exact same indexed word matches 0, not even as a stem. includePressReleases rows are matched client-side against the FDA press-release feed using the same whole-word rule, so a query behaves consistently whichever source a row came from — it will not match "Peperoncini" on the fragment "eperoncini".
What exactly is riskScore and why isn't it called "AI"?
It's a plain weighted formula, computed with no external calls and no model: 45% classification severity (Class I=100, II=60, III=25), 30% recency (linear decay from 100 at today's date to 0 at two years old), 25% distribution scope (100 for nationwide/international language in distributionPattern, scaling down by how many distinct US state codes are mentioned, down to 30 for a single state/city). Rounded to an integer 0-100. We could have marketed this as "AI-powered" like a competitor does for the same idea, but it isn't AI, and saying so would be misleading — this is what it actually computes. Set includeRiskScore: false to skip it.
How do I get only new recalls on a schedule, not the whole match set every time?
Set watchLabel to any name, e.g. "my-class-i-watch". The first run under that label is a free baseline: it records every recall currently matching your other filters and returns zero rows, charged nothing. Every later run with the same label AND the same other filters returns only recalls not already recorded — new since the last run — and only those are charged. Change any filter (a state, a classification, the date window) and that combination gets its own fresh baseline, since it's now a different saved search. The baseline lives in your own Apify account, not ours, so it survives between scheduled runs.
Baseline size cap. A baseline holds up to 60,000 recall ids in one saved record. If a label's baseline grows past that, the oldest ids are dropped — and a dropped id is no longer recognised, so it comes back as "new" on a later run and is charged again. The run that drops them says so explicitly: a warning in the log, a note on the run's status message, and baselineTruncated / baselineTruncatedTotal (this run / the whole life of the label) in the saved record, on the webhookUrl payload, and in RUN_SUMMARY. If you see it, narrow the watch query (productTypes, states, classifications, searchQuery, the date window) or split it across several labels so each baseline stays under the cap.
Does changing reportDateFrom/reportDateTo from a rolling default break watchLabel?
No — leaving both empty (the default one-year rolling window) is treated as "no explicit date filter" for the purpose of deciding whether your search changed, not as a specific date that changes every day. A recall that only enters the rolling window on a later run is correctly reported as new then, which is exactly what a watch should do.
Why has my watch stopped finding anything new? (don't pin a fixed reportDateTo)
reportDateTo defaults to today, and that default rolls forward with today on every run — safe to leave alone. The trap is setting it to a literal date and combining that with watchLabel. All 4 dateField choices are assigned to a recall exactly once — report_date/recall_initiation_date at report/initiation time, center_classification_date when FDA assigns the severity class, termination_date when the recall closes out — and a never-yet-seen recall's relevant date, whatever it turns out to be, is checked against your window the first time our query can see it. Once today's real date passes a fixed reportDateTo, no recall whose date lands after it can ever satisfy the window again, so the watch goes permanently quiet while still logging a healthy "nothing new" every scheduled run. The run detects this and logs two warnings, and sets RUN_SUMMARY.watchDeadDateBound: true. Drop reportDateTo from a watched query (reportDateFrom alone is safe to keep) so the window keeps rolling with today, or re-set it forward yourself before every scheduled run if you genuinely need an exact cutoff.
What does watchChanges add, and does it cost extra to turn on?
No extra fee — a changed recall is billed at the same per-row price as a new one. Plain watchLabel only ever tells you about recalls it has never delivered before; it stays silent forever about one it already sent you, even if that recall later gets terminated or FDA upgrades it to a more serious class. Set watchChanges: true and each run also compares every already-delivered recall's status and classification against what they looked like last time; if either moved, the row is re-delivered tagged with _watchChangeType (which field(s) changed) and _watchPrevious (what they used to be). Verified live: seeding a baseline, editing 2 recalls' recorded status and classification directly, then rerunning returned exactly those 2 rows with the correct change tags and nothing else — and a plain unchanged rerun after that returned 0 rows again. Existing watch labels created before this feature shipped work immediately; the first run under watchChanges just starts detecting drift from that point forward rather than reporting an artificial backlog.
Don't filter on the field you're watching (status / classifications).
status and classifications are sent to openFDA as server-side filters, and they are also the only two fields watchChanges compares. So a run with status: "Ongoing" + watchChanges: true can never tell you a recall was terminated: the moment its status stops being Ongoing, the row stops matching your query, is never returned, and there is nothing left to compare — the run just reports "nothing new". Same for classifications: ["Class II"] and a Class II → Class I upgrade. Verified live 2026-10-11: a baseline seeded with status: "Ongoing", holding a recall recorded as Ongoing that FDA has since terminated, returned 0 changed rows; the identical run with no status filter returned exactly 1 row tagged _watchChangeType: ["status"], _watchPrevious: {"status": "Ongoing"}.
What to do about it. Run watchChanges without those two filters and filter the delivered rows on your own side — every row carries status and classification, so nothing is lost. Or keep two scheduled runs: a filtered one for brand-new recalls, and an unfiltered watchChanges one for transitions. Any run that has this problem says so: two warnings in the log, a note appended to the "nothing new" message so a permanently-blind run can't masquerade as a healthy quiet one, and watchChangeBlindFilters in RUN_SUMMARY (null when watchChanges is off, [] when the run is clean, otherwise the names of the offending filters). Date filters are safe — dateField's four choices are all set-once dates, not fields watchChanges tracks.
What is includePressReleases and when should I turn it on?
The openFDA enforcement API (everything else in this README) is the authoritative historical record, but it is slow: FDA finishes the paperwork and publishes the structured enforcement report anywhere from days to months after the recall is first announced (measured 11 days stale for the newest report on 2026-09-20). FDA's own recall press-release RSS feed carries the announcement itself, and had an item 9 days ahead of the enforcement API in that same check. Set includePressReleases: true to also pull that feed. Two things to know: (1) it's a rolling window of the ~20 most recent press releases (a few weeks of history), not an archive — use it for freshness, layered on top of the enforcement API's completeness, not as a replacement; (2) the same recall typically shows up twice, once as source: "press_release" within days of the announcement and again as source: "enforcement" weeks later once openFDA catches up — that is intentional (see the next question), not a duplicate-data bug, since there's no reliable shared key to merge them on.
Why doesn't includePressReleases deduplicate against the enforcement rows?
A press release and its later enforcement report share no common identifier — no recall number, no event ID, nothing but a firm name and a product description in free text, and fuzzy-matching those reliably enough to auto-merge risked silently dropping a real, distinct recall. We chose the safe failure mode: you may see the same real-world recall twice, tagged with two different source values and two different dates, rather than risk it being incorrectly merged away. Filter or dedupe downstream by firm/product text if your use case needs one row per recall.
Why is includePressReleases sometimes skipped even when I turn it on?
The press-release feed is unstructured — it has no classification, state/country, firm name field, drug identifiers, or product-type split (one feed covers food, drug and device announcements together). If you also set a filter that field depends on (classifications, states, countries, status, recallingFirm, city, voluntaryMandated, brandName/genericName/manufacturerName, an exact recallNumber/eventId lookup, productTypes narrowed to fewer than all three, or dateField set to anything but the default report_date), the Actor skips the press-release source entirely for that run and logs exactly which filter caused it, rather than silently returning zero press releases and letting you think none matched.
How does this compare to other FDA recall scrapers?
Checked live pricing and features again on 2026-09-15 against the highest-user leader (benthepythondev/fda-recall-intelligence, 11 users): they charge $0.05/result tapering to $0.035 on Diamond, plus a per-GB Actor-start fee — we are $0.0035/result on the free plan and $0.0024 on Gold and above, with no start fee, 10-20x cheaper at every tier. Their input set (8 fields) is a subset of ours (20 fields: three date-field choices instead of one, city, voluntary/mandated, free-text search across three fields, state and country, exact recall-number/event-ID lookup, drug-specific brand/generic/manufacturer name filters, and no 1,000-row cap — ours goes to 50,000). Their one real feature, an "AI-powered intelligence score", is now matched by riskScore above — ours is fully documented instead of a black box. Also checked against the real leader by volume (scrapers_lat/openfda-food-recalls-scraper, food-only, checked 2026-09-13, input-schema re-diffed 2026-09-17, pricing re-verified live 2026-09-30): their pricing has since changed — they now charge $0.008/result tapering to $0.006154 on Gold+ (down from the $0.01→$0.008 measured 2026-09-13), plus a new details event at $0.009231→$0.007385 not present at the earlier check, and the separate Actor-start fee from the 2026-09-13 check is gone. We remain cheaper at every tier ($0.0035→$0.0024 vs their $0.008→$0.006154, about 2.3x on the free plan), just by a narrower margin than before — and we cover drug and device recalls too, not just food. The 2026-09-17 re-check found country/recallNumber/eventId filters we lacked (they were already present as output fields, just not filterable on) — closed as countries, recallNumber, eventId above; a 2026-09-18 re-check of the same benthepythondev listing found brandName/genericName/manufacturerName were the same "computed-but-unfilterable" gap, closed the same way, so no remaining input-parity gap against either competitor. A 2026-09-20 audit read the input schema of every FDA-recall listing it checked (a 20-result Store sweep, plus our own prior version) and found all of them read only the same lagging openFDA enforcement API — includePressReleases (above) is a feature none of the listings we have checked offers: the recall the week FDA announces it, not the week the enforcement paperwork catches up. We no longer claim that about every Actor on the Store; the deeper sweep described below shows the niche is far larger than a 20-result sweep can see, and we have not read all of it. Re-verified live again 2026-10-02: both named competitors' pricing is unchanged from the checks above (no drift), and resolving the next five bare handles to their full listings and pricing each live turned up three genuine undercutters we had not disclosed, all food-only. bikram07/fda-recall-monitor (3 users) runs on Apify's FREE pricing model — $0 per row, no exceptions — and, like us, covers drug, device, and food recalls, not just food. maximedupre/fda-food-recalls-scraper (3 users) charges $0.00001 per food recall. copious_atoll/fda-food-recalls (4 users) charges $0.001/result plus a $0.00005 start fee — cheaper than our $0.0035 free-plan rate on food recalls alone, though not cheaper than our $0.0024 Gold-and-above rate. The other two are pricier than us at every tier: ryanclinton/fda-food-recall-monitor charges $0.002/record plus a $0.00005 start fee; inexhaustible_glass/fda-intelligence-scraper charges $0.005/result plus a $0.005 start fee. None of the five offers includePressReleases, riskScore, or a documented watchChanges snapshot, and the three food-only undercutters still require running a separate Actor (or three) to get drug and device recalls in the same schema we deliver in one run — but we are not the cheapest option for food recalls in isolation, and bikram07 is not even the cheapest for all three types, since it is free. Deeper sweep 2026-10-02 (10 search terms instead of one, 288 distinct listings, 285 of them mentioning FDA or recalls, 29 resolved to their live pricing): two claims above are retracted and ten more cheaper rivals disclosed. First, "no new entrant with meaningful traction" was an artifact of the 20-result sweep depth, not a fact: nexgendata/us-government-records-api (9 users), logiover/fda-data-scraper (9 users, a head-on openFDA recall/adverse-event/label export tool we had never named), gabrielaxy/product-recall-aggregator (6), constant_quadruped/fda-catalyst-alerts (6, three of them new in the last 30 days — the fastest-growing listing in the niche) and martc03/us-safety-recalls-mcp (4) each carry more users than any of the five 3-user rivals named above. Second, "three genuine undercutters, all food-only" was wrong — four rivals undercut us across all three recall types, the same scope we sell. gabrielaxy/product-recall-aggregator (6 users, FDA + CPSC + NHTSA) and martc03/us-safety-recalls-mcp (4 users, an MCP server over FDA drug/food/device plus NHTSA and CFPB) both run on Apify's FREE model at $0/row, as does constant_quadruped/fda-catalyst-alerts (6 users, recalls bundled into a biotech-catalyst feed). And martc03/fda-recalls (1 users, re-verified live 2026-10-06) is the closest match to this Actor of anything in the niche — openFDA drug, food and device enforcement, filterable by product type, classification and date — at $0.00001/result plus a $0.00005 start fee, roughly 350x below our $0.0035 free-plan rate. Two more undercut us partially: carranza-tech/fda-recall-monitor (all three types in one unified schema) charges $0.003/result with no start fee, under our free-plan rate but over our $0.0024 Gold-and-above rate; tictechid/vanzi-us-recall-intelligence (FDA + CPSC + USDA FSIS with severity scoring) charges $0.005/result on Free but $0.0015 on Gold and above, which beats our $0.0024 there from the first row. fiery_dream/healthcare-intel (8 users) prices at $0.00001/result + $0.00005 start but sells clinical trials, approvals, safety events and pharma news rather than enforcement reports. Cheaper than us per row but narrower in scope: ryanclinton/fda-device-recalls and pink_comic/fda-device-recall-enforcement at $0.002/result are device-only; cloud9_ai/openfda-drug-scraper and pink_comic/openfda-drug-adverse-events-recalls at $0.002/result are drug-only. Dearer than us, named for completeness: logiover/fda-data-scraper ($0.005 tapering to $0.0035 on Gold+ plus a $0.00005 start fee — its best tier only equals our worst), maydit/us-fda-recalls-scraper ($0.004 → $0.0024, matching our Gold+ rate but adding a start fee we do not charge), ponderable_hydrometer/fda-recalls-scraper ($0.004 + start), johnatan029/fda-recall-intelligence-scraper ($0.00399 + start), nexgendata/us-government-records-api ($0.005–$0.15/record depending on record tier, + $0.005 start), datapilot/government-recalls-monitor ($0.003/result but a $0.035 free-plan start fee, so dearer than us below ~70 rows/run on Free and at every run size on Gold+), zentrafoundry/compliance-risk-tool ($0.01/result) and parseforge/fda-warning-letters-scraper ($0.021 → $0.019, a warning-letters product rather than a recall scraper). So the honest summary is that we are not the cheapest FDA recall Actor at any scope: three rivals are free and one charges $0.00001/row across all three types. What none of them documents is includePressReleases, a published riskScore, a watchChanges snapshot with _watchPrevious field diffs, or the declaredMatches/declaredMatchesIsFloor completeness signals (our 50,000-row per-run ceiling was in this list until the 2026-10-03 sweep below found a rival that pages the whole openFDA archive by cursor) — and every one of the free or near-free all-types rivals has 6 users or fewer, so there is no public track record for their reliability either way.
Full-sweep re-price 2026-10-03 (the same 10 search terms, 287 distinct listings, but all 269 FDA/recall-matching listings resolved to their live pricing this time instead of 29). Every one of the 24 rivals named above was re-verified live 2026-10-03 and not one has changed its price since the checks recorded above. Pricing the other 240, however, shows the sub-$0.001 end of this niche is much more crowded than the earlier 29-listing sample suggested, and eight more rivals that undercut us had never been named here. Head-on openFDA food+drug+device recall exports, cheaper than us at every tier: steadydata/fda-recalls ($0.001 tapering to $0.00065 on Gold+, no start fee, searchable by term/category/class/state/date with up to 50 searches per run), koalastuff/fda-enforcement-report-finder ($0.001 → $0.0007 plus a $0.00005 start fee), datalayer/fda-recall-enforcement ($0.001 → $0.0007, no start fee), themineworks/fda-recalls-scraper ($0.001 → $0.0006 but a $0.005 start fee, so on Gold a run has to clear ~9 rows before its lower row price overtakes our flat $0.0024) and bakos_bence/openfda-recalls ($0.00149 → $0.000745 plus a $0.005 start fee). Cheaper still, on a flat rate with no start fee and no tiers: bgfc97/openfda-data ($0.00075/record, recalls plus adverse events, labels, shortages and NDC) and snow_leo_data/product-recalls-scraper ($0.00075/recall). bikram07/recall-radar is a second Apify-FREE ($0/row) listing from the owner of the already-named bikram07/fda-recall-monitor, this one a deduplicated CPSC + FDA + NHTSA feed. Drug-only but far below everything else: tolvan/harmoney-openfda-recall-monitor ($0.0001/record + $0.00005 start, ~35x under our free-plan rate for drug recalls alone).
Two feature claims of ours are narrowed by that sweep, and one correction. snow_leo_data/product-recalls-scraper advertises 127,501 records across five US feeds in a 53-field schema and states that it pages openFDA by cursor to read the whole archive rather than stopping at openFDA's 25,000-record skip ceiling — so our 50,000-row ceiling is a per-run cap we should not describe as the widest in the niche, and a buyer who needs the full historical archive in one dataset has a cheaper option than us. datalayer/fda-recall-enforcement classifies openFDA's free-text reason_for_recall into root causes (undeclared allergen, sterility failure, nitrosamine impurity, software defect, component recall) and scores repeat-offender firms — a genuine analytical field we do not offer at all, at a lower price than ours; our riskScore grades severity/recency/scope but never parses why the recall happened. The correction: tictechid/vanzi-us-recall-intelligence, described above as "$0.005/result on Free but $0.0015 on Gold and above", also charges a tiered per-GB Actor-start fee ($0.001 on Free, $0.0005 on Gold+) plus an optional include-analytics event ($0.001 → $0.0006/row) that the earlier note omitted. The conclusion is unchanged — a one-row Gold run there costs $0.0005 + $0.0015 = $0.002 against our $0.0024, so it does still beat us from the first row — but it is not the start-fee-free comparison the earlier wording implied.
Full-list unnamed-match check, verified live 2026-10-04 (271 niche matches against the README's 36 named handles, every match with 3+ users live-priced). One more genuine in-scope rival had never been named: benthepythondev/openfda-drug-intelligence (4 users, drug-only adverse events and recalls with an AI-labelled 0-100 severity score — the same idea as our own documented riskScore, not a black box here either). Tiered $0.005/result on Free down to $0.0035 on Diamond, plus a per-GB start fee — dearer than us at every tier, and its cheapest (Diamond) tier only ties our priciest (Free) one. No new undercutter. The rest of the unnamed tail is out of scope for this niche, not just unpriced: fiery_dream/vehicle-intel and ocrad/carfax-ca-scraper (10u) are VIN/vehicle-history tools that only keyword-matched on the bare word "recall"; five listings named cpsc-recalls/cpsc-...-monitor plus pink_comic/nhtsa-vehicle-recalls scrape the Consumer Product Safety Commission or NHTSA, a different government agency's recall data entirely, not FDA's; scrupulous_waterbird_m4w/openfda-drug-events (3u) reads openFDA's FAERS adverse-event endpoint, not the enforcement/recall endpoint this Actor covers. Two MCP-wrapper tools bill per tool-call rather than per-row, so a price comparison would not be apples-to-apples: nexgendata/premium-data-mcp-server (3u) charges $0.05 per MCP tool invocation across five bundled data types (government contracts, business registration, FDA recalls, Amazon prices), and red.cars/regulatory-intelligence-mcp charges $0.05 per search_regulations call covering FDA drug/device enforcement actions only (no food) bundled with SEC filings and sanctions screening.
Full-cohort re-sweep, verified live 2026-10-06 (295 Store listings seen across the search terms, 277 niche matches, 237 unnamed against the README's 43 handles at the time — every one of the 237 live-priced, not just a >=3-user cut, since that cut was thin at only 5 listings and all 5 were CPSC-only). Ten more genuine same-scope undercutters (all three recall types — food, drug and device, from openFDA's enforcement endpoint, like us) had never been named: mooseandraven/openfda-recall-monitor (flat $0.002/record at every plan, no tiers, no start fee); dataio/openfda-recalls-enforcement ($0.003 on Free down to $0.002 on Gold+, no start fee, and decodes severity class plus computes days-open); factpipe/fda-recalls-monitor ($0.003 -> $0.0021, no start fee, "$3 per 1,000"); brightpath-data/openfda-recalls (flat $0.0015/result plus a trivial $0.0001 one-time start); springlike_meadowland/fda-product-recalls-scraper (flat $0.001/result plus a $0.00005 start); s-r/fda-recalls-scraper ($0.001/recall plus a separate $0.001 flat per-run charge, so a 1-row run costs $0.002 but the per-row rate itself is 3.5x under ours); benthepythondev/openfda-scraper (2u — a second, separate listing from the owner of the already-named benthepythondev/fda-recall-intelligence; $0.003 on Free down to $0.0021 on Diamond, plus a negligible ~$0.00004 actor-start fee, so it undercuts us on Free/Bronze/Silver and is within a hair of our own Gold rate); tagadanar/openfda-recall-monitor ($0.002 -> $0.0014 plus a $0.001 one-time start, with an optional digest/Slack-alert event, and a watchLabel-style "only what's new" mode like ours); mouadapi/openfda-recalls ($0.0015 -> $0.001 plus a $0.00005 start, also with a new/changed-since-last-run watch mode). The exception: devilscrapes/fda-recalls-scraper ($0.002/result) looks cheaper per row but carries a $0.20 one-time actor-start fee — a run needs more than ~133 rows before its lower per-row rate overtakes our flat, start-fee-free pricing, so it is dearer than us for most realistic run sizes despite the lower headline rate. None of the ten documents includePressReleases, a published riskScore, or watchChanges-style field-level diffing (only tagadanar and mouadapi have a new/changed watch mode at all, and neither diffs which field changed). The rest of the 237 were genuinely out of scope, not just unpriced: the majority (137) were simply dearer and are not worth naming individually; most of the remainder split into CPSC/NHTSA/EU-Safety-Gate/UK-FSA/France-RappelConso/China-SAMR/Canada/Australia/NZ/UAE listings (a different government agency's recall data entirely, matched only on the generic word "recall"), openFDA listings scoped to a different endpoint than enforcement (adverse events/FAERS, drug labels, UDI, NDC directory, warning letters, drug shortages, Drugs@FDA applications — a different dataset shape even though "FDA" is in the title), one Google-News-RSS recall aggregator (not openFDA data at all), and MCP-per-tool-call pricing (not comparable to a per-row rate).
Full-cohort re-sweep, verified live 2026-10-07 (300 Store listings seen across the search terms, 282 niche matches, 232 unnamed against the README's 53 handles at the time — the >=3-user cut stayed thin at just 7 listings, every one of them CPSC/NHTSA/out-of-scope, so the whole 232-listing unnamed tail was live-priced end to end via a new bin/_batch_price_fda.py). Own price re-verified live first: unchanged, $0.0035 FREE / $0.003 Bronze / $0.0027 Silver / $0.0024 Gold+, no start fee. Four more genuine same-scope undercutters (all three recall types — food, drug and device, from openFDA's enforcement endpoint, like us) had never been named: webdatatools/openfda-recall-monitor (2 users) is the cheapest of the four and undercuts us at every tier — $0.002 on Free down to $0.0012 on Gold+, plus a negligible $0.00005 start fee — and bundles drug/device adverse-event reports and drug labels into the same row schema. yadroo/openfda-records (1 user) also undercuts at every tier ($0.002 → $0.0014 Gold+, plus SPL drug labels and MAUDE device adverse-event reports bundled in) but adds a flat $0.001 start fee on every tier, absorbed after the first row. optimistprime/us-product-recalls-fda-cpsc (1 user) bundles CPSC consumer-product recalls on top of the same FDA three-type coverage, tiered $0.002 → $0.0015 Gold+ plus a flat $0.002 start fee — cheaper than us from roughly the 2nd-3rd row on, at every plan. thirdwatch/fda-recalls-scraper (2 users) only undercuts from Gold up ($0.004 Free, dearer than us, down to $0.002 Gold+, cheaper than our $0.0024), the same partial-undercut shape as several already-named rivals. One crossover, not a true undercutter: dalbian/openfda-drug-device-food-data (2 users, bundles drug labels, FAERS adverse events and 510(k) device clearances alongside recalls) charges a flat $0.03 per search-run plus $0.002/record returned, so its lower per-row rate only overtakes our flat, start-fee-free pricing above roughly 20 rows on Free or 75 rows on Gold+. None of the five documents includePressReleases, a published riskScore, or watchChanges-style field-level diffing. The rest of the 232 were genuinely out of scope, not just unpriced, in the same shape as every prior sweep on this Actor: roughly a quarter were CPSC or NHTSA (a different government agency's recall data), another quarter were EU Safety Gate/UK FSA/France RappelConso/China SAMR/Canada/Australia/NZ/UAE/Germany listings (a different country's recall data), about an eighth were openFDA listings scoped to adverse events/FAERS/drug labels/UDI/NDC rather than the enforcement/recall endpoint this Actor reads, a handful were MCP-per-tool-call pricing (not a comparable per-row rate), and the remainder were simply dearer than us at every tier and not worth naming individually.
Later same-day re-sweep, verified live 2026-10-07 (305 Store listings seen, 287 niche matches, 59 named handles after the sweep above — the >=3-user cut now holds 34 listings, up from 7 a few hours earlier, live-priced via the same bin/_batch_price_fda.py). Own price re-verified unchanged: $0.0035 FREE / $0.003 Bronze / $0.0027 Silver / $0.0024 Gold+, no start fee. Seven more listings crossed the 3-user floor since the sweep above and none had been named. Four are full-scope undercutters (openFDA drug, food and device enforcement, like us) that beat us at every tier: ninhothedev/openfda-scraper ($0.0005/record flat — "drug labels, adverse events, recalls & device/food enforcement" — the deepest undercut found this sweep), chrisp1211/openfda-scraper-max ($0.001 flat, the closest scope match: "drug, food and device data: recalls, enforcement reports, adverse events and drug labels"), gio21/openfda-scraper ($0.001 flat, bundles drug labels, drug/food/device recalls, adverse events and NDC) and agentictools/openfda-safety-monitor ($0.001 flat, drug/device/food adverse events, recalls and enforcement reports). Two only partially undercut us — flat $0.003, cheaper than our Free rate, tying Bronze, dearer from Silver on: hichemdev/openfda-scraper (same 3-type scope plus adverse events and drug labels) and neuton/openfda-food-enforcement-reports-scraper (food enforcement only). One is food-only but undercuts at every tier anyway: pink_comic/fda-food-recall-enforcement-search ($0.002 flat). None of the seven documents includePressReleases, a published riskScore, or watchChanges-style field diffing. The rest of the newly-crossed cohort was genuinely out of scope or already dearer: datapilot/product-recall-monitor scrapes Google News RSS for recall mentions, not openFDA data; scrapemint/product-recall-finder and foo121/recall-aggregator (the latter bundling FDA with NHTSA and CPSC) both charge $0.004-$0.005/record flat, dearer than us at every tier; maximedupre/fda-faers and labrat011/fda-drug-labels-scraper read FAERS adverse events and drug labels respectively, not the enforcement/recall endpoint; fascinating_lentil/clinical-trials-drug-data-aggregator bills per clinical-trial record, not per recall, so its $0.002 headline isn't a comparable unit; six more neuton/openfda-* single-endpoint listings (drug/device adverse events, device UDI, drug labels, drug shortages, Drugs@FDA applications, food adverse events) are adverse-event/label/registry products, not recall data; funnyvalentine69/fda-drug-pipeline-intelligence is a clinical-trials-plus-openFDA aggregator, dearer anyway; the remainder matched only on CPSC, NHTSA, EU Safety Gate, New Zealand, UAE or China SAMR agency recall data — a different government entirely.
Routine re-check, verified live 2026-10-08 (309 Store listings seen, 290 niche matches, up from 287/59-named at the last sweep above). Own price re-verified unchanged: $0.0035 FREE / $0.003 Bronze / $0.0027 Silver / $0.0024 Gold+, no start fee. 221 unnamed remain, but every one that cleared the 3-user floor since the last sweep was either CPSC/NHTSA/EU/NZ/UAE/China-SAMR out-of-scope agency data or one of neuton's single-endpoint openFDA products (adverse events/labels/UDI/shortages — not the enforcement/recall endpoint this Actor reads), the same shape documented above — no new in-scope undercutter. Four below the 3-user floor were spot-checked anyway on the chance a new entrant was pricing aggressively (whitel1ght/fda-recalls, weirworks/drug-device-recall-tracker, zentrafoundry/product-recall-unified-monitor, zentrafoundry/fda-safety-signal-monitor-v2): all dearer than us, $0.003–$0.02/record. One correction: copious_atoll/fda-food-recalls, named above as 3 users, is now 4.
Routine re-check, verified live 2026-10-09 (311 Store listings seen, 292 niche matches, up from 290 at the last sweep above). Own price re-verified unchanged: $0.0035 FREE / $0.003 Bronze / $0.0027 Silver / $0.0024 Gold+, no start fee. 219 unnamed remain; every listing that newly cleared the 3-user floor since the last sweep (k0nkupa/nz-product-recall-alert-monitor, zenolvepro/cpsc-recalls, getascraper/uae-product-recall-monitor, blaidlink/cpsc-supplier-recall-monitor, crawlerbros/autotrader-us-vehicle-catalog, samwise.agency/cpsc-recalls, getascraper/china-samr-product-recall-scraper, pink_comic/cpsc-product-recalls, parseforge/cpsc-recalls-scraper, koalastuff/eu-safety-gate-alert-monitor, and eight more neuton/openfda-* single-endpoint listings covering device/drug/food adverse events, NDC directory, Drugs@FDA applications, drug shortages, drug labels and device UDI) is the same two out-of-scope shapes documented above — a different government agency's recall data, or an openFDA endpoint other than enforcement — not a new in-scope undercutter. The fleet-wide check-price-superiority tiered-ladder scan also found 0 undisclosed on this file.
Unnamed-tail re-price, verified live 2026-10-16 (cycle 1804 — 702 Store listings seen across the search terms, 437 niche matches, 87 named handles, 357 unnamed; the 39 of those unnamed listings that clear the 3-user floor were live-priced end to end). Own price re-read off the platform before any comparison was made: unchanged, $0.0035 FREE / $0.003 Bronze / $0.0027 Silver / $0.0024 Gold+, no start fee. Eleven of the 39 carry a tier below our $0.0024 Gold+ floor, but only one is in scope for what this Actor sells, and it had never been named here: hipersoft/openfda-scraper (4 users, "openFDA Scraper — Drug Recalls, Labels & Adverse Events") is tiered $0.0016 Free → $0.00128 Bronze → $0.00104 Silver → $0.0008 Gold+ plus a $0.00005 start fee, so it undercuts us at every tier — roughly 2.2x cheaper on Free and 3x cheaper on Gold and above — reading the same openFDA drug-recall data we do, with labels and adverse events bundled into the same run. It does not document includePressReleases, a published riskScore, or watchChanges-style field-level diffing, and its recall coverage is drug-side rather than the food + drug + device enforcement scope we deliver in one schema, but on drug recalls alone it is cheaper than us at any volume. The other ten below-floor listings are the two out-of-scope shapes already documented above, not new price competition: six are NHTSA/VIN vehicle products (ahmed_jasarevic/universal-vin-decoder-recall-auditor at 25 users and $0.002 → $0.00197 — the largest unnamed listing in the whole sweep — plus prodiger/vin-decoder $0.001 → $0.0003, sourabhbgp/nhtsa-vehicle-intelligence $0.002 flat, benthepythondev/vehicle-recalls-intelligence $0.0015 → $0.00105, aiqlabs/vin-decoder-recall-audit $0.002 and martc03/nhtsa-recalls $0.00001 — a different government agency's recall data entirely); copious_atoll/uscg-boat-recall-scraper ($0.002) is US Coast Guard boat recalls; studio-amba/safety-gate-scraper ($0.002 + $0.005 start) is EU Safety Gate/RAPEX; pink_comic/fda-510k-medical-device-clearances ($0.002) reads the 510(k) clearance registry, not the enforcement/recall endpoint; and maximedupre/drug-shortage ($0.00001) sells drug shortages. Nine more sit between our floor and our entry — cheaper than our Free rate, dearer from Silver or Gold up: seven neuton/openfda-* single-endpoint listings at $0.003 (device UDI, Drugs@FDA applications, device/drug/food adverse events, drug labels, NHTSA vehicle recalls — the same non-enforcement-endpoint shape as the eight already named above), maximedupre/cpsc-product-recalls at $0.0027 (CPSC) and gio21/vin-decoder-scraper at $0.003 (VIN). Pricing decision: holding at $0.0035 → $0.0024. The one genuine in-scope undercutter carries 4 users, well under the 20-user threshold this fleet uses before it re-prices against a rival, and the deeper structural point from the sweeps above is unchanged — there are already free and $0.00001/row all-types rivals named on this page, so we do not compete here on being cheapest and a cut chasing a 4-user listing would not change that. One forward-dated price worth recording, since every price check in this fleet filters to startedAt <= now and so cannot see it: studio-amba/safety-gate-scraper has a pricing entry scheduled to take effect 2026-10-27 that moves it onto Apify's FREE model ($0 per row). It is EU Safety Gate data and therefore out of scope for us either way, but the same owner's catalogue is worth re-reading after that date in case the flip is catalogue-wide rather than per-listing.
How is webhookUrl different from Apify's own platform webhooks?
Apify's platform webhooks are configured separately per Task/Actor via the Console or the Webhooks API — useful if you already live in the Apify Console, but extra setup if you're calling this Actor's API directly and just want a completion ping. webhookUrl is a plain input field: set it on the run itself and it POSTs a JSON body (actorRunId, defaultDatasetId, finishedAt, pushed, scanned, and — if watchLabel is set — watchSeeding/watchNewCount/watchChangedCount, plus the full summary object described in the next FAQ) once the run finishes and every row is already pushed and charged. Especially useful with watchLabel: your endpoint gets told how many brand-new or changed recalls landed without polling the dataset. It's best-effort — a slow or failing webhook only logs a warning, it never fails the run, changes the result set, or affects billing.
How do I know the run returned everything that matched?
Read the RUN_SUMMARY record from the run's key-value store — no webhook needed:
GET https://api.apify.com/v2/actor-runs/<runId>/key-value-store/records/RUN_SUMMARY?token=<token>
{"finishedAt": "2026-09-22T12:03:58.593Z","mode": "search","declaredMatches": 14381,"scanned": 6,"delivered": 6,"complete": false,"incompleteReason": "max-results","incompleteDetail": "Stopped at maxResults=6; matching recalls past this point were not returned.","skippedSeen": 0,"changedCount": null,"watchChangeBlindFilters": null,"baselineSize": null,"baselineTruncated": null,"baselineTruncatedTotal": null,"pressReleasesRequested": false,"pressReleasesIncluded": false,"productTypes": [{ "productType": "food", "declaredMatches": 3976, "reachableMatches": 3976, "windowsPlanned": 1,"windowsScanned": 0, "scanned": 2, "delivered": 2, "skippedSeen": 0, "status": "ok","complete": false, "incompleteReason": "max-results", "declaredMatchesIsFloor": false }]}
declaredMatches is openFDA's own meta.results.total for your filters, so delivered vs declaredMatches answers the question directly, per product type as well as for the run. Six rows against 14,381 matches looks exactly like a complete result set in the dataset — this is where you find out it isn't. mode is search, watch-seed or watch-incremental.
complete is deliberately not folded into status: a product type can be perfectly ok and truncated at the same time. incompleteReason is one of:
| Reason | Meaning |
|---|---|
max-results | Your maxResults stopped the walk before the match set ran out. |
charge-limit | The run's maximum-cost limit stopped it; raise the limit to get the rest. |
seed-cap | A watchLabel seed hit the 20,000-recall cap. Narrow the query and re-seed, or the first incremental run reports recalls past the cap as new. |
skip-ceiling | A single date window holds more rows than openFDA will page through (skip cannot exceed 25,000). Narrow reportDateFrom/reportDateTo. |
search-request-failed | openFDA stopped answering mid-walk. Rows past that point were never scanned — this is not evidence they don't exist. |
plan-request-failed | openFDA never answered the initial count query for a product type, so nothing of that type was scanned. That type's status is not-scanned and its declaredMatches is null, never 0. |
The null-vs-0 distinction is the point: declaredMatches: null means we never got an answer, while 0 means FDA has no matching recall. declaredMatchesIsFloor: true means the window plan itself was cut short, so the real total is higher. When anything is incomplete the run also sets a human-readable status message, visible at the top of the run in the Apify Console. baselineTruncated/baselineTruncatedTotal (watch mode only) report the "Baseline size cap" defect above — see that FAQ entry. watchChangeBlindFilters is null unless watchChanges is on, [] when the watch run is clean, and otherwise lists the filters that make the watched change undetectable — see "don't filter on the field you're watching" above. watchDeadDateBound is null outside watch mode and true when a watched query also pins a fixed reportDateTo — see "why has my watch stopped finding anything new" below.
Related guides
- Eight government JSON APIs that need no key — and the specific way each one lies to you — how this API's silent-failure shape compares across all eight free government JSON APIs we scrape.
- All FetchSmith scrapers — HTTP-only Actors for public data sources, no browser required.
- The FDA publishes every product recall as JSON — but you can't page past row 25,000, and only drugs come with a barcode — the full write-up of the 25,000-row skip cap, the date-chunking workaround and the drug-only
openfdabarcode fields. - openFDA's
count=shows you the top 1,000 values and never tells you it stopped — the distinct-value ceiling (100 by default, 1,000 max,skiprejected, nometa.resultsto detect it), the exactly-additive date-partition that recovers all 5,526 recalling firms, and whycount=/sort=on a non-.exactfield returns a 500 instead of a 400. - Scraping USAspending's federal awards JSON API — the same "US government publishes it as keyless JSON" pattern, applied to federal contracts and grants.
- Eight ways an "only new since last run" watch mode silently stops working — how
watchLabelis built, why the rolling 365-day default never enters the criteria fingerprint, and why the seed walk pages at 1000 regardless ofmaxResults. - We nearly charged our own buyers twice for rows they'd already paid for — a capped watch-mode baseline can silently evict old-but-current ids on a high-volume run, re-delivering (and re-billing) rows already paid for. Reproduced on this Actor, closed with truncation tracking.
Source code: https://github.com/Fetchsmith/fetchsmith/tree/main/actors/fda-recall-scraper