Guide

How to validate broker HTS classifications using the Tandom HTS API as a second-pass check

Build a second-pass HTS classification audit on a year of broker entries using Tandom's classifier API. Workflow, code, and the obligation behind it.

Updated 11 min readJump to the script
Share:

TL;DR

  • The importer of record carries the 19 USC 1484 "reasonable care" obligation on classification, even when a licensed broker prepared the entry. Periodic review of broker work is named in CBP's Informed Compliance Publication on Reasonable Care as an indicator of diligence.
  • Loop a year of past entries through POST api.tandom.ai/v1/classify (closed beta) with one item per line. Compare the classifier's top-1 to the filed code. Flag any line where the codes differ or where confidenceScore < 0.6. Manual review goes to the flagged subset only.
  • The audit produces an evidence trail (request, response, timestamp, reviewer note per line) that satisfies the documentation prong of reasonable care. Errors surfaced get corrected via Post-Summary Correction (within 314 days of entry) or Prior Disclosure under 19 USC 1592(c)(4) (penalty capped at duty plus interest if filed before CBP initiates investigation).
  • Need to start without classifier-API access? The Tandom HTS Catalog search endpoint is live today and resolves dotted 10-digit codes against the same DB the classifier reads. A simpler audit (does the filed code exist, and what general/special rate does it carry) ships in an afternoon.

Why a second-pass check

A broker is the importer's filer, not the importer's compliance department. Under 19 USC 1484, the importer of record is the party legally responsible for using reasonable care to classify merchandise and report value, regardless of who keys the entry summary. The licensed broker carries a parallel duty under 19 CFR 111.32, but the broker's exercise of reasonable care does not discharge the importer's. CBP audits reach the importer; the importer's defense is the importer's process.

The reasonable-care standard is articulated in CBP's Informed Compliance Publication on Reasonable Care. The publication names periodic review of broker work, written internal classification procedures, and consultation of authoritative sources (CROSS, GRI, Section/Chapter Notes) as indicators a tribunal would weigh in evaluating an importer's diligence. The companion ICP on Tariff Classification covers the substantive HTSUS interpretation hierarchy that the audit's manual-review step relies on.

What an audit cannot do

A re-classification pass is not a binding ruling. The only binding answer to "what is the right HTS code" is a CBP ruling letter under 19 CFR Part 177. An audit's role is to surface disagreements between filed codes and a credible second opinion so the importer can act on them: request a binding ruling, file a Post-Summary Correction, file a Prior Disclosure, or update internal classification procedures.

Audit candidates

  • High-volume SKUs. A 0.5% misclassification rate on a 10,000-line annual entry book is 50 lines, often clustered on the same wrongly-classified SKU. One ruling can resolve the entire cluster going forward.
  • Section 232 / 301 / AD/CVD-adjacent classifications. A misclassification that moves a line out of an HTS heading with an additional duty layer carries financial weight an order of magnitude above the MFN-only delta.
  • Newly-onboarded brokers. First-year work from a new broker, especially on specialty-chapter goods (apparel, electronics, chemicals) where error rates are statistically higher.
  • Vague invoice descriptions. Lines where the broker classified from "plastic parts" or "machine accessories" are the highest-yield audit targets, because the broker had inadequate inputs and the auditor will also flag the description quality.

The audit workflow

A second-pass classification audit has four stages. Each stage either runs automatically against the API or surfaces a finite set of lines for human review.

1. Pull the population

Export 12 to 24 months of entry summary lines from the broker's ABI/ACE system (most brokers can pull this in a CSV format containing entry number, line, HTS, description, origin, quantity, unit price, manufacturer, importer-of-record, entry date). Confirm columns match the classifier's required inputs: description, country of origin, hint code (the filed HTS).

2. Run the classifier

POST batches of items to the classifier endpoint. The script below uses a concurrency cap of 5 in-flight requests so a 1,000-line population finishes in a few minutes without tripping rate limits. Save every request, response, and timestamp to a JSONL log alongside the input CSV.

