# CBP CROSS Customs Ruling Scraper (`jungle_synthesizer/cbp-cross-customs-ruling-scraper`) Actor

Pulls binding classification rulings from CBP's CROSS database — HQ, NY and legacy letters going back decades. Each row carries the ruling number, date, subject, HTS codes, the full letter text, and the modify/revoke graph so you can tell whether a ruling is still good law.

- **URL**: https://apify.com/jungle_synthesizer/cbp-cross-customs-ruling-scraper.md
- **Developed by:** [BowTiedRaccoon](https://apify.com/jungle_synthesizer) (community)
- **Categories:** Business, Other
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.80 / 1,000 record scrapeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## CBP CROSS Customs Ruling Scraper - Binding Classification Rulings

Extract binding customs classification rulings from CBP's CROSS database (Customs Rulings Online
Search System) — the official record of every HQ, NY and legacy ruling letter CBP has issued on
how a product is classified under the Harmonized Tariff Schedule. Each record carries the ruling
number, date, subject, every HTS code it classifies to, the full letter text, and the
modify/revoke graph so you can tell whether a ruling is still good law.

### CBP CROSS Customs Ruling Scraper Features

- Pulls HQ, NY and legacy (L/D/C/numeric) ruling letters from CBP's complete public ruling corpus
- Every HTS code a ruling classifies to, plus a derived `primary_hts` and two-digit `hts_chapter` for joining to a tariff schedule
- Full ruling letter text, not just the subject line
- The revocation graph resolved into `is_currently_good_law` — know whether a ruling you're about to cite has been superseded
- USMCA/NAFTA flags and CBP's own commodity grouping for filtering
- Filter by collection (HQ/NY/all) and ruling date range
- Toggle full-text fetch off for a fast index-only pull of ruling numbers, dates, subjects and HTS codes

### Who Uses CBP Ruling Data and Why?

- **Customs brokers and classification desks** — find every prior ruling for a product description before submitting a new one, and confirm a cited ruling hasn't been revoked
- **Landed-cost and duty-engine vendors** — back a classification decision with binding CBP precedent, not just a schedule lookup
- **Corporate trade-compliance teams** — build HTS decision trees from CBP's own classification language, especially with Section 301 duties now turning on exact HTS codes
- **Customs law firms** — research precedent for protests and prospective ruling requests
- **AI/ML teams** — train classification models on a clean (product description → HTS code) labelled corpus written by CBP's own classification officers

### How CBP CROSS Customs Ruling Scraper Works

1. You set an optional collection filter (HQ, NY, or all), an optional ruling date range, and whether to fetch full letter text.
2. The crawler pulls matching ruling letters from CBP's CROSS search, covering the HQ, NY and legacy collections.
3. Each ruling is normalized into a clean record: the HTS codes it classifies to, the subject, CBP's own categories, the USMCA/NAFTA flags, and the modify/revoke graph.
4. When full-text fetch is on (the default), the actor also pulls the ruling letter body and parses out the requester and stated country of origin where present.

### Input

#### Basic: recent rulings, full text included

```json
{
  "maxItems": 100
}
```

#### HQ rulings only, fast index-only pull

```json
{
  "maxItems": 500,
  "collection": "HQ",
  "fetchFullText": false
}
```

#### Rulings issued in a date window

```json
{
  "maxItems": 200,
  "fromDate": "2026-01-01",
  "toDate": "2026-06-30"
}
```

#### Pull everything (full corpus)

```json
{
  "maxItems": 0
}
```

#### Input Parameters

| Field         | Type    | Default | Description |
|---------------|---------|---------|-----------------------------------------------------------------------------------------------------------|
| maxItems      | integer | `10`    | Maximum number of rulings to return. Set to `0` for no cap. |
| collection    | string  | `"ALL"` | `"ALL"`, `"HQ"` (Headquarters rulings) or `"NY"` (New York rulings). |
| fromDate      | string  | `""`    | Only return rulings dated on or after this date (YYYY-MM-DD). Leave empty for no lower bound. |
| toDate        | string  | `""`    | Only return rulings dated on or before this date (YYYY-MM-DD). Leave empty for no upper bound. |
| fetchFullText | boolean | `true`  | Fetch the full ruling letter (text, requester, country of origin) per record. Turn off for a faster, index-only pull of ruling numbers, dates, subjects and HTS codes. |

