# CPSC Recall Matcher: Models, UPCs & Manufacturers (`felixgoldwin/cpsc-recall-catalog-matcher`) Actor

Compare your product catalog with official CPSC recalls using model, UPC and manufacturer evidence. Get recall candidates, source links and changes against a supplied snapshot. Includes no-match results. Candidates require human review; no-match is not a safety clearance.

- **URL**: https://apify.com/felixgoldwin/cpsc-recall-catalog-matcher.md
- **Developed by:** [Felix GoldWin](https://apify.com/felixgoldwin) (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 catalog product evaluations

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

## CPSC Recall-to-Catalog Matcher — Manufacturer, Model, UPC

Compare a nonsecret product catalog with official U.S. Consumer Product Safety Commission (CPSC) recalls in an explicit date window. Receive published recall links, dates, manufacturers, product labels, model and UPC evidence, conservative confidence labels, and exact changes from a caller-supplied earlier snapshot.

**Every candidate requires human review. A nonmatch does not mean a product is safe.** This Actor does not issue safety, legal, medical or regulatory decisions, and does not determine whether a recall is resolved or withdrawn.

### What it supports

- One fixed public source: `https://www.saferproducts.gov/RestWebServices/Recall`, linked by the official [CPSC API page](https://www.cpsc.gov/Recalls/CPSC-Recalls-Application-Program-Interface-API-Information).
- Documented `format=json`, `RecallDateStart`, and `RecallDateEnd` query parameters. The [official programmer's guide](https://www.cpsc.gov/s3fs-public/pdfs/blk_pdf_CPSC-Recalls-Retrieval-Web-Services-Programmers-Guide_3-25-2015.pdf) documents no pagination cursor or reliable total-count contract. This Actor never invents page parameters.
- Date partitions overlap at their endpoints; one extra end boundary is fetched, then records are filtered to the inclusive requested calendar days. Identical recalls are deduplicated by RecallID. Conflicting copies fail the scan rather than silently overwriting evidence.
- Exact normalized UPC matches preserve leading zeros and identifier length. Only spaces and hyphens are removed. UPC/EAN/GTIN lengths of 8, 12, 13 and 14 are accepted; different padded lengths are not conflated and check digits are not certified.
- Exact whole structured model fields retain punctuation. When structured models are missing, a whole literal model token in published product names/descriptions or recall description becomes lower-confidence evidence. No model ranges, tables, aliases, inferred brands or semantic matches are invented.
- Manufacturer matching is whole-string, case-insensitive, whitespace-normalized equality. Manufacturer locations and legal suffixes are retained; callers should use the exact published organization label when appropriate.

The source frequently leaves structured model/UPC fields empty. October 2026 live samples demonstrated this. Importer, distributor, brand, and manufacturer are distinct roles; an importer is never substituted for a missing manufacturer.

### Input

See [examples/input.json](examples/input.json). Required fields are `recallDateStart`, `recallDateEnd` (YYYY-MM-DD) and `catalog`. Each catalog item has a unique `productId`, optional `manufacturer`, and at least one identifier in `models` or `upcs`. Both identifier lists contain strings. Do not supply customer, health, contact, login or other sensitive information. Extra catalog fields are rejected.

```json
{
  "recallDateStart": "2026-10-01",
  "recallDateEnd": "2026-10-02",
  "catalog": [{"productId": "my-widget", "manufacturer": "Example Co.", "models": ["AB-12"], "upcs": ["001234567890"]}],
  "maxResults": 1,
  "maxBudgetUsd": 0.02
}
```

This illustrative catalog is fictional. The complete example file uses a published October 2026 recall model solely to demonstrate a live candidate; it does not represent an actual customer inventory.

### Output and interpretation

The dataset contains **one completed catalog-product evaluation per input product**, including products for which no candidate was found among the scanned recalls. Each row contains `key`, `evidence.catalogProduct`, `evidence.matchStatus`, `evidence.candidates`, `completeScan`, `scopeKey`, `observedAt`, `humanReviewRequired: true`, `safetyConclusion: null` and `change`.

Candidates include the exact selected source fields, source recall URL, model/UPC/manufacturer evidence, confidence (`high`, `medium`, `low`), and ambiguity reasons. Confidence is a deterministic evidence tier, not a probability or recall applicability verdict. Exact UPC or structured model plus exact manufacturer can be high confidence, but conflicting manufacturers, reused identifiers or recall-level UPC ambiguity reduce it. Multiple products/manufacturers prevent assuming a particular manufacturer belongs to a particular product. Multiple recall candidates remain visible.

`OUTPUT` in the default key-value store contains:

- `diagnostics`: requested catalog/date scope, request URLs, timestamps, raw-response SHA256 hashes, response sizes, retries, partition coverage, byte/request/result budgets, record counts and stop reasons.
- `diagnostics.queryComplete`: every requested API partition was processed without a detected failure or source cap.
- `diagnostics.completeScan`: query processing **and** every catalog evaluation finished within all bounds. This is bounded query completion, not a guarantee the upstream service returned every recall. `globalRecallCoverage` always remains unverified because the API publishes no total/exhaustiveness contract.
- `comparison`: exact normalized `before`/`after` evidence only when the current scan is complete and `previousSnapshot` has the same catalog and dates. Observed-at times and response hashes do not create false changes.
- `previousSnapshot`: reusable only for a complete scan. Paste this object into a subsequent input for the exact same catalog and date window. No global or cross-user state is used. Caller-supplied snapshots are structurally validated, but their historical source authenticity cannot be guaranteed.
- `partialObservations`: bounded diagnostic evidence on incomplete scans. No dataset rows are emitted or custom result events charged for an incomplete scan, and its snapshot is null.

Source disappearance or a lost candidate is never reported as recall withdrawal, successful remedy or product safety. Date scope uses original `RecallDate`; amendments to older recalls outside the window are not monitored. Run a separate wider date window to review older recalls. Changing the date window or catalog intentionally creates a different snapshot scope.

### Bounds

| Input | Default | Hard maximum |
|---|---:|---:|
| Catalog products | required | 250 |
| Models / UPCs per product | 0 / 0 | 10 each |
| Date span | explicit | 366 calendar days |
| Partition days | 7 | 31 |
| Unique recalls | 500 | 1,000 |
| Catalog result rows | 100 | 250 |
| Candidates per product | 25 | 25 |
| HTTP attempts, including retries | 30 | 100 |
| Retries per partition request | 2 | 3 |
| Scan seconds | 90 | 300 |
| Single response bytes | 3 MB | 10 MB |
| Aggregate response bytes | 12 MB | 20 MB |
| Input / reusable snapshot bytes | 1.5 MB / 1 MB | fixed |
| Event budget USD | 0.25 | 20 |

A bounded scan that reaches a cap is incomplete. Increase relevant limits within the hard maxima or narrow the date window. Retry-After is respected when it fits within remaining scan time; longer waits stop the run. Access denials and redirects stop the scan without bypass. Only fixed official API URLs are fetched. Recall links in results are evidence references and are never crawled. No proxies, paid services, login or personal-contact harvesting are used.

### Local use

Requires Node.js 22 or newer.

```sh
npm ci --ignore-scripts
npm test
npm run validate:schema
npm run smoke
```

`smoke` performs a small public read-only live request and writes `artifacts/live-smoke.json` and example output. To run the actual SDK entrypoint locally, place an example input at `storage/key_value_stores/default/INPUT.json`, then run `npm start`. Apify account credentials are unnecessary for local execution. Do not commit local storage or credentials.

### Pricing and billing

Approved listing price: **$0.002 per completed catalog-product evaluation** (one `catalog-product` event per emitted dataset row), plus **$0.005 automatic Actor start per memory unit up to 1 GB**. The approved custom start rate differs from Apify's published default; it must be deliberately configured by the owner. This source does not configure account prices and never manually charges `apify-actor-start`. Default memory is 256 MB; a start reserves at least one unit.

The evaluation charge includes a transparent no-candidate observation because the bounded recall window was actually checked; it is never sold as a safety clearance. Candidate count does not multiply the charge. Incomplete scans deliver diagnostic partial evidence without custom result charges. The price reflects a bounded API fetch plus per-product identifier evaluation, not a measured market price or guaranteed profitability.

On a paid platform run, actual event prices and prior charges come from the SDK. Positive `apify-default-dataset-item` pricing is rejected to prevent double billing. The configured platform event cap is checked before work and each push. `maxBudgetUsd` limits event estimates/charges, not platform compute, storage or network fees. Automatic start can occur before Actor code and cannot be prevented by setting an input budget to zero. Concurrent platform cap changes can interrupt delivery; then comparison/snapshot are suppressed. See [PUBLISHING.md](PUBLISHING.md).

### Verification

See [QA.md](QA.md) for fixture tests, official schema validation, live source evidence, production audit and local SDK memory evidence. Cloud build and controlled Apify runs were verified on 2026-10-11 at 256 MB. Baseline and same-scope unchanged runs each delivered two rows with complete scans. A $0.005 platform event cap made no source requests and emitted no rows; an exact $0.007 cap delivered one no-candidate evaluation. The platform recorded one automatic start plus the expected custom events, with no paid default-dataset event. Owner testing incurred platform usage rather than customer event invoices; customer payment settlement and every maximum-size workload remain unverified. Original QA.md records local build-stage evidence; these cloud checks extend that dated record. No recall coverage or safety guarantee is implied.

# Actor input Schema

## `recallDateStart` (type: `string`):

Required YYYY-MM-DD. Inclusive local filtering. Maximum 366 calendar days. Prefilled dates are a fixed historical demonstration (October 1–2, 2026), not current recall coverage.

## `recallDateEnd` (type: `string`):

Required YYYY-MM-DD. Inclusive local filtering. Maximum 366 calendar days. Prefilled dates are a fixed historical demonstration (October 1–2, 2026), not current recall coverage.

## `catalog` (type: `array`):

1–250 objects with unique productId, optional manufacturer, models and/or upcs string arrays (up to 10 each). No customer, health, contact or secret data. Preserve UPC leading zeros as strings. Prefilled Agrish model is a public historical recall demonstration; replace it with your own nonsecret catalog.

## `partitionDays` (type: `integer`):

Hard bound. Incomplete scans suppress comparisons and reusable snapshots.

## `maxRecalls` (type: `integer`):

Hard bound. Incomplete scans suppress comparisons and reusable snapshots.

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

Hard bound. Incomplete scans suppress comparisons and reusable snapshots.

## `maxCandidatesPerProduct` (type: `integer`):

Hard bound. Incomplete scans suppress comparisons and reusable snapshots.

## `maxRequests` (type: `integer`):

Hard bound. Incomplete scans suppress comparisons and reusable snapshots.

## `maxRetries` (type: `integer`):

Hard bound. Incomplete scans suppress comparisons and reusable snapshots.

## `maxSeconds` (type: `integer`):

Hard bound. Incomplete scans suppress comparisons and reusable snapshots.

## `maxResponseBytes` (type: `integer`):

Hard bound. Incomplete scans suppress comparisons and reusable snapshots.

## `maxTotalBytes` (type: `integer`):

Hard bound. Incomplete scans suppress comparisons and reusable snapshots.

## `maxBudgetUsd` (type: `number`):

Local estimated or configured event budget including automatic start reserve per 1 GB memory unit. Excludes compute and storage. Automatic start may already be charged before code runs.

## `previousSnapshot` (type: `object`):

Paste OUTPUT.previousSnapshot for exact same catalog and dates. Maximum total input 1.5 MB.

## Actor input object example

```json
{
  "recallDateStart": "2026-10-01",
  "recallDateEnd": "2026-10-02",
  "catalog": [
    {
      "productId": "demo-agrish-rail",
      "manufacturer": "Zhongshan Lebo Medical Co., Ltd., of China",
      "models": [
        "AG-12011"
      ],
      "upcs": []
    }
  ],
  "partitionDays": 7,
  "maxRecalls": 100,
  "maxResults": 1,
  "maxCandidatesPerProduct": 25,
  "maxRequests": 3,
  "maxRetries": 2,
  "maxSeconds": 40,
  "maxResponseBytes": 3000000,
  "maxTotalBytes": 12000000,
  "maxBudgetUsd": 0.02
}
```

# Actor output Schema

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

No description

## `diagnostics` (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 = {
    "recallDateStart": "2026-10-01",
    "recallDateEnd": "2026-10-02",
    "catalog": [
        {
            "productId": "demo-agrish-rail",
            "manufacturer": "Zhongshan Lebo Medical Co., Ltd., of China",
            "models": [
                "AG-12011"
            ],
            "upcs": []
        }
    ],
    "maxRecalls": 100,
    "maxResults": 1,
    "maxRequests": 3,
    "maxSeconds": 40,
    "maxBudgetUsd": 0.02
};

// Run the Actor and wait for it to finish
const run = await client.actor("felixgoldwin/cpsc-recall-catalog-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 = {
    "recallDateStart": "2026-10-01",
    "recallDateEnd": "2026-10-02",
    "catalog": [{
            "productId": "demo-agrish-rail",
            "manufacturer": "Zhongshan Lebo Medical Co., Ltd., of China",
            "models": ["AG-12011"],
            "upcs": [],
        }],
    "maxRecalls": 100,
    "maxResults": 1,
    "maxRequests": 3,
    "maxSeconds": 40,
    "maxBudgetUsd": 0.02,
}

# Run the Actor and wait for it to finish
run = client.actor("felixgoldwin/cpsc-recall-catalog-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 '{
  "recallDateStart": "2026-10-01",
  "recallDateEnd": "2026-10-02",
  "catalog": [
    {
      "productId": "demo-agrish-rail",
      "manufacturer": "Zhongshan Lebo Medical Co., Ltd., of China",
      "models": [
        "AG-12011"
      ],
      "upcs": []
    }
  ],
  "maxRecalls": 100,
  "maxResults": 1,
  "maxRequests": 3,
  "maxSeconds": 40,
  "maxBudgetUsd": 0.02
}' |
apify call felixgoldwin/cpsc-recall-catalog-matcher --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,felixgoldwin/cpsc-recall-catalog-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/qhkSrhW4zy4Kc79az/builds/N1oNOSiXlKnRUA88y/openapi.json
