# Changelog of CPSC Recall API (`sourcing-data-studio/cpsc-recall-api`) Actor

- **URL**: https://apify.com/sourcing-data-studio/cpsc-recall-api/changelog.md
- **Full Actor documentation**: https://apify.com/sourcing-data-studio/cpsc-recall-api.md

## Changelog

### 0.1.7 (2026-10-10)

- When a run ends while a request is still waiting, the Actor now starts the crawler again for that request, at most three times and not with a proxy. On 2026-10-09 another Actor of this studio, the Shopify Products API, showed this fault: the crawler returned while a request that its last step had just added was still waiting. That request was never made, and the run still ended as succeeded with a wrong message. It showed only when several runs were started at the same moment. The cause is not known. It has not been seen on this Actor. One run of this Actor with follow-up requests is on record: 51 requests, a single run started through the API on 2026-10-09 to measure the figures in the README, and it read every year it asked for. No run with follow-up requests that was started together with others is on record. This Actor shares the same code, so the same guard is added here. Only a run without search terms that covers more than one calendar year adds follow-up requests (one year after the other, each added by the year before). When the crawler returns while a request is still waiting and nothing stopped the run, the Actor starts the crawler again for it, after a wait of 2, 4 and 8 seconds. Each start is logged as `run.pending`. The run summary counts the starts as `totals.extraPasses` and the requests that were still left at the end as `totals.requestsLeft`. With a proxy the Actor does not start the crawler again: a new start opens a new proxy session, which can mean another proxy address, and the Actor does not move a run to another address by itself. There it only reports the waiting requests. After three starts a request that is still waiting is not read: see `not-read` below.
- New state `not-read` in the run summary: a planned request (a year of a date-only run, or a search term in one field) for which no answer was read, although nothing stopped the run. Before, such a request was listed as `stopped`, and the run summary gave the reason `source-unreachable` although no request had failed. That reason is now used only after three failed requests in a row. A run that ends early without a known reason now has the reason `ended-early`. The three fields of `stoppedEarly` are unchanged.
- Status messages. A run that saved no recalls and left requests unread now ends as failed, names the years of the date range or the searches that were not read, and says: Run it again. Nothing is charged for recalls. A run that saved recalls and left requests unread stays succeeded and names them. It says that the saved recalls are charged and that nothing is charged for what was not read, and to run again only the years (or the search terms) that were not read. For search terms it adds that a second run of a search term saves and charges all its recalls again, also those that the first run saved. With a proxy the message adds that the Actor did not try again by itself. From the code, not from a run: before, a run that ended this way said "N planned requests were not made because several requests in a row failed, so the source seems to be unreachable", and with nothing saved it also said "No recalls matched the search terms, dates and country" about years that had not been asked for. That message is now used only when every request was read.
- A run that the run time limit stopped before every request was made, and that saved no recall, now ends as failed. From the code, not from a run: before, it ended as succeeded and began with "No recalls matched the search terms, dates and country" about requests that had not been made. It now says "No recalls saved: N planned requests were not made because the run time limit was near" and that it is not known whether those requests hold matching recalls. A run that saved recalls and was stopped by the time limit is unchanged. A time limit that comes after the last request was made has cut nothing short, and its message is unchanged.
- Wording: a run that saved one recall now says "Saved 1 recall", not "Saved 1 recalls". One failed request is "1 request failed and was not charged", not "1 requests failed and were not charged". The progress message in Console reads the same way.
- The time limit of the run stays armed until the extra starts are over, so that it also ends their waits. An abort keeps the summary it wrote: the summary of a normal end is no longer written over it. The Actor opens the request queue first, then sets the time limit and the abort handler, and waits for nothing between that and the first start of the crawler. It starts no crawler at all when the run is already stopping, and writes the summary of the stop that happened. (Nothing was awaited between setting them and the first start before, so no stop was lost; the order is kept so while the check for a restart reads the queue.) The check is in the new file `src/first-start.ts`. The crawler library forgets a stop that comes in the first moment of a second start (read in its code, not seen in a run). So each request that is handled now tells the crawler again to stop while the run is stopping, and a start that began with a forgotten stop reads one request at most. An abort is not told again: it never told the crawler to stop.
- A run that the platform restarted does not mark any request as `not-read`, and its status message says only what the Actor knows. This Actor keeps no state across a restart: its counts and the states of its requests are lost, and it saves its recalls only when the reading is over, so recalls that were found before the restart and not yet saved are lost with it. The Actor recognises a restart when the request queue of the run already holds requests when the new process starts, or, with pay per event, when recalls were already saved. With pay per event the message gives the number of recalls the run has saved in all, before and after the restart: "Saved 12 recalls in all, including the recalls saved before the run was restarted." The number is the charging count that Apify keeps for the run, which survives a restart. Without pay per event no count is known. The message then says that the run was restarted and that the recalls saved before the restart are in the dataset. Both messages add that recalls found before the restart and not yet saved may be missing. A restarted run never ends as failed because no recalls were saved, since recalls may have been saved before the restart. Its message never says "Nothing was charged for recalls" or "No recalls matched", and it gives no advice to run it again. The early exit for a **Maximum cost per run** that cannot pay for one recall follows the same rule: after a restart in which recalls were already charged, it no longer says that the limit is too low and that nothing was charged. It says how many recalls the run has saved in all and that it stopped at your maximum cost per run. From the code, not from a run: before, a restarted run ended with the reason `source-unreachable` although no request had failed.
- The pace is unchanged: one request at a time, at most 30 a minute. The crawler counts its 30 a minute from zero each time it starts again, so a run with search terms (at most 40 requests) can make more than 30 requests in the minute after an extra start. It never makes more than one at a time.
- The Actor now checks the certificate of the source. Before, the crawler library accepted a certificate that could not be verified. www.saferproducts.gov passed a strict certificate check from the development machine on 2026-10-09. It was not checked from Apify's servers.
- README: two figures now carry their count and date, measured on 2026-10-09 on the 10,000 newest notices: 124 had a text withheld for privacy (about 1 in 80, before: about 1 in 100) and 1,680 name no country (about 1 in 6, as before). The two sentences of the field table that name a share (a null `description` and an empty `manufacturerCountries`) carry their counts as well: 97 and 1,680 of the same 10,000 notices. The Limits section describes the extra starts, the state `not-read`, a run that Apify restarts and the pace after a new start, and the answer on 0 recalls names the cases that end as failed and the two that do not.

