Pre-shipment Validation API
Validate a proposed HS code before booking a shipment. Catch malformed codes, codes inactive in the destination's tariff schedule, codes that don't match the product description, and country-pair traps (Section 301, AD/CVD, quotas) before the parcel moves — not after the carrier rejects it or customs flags it.
Quickstart
-
1
Sign in to ClassifyHQ and visit Team Settings. Scroll to API Keys and click Generate key. The plaintext key (prefixed
ck_live_…) is shown once — copy it immediately. -
2
Send your first request:
curl -X POST https://classifyhq.ai/api/validate \ -H 'X-Api-Key: ck_live_…' \ -H 'Content-Type: application/json' \ -d '{"hs_code":"6109100012","description":"Cotton t-shirt knitted","origin_country":"CN","destination_country":"US"}' -
3
Inspect
data.verdictin the JSON response. Onwarnorfail, walkdata.findings[]for the rules that fired and act accordingly — block the shipment, surface a warning to ops, or pass through.
Authentication
Every request to /api/validate
must carry a valid API key. The middleware accepts any of three forms:
| Form | Example |
|---|---|
| X-Api-Key header | X-Api-Key: ck_live_abc1xyz… |
| Authorization Bearer | Authorization: Bearer ck_live_abc1xyz… |
| api_key query param | ?api_key=ck_live_abc1xyz… |
Keys are scoped to a single team and are stored only as SHA-256 hashes after generation —
ClassifyHQ cannot retrieve a plaintext key after creation. Lost keys must be revoked
and replaced. Revoke from the same Team Settings page; revocation takes effect
immediately and subsequent calls return 401.
Endpoint
https://classifyhq.ai/api/validate
Request body must be application/json. All checks are deterministic;
no LLM calls are made. Typical p95 latency is under 30 ms.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| hs_code | string | yes | Proposed HS code. 6, 8, or 10 digits, numeric. Dots and dashes are stripped automatically. |
| description | string | no | Plain-language product description. Strongly recommended — required to evaluate the description_match rule. |
| origin_country | string (2) | no | ISO 3166-1 alpha-2 code, e.g. CN, DK, VN. Required to evaluate country_pair_trap. |
| destination_country | string (2) | no | ISO 3166-1 alpha-2 code, e.g. US, GB, NO. Supported destinations: US, EU, GB, CA, NO. Required for country_code_active and required_documents. |
| declared_value | number | no | Declared value of the goods. Must be positive if provided. |
| currency | string (3) | no | ISO 4217 currency code, e.g. USD, EUR, DKK. |
| carrier | string | no | Identifier for the carrier (e.g. dhl-express, postnord). Future use for carrier-specific patterns. |
Response shape
All successful and warn responses use HTTP 200. A fail verdict
uses HTTP 422 so naive integrations that only branch on status codes still
get a clear signal. The body is always a JSON object with a top-level
data envelope plus meta.
{
"data": {
"verdict": "warn",
"input": {
"hs_code": "6109100012",
"origin_country": "CN",
"destination_country": "US"
},
"matched_code": {
"code": "610910",
"description": "T-shirts, singlets and other vests; of cotton, knitted or crocheted"
},
"findings": [
{"rule": "code_well_formed", "result": "pass", "message": "Code is 10 digits.", "context": []},
{"rule": "hs6_active", "result": "pass", "message": "Matched HS6: T-shirts...", "context": []},
{"rule": "country_code_active", "result": "warn", "message": "Code is not a valid leaf in the US tariff schedule.", "context": []},
{"rule": "description_match", "result": "pass", "message": "Description hint matches chapter 61.", "context": []}
],
"required_documents": [
{"name": "Textile country of origin declaration", "reason": "CBP requires textile origin documentation per 19 CFR 102.21."}
],
"evaluated_at": "2026-04-25T07:38:03+00:00"
},
"meta": {
"api_version": "v1",
"documentation": "https://classifyhq.ai/api/docs"
}
}
matched_code is the most specific tariff line we resolved — the destination's
country-specific code if the leaf was found, otherwise the HS6 prefix. matched_code
is null when no HS6 prefix could be matched at all.
Verdict semantics
pass
HTTP 200
Every rule passed. Safe to proceed with the shipment.
warn
HTTP 200
At least one rule produced a warning. Shipment is not blocked, but the importer should review and decide. Common cases: code is plausibly a real prefix but the full extension wasn't found in the destination's leaf data; country-pair surcharge applies; no description provided to sanity-check the chapter.
fail
HTTP 422
A hard failure. Code is malformed, HS6 prefix is unknown to the WCO schedule, or declared_value is non-positive. The shipment will almost certainly be rejected by carrier or customs.
Verdict is a roll-up across all findings: any fail finding produces
fail; otherwise any warn produces warn; otherwise
pass.
Rule reference
Each finding is tagged with a rule identifier so you can branch on
specific signals (e.g. block on code_well_formed.fail, alert on
country_pair_trap.warn, and pass through everything else).
| Rule | Possible results | What it checks |
|---|---|---|
| code_well_formed | pass / fail | HS code, after stripping non-digits, is 6, 8, or 10 digits long. Always evaluated. |
| hs6_active | pass / fail | First 6 digits resolve to a heading or subheading in the current WCO Harmonized System schedule. |
| country_code_active | pass / warn | If destination_country is supplied and the code is >6 digits, the full 8/10-digit extension resolves to a leaf in that country's tariff schedule (US HTSUS, EU CN, UK Global Tariff, CA Customs Tariff, NO Tolltariffen). |
| destination_format | warn | Code is shorter than the destination expects (e.g. supplied 6 digits where US/EU expect 10). Customs may apply a default extension. |
| description_match | pass / warn | Heuristic: the supplied product description contains a keyword consistent with the chapter (e.g. "wool" with chapter 51, "knit" with chapter 61). Skipped silently if no description is supplied. |
| country_pair_trap | warn | Origin/destination/chapter combination triggers a known surcharge or restriction (currently: US Section 301 on selected China-origin chapters). Coverage will expand over time. |
| declared_value | fail | If declared_value was provided, it must be a positive number. Only fires when the value is malformed. |
required_documents is returned alongside the findings as a separate field
(not as rule findings). It lists the customs documents typically required for the
origin/destination/chapter combination — e.g. Lacey Act for chapter 44 wood, textile
country-of-origin declaration for chapters 61–63 into the US.
Errors
| Status | Error code | When |
|---|---|---|
| 401 | missing_api_key | No API key was provided in any accepted form. |
| 401 | invalid_api_key | Key is unknown, malformed, or has been revoked. |
| 422 | — (Laravel validation) | Request body fails validation (missing hs_code, wrong types, country code not 2 chars, etc.). Response carries Laravel's standard errors map. |
| 422 | verdict: fail | Validation completed but produced a fail verdict (e.g. unknown HS6). Response is the normal envelope with data.verdict = "fail". |
| 429 | — (Laravel throttle) | Rate limit exceeded (100 req/min per IP). |
Integration patterns
Shopify webhook on order creation
Subscribe to orders/create. For every line item, look up the SKU's HS code
in your metafields and call /api/validate. On fail, post a
Slack alert to ops; on warn, write a tag to the order so customer service
sees it before fulfilment.
Pre-booking gate from your TMS / ERP
Before pushing a shipment to Ingrid / ShipStation / your carrier, run each line through
/api/validate. Block the booking on fail; surface
warn findings in the booking UI so the operator can confirm.
Nightly catalog sweep
Iterate over your SKU catalog overnight. Persist any warn or
fail findings to a reporting table; review the queue in the morning.
Catches drift introduced by manual edits, broker overrides, or stale country tariff
data.
// Node.js example — Shopify webhook handler
import express from 'express';
const app = express();
app.post('/webhooks/orders-create', express.json(), async (req, res) => {
const order = req.body;
for (const item of order.line_items) {
const r = await fetch('https://classifyhq.ai/api/validate', {
method: 'POST',
headers: {
'X-Api-Key': process.env.CLASSIFYHQ_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
hs_code: item.properties.hs_code,
description: item.title,
origin_country: item.origin_country,
destination_country: order.shipping_address.country_code,
declared_value: item.price,
currency: order.currency,
}),
});
const { data } = await r.json();
if (data.verdict !== 'pass') {
await alertOps(order.id, item.sku, data);
}
}
res.status(200).end();
});
Versioning & rate limits
The current API version is v1, surfaced in meta.api_version.
Breaking changes will increment the major version and run alongside the previous version
for at least 90 days. Rate limit is 100 requests per minute per IP, shared with the
public Tariff Reference API. If you need a higher limit, contact us.