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. 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. 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. 3
    Inspect data.verdict in the JSON response. On warn or fail, walk data.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

POST 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.