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:
| Entry / line | Filed | Suggested | Flag | Conf. | Duty delta |
|---|---|---|---|---|---|
| EI-024-118-A5G smartphone, 6.7-inch OLED, 256GB storage (VN) | 8517.62.00.90 | 8517.13.00.00 | DISAGREEMENT | 0.91 | $0 |
| EI-024-203-BOutdoor LED string lighting set, steel posts (CN) | 9405.49.00.00 | 9405.42.84.10 | DISAGREEMENT|LOW_CONF | 0.55 | +$8,400 (S232 derivative) |
| EI-024-417-CSwitching power supply, 65W USB-C (CN) | 8504.40.85.00 | 8504.40.95.50 | DISAGREEMENT | 0.78 | +$0 (general Free) |
| EI-024-562-DCotton T-shirt, knit, women's, screen-printed (BD) | 6109.10.00.18 | 6109.10.00.27 | DISAGREEMENT | 0.83 | $0 (stat-suffix only) |
| EI-024-718-EAdjustable mechanical bed base, queen, motorized (MX) | 9403.20.00.86 | 9403.20.00.35 | DISAGREEMENT | 0.87 | $0 (general Free) |
| Highest-impact disagreement on the population | +$8,400 | ||||
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.