# Changelog of HS Code Lookup API (`sourcing-data-studio/hs-code-lookup-api`) Actor

- **URL**: https://apify.com/sourcing-data-studio/hs-code-lookup-api/changelog.md
- **Full Actor documentation**: https://apify.com/sourcing-data-studio/hs-code-lookup-api.md

## Changelog

### 0.1.7 (2026-10-10)

- A run that ends before it has read every entry now says so. On 2026-10-09 another Actor of this studio (Shopify Products API) showed a fault: the part that reads the pages returned while one more request was still waiting. That request was never read, and the run ended as succeeded with a wrong message. It showed only when several runs were started at the same moment, and the cause is not known. It was not seen in this Actor. We read the code and found that such an end would have given a wrong message here too. The entries are read one after the other, so every entry behind the waiting request would have stayed unread. The message would have said only that some entries were not read, with no reason, or, when nothing had been saved, "No tariff line matched the entries", which would not have been true.
- New entry state `not-read` in the run summary: the entry was not read to the end although nothing stopped the run. Before, such an entry was listed as `stopped`, with the note that a limit, the time limit or an abort had stopped the run, although none of them had.
- A run that saved nothing and left entries unread now ends as failed. Its status message says how many entries were not read, that the run ended early although no limit was reached, and that nothing was charged for results. It says: Run it again. A run that saved results and left entries unread still ends as succeeded. Its message names up to three of the entries and says: Run again only the entries that were not read. The results that were saved are charged already.
- The message "No tariff line matched the entries" is now said only when every entry got an answer.
- When the part that reads the pages returns while requests are still waiting, and nothing stopped the run (no limit, no time limit, no abort), the Actor now starts it again, at most three times, each time after a longer wait (2, 4 and 8 seconds). Each start is logged as `run.pending`. The run summary counts the starts (`totals.extraPasses`) and the requests that were still left at the end (`totals.requestsLeft`). The gap of about 3 seconds between two requests holds across a new start. With a proxy the Actor does not start again: a new start opens a new proxy session, which can mean another address, and the Actor does not move a run to another address by itself. There it only reports what is left.
- A run that the platform restarted does not mark any entry as `not-read`, and its status message says only what the Actor knows. The Actor keeps no states across a restart, so it cannot tell which entries were read before it. It recognises a restart when the request queue of the run already holds requests when the new process starts, or, with pay per event, when results were already saved. With pay per event the message gives the number of results the run has saved in all, before and after the restart: "Saved 12 tariff results in all, including the results 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 results saved before the restart are in the dataset. It also says how many entries were not finished after the restart, and that they may have been read before it or not read at all, and how many requests were still waiting. The entries that were not finished are listed as `stopped` with the note "Not known: the run was restarted, so this entry may have been read before the restart." A restarted run never ends as failed because no results were saved, since results may have been saved before the restart. Before, it could end as failed. Its message never says "Nothing was charged for results" or "No tariff line matched the entries", and it gives no advice to run it again. The early exit for a **Maximum cost per run** that cannot pay for one result follows the same rule. After a restart, when results were already charged, it no longer says that the limit is too low and that nothing was charged. It says how many results the run has saved in all and that it stopped at your maximum cost per run. After a restart the Actor does not always go on with the entries that were still to be read, because it does not know where the run was. The README says what to do then. The numbers in the run summary of a restarted run (`totals` and `billing`) count only the part of the run after the restart.
- A run that is aborted keeps the summary that the abort wrote. After an abort, the part that reads the pages is not started again, also when the abort comes while the Actor waits to start it again.
- The Actor now checks the certificate of the source. Before, the library that sends the requests accepted a certificate that could not be verified.
- No change to the input or to the rows that are saved.

### 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.
- 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)

- The prefilled example input now asks both tariffs, UK and US, instead of the UK tariff alone, so the first run and the daily test do not depend on one source. No change to the code 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)

- Price: one tariff result now costs $0.005 on the Free and Bronze tiers, $0.0045 on Silver and $0.004 on Gold (before: $0.003, $0.003, $0.0027 and $0.0024). A run saves at most 2,000 results, so the results of one run cost at most $10 on the Free tier.
- No new address after a refusal. A refusal never ends the session and never starts a request from another address (with a proxy, a new session is a new address). The crawler keeps one session for the whole run, and a refusal is turned into an error before the crawler library can end that session. With a proxy, a run therefore uses one proxy address for all its requests, as long as the connection through it works. Before, every request got a session of its own (with a proxy: a new address each time), the library ended it on HTTP 401, 403 and 429, and the error handler ended it on every refusal.
- HTTP 401, 403 and 407, and a page instead of data, are final: the request is made once, the entry is listed as `blocked` in the run summary and nothing is charged. Before, the request was asked for again up to **Retries per request** times, each time from a new session (with a proxy: from another address). A proxy that refuses the connection with HTTP 407 is final too, and the failure text names the proxy.
- HTTP 429 is asked for again after the pause the source asks for (the `Retry-After` header, at most 60 seconds; 15, then 30, then 60 seconds without one), from the same address. The header `x-ratelimit-remaining: 0`, or a `Retry-After` above 60 seconds, ends it: the request is not asked for again. Before, the header `x-ratelimit-remaining` was not read.
- A source that refuses everything is now left alone after three requests (it was five attempts), because a refused request is no longer asked for again. The slow-down between entries (15, then 30 seconds) is unchanged.
- Run summary: an entry that saved results but lost candidates (a request for one of its headings failed or was refused for good, or a candidate was dropped as incomplete) is now listed as `partial`, with a note that says which. Before, it was listed as `ok`. The statuses `ok`, `partial`, `no-match`, `blocked`, `failed` and `stopped` are listed in the README.
- Input schema: the texts of **Retries per request** and **Proxy configuration** say that a refused request is not asked for again from another address.
- README: the `dutyRate` caveats in the field description, in Limits and in a new FAQ answer (UK: the third-country rate, without preferences or additional duties; US: the general column 1 rate only, without the Chapter 99 additional duties such as Section 301, Section 232 and IEEPA-based duties, which change often, and the text of an HTS revision can still list measures that have ended); a "For AI agents" section, a "Run summary" section and the cost ceiling of a run; a new first paragraph.
- Store listing: new title and description for search results and a new list of search terms.

### 0.1 (2026-10-05)

- First version: looks up product descriptions or codes of 4, 6, 8 or 10 digits in the UK Trade Tariff and, optionally, the US Harmonized Tariff Schedule, and returns the national code, official description, 6-digit HS code, parent lines, duty rate, UK import VAT and units. One "tariff-result" event per saved row.
- Fixes after the first review, before publication:
  - UK duty: a line whose rate depends on an additional code, or that only has a duty under authorised use, now has an empty `dutyRate` and a new field `dutyNote` that lists the published rates. The first rate in the source document is never taken as the duty.
  - US tariff: a description returns only lines that contain every word of the entry; rows carry their parent lines from the 4-digit heading down and the duty of the nearest parent line that has one.
  - A service that refuses requests is asked more slowly at once and no longer after at most five failed attempts in a row; the entries not read are marked `blocked` in the run summary.
  - A code that does not exist is a `no-match`, not a failed run. Every entry has a status in the run summary, and the final message says why a run stopped early.
  - Error texts in the log and the run summary name the HTTP status only, never text from a response.
  - A code sent as a JSON number is accepted.
