Global Product Risk & Recall Monitor
Pricing
Pay per usage
Global Product Risk & Recall Monitor
Cross-source product-safety recall intelligence: EU Safety Gate, US openFDA, US CPSC and Health Canada normalized into one deduplicated schema with severity, provenance and change tracking.
Pricing
Pay per usage
Rating
0.0
(0)
Developer
Atlas Assistent
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
3 days ago
Last modified
Categories
Share
One normalized feed of official product-safety recalls and risk alerts from four government sources across three regions — the EU, the United States and Canada. Built for compliance, quality, trust-&-safety and supplier-risk teams who need machine-readable recall intelligence without stitching together four different government APIs.
The actor pulls from EU Safety Gate, openFDA enforcement (food / drug / device), the US CPSC, and Health Canada Recalls & Safety Alerts, then normalizes everything into a single, stable, deduplicated schema with severity normalization, source provenance, canonical URLs, and cross-run change tracking (firstSeen / lastSeen / changedFields).
No search engines are scraped and no data is fabricated. Every record comes from an official public government endpoint and carries its source URL and provenance.
Who is this for (ICP) & use cases
- Compliance & regulatory affairs — monitor recalls affecting your product categories or brands across the EU, US and Canada in one feed; export to your GRC system.
- Retail / marketplace trust & safety — detect when a product you list has been recalled and pull the offending listing quickly (electrocution, choking, fire, contamination, etc.).
- Supplier & supply-chain risk — watch specific manufacturers / brands / countries of origin and get alerted when a supplier is implicated in a recall.
- Quality & product safety engineering — track hazard types (e.g.
Listeria,electric shock, lithium-ion battery fires) across markets to inform design and testing. - Insurance, due diligence & research — build datasets of recall frequency and severity by category, country and time for underwriting or analysis.
- Consumer-safety / news / watchdog apps — feed a "latest dangerous products" surface backed by authoritative sources.
Typical workflows: run incrementally on a schedule (e.g. every 6 hours) and route new/changed records to Slack, a webhook, a database, or a spreadsheet.
Sources, coverage & attribution
| Source | Region | Endpoint (machine-readable) | Coverage |
|---|---|---|---|
| EU Safety Gate (RAPEX) | EU/EEA | `api/download/weeklyReport/list | detail/xml` |
| openFDA Enforcement | US | api.fda.gov/{food,drug,device}/enforcement.json | FDA recall/enforcement reports for food, drugs and medical devices, with recall classification. |
| US CPSC | US | saferproducts.gov/RestWebServices/Recall | Consumer product recalls (excludes food, drugs, cars, which other agencies handle). |
| Health Canada | Canada | recalls-rappels.canada.ca/.../HCRSAMOpenData.json | Recalls & safety alerts for consumer products, food (CFIA), health products, medical devices and vehicles. |
Attribution. Data © the respective authorities: European Commission (DG Justice and Consumers), U.S. Food and Drug Administration, U.S. Consumer Product Safety Commission, and the Government of Canada (Health Canada / CFIA). This actor is an independent tool and is not affiliated with or endorsed by any of these bodies. Always confirm against the official canonicalUrl before acting.
Known source limitations
- EU Safety Gate: the interactive portal search API is behind a bot-protection layer that blocks automation. This actor deliberately uses the official machine-readable weekly-report XML export (
/api/download/weeklyReport/..., listed as an open-data distribution on data.europa.eu) instead — stable and automation-friendly. Alerts carry the weekly report's publication date rather than a per-alert timestamp. Detail URLs are restricted to HTTPS European-Commission hosts and redirects are validated. Weekly detail files are processed oldest-first; if one fails to download, the source is reported truncated and its cursor is not advanced past the failed week, so it is retried on the next run (up to a 520-week / ~10-year per-run ceiling). - openFDA: basic paging is capped at 25,000 records per query (a documented API limit).
report_dateis the single, immutable cursor field (cursorDate) — the incremental window, the keyset watermark and the client-side cursor filter all key off it, exactly the field the query paginates and sorts on (report_date:ascincremental,:descfull-modelookbackDaysbackfill). The more meaningfulrecall_initiation_dateis surfaced aspublishedDate(both raw dates are kept inraw); because the cursor never keys off the initiation date, a recall reported now with an old initiation date is never skipped as "before cursor." The three sub-datasets (food/drug/device) are independently cursored — each advances its own partition cursor, so a failure or truncation in one neither advances over nor skips records in another; a failure in one still returns the others (reported as a partial result). Without an API key the shared rate limit applies (about 240 requests/min, 1,000/day per IP) — this actor stays well under it for normal runs. - US CPSC: recalls carry no severity classification, so
severity.normalizedisunknown(the hazard text is preserved inriskType/hazard). Queried by recall-date window. The endpoint exposes no server-side pagination — a single windowed request returns the complete result set for that window; if a response is unexpectedly large (≥ 9,000 items) the window is treated as possibly truncated and the cursor is held back rather than advanced over a possibly-incomplete window. - Health Canada: published as a single ~15 MB daily open-data snapshot; the actor downloads the whole snapshot once per run (a complete picture) and filters client-side. The publishing
Organizationis exposed asrecallingFirmso the brand/firm filter can find it.Recall class(severity) is frequently blank upstream →unknown.
If a source is temporarily unavailable, the run continues with the remaining sources and reports the failure per-source (see Partial-failure handling). A source's incremental cursor advances only after a successful run whose selected records were written to the dataset, and never past failed, truncated, or filtered-out regions — so a cap, outage, filter change, or write failure can never permanently skip records.
Output schema (schemaVersion 1.1.0)
Each dataset item is one normalized recall/alert:
{"recordId": "…40-char sha1…", // deterministic: sha1(`${source}:${sourceRecordId}`)"schemaVersion": "1.1.0","source": "eu_safety_gate", // eu_safety_gate | openfda | us_cpsc | health_canada"sourceDataset": "weekly_report","sourceRecordId": "10099472","regionScope": "EU", // EU | US | CA"title": "300 LED LIGHT","productName": "String lights — 300 LED LIGHT","brand": null,"recallingFirm": null,"categories": ["Lighting equipment"],"hazard": "The product poses a risk of electric shock…","riskType": "Electric shock","severity": { "normalized": "high", "raw": "Serious risk" }, // raw always preserved"recallType": "safety_alert","reason": "…","actions": "Withdrawal of the product from the market","productDescription": "…","countries": ["Hungary", "People's Republic of China"],"imageUrls": ["https://…"],"publishedDate": "2026-07-10T00:00:00.000Z", // ISO-8601 (openFDA: recall_initiation_date)"cursorDate": "2026-07-10T00:00:00.000Z", // immutable source-native date the cursor orders by (openFDA: report_date; else == publishedDate)"canonicalUrl": "https://ec.europa.eu/safety-gate-alerts/screen/webReport/alertDetail/10099472","provenance": {"source": "eu_safety_gate","endpoint": "https://ec.europa.eu/safety-gate-alerts/api/download/weeklyReport/detail/xml/…","fetchedAt": "2026-07-16T09:42:47.034Z","sourceLastUpdated": "2026-07-10T00:00:00.000Z"},"firstSeen": "2026-07-16T09:42:47.034Z", // stable across runs (named KV store)"lastSeen": "2026-07-16T09:42:47.034Z","changedFields": [], // e.g. ["severity","actions"] when it changes"raw": { "caseNumber": "SR/01958/26", "level": "Serious risk", "…": "…" }}
Severity normalization (raw value always kept in severity.raw):
| Normalized | EU Safety Gate | openFDA | Health Canada | CPSC |
|---|---|---|---|---|
high | Serious risk | Class I | Type I | — |
medium | Other risk | Class II | Type II | — |
low | — | Class III | Type III | — |
unknown | Risk not yet defined | (missing) | (blank) | always |
Input
All fields are optional; sensible defaults ship in the input schema.
{"sources": ["eu_safety_gate", "openfda", "us_cpsc", "health_canada"],"mode": "incremental","maxItems": 200,"lookbackDays": 90,"recheckWindowDays": 7,"fdaDatasets": ["food", "drug", "device"],"product": "","brand": "","category": "","country": "","riskType": "","severity": [],"dateFrom": "","dateTo": "","since": "","includeRaw": true}
| Field | Meaning |
|---|---|
sources | Which sources to query (defaults to all four). |
mode | incremental = only records newer than the last run (cursor stored in a named KV store); full = backfill over lookbackDays. |
maxItems | Per-run budget on records processed and saved, applied after per-source filtering and deduplication. Selection is a balanced round-robin across sources so every selected source is represented. In incremental mode the dataset receives only the new/changed records within that budget; the cursor still advances over the processed (already-known) records so the next run continues forward. |
lookbackDays | First-run incremental window and full-mode backfill window. |
recheckWindowDays | Incremental overlap (days, default 7): re-fetch & re-compare already-committed records whose cursorDate is within this window below the cursor, so upstream edits are surfaced. 0 disables it. The cursor is never regressed by these re-checks. |
fdaDatasets | Which openFDA enforcement datasets to include. |
product/brand/category/country/riskType | Case-insensitive substring filters. |
severity | Keep only these normalized levels. |
dateFrom/dateTo | Inclusive ISO date bounds on publishedDate. |
since | Override the automatic incremental cursor. |
includeRaw | Include the compact raw audit object. |
Example: watch lithium-battery fire risks across all regions
{ "mode": "full", "lookbackDays": 365, "riskType": "battery", "maxItems": 500 }
Example: only EU + Canada high-severity food recalls this year
{ "sources": ["eu_safety_gate", "health_canada"], "severity": ["high"], "category": "food", "dateFrom": "2026-01-01" }
Modes, dedup & change tracking
- Deterministic IDs.
recordIdis a stablesha1(source:sourceRecordId); re-running never changes an item's ID, so downstream upserts are trivial. - Deduplication. Records are deduped by
recordIdbefore filtering, with a documented merge policy that is a pure function of the complete raw occurrence set for thatrecordId. The guarantee is order-independence over that complete set (not associativity): given all raw occurrences,dedupeRecordsreturns a byte-identical merged record for any input permutation — 3+ conflicting occurrences merge to the same result under any order. It is not a recursive/associative merge: a merged record drops the per-occurrence candidate history (individualpublishedDates, blank-vs-present candidates, the pre-canonical array multiset), so callers must not re-merge already-merged outputs — always merge the complete raw set in onededupeRecordspass (the internal whole-group reducer is module-private to prevent recursive misuse). Within that pass: array fields (images, categories, countries) are case-insensitively unioned with a canonical casing (the lexicographically smallest variant) and a deterministic sort; scalar fields take the value from the record with the newerpublishedDate, falling back to the next occurrence when blank (most-complete), and on an equalpublishedDatethe tie is broken by the larger value (never by input order); severity keeps the highest level; provenance keeps the most recently fetched (ties broken by a total order on endpoint, then source metadata). - Incremental cursors. State lives in a named key-value store (
global-product-risk-monitor-state) that survives scheduled runs. The incremental cursor is a keyset watermark(cursorDate, recordId)partitioned by source and by query configuration (filters + FDA datasets); openFDA is partitioned further, one cursor per food/drug/device sub-dataset.cursorDateis the immutable source-native ordering date (openFDA:report_date; every other source:= publishedDate). A source is processed oldest-first and its cursor advances to the newest record that was selected and written to the dataset, and only after those writes succeed — it advances up to but never past a failed source, a truncated (over-cap) window, or a filtered-out region. This makes caps, source outages, filter/config changes and dataset-write failures all retryable: nothing is permanently skipped, and a backlog larger thanmaxItemsdrains forward across runs. (Overlapping runs are a separate concern — see Concurrency below; they share one state document and must not overlap.) - Change tracking, recheck window & new/changed output. The per-record snapshot yields
firstSeen(fixed),lastSeen(advances every run) andchangedFields. In incremental mode the dataset receives only genuinely new or changed records; unchanged records are still recorded in state (so the cursor advances) but are not re-emitted. To catch upstream edits to records that are already committed (same publication key), each incremental run additionally re-fetches and re-compares records whosecursorDatefalls within a bounded recheck window below the cursor (recheckWindowDays, default 7), emitting any that changed without regressing the cursor. Edits to records older than that window are not detected — widenrecheckWindowDays(at the cost of a larger re-fetch) if you need a longer look-back. Full mode emits everything selected within the lookback window. - Partial-failure handling. Each source — and, for openFDA, each food/drug/device sub-dataset — runs independently; a failure is logged and reported in
LAST_RUN_SUMMARY/OUTPUT(withtruncated/failedflags) without discarding the data that succeeded. A failed source/sub-dataset does not advance its cursor at all; a truncated (but successful) one advances only to the newest fully-captured record and holds below the uncaptured region, so the remainder is retried next run. The run only fails if every source fails (in which case no cursor is advanced). - Concurrency. All partitions — every source, sub-dataset and query configuration — share a single key-value document (one
STATEkey in the named store). Each run does a read‑modify‑write of that one JSON blob, and the Apify key-value store offers no compare-and-swap (CAS). So any two overlapping runs contend on the same document, regardless of configuration — even runs with different filters/FDA datasets, because their independent partition cursors still live in the same shared blob. When runs overlap, each re-reads and merges state just before persisting (furthest-advanced cursor wins, entries are unioned), which reduces the window for lost progress but cannot guarantee none: a read‑modify‑write race can still drop one run's cursor advance or cause a record to be re-emitted; the last writer can clobber a concurrent writer's partition updates entirely. A hard "no lost progress" guarantee would require CAS, which the platform does not provide; the merge is a best-effort mitigation, not a substitute for serial execution. Operational requirement: for the Actor account/store, keepmaxConcurrency = 1and run non-overlapping schedules — set schedules so a run always finishes before the next starts, across all configurations, never just within one. Do not rely on different configurations being able to run concurrently.
Run it via API / schedule / webhook
Apify API — start a run:
curl -X POST "https://api.apify.com/v2/acts/atlas_digital_assistant~global-product-risk-monitor/runs?token=YOUR_TOKEN" \-H "Content-Type: application/json" \-d '{ "mode": "incremental", "maxItems": 200 }'
Get the dataset items of the last successful run:
$curl "https://api.apify.com/v2/acts/atlas_digital_assistant~global-product-risk-monitor/runs/last/dataset/items?token=YOUR_TOKEN&format=json&status=SUCCEEDED"
Schedule. Use Apify Schedules to run incremental every few hours. The first run seeds lookbackDays; subsequent runs only return new/changed records.
Webhooks. Add an Apify webhook on ACTOR.RUN.SUCCEEDED pointing at your endpoint (or a Make/Zapier/n8n hook) to push new records into Slack, a database or a ticketing system. Filter on changedFields.length > 0 for "changed since last time" alerts.
Compute & pricing
Runtime is dominated by the Health Canada snapshot download (~15 MB) plus a handful of small JSON/XML requests.
- Illustrative, not benchmarked. The figures below are rough order-of-magnitude expectations, not measured guarantees — this repo ships no automated CU/runtime benchmark, so treat them as estimates only. A typical
incrementalrun withmaxItems: 200is expected to finish in roughly a minute on the smallest (1 GB / shared-CPU) unit and to consume on the order of ~0.002–0.01 compute units (CU); large backfills cost more. - Measure your own cost. Your exact runtime, memory and CU are shown in each run's Runs → Compute tab — use those numbers, not these estimates, for pricing decisions.
- Store pricing model. This actor fits a pay-per-result or low monthly-rental model; set concrete prices from your measured costs above.
Privacy, legal & fair use
- Public official data only. All records originate from public government transparency endpoints intended for redistribution; no authentication or personal accounts are used.
- No secrets are stored in the repo, image, input or output. No API key is required for the default configuration.
- Personal data: recall notices are about products and firms, not individuals. Occasional company/importer names may appear as published by the authority; treat them per the source's terms and applicable law (e.g. GDPR for EU data — the actor stores only what the authority already publishes).
- Respectful access: requests are modest in volume, use a descriptive User-Agent, and apply retry/back-off with
Retry-Afterhandling. No search engines are scraped. - Not legal/medical advice. openFDA data in particular is unvalidated ("do not rely on openFDA to make decisions regarding medical care"). Always verify against the
canonicalUrlbefore acting.
Please review each source's terms: openFDA terms · EU Safety Gate · CPSC · Health Canada open data.
Local development
npm installnpm run typecheck && npm run lint && npm run buildnpm test # unit tests (mocked fixtures)npm run test:smoke # real-source smoke tests (network)npm run smoke:local # full end-to-end run against local storage
Changelog
- 1.1.0 — Reliability & correctness overhaul. Incremental state redesigned to per-source, per-configuration keyset cursors that commit only after successful dataset writes and never advance past failed/truncated/filtered regions (no permanently-skipped records). Release-gate hardening: openFDA now orders/paginates/watermarks on a single immutable
cursorDate=report_date(initiation date surfaced aspublishedDate) so newly-reported older-initiated recalls are never skipped; openFDA food/drug/device are independently cursored so one failing sub-dataset can't advance over another; a bounded, configurable recheck window (recheckWindowDays) re-surfaces upstream edits to already-committed records without regressing the cursor; deduplication is now deterministic (order-independent) on equalpublishedDate; dataset schema hardened + structurally tested. Filters now apply before per-source caps; deduplication runs before filtering with a documented merge policy. openFDA full-mode lookback + recency sort and independent food/drug/device sub-source failures; EU full lookback with oldest-first contiguous-week processing, partial-failure reporting and HTTPS host allow-listing + redirect validation; CPSC completeness guard; Health CanadaOrganizationexposed asrecallingFirm; openFDAcountriesno longer contaminated with US state codes. HTTP body-size/timeout limits, HTTP-dateRetry-After, capped backoff, JSON-parse retries. Strict calendar-date validation, tighter input/dataset schemas, reproduciblenpm ciDocker build.schemaVersion→ 1.1.0. - 1.0.0 — Initial release: EU Safety Gate + openFDA + US CPSC + Health Canada; normalized schema, deterministic IDs, dedup, severity normalization, balanced multi-source selection, incremental/full modes, and named-store change tracking.