3. Diff and flag

For each line, compare:

  • filed_hts_dotsstripped vs. suggestedHtsCode_dotsstripped. If different, flag.
  • confidenceScore. If below 0.6, flag (regardless of agreement).
  • htsValidationWarning field on the response. If present, flag.

A typical 1,000-line population yields a flagged subset in the 5 to 20% range depending on broker quality and SKU complexity. The exact rate is itself a metric the importer can track over time.

4. Manual review of the flagged subset

A licensed broker, customs counsel, or trained classifier reviews each flagged line, consults primary sources (CROSS, GRI, Chapter and Section Notes), and records a disposition: (a) filed code is correct, classifier wrong; (b) filed code is wrong, classifier right; (c) both reasonable, request a binding ruling; (d) description inadequate, escalate to invoice review. Record the reviewer name, date, and reasoning per line.

The script

A self-contained Node.js worker that reads a CSV, calls the classifier API in batches with concurrency control, and emits two outputs: a full JSONL log with every request and response, and a flagged-subset CSV ready for the manual reviewer. Drop it into any compliance team's tooling repo.

Inputs. entries.csv with columns: entry_no, line, filed_hts, description, country_of_origin, manufacturer, entry_date, declared_value.

Outputs. classifier_log.jsonl (full audit trail) and flagged.csv (the manual-review queue).

// audit.mjs (Node 20+, ESM)
import fs from "node:fs";
import { parse } from "csv-parse/sync";
import { stringify } from "csv-stringify/sync";

const TANDOM_API = "https://api.tandom.ai/v1/classify"; // closed beta
const API_KEY = process.env.TANDOM_API_KEY;
const BATCH_SIZE = 25;     // items per request
const CONCURRENCY = 5;     // in-flight requests
const CONFIDENCE_FLOOR = 0.6;

const stripDots = (s) => (s ?? "").replace(/\./g, "");

async function classifyBatch(items) {
  const res = await fetch(TANDOM_API, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Authorization": `Bearer ${API_KEY}`,
    },
    body: JSON.stringify({ items }),
  });
  if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
  return await res.json();
}

async function runWithConcurrency(jobs, limit) {
  const results = [];
  let next = 0;
  const workers = Array(limit).fill(0).map(async () => {
    while (next < jobs.length) {
      const i = next++;
      results[i] = await jobs[i]();
    }
  });
  await Promise.all(workers);
  return results;
}

const rows = parse(fs.readFileSync("entries.csv"), {
  columns: true,
  skip_empty_lines: true,
});

const batches = [];
for (let i = 0; i < rows.length; i += BATCH_SIZE) {
  batches.push(rows.slice(i, i + BATCH_SIZE));
}

const logStream = fs.createWriteStream("classifier_log.jsonl");
const flagged = [];

const jobs = batches.map((batch) => async () => {
  const items = batch.map((r) => ({
    description: r.description,
    countryOfOrigin: r.country_of_origin,
    hsCodeHint: r.filed_hts,
  }));
  const t0 = Date.now();
  const resp = await classifyBatch(items);
  const elapsedMs = Date.now() - t0;

  resp.results.forEach((result, idx) => {
    const r = batch[idx];
    logStream.write(JSON.stringify({
      entry_no: r.entry_no,
      line: r.line,
      filed_hts: r.filed_hts,
      suggested: result.suggestedHtsCode,
      confidence: result.confidenceScore,
      confidenceLevel: result.confidenceLevel,
      primarySource: result.primarySource,
      htsValidationWarning: result.htsValidationWarning ?? null,
      timestamp: new Date().toISOString(),
      elapsedMs,
    }) + "\n");

    const disagreement =
      stripDots(result.suggestedHtsCode) !== stripDots(r.filed_hts);
    const lowConfidence = result.confidenceScore < CONFIDENCE_FLOOR;
    const validationWarning = !!result.htsValidationWarning;

    if (disagreement || lowConfidence || validationWarning) {
      flagged.push({
        entry_no: r.entry_no,
        line: r.line,
        filed_hts: r.filed_hts,
        suggested_hts: result.suggestedHtsCode,
        confidence: result.confidenceScore.toFixed(2),
        flag: [
          disagreement && "DISAGREEMENT",
          lowConfidence && "LOW_CONFIDENCE",
          validationWarning && "VALIDATION_WARNING",
        ].filter(Boolean).join("|"),
        description: r.description,
        country_of_origin: r.country_of_origin,
        manufacturer: r.manufacturer,
        entry_date: r.entry_date,
      });
    }
  });
});