### 0.1.6 (2026-10-09)

- README: the Python example now works with version 3 of Apify's Python client, the version `pip install apify-client` installs today (it needs Python 3.11 or later). The example read the dataset id as `run["defaultDatasetId"]`. Client 3 returns a typed object, so that line stopped with `TypeError: 'Run' object is not subscriptable`, after the run had finished and been charged. It now reads `run.default_dataset_id`, and the line above it says `pip install "apify-client>=3"`.
- The faulty example was added on 2026-10-09 and had not been run before it was published. The corrected one was run against this Actor the same day with client 3.3.0. The curl example was run the same day and is unchanged.
- No change to the code, the input or the output.

### 0.1.5 (2026-10-09)

- README: the opening now states the price: what one saved row costs on the Free tier and what a typical query costs, from the same figures as the cost section. The sentence above the sample rows no longer names a "quick-test example".
- README: the output example now comes right after the feature list; the API section shows a curl, a JavaScript and a Python example; the cost section adds the price of a typical query. No change to the code, the input or the output.

### 0.1.4 (2026-10-06)

- README: it now says that titles, descriptions and links of a recall are shown as the CPSC published them. No change to the code, the input or the output. (This entry was added on 2026-10-09; the build has been live since 2026-10-06.)

### 0.1.3 (2026-10-06)

- Individuals in the firm lists (owner ruling M20.3): when a manufacturer, importer or distributor is a private individual, the name is replaced by `[individual, name withheld]` and the new field `withheldNames` counts the replacements (0 when there are none). The notice is always kept. The check is cautious on purpose: it replaces only a firm in China, Hong Kong or Taiwan that is named by two words with no sign of a company or a brand, with a common first name or a common Chinese family name among them, and whose name is not part of a product name of the notice. A company name is never replaced, and a sole trader that is named any other way stays as CPSC published it. On all 14,527 firm entries of the notices from 1973 to October 2026 it replaces 1 value and no company name.
- No new address after a refusal (rule 4, "if a source blocks, fail gracefully"): HTTP 401, 403 and 407 and the "Access Denied" page of the edge are no longer retried. The page is listed as `blocked` once and nothing is charged. After HTTP 429 the Actor waits as long as Retry-After says (at most 60 seconds) or by the back-off, and asks again from the same session; `x-ratelimit-remaining: 0`, or a Retry-After above 60 seconds, ends the tries for that page. No session is retired for a refusal. Before, the Actor retired the session after a block, and Crawlee itself retired it on 401, 403 and 429 and handed every retry a fresh session, so a proxy gave a new address each time. The crawler now keeps one session for the whole run; only a connection error rotates it. `blockedAttempts` in the run summary now counts every refused attempt, the last one of a page included. The proxy and retry texts of the input schema and the README no longer advise a proxy after a block or a 429.
- Store listing: new search terms, SEO title and description, a new first paragraph in the README and a section for AI agents.

### 0.1 (2026-10-05)

- First version: unofficial CPSC recall API that searches US consumer product recalls by product keyword, recall date and manufacturer country, with one pay-per-event charge per saved recall.
- Review fixes before publishing: contact persons and more phone number shapes of old notices are kept out (a text that names a person is withheld and counted in the run summary); country names with qualifiers such as "China (plug)" are normalised and an unknown country in the input stops the run early; links with a typo are repaired instead of dropping the recall or the image; the sales sentence of a notice is in `soldAt`; a failed year no longer ends a date-only run; dates are limited to 1973 until today; a run in which every recall fails the checks ends as failed; no proxy by default.
