# US CPSC Product Recalls Scraper (Recall Data JSON) (`steadfetch/cpsc-recall-monitor`) Actor

Pull official CPSC product recall notices from the public SaferProducts API: recall dates, products, hazards, remedies and involved organisations. For product-safety monitoring, compliance alerts and building a recall database. Schema-validated JSON, pay per result.

- **URL**: https://apify.com/steadfetch/cpsc-recall-monitor.md
- **Developed by:** [Steadfetch](https://apify.com/steadfetch) (community)
- **Categories:** News, Other
- **Stats:** 2 total users, 1 monthly users, 33.3% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.50 / 1,000 recall delivereds

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

## steadfetch/cpsc-recall-monitor

Monitor U.S. CPSC product recalls from the **official SaferProducts.gov public API**. Every recall is normalized to a stable, English-field schema, validated before it reaches the dataset, and delivered only once per user so a scheduled monitor never bills the same recall twice.

No website scraping, no login, no API key. The source is the keyless REST endpoint `https://www.saferproducts.gov/RestWebServices/Recall` returning JSON.

### What it does

- Queries the official CPSC SaferProducts.gov recall API for an inclusive date range (`since` / `until`).
- Optionally filters recalls by a case-insensitive keyword on the title and description.
- Normalizes each recall into the fields below and validates it against the published dataset schema.
- Deduplicates by `recall_number`: the first run delivers all matching recalls, and later runs deliver only recalls that have never been delivered to that Apify user.
- Excludes `ConsumerContact` and every other personal-contact field. Only product and company data is published.
- Skips malformed records instead of inventing values, and fails the run if any item would violate the output schema.

#### Input

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `since` | string (YYYY-MM-DD) | 30 days before today | Start of the inclusive recall-date range. |
| `until` | string (YYYY-MM-DD) | no upper bound | End of the inclusive recall-date range. |
| `keyword` | string | none | Case-insensitive keyword matched against the title and description. |
| `maxResults` | integer (1-5000) | 1000 | Maximum number of **new** recalls delivered in one run. |
| `monitorMode` | boolean | `true` | Deliver only recalls not delivered before. Set to `false` for a full backfill that ignores monitor state. |

Example input:

```json
{
    "since": "2026-10-01",
    "maxResults": 10
}
```

#### Monitor state

The Actor is a monitor by design. Delivered `recall_number` values are stored in the named key-value store `cpsc-recall-monitor-state`, under the key `seen-<apify-user-id>` (locally `seen-local`). The user ID in the key isolates tenants: one customer's runs can never hide recalls from another customer's runs. Set `monitorMode: false` to skip this state entirely (the Store default input does, so its daily probe always gets recalls).

- First run: delivers every matching recall in the requested range.
- Later runs: delivers only recalls whose `recall_number` has not been delivered before.
- Filtered-out, malformed or failed records are **not** marked as seen, so they remain eligible for a later run.
- Clearing that key resets the monitor and makes the next run a full backfill.

#### Output fields

`recall_number`, `recall_id`, `recall_date`, `title`, `description`, `product_names`, `hazards`, `remedies`, `units`, `injuries`, `manufacturer_countries`, `last_publish_date`, `source_url`, plus the common Steadfetch envelope fields `url`, `title` and `scrapedAt`.

`remedies` contains the short CPSC remedy options (`Refund`, `Repair`, `Replace`, `Dispose`), not the long consumer instructions.

### Example output

```json
{
    "url": "https://cpsc.gov/Recalls/2027/HatsanUSA-Recalls-Blitz-Air-Rifles-Due-to-Risk-of-Serious-Injury-or-Death-from-Projectile-Hazard",
    "title": "HatsanUSA Recalls Blitz Air Rifles Due to Risk of Serious Injury or Death from Projectile Hazard",
    "scrapedAt": "2026-10-20T08:00:00.000Z",
    "recall_number": "27025",
    "recall_id": 11026,
    "recall_date": "2026-10-15",
    "description": "This recall involves all units of the Hatsan Blitz and Blitz 777 pellet shooting air rifles...",
    "product_names": ["Blitz and Blitz 777 air rifles"],
    "hazards": ["The recalled air rifles can fire unexpectedly when releasing the bolt before the air rifle is fully cocked, posing a risk of serious injury or death from projectile hazard."],
    "remedies": ["Replace"],
    "units": "About 16,170",
    "injuries": "The company has received one report of an accidental misfire. No injuries have been reported.",
    "manufacturer_countries": ["Turkey"],
    "last_publish_date": "2026-10-15",
    "source_url": "https://cpsc.gov/Recalls/2027/HatsanUSA-Recalls-Blitz-Air-Rifles-Due-to-Risk-of-Serious-Injury-or-Death-from-Projectile-Hazard"
}
```

### Project structure

```
.actor/actor.json          Actor definition (payPerEvent -> pay_per_event.json, storages.dataset -> dataset_schema.json)
.actor/input_schema.json   INPUT schema (since, until, keyword, maxResults)
.actor/dataset_schema.json Dataset schema: item JSON Schema (fields) + Store card table view (views)
.actor/output_schema.json  Actor output schema: links the default dataset as the results output
.actor/pay_per_event.json  PPE pricing: the two synthetic platform events and their prices
src/main.ts                Entry point: fetch, normalize, dedupe against monitor state, validate, push
src/fetch.ts               HTTP client for the official CPSC JSON API (retries, timeouts, date validation)
src/extract.ts             Pure normalization and filtering (fixture-tested, no network)
src/monitor-state.ts       Cross-run dedupe state (per-user key in a named key-value store)
src/validate-output.ts     Dataset item schema validator (Ajv) shared by the run and the tests
test/                      Offline tests: extraction, HTTP client, state, schemas, pricing, publish gate
```

This folder lives inside the Steadfetch Actors monorepo: the publish gate (`tools/publish-check.mjs`), the shared schema validator (`tools/schema-validator/`) and the GitHub Actions CI are shared across all Actors.

### Pay-per-event pricing

The Actor uses the Apify **pay-per-event (PPE)** model with the two synthetic platform events (`.actor/pay_per_event.json`, referenced from `actor.json` as `payPerEvent`):

| Event | Charged when | Price |
| --- | --- | --- |
| `apify-actor-start` | a run starts (synthetic, charged automatically) | 0.00005 USD per run (Apify-recommended default; includes 5 seconds of free compute per run) |
| `apify-default-dataset-item` | a **new** recall is saved to the dataset (synthetic, charged automatically) | 0.0035 USD per recall = **3.50 USD per 1,000 new recalls** |

There is deliberately **no custom per-item event and no `Actor.charge()` call** in the code: the platform charges `apify-default-dataset-item` for every pushed item, and a custom event on top would bill users twice. Because already-delivered recall numbers are removed before the push, they are never charged again.

Price rationale: direct competitors on the Apify Store charge between 2.7 and 50 USD per 1,000 recalls. 3.50 USD per 1,000 sits inside the Steadfetch 1-10 USD band and below the competitor median.

**Cost per 1,000 recalls (estimated):** under 0.10 USD. The source needs one keyless JSON request per run (no browser, no proxy) plus a few seconds of 256 MB compute to normalize and validate the records. That leaves a gross margin above 97% at the 3.50 USD price.

After `apify push`, apply the prices to the platform with `node ../../../tools/apply-pricing.mjs --apply` (pushing alone does not set them).

### Output schema validation

Nothing at Steadfetch is published without automated output-schema tests:

- `npm test` runs lint, typecheck and Vitest offline: the schema is compiled with Ajv, valid and invalid fixture items behave as expected, and extraction plus the HTTP client run against committed API fixtures (no network, no credit).
- `npm run publish:check` runs the Steadfetch publish gate (schema compilation, PPE price band, English listing, CI presence). It must exit 0 before publishing.
- CI runs both on every push, so a failing schema blocks merge and release.

### FAQ

**Does the Actor need an API key or a paid subscription?**
No. The CPSC SaferProducts.gov recall API is public, free and keyless. No Actor secret, login or account is needed.

**Does it scrape the SaferProducts.gov website?**
No. It calls the official REST endpoint and asks for JSON (`format=json`). It never parses the website HTML.

**Does it collect personal data?**
No. Only product and company recall fields are mapped. `ConsumerContact` and any other personal-contact data are deliberately excluded.

**Why do I get fewer results on the second run?**
That is the monitor behavior: recalls already delivered to your Apify user are deduplicated by `recall_number` and are not charged again. Clear the `seen-<your-user-id>` key in the `cpsc-recall-monitor-state` store to force a full backfill.

**When am I charged?**
Once per run start (`apify-actor-start`) and once per **new** recall saved to the dataset (`apify-default-dataset-item`), as shown in the pricing table. A run that delivers no new recalls only incurs the start event.

**Are the dates inclusive and in which timezone?**
`since` and `until` are inclusive calendar dates interpreted by the CPSC API. Normalized recall dates are published as `YYYY-MM-DD` and `scrapedAt` is UTC.

**Is the output schema stable?**
Yes. New fields are added additively and required fields never change; your downstream code can rely on `recall_number`, `recall_date`, `title` and `source_url`.

**Can I request a new source?**
Yes, open an issue in this repository describing the public page or API you want tracked.

### Maintenance

This Actor is **tested daily**: automated runs keep the source reachable and the output schema valid. Schema changes are announced through repository releases.

# Actor input Schema

## `since` (type: `string`):

Start date (YYYY-MM-DD, inclusive). Defaults to 30 days before today.

## `until` (type: `string`):

End date (YYYY-MM-DD, inclusive). Omit for no upper bound.

## `keyword` (type: `string`):

Optional case-insensitive keyword matched against the recall title and description.

## `maxResults` (type: `integer`):

Maximum number of new recall records to deliver in this run.

## `monitorMode` (type: `boolean`):

When enabled, recall numbers already delivered to this Apify user are not pushed or charged again. Disable for a full backfill. The Store default input disables it so the daily probe always sees recalls.

## Actor input object example

```json
{
  "since": "2026-10-01",
  "until": "2026-10-20",
  "keyword": "laminator",
  "maxResults": 10,
  "monitorMode": true
}
```

# Actor output Schema

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

Dataset items validated against the item schema in dataset_schema.json (fields).

# 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 = {
    "since": "2026-10-01",
    "maxResults": 10,
    "monitorMode": false
};

// Run the Actor and wait for it to finish
const run = await client.actor("steadfetch/cpsc-recall-monitor").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 = {
    "since": "2026-10-01",
    "maxResults": 10,
    "monitorMode": False,
}

# Run the Actor and wait for it to finish
run = client.actor("steadfetch/cpsc-recall-monitor").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 '{
  "since": "2026-10-01",
  "maxResults": 10,
  "monitorMode": false
}' |
apify call steadfetch/cpsc-recall-monitor --silent --output-dataset

```

## MCP server setup

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

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/B7SxSFUAod8nRIpwo/builds/02rNSObuRVUXHlKIe/openapi.json
