# US Customs Rulings Watch: HTS Code Monitor (`persistent_parrot/us-customs-rulings-watch`) Actor

Search or monitor US customs binding rulings by HTS code or keyword and get new, modified and revoked rulings as clean JSON; not affiliated with U.S. Customs and Border Protection.

- **URL**: https://apify.com/persistent_parrot/us-customs-rulings-watch.md
- **Developed by:** [Bright Path](https://apify.com/persistent_parrot) (community)
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $8.00 / 1,000 ruling 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

## US Customs Rulings Watch: HTS Code Monitor

Track US Customs and Border Protection (CBP) binding rulings by HTS code or keyword, and get an alert-style feed of rulings that are **new, modified or revoked** since your last run. Data comes from CBP's public CROSS database (rulings.cbp.gov), a US federal government work.

### What it does

- Finds US CBP binding rulings by HTS code (4, 6, 8 or 10 digits) or keyword, or fetches the rulings whose numbers you list, and returns each one as a parsed row with tariff numbers, relations and a single status.
- In monitor mode, on a schedule, returns only rulings that are new, modified or revoked since the last run with the same watchlist.
- Optionally adds the ruling letter text, with the letter header and address block dropped and the requester's personal data and names after a title removed; names without a title are not detected and can remain (see Privacy).

### Who it is for

- **Customs brokers and in-house trade compliance staff** who must know when a ruling behind a classification changes.
- **Importers and sourcing teams** who want to be told when the codes they import under get a new ruling or an old one is revoked.
- **Trade-data and compliance tool builders** who need CROSS rulings as clean JSON instead of web pages.

### Modes

| Mode | What it does |
|---|---|
| `search` | Rulings matching your HTS codes and keywords. |
| `monitor` | Same search, but only rulings that are new or changed since the last run with the same watchlist. Schedule it daily. |
| `lookup` | Fetches the ruling numbers you list (for example `N349485`, `H338307`, `088031`). |

### Input examples

Search one heading (five rulings, finishes in well under a minute, about $0.04):

```json
{ "mode": "search", "htsCodes": ["6402"], "maxResultsPerTerm": 5 }
```

Daily watchlist, first run reports the last 90 days:

```json
{ "mode": "monitor", "htsCodes": ["6402.99", "6403", "8517.13.0000"], "keywords": ["lithium battery"], "firstRunDays": 90 }
```

Look up rulings with their text:

```json
{ "mode": "lookup", "rulingNumbers": ["N349485", "H338307"], "includeText": true }
```

Monitor baselines are keyed by your watchlist and the scheduled task, so two tasks never share state; changing the watchlist starts a fresh baseline (set `stateKey` to keep one). Other options: `fromDate` (only rulings dated on or after), `maxResultsPerTerm` (default 100, newest first), `maxRows` (default 1000; the most rows one run delivers and charges. In monitor mode every code is still searched, and extra new or changed rulings are held back and reported on the next run), `stateKey` (name a monitor so several watchlists can run side by side).

### Output example

A real row from the Actor's own search run on Apify on 22 October 2026 (`{"mode":"search","htsCodes":["6402"],"maxResultsPerTerm":100}`):

```json
{
  "rulingNumber": "H338307",
  "collection": "HQ",
  "rulingDate": "2025-08-28",
  "subject": "Revocation of NY N336132; Tariff Classification of Men’s Footwear from Vietnam",
  "category": "Classification",
  "tariffs": ["6402.19.90"],
  "hts": [{ "raw": "6402.19.90", "digits": "64021990", "chapter": "64", "heading": "6402", "subheading": "6402.19", "isChapter99": false }],
  "htsChapters": ["64"],
  "isUsmca": false,
  "isNafta": true,
  "operationallyRevoked": false,
  "relatedRulings": ["M81358", "J85526", "G85532", "D88063", "A88498", "A82723", "A88497", "N336132"],
  "revokes": ["N336132"],
  "revokedBy": [],
  "modifies": [],
  "modifiedBy": [],
  "status": "active",
  "rulingUrl": "https://rulings.cbp.gov/ruling/H338307",
  "textStatus": "omitted",
  "text": null,
  "matchedTerms": ["6402"],
  "matchedBy": "tariff",
  "dateModified": null,
  "changeType": null,
  "checkedAt": "2026-10-22T09:56:21.381Z"
}
```

`changeType` is `new` or `changed` in monitor mode and `null` otherwise; `dateModified` is filled when CBP lists the ruling as recently modified. With `includeText` the `text` field holds the redacted letter and `textStatus` is `included` (or `withheld`, see Privacy).
A run also writes an `OUTPUT` record with rows per term, CBP's total hits per term, ruling numbers not found, invalid numbers, CBP's last update date and rows charged.

### What you get

- Rulings matched by HTS code (4, 6, 8 or 10 digits, dots optional) or by keyword. A code shorter than 10 digits also finds every sub-code under it (`6402` finds 6402.99.9065). Rulings with a tariff number under that code are returned, plus rulings whose CBP record has no tariff number, which are marked `matchedBy: "search"`. A 10-digit code finds only rulings that list that exact 10-digit number, so a ruling filed only under the 8-digit line (for example 6402.91.50) is not found by a watch on 6402.91.5020; to include those, also watch the 8-digit code.
- Each ruling parsed into fields: date, HQ or NY, subject, every tariff number split into chapter, heading and subheading, a flag for Chapter 99 additional-duty lines, USMCA / NAFTA flags.
- **Relations and status**: which rulings it revokes or modifies, which rulings revoke or modify it, and a single `status` (`active`, `modified`, `revoked`, `unknown`).
- **Monitor mode**: first run saves a baseline; every later run returns only rulings that are new or changed. A watched ruling also counts as changed when a newly issued ruling lists it as revoked or modified.
- Optional ruling text with the letter header and address block dropped and the requester's personal data removed, plus personal names after a title (Mr/Ms/Mrs/Miss/Dr and similar), US street addresses, emails, Social Security number shapes and phone numbers with area code. Names without a title are not detected, so a letter may still contain them (see Privacy).

### Pricing

Pay per event, nothing else:

- **$8 per 1,000 rulings** delivered.
- **$3 per 1,000** extra for each ruling delivered with text (`includeText`).

No charge for empty results, input errors or unknown ruling numbers. A ruling whose text had to be withheld is still a delivered ruling and is charged at the ruling price; only the text charge is skipped.

### Data source and licence

Rulings come from CBP's public CROSS database (rulings.cbp.gov). They are works of the US federal government, and CBP's website policy says its information is in the public domain and may be reproduced, published or otherwise used without permission, and asks that CBP be cited as the source.

Source: U.S. Customs and Border Protection, CROSS (rulings.cbp.gov). Public domain US government work. Every row carries `rulingUrl`, the link to the ruling on rulings.cbp.gov.

This is an independent tool. It has no affiliation with CBP (U.S. Customs and Border Protection) or the Department of Homeland Security, and neither body has endorsed or sponsored it.

### Update schedule and freshness

- No stored copy: every run asks CROSS when it runs. CBP updates CROSS in batches, so a ruling appears when CBP posts it, not on its ruling date; the `OUTPUT` record shows CBP's last update date.
- Monitor mode is made for a daily schedule (Apify Schedules). The first run saves a baseline and reports rulings dated within `firstRunDays` (default 30); later runs report only what is new or changed.

### Privacy

Ruling letters open with the requester's name and address, often a private person. This Actor never outputs that block. Text is off by default. When on, the letter header and address block are dropped (text starts after the salutation).

**Removed (replaced with `[REDACTED]`):**

- the requester's personal name (taken from the salutation and address block) and the street number and ZIP from the address block, wherever they appear (a number such as 1500 is kept when it is clearly a quantity, for example `1500 cc`);
- anywhere in the letter: a name after a title (Mr, Mrs, Ms, Mx, Miss, Dr, Prof, Professor, Messrs, Mmes, Mme, Sr, Sra, `M.`, in any case), including initials (also between name words, as in Mr. John Q. Smith), particles such as de la, van der, al-, d', a name split across one line or page break, and every name in a Messrs/Mmes list;
- US street addresses (number, street name, street suffix) and state plus ZIP;
- emails, US Social Security number shapes with separators (123-45-6789), phone numbers with area code (10-digit US format, optional +1), UK national numbers and `+` or `00` international numbers (Unicode dashes and no-break spaces count as separators);
- everything after a `cc`, `bcc` or `copy to` followed by a colon, so an engine size written as `50 cc:` also drops the rest of the text.

**Not removed:** names of people without a title (a letter may still contain such names, for example in the body or in a list of clients); names of CBP officials in the signed letter; company names and business addresses; 7-digit phone numbers without an area code (555-1234); 9-digit Social Security numbers without separators; P.O. boxes; state names written in full before a ZIP; street addresses written in a non-US shape. The text is withheld rather than guessed when the letter has no salutation ending in a colon, or when it is addressed to a CBP role or office and not a person ("Dear Port Director:", "Dear Sir or Madam:"). In a sample of 40 recent letters, about a quarter were withheld, mostly protest letters sent to a Port or Center Director. The `subject` field is always cut where a letter salutation ("Dear ...") starts and then gets the same email, phone, address and titled-name redactions as the text. Treat the text as public CBP records that may include personal details we could not detect.

### Limits and honest coverage

- Independent tool. No affiliation with CBP (U.S. Customs and Border Protection) or the Department of Homeland Security, and neither has endorsed or sponsored it. Data is CBP's public CROSS database, a US government work (public domain). Not legal advice.
- Coverage is exactly what CBP publishes in CROSS (about 222,000 rulings). Anything CBP has not published is not here.
- HTS and keyword matching uses CBP's own search (an HTS code shorter than 10 digits is searched as a prefix, then rows are filtered on our side by their tariff numbers). A ruling whose tariff field is empty or has no HTS number is kept and marked `matchedBy: "search"`; a ruling that only mentions the code in its text but is classified elsewhere is dropped. A 10-digit code is not searched as a prefix and finds only rulings that list that exact number (see What you get). Each code reads at most about `maxResultsPerTerm / 100 + 5` pages.
- CBP updates CROSS in batches, so changes appear when CBP posts them, not on the ruling date.
- Monitor mode sees changes to rulings that your searches return, plus rulings CBP lists as recently modified and rulings newly issued rulings say they revoke or modify. A very old ruling revoked outside your watched codes is not tracked.
- Rulings are binding for the importer they were issued to; this data is information, not legal advice. Check the linked ruling before relying on it.
- To be polite to CBP the Actor makes one request every two seconds, so a very large run takes time: about 100 rulings per request.
- Text lookups cost one extra request per ruling.
- Some older rulings are stored by CBP with a lowercase letter (for example `h86911`, `f87772`), and CBP's ruling page only answers to that exact form. `lookup` mode writes ruling numbers in capitals, so such a ruling is listed in `notFound` and not charged. `search` returns these rulings normally, and `monitor` reports them when they appear in your code searches; a later change to, or revocation of, such an old ruling may not be flagged.
- Run time, measured on Apify: 100 rulings without text in about 6 seconds, 20 rulings with text in about 48 seconds. Large runs with text are slow: the tester estimated about 37 minutes for 1,000 rulings with text, so set the run timeout accordingly.
- In the Apify Console the form is prefilled with `maxResultsPerTerm` 5 (the default without the prefill is 100). For a monitor, set it to 100 or higher so a busy day does not push new rulings out of view.

### FAQ

**How do I watch an HTS code?** Use `"mode": "monitor"` with your codes in `htsCodes` (dots optional) and put the Actor on a daily Apify schedule. The first run saves a baseline; later runs return rulings that are new, modified or revoked.

**Why does a row say `matchedBy: "search"`?** CBP's record for that ruling has no tariff number, so the code could not be checked against it. The row is kept because CBP's own search returned it. `matchedBy: "tariff"` means one of the ruling's tariff numbers starts with your code.

**Why does a 10-digit code find fewer rulings than its 8-digit parent?** A 10-digit code finds only rulings that list that exact number. Rulings filed only under the 8-digit line are found by the 8-digit code.

**Why is `text` empty on some rulings?** With `includeText` on, the text is withheld when the letter has no salutation ending in a colon or is addressed to a CBP office or role. About a quarter of 40 sampled letters were withheld. The row is still delivered and charged at the ruling price; see Pricing.

**Is this legal advice?** No. Rulings are binding for the importer they were issued to; check the linked ruling before relying on it.

**How much does a first test cost?** The first input example (five rulings) costs about $0.04.

**Something looks wrong. Where do I report it?** Open an issue on this Actor's page with the run link and the `rulingNumber`. We aim to reply within 24 hours.

### Changelog

- **22 Oct 2026, first release.** Search, monitor and lookup modes; optional redacted ruling text. HTS codes shorter than 10 digits are searched with a wildcard so their sub-codes are found. In monitor mode `maxRows` limits the rows delivered and charged per run, every code is still searched, and extra new or changed rulings are held back and reported on the next run.

# Actor input Schema

## `mode` (type: `string`):

Search: rulings matching your HTS codes and keywords. Monitor: the first run saves a baseline; later runs return only rulings that are new, modified or revoked. Lookup: fetch the rulings you name.

## `htsCodes` (type: `array`):

Search and monitor modes (max 50). 4, 6, 8 or 10 digits, dots optional: 6402, 640299 and 6402.99 all work. A code shorter than 10 digits also finds its sub-codes (6402 finds 6402.99.9065). A 10-digit code finds only rulings that list that exact number; a ruling filed only under the 8-digit line is found by the 8-digit code.

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

Search and monitor modes (max 20). Words or phrases of 2 to 100 characters, matched by CBP's own search. Blank entries are ignored.

## `rulingNumbers` (type: `array`):

Lookup mode only (max 200). For example N349485, H338307 or 088031. Invalid numbers are listed in the OUTPUT record.

## `fromDate` (type: `string`):

Only rulings dated on this day or later (YYYY-MM-DD). Leave empty to include every date.

## `maxResultsPerTerm` (type: `integer`):

Stop paging a code or keyword after this many rulings. Newest first. For a monitor, keep the default 100 or higher so a busy day does not push new rulings out of view.

## `maxRows` (type: `integer`):

Cost control: the most rows one run delivers. In monitor mode every code is still searched; extra new or changed rulings are held back and reported on the next run.

## `includeText` (type: `boolean`):

Add the body of each ruling letter with the letter header and address block dropped and the requester's name, address numbers, emails and phone numbers removed (see README Privacy). Slower (one extra request per ruling) and billed as a separate event.

## `firstRunDays` (type: `integer`):

Monitor mode, first run only: report rulings dated within this many days; older ones are stored silently as the baseline.

## `stateKey` (type: `string`):

Monitor mode only. Names the saved baseline; runs with the same name share one baseline. Empty = derived from your codes and keywords plus the scheduled task ID, so two tasks never share state.

## Actor input object example

```json
{
  "mode": "search",
  "htsCodes": [
    "6402"
  ],
  "keywords": [],
  "rulingNumbers": [],
  "maxResultsPerTerm": 5,
  "maxRows": 1000,
  "includeText": false,
  "firstRunDays": 30,
  "stateKey": ""
}
```

# Actor output Schema

## `dataset` (type: `string`):

All matching rulings (one row per ruling; new, modified or revoked in monitor mode).

## `summary` (type: `string`):

Run summary (rows per term, CBP total hits, numbers not found, rows charged).

# 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 = {
    "htsCodes": [
        "6402"
    ],
    "maxResultsPerTerm": 5
};

// Run the Actor and wait for it to finish
const run = await client.actor("persistent_parrot/us-customs-rulings-watch").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 = {
    "htsCodes": ["6402"],
    "maxResultsPerTerm": 5,
}

# Run the Actor and wait for it to finish
run = client.actor("persistent_parrot/us-customs-rulings-watch").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 '{
  "htsCodes": [
    "6402"
  ],
  "maxResultsPerTerm": 5
}' |
apify call persistent_parrot/us-customs-rulings-watch --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,persistent_parrot/us-customs-rulings-watch"
        }
    }
}
```

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/gxkLABnztvWM2vI63/builds/OHe0NWDtsY3FdXXfN/openapi.json