await runWithConcurrency(jobs, CONCURRENCY);
logStream.end();

fs.writeFileSync(
  "flagged.csv",
  stringify(flagged, { header: true }),
);

console.log(`Total: ${rows.length}. Flagged: ${flagged.length}. ` +
  `Flag rate: ${((flagged.length / rows.length) * 100).toFixed(1)}%.`);

Fallback for the live HTS Catalog search API. While classifier API access is in closed beta, an interim audit can hit GET /api/hts/search?q=<dotted-code> on tariffs.tandom.ai to confirm every filed code (a) exists in the HTSUS, (b) has a defined general/special rate, and (c) matches the description text on the response. This catches the single largest class of broker error (a code that does not exist or has been retired) without a beta-key dependency. The loop structure is identical, just swap the POST body for a GET query string and parse the simpler response shape.

Worked example

A consumer-electronics importer pulls a 1,000-line population covering 12 months of broker entries. The script above runs in about 4 minutes at concurrency 5 against a batched endpoint. The full population shape:

  • 1,000 lines across roughly 240 unique SKUs.
  • 72 lines flagged (7.2% flag rate). 41 disagreements, 24 low-confidence, 7 validation warnings.
  • Manual review of the 72 lines takes a licensed reviewer about 6 hours at 5 minutes per line.

The five highest-impact disagreements out of the 41 flagged for code mismatch:

flagged.csvTop 5 by duty exposure
Population1,000 lines / 72 flagged
Entry / lineFiledSuggestedFlagConf.Duty delta
EI-024-118-A5G smartphone, 6.7-inch OLED, 256GB storage (VN)8517.62.00.908517.13.00.00DISAGREEMENT0.91$0
EI-024-203-BOutdoor LED string lighting set, steel posts (CN)9405.49.00.009405.42.84.10DISAGREEMENT|LOW_CONF0.55+$8,400 (S232 derivative)
EI-024-417-CSwitching power supply, 65W USB-C (CN)8504.40.85.008504.40.95.50DISAGREEMENT0.78+$0 (general Free)
EI-024-562-DCotton T-shirt, knit, women's, screen-printed (BD)6109.10.00.186109.10.00.27DISAGREEMENT0.83$0 (stat-suffix only)
EI-024-718-EAdjustable mechanical bed base, queen, motorized (MX)9403.20.00.869403.20.00.35DISAGREEMENT0.87$0 (general Free)
Highest-impact disagreement on the population+$8,400
How to read this. DISAGREEMENT means the engine's top-1 differs from the filed code at any digit. LOW_CONF additionally flags lines where the engine's confidenceScore is below 0.6. Duty delta is computed by re-running the suggested code through the duty calculator with the entry's origin, value, and date.

Three of the five disagreements are statistical-suffix slips inside the same heading (different stat-suffix at digits 9-10, which is common with apparel and lighting where the suffix encodes assembly type or material content). Two cross subheading boundaries with material duty consequences. The Section 232-derivative line (lighting set with steel housing) is the highest-dollar exposure on the population.

Disposition

  • Lines 4 and 5 (apparel stat-suffix slips): broker corrects stat-suffix going forward. No PSC because there is no duty consequence (general rate is identical at the subheading).
  • Line 1 (smartphone vs. other transmission apparatus): the classifier is correct based on heading-2 explanatory note 5 for 8517.13. PSC filed within the 314-day window for the 11 entries in scope. Net duty refund: zero (both codes are general "Free"), but the filed code becomes the audit-trail answer.
  • Line 2 (lighting set with steel housing): the classifier flagged Section 232 derivative exposure. Engineering supplied a bill-of-materials confirming 100% steel content in the housing, 35% by total entered value. Section 232 50% on the steel-content portion was not previously declared. Prior Disclosure filed under 19 USC 1592(c)(4); penalty capped at duty plus interest.
  • Line 3 (power supply): both codes plausible, request a binding ruling under 19 CFR 177.