### CBP CROSS Customs Ruling Scraper Output Fields

#### Example Output

```json
{
  "ruling_number": "H335012",
  "collection": "hq",
  "ruling_date": "2026-10-02",
  "ruling_year": 2026,
  "subject": "Re: Request for Binding Ruling; Classification of Casio Colpocamera Stand model number CST-100M",
  "tariffs": ["9620.00.70"],
  "primary_hts": "9620.00.70",
  "hts_chapter": "96",
  "categories": ["Classification"],
  "commodity_grouping": null,
  "is_usmca": false,
  "is_nafta": false,
  "operationally_revoked": false,
  "modifies": [],
  "modified_by": [],
  "revokes": [],
  "revoked_by": [],
  "related_rulings": [],
  "is_currently_good_law": true,
  "ruling_text": "HQ H335012 October 2, 2026 ... [full letter text]",
  "requester": "Ms. Ewa Szczepanski Casio America, Inc.",
  "country_of_origin": "Japan",
  "doc_url": "https://rulings.cbp.gov/api/getdoc/hq/2026/H335012.pdf",
  "url": "https://rulings.cbp.gov/docs/hq/2026/H335012",
  "internal_id": 222635,
  "source": "rulings.cbp.gov",
  "scraped_at": "2026-10-15T07:30:47.720Z"
}
```

#### Output Field Reference

