# EU Product Recall Compliance Matcher (Safety Gate) (`goobertje/eu-product-recall-compliance-matcher`) Actor

Audit products by brand, model, EAN or CAS/EC number against official EU Safety Gate recall alerts, or stream the latest alerts with delta tracking. Returns alert details, match type and confidence. SVHC matching needs your own ECHA export.

- **URL**: https://apify.com/goobertje/eu-product-recall-compliance-matcher.md
- **Developed by:** [goober](https://apify.com/goobertje) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 item auditeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## EU Safety Gate recall checker: audit your product list against RAPEX alerts

Find out whether any product in your catalogue appears in an **EU Safety Gate (formerly RAPEX) dangerous-products alert**. You send a list of items (brand, model number, EAN barcode, product name); you get one result row per item with the matching alerts, how they matched, a confidence score and a link to the official alert. Built for e-commerce sellers, marketplace compliance teams, importers and distributors.

There is also a **feed mode**: the newest Safety Gate alerts since a date, optionally only those you have not received before.

**Important limit:** the ECHA REACH Candidate List (SVHC) is **not built in**. ECHA blocks automated access and its legal notice forbids copying its database. You can paste your own ECHA export to get SVHC matches (see below).

### Why use this Actor

- Official source: the European Commission's Safety Gate weekly-report XML downloads. No page scraping, no browser, no proxies.
- Exact matching on barcode (EAN/UPC/GTIN) and model number, plus fuzzy brand + product matching, each with a documented confidence.
- Honest "no match found" rows, so a clean result is visible and auditable (`coverage` shows what was searched).
- Feed mode with delta: only alerts not delivered before.
- Invalid or unusable input rows are never billed.

### Quick start

Click **Start** with the prefilled input (about 30 seconds, 3 items):

```json
{
  "mode": "audit",
  "items": [
    {"reference": "demo-1", "brand": "Goki", "productName": "Wooden pull-along ship", "ean": "4013594548496"},
    {"reference": "demo-2", "brand": "Wicked Costumes", "productName": "Dinosaur toy costume"},
    {"reference": "demo-3", "brand": "Acme Gizmo Co", "ean": "4006381333931"}
  ],
  "maxItems": 3,
  "lookbackMonths": 6
}
```

You will see three dataset rows: an exact barcode match (confidence 1.0), a fuzzy brand + product match (0.7) and a `no_match_found` row.

### What you get

One dataset row per audited item.

| Field | Meaning |
|---|---|
| `status` | `match_found`, `no_match_found`, `skipped_unauditable` or `invalid_input` |
| `matchCount`, `bestConfidence` | number of matches (all) and the highest confidence |
| `safetyGateMatches[]` | `matchType`, `confidence`, `confidenceLevel`, `matchedOn`, and the full `alert` |
| `alert` | alert number, link, date, category, brand, model, batch, barcode, risk type, hazard, measures, countries, risk level |
| `svhcMatches[]` | matches against the Candidate List rows you supplied |
| `warnings`, `coverage` | e.g. EAN ignored; reports and alerts searched |

Real sample from a live run on 2026-10-18 (full file: `sample_output.json`; trimmed):

```json
{
  "mode": "audit",
  "reference": "demo-1",
  "input": {"reference": "demo-1", "brand": "Goki", "productName": "Wooden pull-along ship", "ean": "4013594548496"},
  "status": "match_found",
  "charged": true,
  "matchCount": 1,
  "bestConfidence": 1.0,
  "safetyGateMatches": [
    {
      "matchType": "barcode_exact",
      "confidence": 1.0,
      "confidenceLevel": "exact",
      "matchedOn": ["barcode", "brand", "product"],
      "alert": {
        "alertNumber": "OR/00117/26",
        "alertUrl": "https://ec.europa.eu/safety-gate-alerts/screen/webReport/alertDetail/10119156",
        "alertDate": "2026-10-16",
        "reportWeek": "2026-W41",
        "productCategory": "Toys",
        "productType": "Wooden toy",
        "brand": "Goki",
        "productName": "Pull-along ship with bead maze",
        "modelOrType": "54849",
        "batchNumber": "124359",
        "barcode": "4013594548496",
        "riskType": "Strangulation",
        "hazardDescription": "The product has a long cord. The cord may become wrapped around the child's neck ...",
        "measuresTaken": "Type of economic operator taking notified measure(s): Distributor | Category of measure(s): Recall of the product from end users | Date of entry into force: Unknown",
        "recallUrl": "https://www.zakladi.si/opoziv-goki-54849",
        "notifyingCountry": "Slovenia",
        "countryOfOrigin": "Unknown",
        "riskLevel": "Other risk levels"
      }
    }
  ],
  "coverage": {"reportsChecked": 26, "alertsIndexed": 1814, "earliestReportDate": "2026-04-24", "latestReportDate": "2026-10-16"}
}
```

Feed rows contain the alert fields directly plus `mode: "feed"`, `isNew` (delta mode) and `checkedAt`. Hazard texts and measures are passed through as published. Product photos are not included.

### Input fields

| Field | Meaning |
|---|---|
| `mode` | `audit` (default) or `feed` |
| `items` | objects with any of `brand`, `productName`, `category`, `modelNumber`, `ean` (aliases `gtin`, `barcode`, `upc`), `casNumber`, `ecNumber`, `substanceName`, `reference` |
| `maxItems` | caps audited items per run (cost cap), default 100 |
| `lookbackMonths` | months of weekly reports to search, default 6, maximum 36 |
| `minConfidence` | default 0.5 (hides weak brand-only hits) |
| `maxMatchesPerItem` | default 10 (`matchCount` still shows the total) |
| `svhcCandidateList` | optional, your own ECHA rows |
| `sinceDate`, `maxAlerts`, `deltaMode`, `deltaStoreName` | feed mode: date window, cap, delta state store |
| `cacheStoreName` | report cache store; empty disables caching |

### Matching rules and confidence

Confidence is a fixed rule score, not a statistical probability.

| Match type | Confidence | Rule |
|---|---|---|
| `barcode_exact` | 1.00 (exact) | Item EAN/UPC/GTIN equals a barcode published in the alert. Only valid GTIN lengths (8, 12, 13, 14) with a correct GS1 check digit are used on both sides, compared left-padded to 14 digits, so EAN-13, UPC-A and GTIN-14 forms match. Float-typed cells (`4006381333931.0`), spaces and dashes are cleaned; 7 to 11 digit codes are left-padded; other lengths or bad check digits are ignored with a warning. |
| `model_exact` | 0.95 (exact) or 0.70 | Normalised model number equals a model in the alert. It must mix letters and digits, or be at least 6 characters, and not be a placeholder such as "unknown". 0.95 if the brand also matches, 0.70 if the brand is missing on either side. If both brands are known and differ, the hit is discarded. |
| `brand_product_fuzzy` | 0.50 to 0.70 (fuzzy) | All brand words appear in the alert's brand (minus 0.10 if only in its product text). Legal-form and placeholder words (co, inc, de, eu, home, max, unknown, generic, ...) are ignored; the brand needs 4+ characters or must equal the alert brand. At least 2 item keywords (and at least half) must appear in the alert's category, product type or name. Confidence = 0.50 + 0.20 x overlap. Only a trailing plural "s" is stripped, so irregular plurals and other languages are not matched. |
| `brand_only` | 0.30 (weak) | Brand matches, item has no product keywords. Hidden at the default `minConfidence`. |

An alert matching by several rules is listed once, with the highest confidence and all evidence in `matchedOn`. Fuzzy matches are leads, not findings: open the `alertUrl` and compare the product.

### ECHA REACH Candidate List (SVHC): bring your own list

Not built in, deliberately:

- `echa.europa.eu` answers automated requests (even `robots.txt`) with a JavaScript bot challenge (HTTP 403). The Actor never tries to get around bot protection.
- ECHA's legal notice (chem.echa.europa.eu) forbids replicating "whole or substantial parts of databases" without written permission, reserves the right to block systematic automated collection, and says CAS information belongs to the American Chemical Society.

Instead, paste the Candidate List rows **you exported from ECHA yourself** into `svhcCandidateList`. Headers are normalised (lower-case, punctuation removed), so the real ECHA headers work unchanged: `Substance name`, `EC / List no.`, `CAS no.`, `Reason for inclusion`, `Date of inclusion`. Also accepted: `substanceName`, `name`, `casNumber`, `cas`, `ecNumber`, `ec`, `reasonForInclusion`, `dateOfInclusion`. Cells with several values (`a; b`, `a / b`) are split. Items with `casNumber`, `ecNumber` or `substanceName` are matched (CAS exact 1.0, EC exact 1.0, exact normalised name 0.9). Invalid CAS numbers are ignored with a warning. A supplied list with no usable row fails the run with a clear message (nothing billed).

### Pricing (pay per event)

Event `item-audited`, **$3.00 per 1,000 events** (set in the Apify Console, not in this repository).

- Audit mode: one event per audited item, whether or not a match was found. A verified "no match" is what you pay for.
- Feed mode: one event per alert returned.
- **Free:** `skipped_unauditable` and `invalid_input` rows, and any run where the weekly reports could not be fully downloaded (the run fails before the first row is written).
- `maxItems` / `maxAlerts` and your maximum cost per run cap the spend; the Actor stops at the limit.
- Example: a 2,000-product catalogue audited once a week costs 2,000 x $0.003 = $6.00 per run.

### Use with AI agents (MCP, Claude, Cursor, n8n, Make)

The Actor can be called by AI agents through the Apify MCP server:

```json
{ "mcpServers": { "apify": { "url": "https://mcp.apify.com?tools=goobertje/eu-product-recall-compliance-matcher" } } }
```

Sign in with OAuth in the browser on first use. Example prompts and the input they map to:

1. "Is the Goki ship with EAN 4013594548496 in a Safety Gate alert?" ->
   `{"mode":"audit","items":[{"brand":"Goki","ean":"4013594548496"}],"maxItems":1}`
2. "Check these two supplier models for recalls over the last two years: Wicked Costumes KA-4429 and Faburo AGFATT7013." ->
   `{"mode":"audit","items":[{"brand":"Wicked Costumes","modelNumber":"KA-4429"},{"brand":"Faburo","modelNumber":"AGFATT7013"}],"lookbackMonths":24,"maxItems":2}`
3. "Show me the 20 newest Safety Gate alerts since 1 October that I have not seen yet." ->
   `{"mode":"feed","sinceDate":"2026-10-01","maxAlerts":20,"deltaMode":true,"deltaStoreName":"my-agent-feed"}`

In n8n, Make or Zapier, use the Apify node or module, pick this Actor and paste the same JSON.

### Use cases

- Weekly catalogue check for marketplace sellers before and after sourcing.
- Supplier due diligence on brands, EANs and model numbers for importers and distributors.
- Product-safety alert feed into a compliance or ticketing system (Safety Gate / RAPEX monitoring).
- Evidence trail: a dated "no match found" row with the searched period.

### FAQ

**Where does the data come from, and is it allowed?** From the European Commission's official Safety Gate weekly-report XML downloads, reusable under CC BY 4.0 (Decision 2011/833/EU). `robots.txt` does not disallow these paths. Each row carries the source line.

**How fresh is it?** Alerts appear in the weekly report published on Fridays. `alertDate` is that report's publication date, so alerts of the current week show up with the next Friday report.

**How much does a typical run cost?** The prefilled 3-item run costs $0.009. 1,000 items cost $3.00.

**What if the source blocks or changes?** The Actor backs off, then stops on repeated 403/429 and fails clearly without billing. If the XML structure changes, the run fails instead of returning guesses.

**Can I schedule it or get only new items?** Yes. Schedule weekly (Fridays). Feed mode with `deltaMode` returns only alerts not delivered before.

**What is not included?** ECHA SVHC data (bring your own), product photos, and anything not published as a Safety Gate alert.

### Limits and honest caveats

- **Barcodes are only sometimes published.** In a sample week (2026-W41) about half of the alerts had a barcode, about 80% a model/type number, about 30% a batch number and about 60% a brand. An EAN match works only if the authority published the barcode. "No match found" means no matching alert in the searched period was found by these rules. It does not mean the product is safe or compliant.
- Only published Safety Gate alerts are covered; products can be dangerous without an alert.
- Many alerts have no brand or only partial model data. Brand spelling variants are not covered beyond punctuation, accents and case.
- Searched history is `lookbackMonths` (maximum 36). Cold-cache runtime is about 1.1 seconds per weekly report: 6 months about 30 seconds, 36 months (about 157 reports) about 5 minutes. Parsed reports are cached in a named store for up to 90 days (reports from the last 4 weeks refresh daily because they can be corrected).
- Alerts are de-duplicated by alert number only (the newest report's copy wins).

### Legal

Safety Gate data is published by the European Commission and reusable under CC BY 4.0 (Commission reuse policy, Decision 2011/833/EU), with attribution: "Source: European Commission, Safety Gate". The Actor uses plain HTTPS to the official download endpoints with an identifying User-Agent, at most one request per second. Results are informational, not legal advice; always verify against the official alert.

Not affiliated with, endorsed by or sponsored by the European Commission, the Safety Gate / RAPEX network or ECHA.

Found a problem or want a field added? Open an issue on the Actor page.

# Actor input Schema

## `mode` (type: `string`):

audit: check each item in 'items' against Safety Gate alerts and return one result row per item. feed: return the newest Safety Gate alerts since a date (optionally only those not delivered before). Example: "audit".

## `items` (type: `array`):

List of objects, each with any of: brand, productName, category, modelNumber, ean (EAN/GTIN/UPC barcode), casNumber, ecNumber, substanceName and an optional reference that is echoed back. Barcode and model matches are exact; brand + product matches are fuzzy. An item needs at least a brand, a barcode or a model number of 4+ characters (or CAS/EC/substance together with svhcCandidateList). Example: \[{"brand":"Goki","ean":"4013594548496"}].

## `maxItems` (type: `integer`):

Maximum number of input items audited per run (each audited item is one billed event, so this caps your cost). Items beyond the cap are ignored. Example: 100.

## `lookbackMonths` (type: `integer`):

How many calendar months of weekly Safety Gate reports to search, counted back from today. More months means more downloads (about 1 second and 190 KB per weekly report; 6 months is about 27 reports, 36 months is about 157 reports and takes roughly 5 minutes the first time). Reports are cached in a named store, so later runs only download new weeks. Example: 24.

## `minConfidence` (type: `number`):

Only matches with at least this confidence (0 to 1) are returned. Rules: barcode exact = 1.0, model exact = 0.95 (brand also matches) or 0.80, brand + product keywords fuzzy = 0.50 to 0.70, brand only = 0.30. The default 0.5 hides brand-only hits. Example: 0.5.

## `maxMatchesPerItem` (type: `integer`):

Maximum number of Safety Gate alerts listed per audited item (best confidence first, then newest). The total number of matches is still reported in matchCount. Example: 10.

## `svhcCandidateList` (type: `array`):

This Actor ships no ECHA data. If you also want REACH SVHC checks, paste the Candidate List rows you exported from ECHA yourself as objects; the real ECHA headers "Substance name", "EC / List no.", "CAS no.", "Reason for inclusion", "Date of inclusion" work as-is, as do substanceName, casNumber, ecNumber. Multi-value cells such as "a; b" are split. A supplied list with no usable row fails the run. Items with casNumber, ecNumber or substanceName are then matched against these rows (CAS exact 1.0, EC exact 1.0, name exact 0.9). Example: \[{"substanceName":"Bisphenol A","casNumber":"80-05-7","ecNumber":"201-245-8"}].

## `sinceDate` (type: `string`):

Feed mode only: return alerts from weekly reports published on or after this date (YYYY-MM-DD, UTC). Defaults to the last 30 days. Safety Gate publishes a weekly report every Friday, so an alert's date is its report's publication date. Example: "2026-10-01".

## `maxAlerts` (type: `integer`):

Feed mode only: maximum number of alerts returned, newest first (each returned alert is one billed event). Example: 50.

## `deltaMode` (type: `boolean`):

Feed mode only: remember delivered alert numbers in the named store below and return only alerts that were not delivered by an earlier delta run. The first run returns the newest alerts since the start date. Example: true.

## `deltaStoreName` (type: `string`):

Name of the named Key-Value Store that remembers delivered alerts when delta mode is on (lowercase letters, digits and hyphens). Use a different name per feed you run. Example: "my-toys-feed".

## `cacheStoreName` (type: `string`):

Name of the named Key-Value Store used to cache parsed weekly reports between runs, which makes repeat runs much faster. Leave empty to disable caching. Example: "eu-recall-matcher-cache".

## `requestDelaySecs` (type: `number`):

Pause between requests to ec.europa.eu. Minimum and default 1 second; the Actor never goes faster. Example: 1.

## Actor input object example

```json
{
  "mode": "audit",
  "items": [
    {
      "reference": "demo-1",
      "brand": "Goki",
      "productName": "Wooden pull-along ship",
      "ean": "4013594548496"
    },
    {
      "reference": "demo-2",
      "brand": "Wicked Costumes",
      "productName": "Dinosaur toy costume"
    },
    {
      "reference": "demo-3",
      "brand": "Acme Gizmo Co",
      "ean": "4006381333931"
    }
  ],
  "maxItems": 3,
  "lookbackMonths": 6,
  "minConfidence": 0.5,
  "maxMatchesPerItem": 10,
  "svhcCandidateList": [],
  "maxAlerts": 50,
  "deltaMode": false,
  "deltaStoreName": "eu-recall-feed-state",
  "cacheStoreName": "eu-recall-matcher-cache",
  "requestDelaySecs": 1
}
```

# Actor output Schema

## `results` (type: `string`):

Audit rows with Safety Gate matches and confidence, or feed alerts.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "items": [
        {
            "reference": "demo-1",
            "brand": "Goki",
            "productName": "Wooden pull-along ship",
            "ean": "4013594548496"
        },
        {
            "reference": "demo-2",
            "brand": "Wicked Costumes",
            "productName": "Dinosaur toy costume"
        },
        {
            "reference": "demo-3",
            "brand": "Acme Gizmo Co",
            "ean": "4006381333931"
        }
    ],
    "maxItems": 3,
    "lookbackMonths": 6
};

// Run the Actor and wait for it to finish
const run = await client.actor("goobertje/eu-product-recall-compliance-matcher").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "items": [
        {
            "reference": "demo-1",
            "brand": "Goki",
            "productName": "Wooden pull-along ship",
            "ean": "4013594548496",
        },
        {
            "reference": "demo-2",
            "brand": "Wicked Costumes",
            "productName": "Dinosaur toy costume",
        },
        {
            "reference": "demo-3",
            "brand": "Acme Gizmo Co",
            "ean": "4006381333931",
        },
    ],
    "maxItems": 3,
    "lookbackMonths": 6,
}

# Run the Actor and wait for it to finish
run = client.actor("goobertje/eu-product-recall-compliance-matcher").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "items": [
    {
      "reference": "demo-1",
      "brand": "Goki",
      "productName": "Wooden pull-along ship",
      "ean": "4013594548496"
    },
    {
      "reference": "demo-2",
      "brand": "Wicked Costumes",
      "productName": "Dinosaur toy costume"
    },
    {
      "reference": "demo-3",
      "brand": "Acme Gizmo Co",
      "ean": "4006381333931"
    }
  ],
  "maxItems": 3,
  "lookbackMonths": 6
}' |
apify call goobertje/eu-product-recall-compliance-matcher --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,goobertje/eu-product-recall-compliance-matcher"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/KRmzd4LLHz7AJIms8/builds/sZq88sjgt5KhUPtUR/openapi.json