The audit closes with a memo to the file documenting the process, the flag rate, the dispositions, and the corrective actions taken with the broker. That memo is the reasonable-care evidence on the next CBP review.

Interpreting the output

The classifier returns four signals on every line. Each maps to a different reviewer action.

suggestedHtsCode and confidenceScore

The 10-digit code the engine commits to and a 0-1 score. The score reflects how strongly the engine's chosen primary source (CROSS ruling, exact prior product match, GRI walk) supports the code. Above 0.85 with a confirmed primary source maps to confidenceLevel "high"; 0.6 to 0.85 maps to "medium"; below 0.6 maps to "low" or "unclassified."

primarySource

Names the source the engine relied on. Possible values: cross_ruling (a CROSS ruling number that the engine retrieved and weighed), product_match (a prior classified product in the catalog with overlapping description and identical material/use), gri (the General Rules of Interpretation walk produced a unique answer), ai_only (no primary source available; the classifier is committing on description plus chapter-note knowledge alone). On audit, ai_only with confidenceScore above 0.6 is still useful as a second opinion; ai_only with confidenceScore below 0.6 is essentially "this line needs a human."

htsValidationWarning

Set when the suggested code does not parse cleanly against the current HTSUS. Common causes: a broker filed against a stat-suffix that was retired between the entry date and the audit date, a typo (digit transposition), or a code that exists in the HTSUS but has a footnote restricting its application (e.g., textile-quota lines that require an export visa). Always flag.

aiClassification.alternates

When the engine surfaces alternates, it ranks them with confidence scores. Use the top alternate as the de-facto "second opinion" when the primary suggestion disagrees with the filed code: if both engine top-1 and top-2 alternates differ from the filed code, the filed code is highly likely wrong. If the broker's filed code is the engine's top-2 alternate with a score within 0.05 of top-1, it is a defensible call and the right next step is a binding ruling, not a PSC.

Common pitfalls

The mistakes auditors make first time through.

Treating low confidence as a wrong-code signal

Low confidence means the engine could not commit, not that the broker's code is wrong. A vague description (e.g., "industrial part") forces low confidence even when the broker had access to better facts (engineering specs, BOMs) at the time of entry. Treat low confidence as an "investigate" flag, not a "broker wrong" flag.

Stripping dots inconsistently

Brokers store HTS codes in dotted (8471.30.01.00), undotted (8471300100), and short (8471.30.0100) formats. Always dots-strip both sides before comparing or false disagreements will overwhelm the flag count.

Ignoring stat-suffix-only disagreements

A disagreement at digits 9-10 with identical digits 1-8 may have zero duty consequence (general rate is set at the 8-digit tariff item) or it may matter (Census uses stat-suffix for trade statistics, and some Chapter 99 attachments key off 10-digit codes). Compute the duty delta on every flagged line before triaging.

Not preserving the API response payload

The audit-trail value of the workflow rests on saving the full response (primarySource, alternates, validation warnings) per line. A spreadsheet with just the suggested code is not enough. CBP audits look for the underlying reasoning. Save JSONL and retain it for the seven-year importer record-keeping period under 19 CFR 163.4.

Re-classifying without checking AD/CVD scope

A correct HTS code does not answer the AD/CVD question. Antidumping and countervailing scope is described in product terms in each order's Federal Register text; HTS codes in orders are advisory. Feed the classifier's top-1 into the Tandom AD/CVD lookup as the second leg of any complete audit.

Filing PSC outside the 314-day window

