Practical guide
Product Recall API: Pull CPSC, Health Canada and UK OPSS Recalls (2026)
Last reviewed: October 25, 2026
Pull official recalls from the US, Canada and UK, preserve their source links, and compare published brands and barcodes with your catalog. Start with the public feeds, then save seen IDs for daily monitoring.
Which consumer product recall APIs and open-data feeds are free?
These official interfaces worked without an API key or account in the supplied October 24, 2026 verification. They are free public feeds with source-specific reuse conditions. Access and field availability can change; keep each original notice URL.
| Source | Official interface and coverage | Reuse and limitations |
|---|---|---|
| US CPSC | Recalls RestWebServices JSON API: https://www.saferproducts.gov/RestWebServices/Recall?format=json. Consumer product recall notices. | Keyless recall retrieval. CPSC public-domain text and recall notices may be redistributed with credit and no implied endorsement; its policy also permits recall photographs. Unrelated stock images have separate rights. |
| Health Canada | Daily HCRSAMOpenData.json export plus official detail pages. Includes consumer products, food, vehicles and health products from the centralized Canadian service. | Open Government Licence - Canada. Preserve attribution and source links; personal information, official symbols and third-party rights are excluded. Export rows include warnings, so explicitly select recalls. |
| UK OPSS / GOV.UK | Search API and Content API for product-safety-alert-report-recall, filtered to Product Recall. The verified document-type value is product_safety_alert_report_recall. | Open Government Licence v3.0. Attribute OPSS and GOV.UK. Excludes personal data and unlicensed third-party material; no endorsement. Safety alerts and reports are separate notice types. |
The CPSC recall service is distinct from the SaferProducts OData incident service, which asks for an application key. Use the recall endpoint below, not an incident or harm-report API.
EU Safety Gate / RAPEX is a coverage gap. The historical XML URLs ending in ?event=main.weeklyReports.XML and ?event=main.weeklyReport.XML&web_report_id=3522 now redirect to an HTML application. A successful HTML response is not an XML feed. The supplied review could not verify a current documented machine-readable replacement; do not treat the legacy URLs as working recall exports.
US CPSC Recalls JSON API: curl and jq
Fetch notices on or after a chosen date from the official CPSC Recalls API. This Bash example saves only a validated JSON array, then projects published identifiers, dates and product names. It needs curl and jq.
# Run in Bash. Adjust the start date for your collection window.
set -euo pipefail
curl --fail --silent --show-error --max-time 30 \
'https://www.saferproducts.gov/RestWebServices/Recall?format=json&RecallDateStart=2026-09-24' \
> cpsc.json.tmp
jq -e 'type == "array"' cpsc.json.tmp > /dev/null
mv cpsc.json.tmp cpsc.json
jq '[.[] | {id: ("cpsc:" + (.RecallID | tostring)),
title: .Title, recallDate: .RecallDate,
sourceUrl: .URL, products: [.Products[]?.Name]}]' cpsc.json
The verified request returned 42 recalls on October 24, 2026. That is an observation, not a guaranteed count. RecallID is the native identity; RecallNumber is a separate published reference. Keep URL as the official notice link. The response can include product, hazard, remedy, unit, model, UPC and image fields when supplied, but explicit brands are often absent.
This projection deliberately omits contact fields. If you publish other free text, review it for personal information and retain provenance. A date query is a collection window, not proof that every relevant recall has been captured.
Health Canada and UK OPSS recall endpoints
Start with the requested GOV.UK discovery URL: https://www.gov.uk/api/search.json?filter_format=product-safety-alert-report-recall&count=12. Do not assume that its format filter selects only recalls. The verified search in the supplied source review uses filter_content_store_document_type=product_safety_alert_report_recall and filter_product_alert_type=product-recall, with order=-public_timestamp. For routine collection, use that explicit filtered query; the example below inspects the discovery results through the Content API and accepts only notices whose metadata labels them Product Recall.
# Save as inspect_uk_recalls.py; run: python3 inspect_uk_recalls.py
import json
import re
import time
from urllib.request import Request, urlopen
ORIGIN = "https://www.gov.uk"
SEARCH = (ORIGIN + "/api/search.json?"
"filter_format=product-safety-alert-report-recall&count=12")
def fetch_json(url):
request = Request(url, headers={"User-Agent": "RecallGuideReader/1.0",
"Accept": "application/json"})
with urlopen(request, timeout=30) as response:
body = response.read(8 * 1024 * 1024 + 1)
if len(body) > 8 * 1024 * 1024:
raise ValueError("Response exceeds size limit")
return json.loads(body)
search = fetch_json(SEARCH)
if not isinstance(search.get("results"), list):
raise ValueError("Expected Search API results array")
for hit in search["results"][:12]:
path = hit.get("link", "")
if not re.fullmatch(r"/product-safety-alerts-reports-recalls/[a-z0-9-]+", path):
continue
time.sleep(0.75) # Conservative spacing, not an official API quota.
notice = fetch_json(ORIGIN + "/api/content" + path)
details = notice.get("details") or {}
metadata = details.get("metadata") or {}
if metadata.get("product_alert_type") != "product-recall":
continue
print(json.dumps({
"id": "uk-opss:" + str(notice["content_id"]),
"title": notice["title"],
"recallDate": metadata.get("product_recall_alert_date"),
"publicTimestamp": hit.get("public_timestamp"),
"sourceUrl": ORIGIN + path,
"attribution": "Source: OPSS, GOV.UK. Open Government Licence v3.0."
}))
The Search API returns a results array and total. Search hits contain fields such as title, link, description and public timestamp. Content records provide content_id, details.metadata and an HTML details.body. Read labelled product tables for brand, category, model, hazard and remedy; do not render the body as trusted HTML. This one-page example may produce fewer than 12 recalls after filtering and is not an archive crawl. A failure stops the script; preserve earlier good data rather than interpreting it as an empty feed.
For Canada, download the daily English JSON export once per daily pass, then reuse the local copy. It is a full array, not a small latest-recalls response: verification observed about 15.8 MB and over 34,000 notices. Fields include NID, Title, URL, Product, Issue, Category and Last updated. Use streaming parsing for memory-constrained collectors.
Keep rows that explicitly describe recalls, exclude warnings, and validate dates before selecting a recent window. Fetch their official /en/alert-recall/ detail pages for labelled brand, company, model, UPC, units and original recall date. The export is sorted by last updated for the Actor's selection; last updated is not necessarily the recall date. If no original date is available, retain that fallback with dateBasis: last_updated. The UK equivalent fallback is dateBasis: public_timestamp. PDFs and arbitrary seller pages are outside this workflow.
Normalize fields and match brands and GTIN barcodes
Keep stable source-prefixed IDs and preserve the source's own values before adding normalized comparisons. The same product recalled in Canada and the US remains two notices. Country is the regulator's market (US, CA or GB), not the product's manufacturing origin.
| Field | Normalization rule |
|---|---|
id, source, sourceUrl | Use CPSC RecallID, Canadian NID or UK content_id with a source prefix. Retain the official notice URL. |
title, productName, brand | Read explicit labels. Keep unknown brands null; do not infer them from a title. |
gtins, modelNumbers | Keep strings and leading zeros. Use only labelled identifier fields; missing lists stay empty. |
recallDate, dateBasis | Prefer the original recall date; mark any last-updated or public-timestamp fallback. |
hazard, remedy, unitsAffected | Preserve qualifiers such as About. Keep absent values null. |
attribution, detailsStatus | Carry source and licence credit per row and record unavailable details explicitly. |
For a brand watchlist, case-insensitive literal matching across explicit brand, product, title and manufacturer/importer fields is a useful first pass. Short terms can match unrelated words; review model, lot, product category, jurisdiction and affected dates before acting. Missing CPSC brand labels make product keywords useful, but a keyword hit is not proof of an exact catalog match.
# Compare strings from explicitly labelled barcode fields only.
import re
def gtin14(value):
if not isinstance(value, str):
return None
digits = re.sub(r"[ -]", "", value)
if not re.fullmatch(r"(?:[0-9]{8}|[0-9]{12}|[0-9]{13}|[0-9]{14})", digits):
return None
return digits.zfill(14)
wanted = gtin14("774021599002")
published = ["774021599002", "774021599026"]
matched = wanted is not None and any(
gtin14(value) == wanted for value in published
)
print(matched) # True
This accepts 8/12/13/14-digit GTIN / UPC / EAN values with optional spaces or ASCII hyphens. It compares zero-padded GTIN-14 strings and checks length and characters, not check digits or product ownership. Never cast barcodes to integers, scrape arbitrary digit sequences from prose, or equate a model number with a barcode. A missing barcode stays [] and cannot match a barcode-only watch. Keep both the original published string and the comparison form.
Monitor newly published recalls each day
Collect and normalize all selected source windows successfully before running this local diff. Save normalized rows as recalls.json, including id, sourceUrl and attribution. The first run establishes a baseline; later runs write previously unseen IDs. Keep the state file between runs and use a separate file for each watch configuration.
# Save as new_recalls.py; run: python3 new_recalls.py
import json
from pathlib import Path
rows = json.loads(Path("recalls.json").read_text(encoding="utf-8"))
if not isinstance(rows, list):
raise ValueError("Expected normalized recall array")
for row in rows:
if not all(isinstance(row.get(k), str) and row[k]
for k in ("id", "sourceUrl", "attribution")):
raise ValueError("Missing identity or provenance")
state = Path("seen-recalls.json")
first_run = not state.exists()
saved = [] if first_run else json.loads(state.read_text(encoding="utf-8"))
if not isinstance(saved, list) or not all(isinstance(x, str) for x in saved):
raise ValueError("Invalid seen-ID state")
seen = set(saved)
unique = {row["id"]: row for row in rows}
fresh = [] if first_run else [row for key, row in unique.items() if key not in seen]
output = Path("new-recalls.json")
output.with_suffix(".tmp").write_text(json.dumps(fresh, indent=2), encoding="utf-8")
output.with_suffix(".tmp").replace(output)
seen.update(unique)
state.with_suffix(".tmp").write_text(json.dumps(sorted(seen)), encoding="utf-8")
state.with_suffix(".tmp").replace(state)
print(f"Baseline: {first_run}; new recalls: {len(fresh)}")
Schedule sequential daily runs. The saved output and seen-state updates are separate file operations; a crash between them needs review. For automatic alert delivery, use a durable outbox and acknowledge delivery separately. This small diff tracks new IDs, not edits, withdrawals or delivery acknowledgements. Do not overwrite the last good snapshot or advance seen state after a failed collection. Keep a status log even when zero rows are found.
No numeric public API quota was found in the reviewed CPSC, Canadian or UK documentation. A daily pass is a conservative starting point, with one request at a time per host, at least 750 ms spacing and any longer robots crawl delay honored. Cache the Canadian export because it updates daily. The Actor uses a one-hour successful source cache; this is its own polling policy, not a published regulator quota. Respect Retry-After, back off on failures and avoid overlapping jobs. Recent bounded windows can turn over between polls.
Retain attribution on every row and in every export or display. For CPSC use Source: U.S. Consumer Product Safety Commission (CPSC). Recall notices may be freely copied and distributed. No endorsement implied. For Canada use Source: Health Canada, Government of Canada. Contains information licensed under the Open Government Licence - Canada. For the UK use Source: Office for Product Safety and Standards, GOV.UK. Contains public sector information licensed under the Open Government Licence v3.0. Include the applicable licence link and the official notice URL per row. These credits do not grant rights to excluded personal information, logos or unlicensed third-party material.
The easy option: Product Recall Monitor
Product Recall Monitor: CPSC, Health Canada & UK OPSS combines the three verified sources into normalized rows. It supports literal brand/keyword watchlists, exact GTIN comparison, category and country filters, JSON/CSV/Excel exports and Apify API access. Planned price: $3.00 per 1,000 recalls; the final price is shown on the Store page, and a configured Actor-start charge can also apply.
{
"sources": ["cpsc", "health-canada", "uk-opss"],
"watchlist": ["Simmons", "car seat"],
"gtins": ["774021599002"],
"categories": [],
"countries": [],
"recalledWithinDays": 30,
"maxResults": 40,
"monitorMode": true,
"monitorKey": "catalog-watch",
"requestDelayMs": 750
}
A watchlist term or an exact GTIN can match; category and country restrictions combine with that group using AND. Keep the same monitor key and filters for repeat checks. Unlike the local baseline script, the Actor's first monitor run emits current matches; later runs emit only unseen source IDs with firstSeenAt. Edits to an existing ID do not trigger a new event. Alert delivery must be configured through your chosen integration.
Coverage is bounded: the latest 120 dated CPSC notices within 365 days, 12 eligible Canadian recalls and 12 UK recalls, followed by your age and watch filters. Increasing maxResults does not expand those windows. No EU Safety Gate, Australian ACCC or US NHTSA rows are included. Missing detail fields and PDFs can limit matches. Check RUN_SUMMARY for failures, cache age, warnings and capped windows; all sources being unavailable can still produce a successful run with zero rows.
Keep the named monitor state and delivery-claims queue between runs. Durable claims prevent repeat recall charges in the same watch, but a crash after claiming and before writing can leave a row undelivered and withheld from automatic replay. Review failed runs before resetting a watch. Snapshot reruns can emit and bill the same notice again. This is a recent watchlist for review, not a complete product safety check.
Frequently asked questions
Which product recall APIs work without an API key?
The reviewed CPSC RestWebServices recall JSON API, Health Canada daily open-data JSON export and GOV.UK Search and Content APIs worked without a key or account on October 24, 2026. The separate SaferProducts OData API requires a key and is not used here.
Can I match recalls to UPC, EAN or GTIN barcodes?
Compare explicitly labelled 8, 12, 13 or 14-digit identifiers as strings padded to GTIN-14. Preserve leading zeros. Many notices omit barcodes, so a barcode-only watch can miss relevant recalls. Confirm the model, lot and affected market in the official notice.
Does EU Safety Gate still provide the historical RAPEX XML feeds?
The reviewed legacy RAPEX XML URLs redirected to an HTML application on October 24, 2026. A current documented machine-readable replacement was not verified. These examples and the linked Actor do not include EU Safety Gate.
Does daily monitoring detect edits to a recall?
A seen-ID comparison detects previously unseen notices. It does not detect an edit to an existing ID. Track a separate content hash or updated timestamp if you also need changes, and preserve the official notice link for review.
Does a zero-result run mean my products are safe?
No. Filters, missing identifiers, bounded source windows or source failures can produce zero results. Check collection status and the original notices. A watchlist match is a review signal, and this workflow does not provide complete global recall coverage.
Sources
Official sources below were checked in the supplied October 24, 2026 review. This guide was reviewed October 25, 2026 using that recorded evidence; no new endpoint checks were made.
- CPSC: recall API documentation and programmer guide (checked October 24, 2026)
- CPSC: verified recall JSON endpoint (checked October 24, 2026)
- CPSC: copyright and recall notice reuse policy (checked October 24, 2026)
- Government of Canada: daily recall dataset and licence assignment (checked October 24, 2026)
- Health Canada: daily English JSON export (checked October 24, 2026)
- Open Government Licence - Canada (checked October 24, 2026)
- GOV.UK: Search API documentation (checked October 24, 2026)
- GOV.UK: Content Store API documentation (checked October 24, 2026)
- GOV.UK: verified Product Recall search (checked October 24, 2026)
- OPSS: product safety alerts, reports and recalls (checked October 24, 2026)
- UK Open Government Licence v3.0 (checked October 24, 2026)
- European Commission: historical RAPEX XML examples (checked October 24, 2026)
- European Commission: current Safety Gate HTML application (checked October 24, 2026)
- SaferProducts: robots policy location (404 observed) (checked October 24, 2026)
- Health Canada: robots policy (checked October 24, 2026)
- GOV.UK: robots policy (checked October 24, 2026)
Tool behavior and limitations follow the supplied Actor README: Product Recall Monitor documentation.