| Field                 | Type     | Description |
|-----------------------|----------|-----------------------------------------------------------------------------------|
| ruling_number         | string   | CBP's public ruling identifier, e.g. H357206, N363447, 955022 |
| collection            | string   | `hq`, `ny`, or `legacy` |
| ruling_date           | string   | Date the ruling was issued (YYYY-MM-DD) |
| ruling_year           | number   | Year the ruling was issued |
| subject               | string   | Ruling subject line |
| tariffs               | string\[] | Every HTS code this ruling classifies to — empty for non-classification rulings (Valuation, country-of-origin marking, etc.) |
| primary_hts           | string   | First/controlling HTS code, or null |
| hts_chapter           | string   | Two-digit HTS chapter derived from primary_hts |
| categories            | string\[] | CBP's own subject categories for this ruling |
| commodity_grouping    | string   | CBP's commodity grouping label, where assigned |
| is_usmca              | boolean  | Whether this ruling addresses USMCA |
| is_nafta              | boolean  | Whether this ruling addresses NAFTA |
| operationally_revoked | boolean  | Whether CBP has flagged this ruling as operationally revoked |
| modifies              | string\[] | Ruling numbers this ruling modifies |
| modified_by           | string\[] | Ruling numbers that modify this ruling |
| revokes               | string\[] | Ruling numbers this ruling revokes |
| revoked_by            | string\[] | Ruling numbers that revoke this ruling |
| related_rulings       | string\[] | Other ruling numbers CBP links to this one |
| is_currently_good_law | boolean  | Derived: true when the ruling is neither operationally revoked nor revoked by a later ruling |
| ruling_text           | string   | Full ruling letter text (present when fetchFullText is on) |
| requester             | string   | Importer, broker or counsel who requested the ruling, parsed from the letter header (best effort — not every letter yields a clean match) |
| country_of_origin     | string   | Country of origin stated in the ruling body, where present (best effort — most rulings don't state one) |
| doc_url               | string   | Direct PDF link for the original ruling letter |
| url                   | string   | Public CROSS web page for this ruling |
| internal_id           | number   | CROSS internal numeric id |
| source                | string   | Source site name |
| scraped_at            | string   | UTC timestamp this record was collected |

### FAQ

**How many rulings does this cover?**
CROSS holds 221,000+ searchable ruling letters across the HQ, NY and legacy collections, going back to the 1980s. The scraper pulls from the full corpus, not a subset.

**Does every ruling have an HTS code?**
No. Most rulings are classification decisions and carry one or more HTS codes in `tariffs`. CROSS also publishes non-classification letters — Valuation, country-of-origin marking, IPR — and those legitimately have an empty `tariffs` array.

**Why is `requester` or `country_of_origin` sometimes empty?**
Both are parsed from the free-text ruling letter, not a structured CBP field. Most letters name the requester in a standard header, so that field fills reliably; a stated country of origin is less common — many rulings classify a product without ever naming where it's made.

**How do I know if a ruling is still valid?**
Check `is_currently_good_law`. It's derived from CBP's own `operationally_revoked` flag and the `revoked_by` list — false means a later ruling superseded this one, or CBP flagged it directly.

**What does it cost to run?**
Priced per record. A quick sample of 100 rulings costs pennies; a full-corpus pull is priced the same per row regardless of how large the run.

**Can I pull just HQ or just NY rulings?**
Yes — set `collection` to `"HQ"` or `"NY"`. Leave it at `"ALL"` (the default) to search both plus the legacy collections.

### Need More Features?

Need custom fields, filters, or a different data source? [File an issue](https://console.apify.com/actors/issues) or get in touch.

### Why Use CBP CROSS Customs Ruling Scraper?

- **The full letter, not just the headline** — most tariff tools stop at the subject line; this one carries the complete ruling text.
- **HTS codes as a real join key** — `tariffs` is a structured array, so you can go from an HTS code to every ruling that ever classified to it.
- **Know what's still valid** — the modify/revoke graph is resolved into one boolean, so you're not citing a dead ruling in a protest.
- **The whole corpus, priced per record** — one run covers HQ, NY and legacy letters back to the 1980s.

# Actor input Schema

## `sp_intended_usage` (type: `string`):

What will this data feed? E.g. lead lists, KYB checks, price tracking.

## `sp_improvement_suggestions` (type: `string`):

Provide any feedback or suggestions for improvements.

## `sp_contact` (type: `string`):

We'll personally help with your use case. No spam.

## `maxItems` (type: `integer`):

Maximum number of rulings to return. Leave at the default for a quick sample; raise it for a bulk pull.

## `collection` (type: `string`):

Restrict to CBP's own HQ (Headquarters) or NY (New York) ruling collection, or search all collections.

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

Only return rulings dated on or after this date (YYYY-MM-DD). Leave empty for no lower bound.

## `toDate` (type: `string`):

Only return rulings dated on or before this date (YYYY-MM-DD). Leave empty for no upper bound.

## `fetchFullText` (type: `boolean`):

Fetch the full ruling letter (text, requester, country of origin, modify/revoke graph) for every matching record. Turn this off for a fast index-only pull of ruling numbers, dates, subjects and HTS codes.

## Actor input object example

```json
{
  "sp_contact": "Share your email here...",
  "maxItems": 10,
  "collection": "ALL",
  "fetchFullText": true
}
```

# Actor output Schema

## `results` (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 = {
    "sp_contact": "Share your email here...",
    "maxItems": 10,
    "collection": "ALL",
    "fromDate": "",
    "toDate": "",
    "fetchFullText": true
};

// Run the Actor and wait for it to finish
const run = await client.actor("jungle_synthesizer/cbp-cross-customs-ruling-scraper").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 = {
    "sp_contact": "Share your email here...",
    "maxItems": 10,
    "collection": "ALL",
    "fromDate": "",
    "toDate": "",
    "fetchFullText": True,
}

# Run the Actor and wait for it to finish
run = client.actor("jungle_synthesizer/cbp-cross-customs-ruling-scraper").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 '{
  "sp_contact": "Share your email here...",
  "maxItems": 10,
  "collection": "ALL",
  "fromDate": "",
  "toDate": "",
  "fetchFullText": true
}' |
apify call jungle_synthesizer/cbp-cross-customs-ruling-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,jungle_synthesizer/cbp-cross-customs-ruling-scraper"
        }
    }
}
```

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/KO6CwX7VBQCmIVZ3T/builds/WpyRz9206w7SKdvUx/openapi.json
