# EU Safety Gate recall alerts by GTIN, brand and category (`kittiwake/eu-recall-watchlist`) Actor

Every product-safety alert the EU Safety Gate publishes, normalised into one row per case, filterable by category, risk level and notifying country, and matchable against your own GTIN or brand watchlist.

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

## Pricing

from $2.00 / 1,000 recall alert returneds

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## EU Safety Gate recall alerts by GTIN, brand and category

Every product-safety alert the **EU Safety Gate** publishes, turned into clean rows you can filter,
schedule and match against your own catalogue.

The European Commission publishes a Safety Gate report **every Friday**: dangerous non-food products
found in the EU, what the hazard is, and what an authority ordered or a company did about it. It is
public — but it arrives as an XML export with run-together fields and dd/mm/yyyy dates, which is not
something you can drop into a spreadsheet or a database.

This Actor reads it and gives you one tidy row per alert, with the messy parts already parsed.

### What it is for

- **Marketplaces and retailers** — check new alerts against the GTINs you list, every week, and get
  the hits and nothing else.
- **Importers, distributors and manufacturers** — watch your own brands and your competitors'.
- **Compliance, QA and product-safety teams** — a filtered feed by category, risk level or notifying
  country, into your own systems.
- **Analysts** — the whole archive back to 2005 is reachable, one weekly report at a time.

### What makes it different from just downloading the XML

| | raw Safety Gate export | this Actor |
|---|---|---|
| `measures` | one run-together string: `…were ordered: ManufacturerCategory of measure(s): Recall of the product from end usersDate of entry into force: 14/08/2026` | a list of `{economicOperator, category, inForceFrom, ordered}` — and `ordered` tells you **mandatory vs voluntary** |
| dates | `14/08/2026`, or the literal `Unknown` | ISO `2026-08-14`, or `null`. Never a wrong date |
| barcodes | as printed, sometimes empty | digits only, plus a **GTIN check-digit result** — and `null` when it isn't checkable, so you can tell "invalid" from "unknown" |
| online traders | `Other(www.a.dewww.b.de) / ` | `[{platform: "Other", reference: "www.a.de"}, {platform: "Other", reference: "www.b.de"}]` |
| your catalogue | you write the matching | **`watchBarcodes` / `watchBrands`** — matched rows carry `watchlistMatch`, and `onlyWatchlistMatches` returns just those |
| duplicates | a case can reappear in a later report | deduplicated by case number within a run |
| attribution | you have to add it yourself | the Commission-required credit line is on every row (`source`) |

The raw `measures` string is kept in `measuresRaw`, so you can always audit the parse.

### Quick start

**Everything from the last 4 weeks:**

```json
{ "weeks": 4 }
```

**Only serious risks in toys and childcare, notified by Germany or France:**

```json
{
  "weeks": 8,
  "categories": ["Toys", "Childcare articles and children's equipment"],
  "riskLevels": ["Serious risk"],
  "notifyingCountries": ["Germany", "France"]
}
```

**Watch your own catalogue and return only the hits** — the way most people run this on a schedule:

```json
{
  "sinceDate": "2026-08-01",
  "watchBarcodes": ["4067683465416", "5901234123457"],
  "watchBrands": ["JOYHUT", "Acme Toys"],
  "onlyWatchlistMatches": true
}
```

**Run it weekly.** Set `sinceDate` to the date of your last run, or leave `weeks: 1` and schedule it
for Saturday — the Commission publishes on Friday.

### Input

| field | type | default | what it does |
|---|---|---|---|
| `weeks` | integer | `4` | How many of the most recent weekly reports to read |
| `sinceDate` | `YYYY-MM-DD` | — | Read every report published on or after this date. Overrides `weeks` |
| `categories` | string\[] | all | e.g. `Toys`, `Cosmetics`, `Electrical appliances and equipment` |
| `riskLevels` | string\[] | all | e.g. `Serious risk` |
| `notifyingCountries` | string\[] | all | e.g. `Germany` |
| `alertTypes` | string\[] | both | `Consumer` or `Professional` |
| `keywords` | string\[] | — | Free text across product, brand, description, hazard, category, risk type and model |
| `watchBarcodes` | string\[] | — | Your GTINs. Punctuation ignored |
| `watchBrands` | string\[] | — | Your brands. Case-insensitive, matched both ways |
| `onlyWatchlistMatches` | boolean | `false` | Return only watchlist hits |
| `includePictures` | boolean | `false` | Add product photo URLs |
| `maxAlerts` | integer | — | Stop after N alerts |
| `requestDelayMs` | integer | `1000` | Pause between report requests |

All filters are case-insensitive, and an empty list means "no filter on that field".

### Output

One row per alert:

```json
{
  "caseNumber": "SR/02365/26",
  "alertUrl": "https://ec.europa.eu/safety-gate-alerts/screen/webReport/alertDetail/10099536",
  "reportReference": "Report-2026-35",
  "reportDate": "2026-09-04",
  "category": "Toys",
  "product": "Toy gun set",
  "brand": null,
  "productName": "City Super Police Set",
  "modelNumber": "No. 333",
  "barcode": "4067683465416",
  "barcodeChecksumValid": true,
  "riskType": "Choking",
  "riskLevel": "Serious risk",
  "danger": "The toy has small parts (the suction cups) that can easily detach. …",
  "description": "Toy gun set for shooting suction cup darts, with three darts.",
  "measures": [
    { "economicOperator": "Manufacturer",
      "category": "Recall of the product from end users",
      "inForceFrom": "2026-08-14",
      "ordered": true }
  ],
  "notifyingCountry": "Germany",
  "countryOfOrigin": "People's Republic of China",
  "alertType": "Consumer",
  "recallUrl": "https://www.landahl.de/produktrueckruf-city-super-police-set-46541",
  "onlineTraders": [
    { "platform": "Other", "reference": "www.jeschke-kg.de" },
    { "platform": "Other", "reference": "www.landahl.de" }
  ],
  "source": "Alerts from the Rapid Alert System for dangerous non-food products, published free of charge on the Safety Gate website (https://ec.europa.eu/safety-gate-alerts) © European Union, 2005 – 2026"
}
```