Per 19 CFR 101.9, Post-Summary Correction is available within 314 days of the entry date and only on entries that are not yet liquidated. Beyond 314 days, the path is Prior Disclosure under 19 USC 1592(c)(4) for entries with duty consequences. An audit that surfaces a year-old error and tries to file PSC will get rejected; switch to Prior Disclosure.

Treating broker confirmation as a defense

"The broker confirmed the code" is not a reasonable-care defense for the importer. The broker confirms the broker's process; the importer documents the importer's review of the broker's process. The audit IS the importer's documented review.

Auditing the broker's worst quarter

A targeted audit of the broker's known weak chapter (e.g., a forwarder strong on industrial parts but weak on textiles) produces a flag rate that does not generalize. Audit a stratified random sample across chapters first; deep-dive on one chapter only after the stratified sample identifies it.

Forgetting to include broker hint in the API call

The classifier weights the broker's filed code as a hint, not a ground truth. Sending the call without hsCodeHint changes the engine's behavior and produces noisier alternates. Include the hint on every call so the engine can either corroborate or override it explicitly.

Glossary

Reasonable care
The 19 USC 1484 standard that the importer of record exercise "reasonable care" in entering, classifying, and valuing imported merchandise. CBP's Informed Compliance Publication on Reasonable Care lists indicia: written procedures, broker review, consultation of authoritative sources, prior rulings, and qualified personnel.
Importer of record
The party identified on the entry summary as legally responsible for the entry. Carries the substantive classification and valuation duties under 19 USC 1484 regardless of who keys the entry.
Post-Summary Correction (PSC)
CBP's mechanism for an importer to correct an entry summary after filing but before liquidation. Available within 314 days of entry date per 19 CFR 101.9. Primary path for non-fraud-based corrections inside that window.
Prior Disclosure
19 USC 1592(c)(4) procedure for an importer to disclose a past violation to CBP before CBP discovers it. Caps penalty at the duty owed plus interest, vs. uncapped penalties on CBP-initiated 1592 cases.
CROSS
Customs Rulings Online Search System. CBP's database of binding ruling letters under 19 CFR Part 177. Authoritative source for "what code does CBP say applies to this product" on prior similar facts.
GRI (General Rules of Interpretation)
The six-rule HTSUS interpretation hierarchy. Rule 1 (heading-text plus Section/Chapter Notes) is the dominant anchor; the rest resolve ties (relative specificity, essential character, last in numerical order).
confidenceScore
The classifier's 0-1 self-reported certainty in its top-1 suggested code. Reflects strength of the underlying primary source. 0.6 is a defensible flag floor for first-pass triage.
primarySource
The classifier's response field naming the underlying authoritative source (cross_ruling, product_match, gri, ai_only) the engine relied on. Required for audit trail.
htsValidationWarning
Response field set when the suggested code does not parse cleanly against the current HTSUS (retired stat-suffix, typo, footnote restriction). Always flag for review.
Stat-suffix
Digits 9-10 of a 10-digit HTSUS code. US-specific statistical breakouts beneath the 8-digit international tariff item. Set duty rate at the 8-digit level; statistical reporting uses the 10-digit. Disagreements at digits 9-10 with identical 1-8 typically have no duty consequence.
ABI/ACE
Automated Broker Interface / Automated Commercial Environment. CBP's entry-filing platform; brokers can export 12 to 24 months of entry summary lines from their ABI client.
Binding ruling
CBP determination under 19 CFR Part 177 on the classification (or value, origin, marking) of a specific product. Binding on CBP and the requestor for substantially identical transactions until modified or revoked.
Focused Assessment
CBP audit program targeting importer compliance with classification, valuation, and special-program claims. Reasonable-care evidence (written procedures, audit memos, broker review records) is the importer's primary defense.
Recordkeeping (19 CFR 163.4)
The five-year (entries) and seven-year (general books and records) importer recordkeeping retention requirement. Audit artifacts (request payloads, API responses, reviewer notes) fall under this retention.

FAQ

High-intent questions importers and compliance teams ask before starting an audit.