An empty field in the source becomes `null`, never `""` — so a missing barcode and a blank one are
the same thing, and `barcodeChecksumValid` is `null` rather than `false` when there is nothing to
check.

### What you pay for

| event | price | when |
|---|---|---|
| **Recall alert returned** | $0.002 | one alert written to the dataset, after it is saved |
| Weekly report read | $0.01 | one weekly report fetched and parsed — only when both succeed |
| Watchlist match | $0.05 | an alert matched one of your barcodes or brands. Zero if you supply no watchlist |

Nothing is charged for a request that failed, and the run stops when it reaches the spending limit
you set — the rows already saved are yours, and nothing beyond the limit is billed.

### What this Actor does not do

- **No personal data.** The source has none: it describes products, hazards and the measures taken.
  There is no name, contact, address or user content in the feed or in the output. Brands, shop
  domains and a company's own recall page are business identifiers and are passed through as
  published.
- **No logins, no proxies, no CAPTCHA solving.** It reads a public, unauthenticated XML export from
  `ec.europa.eu` — the same file the portal's own **Download** button gives you — with one
  identifying User-Agent and a pause between requests. If the Commission ever refuses a request,
  this Actor fails loudly rather than trying again differently.
- **No guessing.** An unparseable date is `null`, not a plausible-looking wrong date. An
  unrecognised `measures` shape is kept whole rather than silently dropped.

### Source, accuracy and attribution

Data comes from the **EU Safety Gate** rapid alert system, `ec.europa.eu/safety-gate-alerts`. This
Actor reformats what the Commission publishes and adds nothing to it. Where an alert is incomplete,
it is incomplete here too — the `alertUrl` on every row takes you to the official page, which is
always the authority.

Reports are published weekly, on Fridays. Nothing here is legal advice about your obligations.

Reuse of Safety Gate alerts is authorised by the Commission's Safety Gate disclaimer (reuse policy
under Commission Decision 2011/833/EU), **provided the original meaning is not distorted and the
source is acknowledged**. The required attribution is reproduced verbatim below and on every row of
this Actor's output (`source` field), so it travels with the data even if you filter or re-export
individual rows:

> *"Alerts from the Rapid Alert System for dangerous non-food products, published free of charge on
> the Safety Gate website (https://ec.europa.eu/safety-gate-alerts) © European Union, 2005 – 2026"*

### Support

Use the **Issues** tab on this Actor. Include the run id and the input you used.

# Actor input Schema

## `weeks` (type: `integer`):

How many of the most recent weekly reports to read. The Safety Gate publishes one report every Friday. Ignored when 'Published since' is set.

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

Read every weekly report published on or after this date. Overrides 'Weeks to read'. Use this for an incremental daily or weekly schedule: set it to the date of your last run.

## `categories` (type: `array`):

Keep only these Safety Gate categories, e.g. Toys, Electrical appliances and equipment, Cosmetics. Leave empty for all.

## `riskLevels` (type: `array`):

Keep only these risk levels, e.g. 'Serious risk'. Leave empty for all.

## `notifyingCountries` (type: `array`):

Keep only alerts notified by these countries, e.g. Germany, France. Leave empty for all.

## `alertTypes` (type: `array`):

Keep only these alert types: Consumer or Professional. Leave empty for both.

## `keywords` (type: `array`):

Keep only alerts whose product, brand, description, hazard, category, risk type or model number contains any of these words.

## `watchBarcodes` (type: `array`):

Your own product barcodes. An alert whose barcode matches one of these is flagged with watchlistMatch. Non-digits are ignored, so 4067683465416 and 4-067683-465416 are the same.

## `watchBrands` (type: `array`):

Brand names you stock or manufacture. Matched case-insensitively against the alert's brand and product name.

## `onlyWatchlistMatches` (type: `boolean`):

Return only the alerts that hit your watchlist. Requires at least one barcode or brand above.

## `includePictures` (type: `boolean`):

Add the Safety Gate product photo URLs to each row.

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

Stop after this many alerts. Leave empty for no limit.

## `requestDelayMs` (type: `integer`):

Pause between weekly-report requests. The default is deliberately polite; lowering it is not recommended.

## Actor input object example

```json
{
  "weeks": 4,
  "onlyWatchlistMatches": false,
  "includePictures": false,
  "requestDelayMs": 1000
}
```

# Actor output Schema

## `alerts` (type: `string`):

No description

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("kittiwake/eu-recall-watchlist").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("kittiwake/eu-recall-watchlist").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 '{}' |
apify call kittiwake/eu-recall-watchlist --silent --output-dataset

```

## MCP server setup

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

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/FLDwNftt4M1Dgf5o1/builds/jBd3sz1deb2nmq35u/openapi.json