Is broker classification review a regulatory obligation or a cost-saving exercise?
Both, but the regulatory obligation comes first. 19 USC 1484 puts the importer of record on the hook for using "reasonable care" when classifying merchandise, regardless of whether a broker prepared the entry. CBP's Informed Compliance Publication on Reasonable Care lists periodic review of broker work as one of the named indicia of reasonable care. A second-pass check on a sampled or full population of past entries is one of the cleanest ways to evidence that diligence on audit. The duty-savings angle is real (misclassification routinely costs five to seven figures over a year on commoditized SKUs) but it is downstream of the compliance obligation.
Can I do a second-pass review without an API by manually re-classifying every line?
Yes for tens of lines. No for a year of broker entries. A typical importer ships hundreds to thousands of unique SKU/origin combinations a year. Manual re-classification at five to fifteen minutes per line yields a multi-week project that produces the same answer as a thirty-minute automated run. The compliance value is the same; the cost is the difference. The Tandom classifier API at api.tandom.ai/v1/classify (closed beta, request access) accepts a batch of line items and returns top-1 plus alternates with confidence scores so the broker only re-reviews the flagged subset.
What confidence threshold should I use to flag a line for review?
0.6 on the classifier's confidenceScore is a defensible default for first-pass triage. Below 0.6 means the engine could not consult an authoritative source (CROSS ruling, prior product match, GRI-derived match) confidently enough to commit. Above 0.6 with a different top-1 code than the broker filed is the higher-signal flag, because the classifier is confident the other code fits better. The two flags answer different questions: low confidence says "this line was hard for the engine, was it hard for the broker too," different top-1 says "the engine and the broker disagree, who is right."
Does the classifier consider AD/CVD scope or just the HTS code?
The classifier returns the 10-digit HTS code and confidence. AD/CVD scope is a separate engine because scope is described in product terms in the order's Federal Register text, not in the HTS code. The Tandom AD/CVD lookup at compliance.tandom.ai/adcvd-catalog runs on every active order against the HTS code, country, manufacturer, and entry date. A complete audit feeds the classifier's top-1 code into the AD/CVD lookup as the second leg.
What inputs does the classifier need from my entry CSV?
Description (free-text product description, the same one the broker classified from), country of origin (ISO 2-letter), and a hint code if the broker filed one. Optional: quantity, unit, unit price, material, seller name. The classifier weights the hint heavily but does not blindly accept it. If the description and the hint disagree, the classifier returns the description-supported top-1 and surfaces the hint as a lower-ranked alternate with a note.
How do I document the result for a CBP Focused Assessment or audit?
Save the request payload, the API response, and the timestamp for every line, plus the manual reviewer's disposition note for any flagged line. CBP audits look for a documented process applied consistently. A spreadsheet of "engine top-1 vs filed code, action taken if disagreement, who reviewed and when" satisfies the documentation prong of reasonable care. Tandom's classifier responses include a primarySource field naming the source the engine used (CROSS ruling number, product match, GRI step) so the audit trail goes one level deeper than "the AI said so."
Will running a re-classification expose me to a CBP penalty if it surfaces past errors?
The opposite. Filing a Prior Disclosure under 19 USC 1592(c)(4) before CBP discovers the error caps the penalty at the duty owed plus interest. Discovering errors through a self-audit, filing a Post-Summary Correction within the 314-day window where eligible, or filing a Prior Disclosure where the window has closed, is the recognized compliance path. The penalty regime is built around the assumption that compliant importers will surface and correct errors. The risk is in the entries you do not review.
How does the classifier handle vague broker descriptions like "plastic parts" or "electronic components"?
It returns a low confidence score (typically below 0.4) and surfaces alternates with notes about what additional inputs would resolve the ambiguity (e.g., material composition, end use, dimensions). A vague description is the broker's signal that the importer's commercial invoice was inadequate, not a flaw in the classifier. The audit's value here is to flag every vague description in the population so the importer can ask the supplier for proper invoice detail going forward, which is itself a reasonable-care indicator under the ICP.
Share: