Guides · Customs classification API

Turn a product title into a
defensible HS code dossier.

A product title is rarely enough to classify, and a code without proof gets rejected by the forwarder. The Customs Classification API asks for the missing facts instead of guessing, returns the national line for the destination and the date you ship, quotes the notes behind each candidate, and keeps a dossier you can hand to a licensed broker. One product or a 2,000-item catalog.

The pain first

Why HS code classification breaks in production

We read importers, merchants and brokers on Reddit and trade forums (22–23 September 2026). They do not want “description → HS code”. They want product + origin + destination + date → the local classification, the import obligations, the full cost, the sources and a validation status. Seven pains came back, each with what the API answers today and what it still does not.

1

The product facts are missing

“Most commercial invoices I’ve seen in my career are not descriptive enough on their own for classification” (thread)

“we have to do the HS codes ourselves” (thread)

What the API answers today

POST /v2/customs/classifications answers needs_information with missing_attributes and questions (fact, question, why, discriminates) instead of guessing. Send what you know in facts, and a kit as components (up to 10).

Not yet

Photos or documents as input.

2

Volume turns one error into catalog debt

“i sell over 10k products and now EU want me to fill all tariff/HS code in all products” (thread)

“I have 15,000 products but only 5 product categories” (thread)

What the API answers today

POST /v2/customs/classification-batches: up to 2,000 items, Idempotency-Key replay within 24 h, per-item status and error, items in submission order, an items.csv export, and the webhooks classification_batch.completed / .failed.

Not yet

Rules by product family, or category inheritance.

3

The “API” need is often data transport

“They need the HS Code in the line item details” (thread)

“Updating spreadsheets, copying values into Shopify” (thread)

What the API answers today

Connectors: Shopify pull → batch CSV → apply, plus CSV profiles for Odoo, SAP GTS and WooCommerce, shipped in @cleo-legal/sdk 0.9.0 as npx @cleo-legal/sdk connectors … (installable from the GitHub release today; npm registry publication pending). They write back only human-approved codes by default. See Connectors.

Not yet

The Shopify connector has not been run against a live Shopify store.

4

A confidence score without proof destroys trust

“our freight forwarder keeps rejecting the HS codes we provide” (thread)

An AI tool returned a code that did not exist, with a high score. (thread)

What the API answers today

Every candidate carries verbatim evidence (HS Section and Chapter Notes, GRI, and for EU systems the CN Additional Notes and EU classification regulations), evidence_source, set_aside_reason on alternatives, and needs_review when the evidence cannot support the top candidate.

Not yet

National rulings outside the EU (US CROSS was probed: zero results for every term).

5

A code is not portable across countries or years

The same product gets a different complete code in each destination. (thread)

What the API answers today

country and as_of on every call; HS6 and the national line returned separately; GET /v2/customs/codes/validate and the GET /v2/customs/codes/changes feed. 112 of 249 territories answer at the national line.

Not yet

137 territories answer at HS6 only. See Coverage.

6

The consequence arrives too late: checkout, hold, audit

“My DHL shipment is now held at US customs” (thread)

Merchants report recurring problems with a duty calculator at checkout. (thread)

What the API answers today

Classify the catalog before the parcel leaves. POST /v2/compliance/check carries one code through obligations, dual-use, duties and landed cost and returns a readiness with its blockers; landed cost says in the number when a component could not be priced. See The full decision.

Not yet

DDP collection at checkout; anti-dumping, countervailing and safeguard values (0 rows); specific per-unit duties are not computed.

7

Liability imposes a human workflow

“Everything is manually keyed into the entry software” (thread)

Brokers have to be able to explain each classification decision. (thread)

What the API answers today

persist: true stores the classification; GET /v2/customs/classifications/{id}/dossier renders it as JSON or PDF, kept 400 days. A named reviewer approves, rejects or requests changes with POST /v2/customs/classifications/{id}/review (and the same route on a batch item); an approved record is locked, every decision is kept in an append-only history, and the dossier shows the review state. See Human review.

Not yet

Decisions are recorded one record at a time (no bulk approval route), and reviewer is the name or e-mail you send, not a Cleo user account.

Customs classification API

Classify a product to an HS code with facts, not guesses

Send what you know. POST /v2/customs/classifications takes an item_id, a description, a destination country, optional structured facts (material, composition, function, use, process, dimensions, weight, power, voltage, presentation, audience…), an optional system and an optional as_of date. The description is not parsed into facts: a fact counts only when you send it in facts.

A bare “T-shirt” for the United States comes back needs_information: knitted or woven splits the chapter, the fibre splits the subheading, the wearer splits the heading. Ask your supplier those three questions, or show them to your merchant, then call again with facts (see the same T-shirt with facts under Evidence). This bare request was captured before the notes index was built, so its evidence_source reads unavailable.

composition is an array of { material, percent } objects, for example [{ "material": "cotton", "percent": 100 }]. Any other shape is rejected with a 400 whose body lists fieldErrors.

Runnable versions of every flow on this page (cURL, TypeScript and Python: needs_information, the full decision, a batch with Idempotency-Key, human review, signed webhooks) are in the cleo-customs-examples repository.

curl · captured 2026-09-25
curl -X POST "https://api.legaldata.cleolabs.co/v2/customs/classifications" \
  -H "Authorization: Bearer $CLEO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"item_id":"SKU-TSHIRT-9","description":"T-shirt","country":"US"}'
response · 200 · trimmed to 2 of 5 candidates
{
  "data": {
    "item_id": "SKU-TSHIRT-9",
    "status": "needs_information",
    "query_facts": {},
    "missing_attributes": [
      "process",
      "composition",
      "audience"
    ],
    "questions": [
      {
        "fact": "process",
        "question": "Is the garment knitted or crocheted (chapter 61) or woven (chapter 62)?",
        "why": "Chapters 61 and 62 split on knitted vs woven fabric.",
        "discriminates": "chapter"
      },
      {
        "fact": "composition",
        "question": "What is the fibre composition by weight (e.g. cotton 60%, polyester 40%)?",
        "why": "Subheadings split on the predominant fibre (cotton, synthetic, wool, other).",
        "discriminates": "subheading"
      },
      {
        "fact": "audience",
        "question": "Is it for men or boys, for women or girls, or for babies?",
        "why": "Headings 6103/6104, 6105/6106, 6107/6108 split by wearer (men or boys vs women or girls); heading 6111 covers babies' garments.",
        "discriminates": "heading"
      }
    ],
    "candidates": [
      {
        "code": "6109100004",
        "system": "hts",
        "country": "US",
        "hs6_parent": "610910",
        "title": {
          "en": "T-shirts, all white, short hemmed sleeves, hemmed bottom, crew or round neckline, or V-neck with a mitered seam at the center of the V, without pockets, trim or embroidery (352)"
        },
        "confidence": 0.92,
        "rationale": "This is the most specific US HTS code for T-shirts, explicitly describing the garment type with detailed specifications for construction and style. It directly matches the product category with precise classification criteria.",
        "evidence": [],
        "evidence_available": false,
        "set_aside_reason": null,
        "ambiguous": true
      },
      {
        "code": "610910",
        "system": "hs6",
        "country": null,
        "hs6_parent": "610910",
        "title": {
          "en": "T-shirts, singlets and other vests; of cotton, knitted or crocheted"
        },
        "confidence": 0.88,
        "rationale": "HS6 code 610910 covers T-shirts and vests of cotton that are knitted or crocheted, which is the standard construction method for most T-shirts. This is the appropriate harmonized tariff classification at the 6-digit level.",
        "evidence": [],
        "evidence_available": false,
        "set_aside_reason": null,
        "ambiguous": true
      }
    ],
    "coverage": {
      "level": "national",
      "national_systems": [
        "hts"
      ],
      "via_bloc": null,
      "hint": null
    },
    "provenance": {
      "question_config_version": "2026-09-24",
      "families": [
        "apparel-knitted-61"
      ],
      "reranker": "bedrock-haiku",
      "acceptance_policy": "provisional_floor_0.7",
      "evidence_source": "unavailable",
      "evidence_rejected": 0
    }
  },
  "advisory_disclaimer": "These suggestions are informational only. Final customs classification, export licensing and product certification must be validated by a licensed broker or competent authority. Misclassification or missed dual-use controls carry legal liability."
}
Kits and gift sets

Classify a kit: one HS code per component

A gift box is one SKU and several goods. Send it with 2 to 10 components; each one is classified on its own with the kit’s country, system and date, and kit_analysis says whether a GRI 3(b) set treatment may apply. Cleo never decides essential character. Here, a French gift box (scented soy-wax candle, ceramic holder, safety matches) with facts on each component: the candle comes back needs_review on 340600 (no note was cited to support it), the holder ambiguous on 691390 with Chapter 69 Notes 1 and 2, and the matches ambiguous on 36050000. The whole item is needs_review, and kit_analysis.may_be_set is false.

request body · captured 2026-09-25
curl -X POST "https://api.legaldata.cleolabs.co/v2/customs/classifications" \
  -H "Authorization: Bearer $CLEO_API_KEY" \
  -H "Content-Type: application/json" \
  -d @coffret.json   # the body below

{
  "item_id": "SKU-COFFRET-01",
  "description": "Coffret cadeau : bougie parfumée en cire de soja avec photophore en céramique et boîte d'allumettes",
  "country": "FR",
  "lang": "fr",
  "facts": {
    "use": "home decoration gift set",
    "material": "soy wax, ceramic, wood and phosphorus matches"
  },
  "components": [
    {
      "component_id": "c1",
      "description": "bougie parfumée en cire de soja",
      "quantity": 1,
      "facts": {
        "material": "soy wax",
        "use": "scented candle"
      }
    },
    {
      "component_id": "c2",
      "description": "photophore en céramique (porte-bougie)",
      "quantity": 1,
      "facts": {
        "material": "ceramic",
        "use": "candle holder"
      }
    },
    {
      "component_id": "c3",
      "description": "boîte d'allumettes de sûreté en bois",
      "quantity": 1,
      "facts": {
        "material": "wood, safety matches",
        "use": "lighting candles"
      }
    }
  ]
}
response · 200 · trimmed to 2 candidates per component
{
  "data": {
    "item_id": "SKU-COFFRET-01",
    "status": "needs_review",
    "missing_attributes": [],
    "coverage": {
      "level": "national",
      "national_systems": [
        "cn8"
      ],
      "via_bloc": "EU",
      "hint": null
    },
    "provenance": {
      "acceptance_policy": "provisional_floor_0.7+cited_evidence_required",
      "evidence_source": "index",
      "evidence_rejected": 0
    },
    "kit_analysis": {
      "may_be_set": false,
      "headings": [],
      "note": ""
    },
    "components": [
      {
        "component_id": "c1",
        "quantity": 1,
        "status": "needs_review",
        "missing_attributes": [],
        "candidates": [
          {
            "code": "340600",
            "system": "hs6",
            "country": null,
            "title": {
              "en": "Candles, tapers and the like"
            },
            "confidence": 0.92,
            "evidence": [],
            "ambiguous": false
          },
          {
            "code": "152110",
            "system": "hs6",
            "country": null,
            "title": {
              "en": "Vegetable waxes (other than triglycerides); whether or not refined or coloured"
            },
            "confidence": 0.08,
            "evidence": [],
            "ambiguous": false
          }
        ]
      },
      {
        "component_id": "c2",
        "quantity": 1,
        "status": "ambiguous",
        "missing_attributes": [],
        "candidates": [
          {
            "code": "691390",
            "system": "hs6",
            "country": null,
            "title": {
              "en": "Ceramic statuettes and other ornamental ceramic articles; other than of porcelain or china"
            },
            "confidence": 0.92,
            "evidence": [
              {
                "kind": "chapter_note",
                "ref": "Chapter 69 Note 2",
                "url": "https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32025R1926",
                "excerpt": "This chapter does not cover:\n\n(a)\n\nproducts of heading 2844 ;\n\n(b)\n\narticles of heading 6804 ;\n\n(c)\n\narticles of Chapter 71 (for example, imitation jewellery);\n\n(d)\n\ncermets of heading 8113 ;\n\n(e)\n\narticles of Chapter 82;\n\n(f)\n\nelectrical insulators (heading 8546 ) or fittings of insulating material of heading 8547 ;\n\n(g)\n\nartificial teeth (heading 9021 );\n\n(h)\n\narticles of Chapter 91 (for example, clocks and clock cases);\n\n(ij)\n\narticles of Chapter 94 (for example, furniture, luminaires and lighting fittings, prefabricated buildings);",
                "source_version": "32025R1926"
              },
              {
                "kind": "chapter_note",
                "ref": "Chapter 69 Note 1",
                "url": "https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32025R1926",
                "excerpt": "This chapter applies only to ceramic products which have been fired after shaping:\n\n(a)\n\nheadings 6904 to 6914 apply only to such products other than those classifiable in headings 6901 to 6903 ;\n\n(b)\n\narticles heated to temperatures less than 800 °C for purposes such as curing of resins, accelerating hydration reactions, or for the removal of water or other volatile components, are not considered to be fired.",
                "source_version": "32025R1926"
              }
            ],
            "ambiguous": true
          },
          {
            "code": "691310",
            "system": "hs6",
            "country": null,
            "title": {
              "en": "Ceramic statuettes and other ornamental ceramic articles, of porcelain or china"
            },
            "confidence": 0.85,
            "evidence": [
              {
                "kind": "chapter_note",
                "ref": "Chapter 69 Note 2",
                "url": "https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32025R1926",
                "excerpt": "This chapter does not cover:\n\n(a)\n\nproducts of heading 2844 ;\n\n(b)\n\narticles of heading 6804 ;\n\n(c)\n\narticles of Chapter 71 (for example, imitation jewellery);\n\n(d)\n\ncermets of heading 8113 ;\n\n(e)\n\narticles of Chapter 82;\n\n(f)\n\nelectrical insulators (heading 8546 ) or fittings of insulating material of heading 8547 ;\n\n(g)\n\nartificial teeth (heading 9021 );\n\n(h)\n\narticles of Chapter 91 (for example, clocks and clock cases);\n\n(ij)\n\narticles of Chapter 94 (for example, furniture, luminaires and lighting fittings, prefabricated buildings);",
                "source_version": "32025R1926"
              },
              {
                "kind": "chapter_note",
                "ref": "Chapter 69 Note 1",
                "url": "https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32025R1926",
                "excerpt": "This chapter applies only to ceramic products which have been fired after shaping:\n\n(a)\n\nheadings 6904 to 6914 apply only to such products other than those classifiable in headings 6901 to 6903 ;\n\n(b)\n\narticles heated to temperatures less than 800 °C for purposes such as curing of resins, accelerating hydration reactions, or for the removal of water or other volatile components, are not considered to be fired.",
                "source_version": "32025R1926"
              }
            ],
            "ambiguous": true
          }
        ]
      },
      {
        "component_id": "c3",
        "quantity": 1,
        "status": "ambiguous",
        "missing_attributes": [],
        "candidates": [
          {
            "code": "36050000",
            "system": "cn8",
            "country": "EU",
            "title": {
              "de": "Zündhölzer, ausgenommen pyrotechnische Waren der Position 3604",
              "en": "Matches, other than pyrotechnic articles of heading 3604",
              "es": "Fósforos (cerillas) (excepto los artículos de pirotecnia de la partida 3604)",
              "fr": "Allumettes, autres que les articles de pyrotechnie du no 3604",
              "it": "Fiammiferi, diversi dagli articoli pirotecnici della voce 3604"
            },
            "confidence": 0.95,
            "evidence": [],
            "ambiguous": true
          },
          {
            "code": "360500",
            "system": "hs6",
            "country": null,
            "title": {
              "en": "Matches; other than pyrotechnic articles of heading no. 3604"
            },
            "confidence": 0.94,
            "evidence": [],
            "ambiguous": true
          }
        ]
      }
    ]
  },
  "advisory_disclaimer": "These suggestions are informational only. Final customs classification, export licensing and product certification must be validated by a licensed broker or competent authority. Misclassification or missed dual-use controls carry legal liability."
}
Evidence and statuses

HS code evidence you can show your forwarder

Every candidate carries an evidence array: verbatim excerpts of stored notes and EU classification regulations that the reranker cited by id. The model only picks ids; it never writes evidence, and a cited id outside the offered set is dropped and counted in provenance.evidence_rejected. Each excerpt is a prefix of the stored text, at most 600 characters, with its ref, url and source_version.

provenance.evidence_source says whether the notes index was available (index) or not (unavailable). On 25 September 2026 the index holds 102 note blocks from Regulation (EU) 2025/1926: 6 GRI, 9 Section Notes and 87 Chapter Notes. EU classification regulations: 1 indexed on 25 September 2026; the rest is being re-parsed.

Evidence is scoped to the tariff system

The same men’s knitted cotton T-shirt, with the same facts, for France and for the United States, captured on 29 September 2026. Both come back ambiguous; the confidences are in the responses. For France (EU, cn8) the evidence includes a CN Additional Note, “Chapter 61 Additional Note 2”. For the United States the evidence is the WCO HS notes only: no CN Additional Note is offered outside the EU.

France · request · captured 2026-09-29
curl -X POST "https://api.legaldata.cleolabs.co/v2/customs/classifications" \
  -H "Authorization: Bearer $CLEO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"item_id":"SKU-TS-100","description":"T-shirt en coton, maille (tricoté), 100 % coton, pour hommes","country":"FR","lang":"fr","facts":{"process":"knitted","composition":[{"material":"cotton","percent":100}],"audience":"men"}}'
France · response · 200 · trimmed to 2 of 5 candidates
{
  "data": {
    "item_id": "SKU-TS-100",
    "status": "ambiguous",
    "missing_attributes": [],
    "candidates": [
      {
        "code": "61091000",
        "system": "cn8",
        "country": "EU",
        "confidence": 0.54,
        "evidence": [
          {
            "kind": "chapter_note",
            "ref": "Chapter 61 Note 1",
            "url": "https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32025R1926",
            "excerpt": "This chapter applies only to made-up knitted or crocheted articles.",
            "source_version": "32025R1926"
          },
          {
            "kind": "additional_note",
            "ref": "Chapter 61 Additional Note 2",
            "url": "https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32025R1926",
            "excerpt": "For the purposes of heading 6109 , the terms 'singlets' and 'other vests' include garments, even if of a fancy design, worn next to the body, without collar, with or without sleeves, including those with shoulder straps.\n\nThese garments, which are intended to cover the upper part of the body, often possess many characteristics in common with those of T-shirts or with more traditional kinds of singlets and other vests of heading 6109 .",
            "source_version": "32025R1926"
          },
          {
            "kind": "chapter_note",
            "ref": "Chapter 61 Note 5",
            "url": "https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32025R1926",
            "excerpt": "Heading 6109 does not cover garments with a drawstring, ribbed waistband or other means of tightening at the bottom of the garment.",
            "source_version": "32025R1926"
          }
        ],
        "set_aside_reason": null,
        "ambiguous": false
      },
      {
        "code": "610910",
        "system": "hs6",
        "country": null,
        "confidence": 0.26,
        "evidence": [
          {
            "kind": "chapter_note",
            "ref": "Chapter 61 Note 1",
            "url": "https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32025R1926",
            "excerpt": "This chapter applies only to made-up knitted or crocheted articles.",
            "source_version": "32025R1926"
          },
          {
            "kind": "additional_note",
            "ref": "Chapter 61 Additional Note 2",
            "url": "https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32025R1926",
            "excerpt": "For the purposes of heading 6109 , the terms 'singlets' and 'other vests' include garments, even if of a fancy design, worn next to the body, without collar, with or without sleeves, including those with shoulder straps.\n\nThese garments, which are intended to cover the upper part of the body, often possess many characteristics in common with those of T-shirts or with more traditional kinds of singlets and other vests of heading 6109 .",
            "source_version": "32025R1926"
          }
        ],
        "set_aside_reason": "It is the 6-digit parent, less specific than the CN8 line required for FR.",
        "ambiguous": false
      }
    ],
    "coverage": {
      "level": "national",
      "national_systems": [
        "cn8"
      ],
      "via_bloc": "EU",
      "hint": null
    },
    "provenance": {
      "reranker": "bedrock-sonnet-5.5",
      "acceptance_policy": "provisional_floor_0.7+signal_agreement_v1+thresholds_sonnet_v1+cited_evidence_required",
      "acceptance_checks": [
        "calibrated_confidence_below_floor"
      ],
      "evidence_source": "index",
      "evidence_rejected": 0,
      "tree_descent": {
        "hs6": [
          "610910"
        ],
        "children_count": 1,
        "chosen": "61091000",
        "path": [
          "T-shirts, singlets and other vests, knitted or crocheted",
          "Of cotton (6109.10)",
          "61091000 Of cotton"
        ]
      }
    }
  }
}
United States · request · captured 2026-09-29
curl -X POST "https://api.legaldata.cleolabs.co/v2/customs/classifications" \
  -H "Authorization: Bearer $CLEO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"item_id":"SKU-TS-US","description":"Men's knitted cotton T-shirt, 100% cotton","country":"US","facts":{"process":"knitted","composition":[{"material":"cotton","percent":100}],"audience":"men"}}'
United States · response · 200 · trimmed to 2 of 5 candidates
{
  "data": {
    "item_id": "SKU-TS-US",
    "status": "ambiguous",
    "missing_attributes": [],
    "candidates": [
      {
        "code": "610910",
        "system": "hs6",
        "country": null,
        "confidence": 0.81,
        "evidence": [
          {
            "kind": "chapter_note",
            "ref": "Chapter 61 Note 1",
            "url": "https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32025R1926",
            "excerpt": "This chapter applies only to made-up knitted or crocheted articles.",
            "source_version": "32025R1926"
          },
          {
            "kind": "chapter_note",
            "ref": "Chapter 61 Note 5",
            "url": "https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32025R1926",
            "excerpt": "Heading 6109 does not cover garments with a drawstring, ribbed waistband or other means of tightening at the bottom of the garment.",
            "source_version": "32025R1926"
          }
        ],
        "set_aside_reason": null,
        "ambiguous": false
      },
      {
        "code": "6109100004",
        "system": "hts",
        "country": "US",
        "confidence": 0.03,
        "evidence": [
          {
            "kind": "chapter_note",
            "ref": "Chapter 61 Note 1",
            "url": "https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32025R1926",
            "excerpt": "This chapter applies only to made-up knitted or crocheted articles.",
            "source_version": "32025R1926"
          },
          {
            "kind": "chapter_note",
            "ref": "Chapter 61 Note 5",
            "url": "https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32025R1926",
            "excerpt": "Heading 6109 does not cover garments with a drawstring, ribbed waistband or other means of tightening at the bottom of the garment.",
            "source_version": "32025R1926"
          }
        ],
        "set_aside_reason": "It is a narrower US statistical line whose all-white, no-trim conditions are not established by the facts, so the broader 610910 ranks above it.",
        "ambiguous": false
      }
    ],
    "coverage": {
      "level": "national",
      "national_systems": [
        "hts"
      ],
      "via_bloc": null,
      "hint": null
    },
    "provenance": {
      "reranker": "bedrock-sonnet-5.5",
      "acceptance_policy": "provisional_floor_0.7+signal_agreement_v1+thresholds_sonnet_v1+cited_evidence_required",
      "acceptance_checks": [
        "calibrated_confidence_below_floor"
      ],
      "evidence_source": "index",
      "evidence_rejected": 0
    }
  }
}

Each answer holds five candidates; two are printed above. Confidence values and candidate order can differ from one call to the next. Pin system when you want the national line.

Evidence kinds
  • gri — A General Interpretative Rule. It applies to every product, so a GRI alone never supports classified.
  • section_note — An HS Section Note, offered for every tariff system.
  • chapter_note — An HS Chapter Note, offered for every tariff system.
  • additional_note — A CN Additional Note. EU only: system cn8 or an EU importing country.
  • ruling — An EU classification regulation. EU only, at most 3 per 6-digit subheading offered to the reranker.
Classification statuses
  • classified — Every splitting fact present, a top candidate that is not ambiguous, a confidence at or above the floor of the model that answered (provenance.acceptance_policy names it), independent signals that agree, and (when the notes index is available) at least one cited note or ruling.
  • needs_information — A fact that splits the chapter, heading or subheading of a contending candidate is missing. The questions tell you which.
  • needs_review — The evidence cannot support the top candidate. Missing support, not hesitation.
  • ambiguous — Hesitation between codes.
  • multi_line — A kit whose classified components need different codes; kit_analysis names the possible GRI 3(b) set treatment. Cleo never decides essential character.
  • no_match — Nothing was retrieved.
  • unsupported_jurisdiction — The country is not served by the registry.
  • stale_dataset — No enabled catalog covers the as_of date you asked for.

Alternatives that were not retained are listed in alternatives_set_aside, each with a set_aside_reason. The rationale on a candidate is model-written; the evidence is quoted. Show both, labelled.

Classification dossier

Keep a customs classification dossier (JSON or PDF)

Add persist: true and the response carries a classification_id. GET /v2/customs/classifications/{id}/dossier renders it as JSON, or as a PDF with ?format=pdf: the description and facts sent, the retained code or the questions still open, every candidate with its rationale and evidence, the dataset versions and the source and licence basis of each catalog as captured when the classification was persisted. A code is retained only under classified; every other status says “no code retained”. A dossier is readable by any API key of the account that persisted the classification, is kept 400 days, and shows the human review state (see Human review). GET /v2/customs/classifications lists the account’s persisted classifications, newest first.

curl · persist · captured 2026-09-25
curl -X POST "https://api.legaldata.cleolabs.co/v2/customs/classifications" \
  -H "Authorization: Bearer $CLEO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"item_id":"SKU-PUMP-7","description":"Pompe hydraulique à engrenages pour tracteurs agricoles, 250 bar, corps en fonte","country":"FR","lang":"fr","persist":true,"facts":{"material":"cast iron","use":"hydraulic power for agricultural tractors","process":"gear pump"}}'
response · 200 · trimmed to 2 of 5 candidates
{
  "data": {
    "item_id": "SKU-PUMP-7",
    "status": "needs_information",
    "query_facts": {
      "material": "cast iron",
      "use": "hydraulic power for agricultural tractors",
      "process": "gear pump"
    },
    "missing_attributes": [
      "function"
    ],
    "questions": [
      {
        "fact": "function",
        "question": "Quelle est la fonction de la machine (ex. pomper des liquides, comprimer un gaz, produire une puissance mécanique, découper ou façonner une matière, réfrigérer) ?",
        "why": "Headings within chapter 84 split by the machine's function (pumps, compressors, engines, refrigerating equipment, machine tools, and so on).",
        "discriminates": "heading"
      }
    ],
    "candidates": [
      {
        "code": "84136031",
        "system": "cn8",
        "country": "EU",
        "hs6_parent": "841360",
        "title": {
          "de": "Hydropumpen",
          "en": "Hydraulic fluid power",
          "es": "Bombas oleohidráulicas",
          "fr": "Pompes oléohydrauliques",
          "it": "Pompe oleoidrauliche"
        },
        "confidence": 0.92,
        "rationale": "Heading 84136031 covers hydraulic fluid power pumps, which directly matches this gear pump's primary function of providing hydraulic power transmission. The cast iron body and 250 bar rating are typical specifications for hydraulic pumps classified under this heading.",
        "evidence": [
          {
            "kind": "chapter_note",
            "ref": "Chapter 84 Note 2",
            "url": "https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32025R1926",
            "excerpt": "Subject to the operation of note 3 to Section XVI and subject to note 11 to this chapter, a machine or appliance which answers to a description in one or more of the headings 8401 to 8424 , or heading 8486 and at the same time to a description in one or more of the headings 8425 to 8480 is to be classified under the appropriate heading of the former group or under heading 8486 , as the case may be, and not the latter group.",
            "source_version": "32025R1926"
          }
        ],
        "evidence_available": true,
        "set_aside_reason": null,
        "ambiguous": true
      },
      {
        "code": "84136061",
        "system": "cn8",
        "country": "EU",
        "hs6_parent": "841360",
        "title": {
          "de": "Hydropumpen",
          "en": "Hydraulic fluid power",
          "es": "Bombas oleohidráulicas",
          "fr": "Pompes oléohydrauliques",
          "it": "Pompe oleoidrauliche"
        },
        "confidence": 0.9,
        "rationale": "Heading 84136061 is another hydraulic fluid power classification that encompasses gear pumps used in power transmission systems. This code is equally applicable to hydraulic pumps serving agricultural machinery.",
        "evidence": [
          {
            "kind": "chapter_note",
            "ref": "Chapter 84 Note 2",
            "url": "https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32025R1926",
            "excerpt": "Subject to the operation of note 3 to Section XVI and subject to note 11 to this chapter, a machine or appliance which answers to a description in one or more of the headings 8401 to 8424 , or heading 8486 and at the same time to a description in one or more of the headings 8425 to 8480 is to be classified under the appropriate heading of the former group or under heading 8486 , as the case may be, and not the latter group.",
            "source_version": "32025R1926"
          }
        ],
        "evidence_available": true,
        "set_aside_reason": "Ranks below index 1 due to potential subheading specificity differences, though both are valid hydraulic pump classifications.",
        "ambiguous": true
      }
    ],
    "coverage": {
      "level": "national",
      "national_systems": [
        "cn8"
      ],
      "via_bloc": "EU",
      "hint": null
    },
    "provenance": {
      "families": [
        "machinery-84"
      ],
      "acceptance_policy": "provisional_floor_0.7+cited_evidence_required",
      "evidence_source": "index",
      "evidence_rejected": 1
    },
    "classification_id": "5a0a0889-c975-429a-a729-ef8fd6d9cd1e"
  }
}

A hydraulic gear pump for France, persisted: it comes back needs_information and asks for the machine’s function, because the headings of chapter 84 split by function. Its top candidate, 84136031 (cn8, 0.92), cites the verbatim “Chapter 84 Note 2”; evidence_source is index and one cited id outside the offered set was rejected (evidence_rejected: 1).

curl · the dossier of that classification
# JSON
curl -H "Authorization: Bearer $CLEO_API_KEY" \
  "https://api.legaldata.cleolabs.co/v2/customs/classifications/5a0a0889-c975-429a-a729-ef8fd6d9cd1e/dossier"

# PDF
curl -H "Authorization: Bearer $CLEO_API_KEY" -o dossier.pdf \
  "https://api.legaldata.cleolabs.co/v2/customs/classifications/5a0a0889-c975-429a-a729-ef8fd6d9cd1e/dossier?format=pdf"
response · 200 · JSON · trimmed to 1 of 5 considered candidates
{
  "data": {
    "classification_id": "5a0a0889-c975-429a-a729-ef8fd6d9cd1e",
    "dossier_source": "classification",
    "item_id": "SKU-PUMP-7",
    "description": "Pompe hydraulique à engrenages pour tracteurs agricoles, 250 bar, corps en fonte",
    "country": "FR",
    "created_at": "2026-09-25T10:50:48.317457+00:00",
    "dataset_versions": [],
    "sources_and_licences": [],
    "facts_summary": "Material: cast iron. Use: hydraulic power for agricultural tractors. Process: gear pump.",
    "item": {
      "status": "needs_information",
      "code_status": "no code retained",
      "retained_candidate": null,
      "considered_candidates": [
        {
          "code": "84136031",
          "system": "cn8",
          "country": "EU",
          "title": {
            "de": "Hydropumpen",
            "en": "Hydraulic fluid power",
            "es": "Bombas oleohidráulicas",
            "fr": "Pompes oléohydrauliques",
            "it": "Pompe oleoidrauliche"
          },
          "confidence": 0.92,
          "rationale": "Heading 84136031 covers hydraulic fluid power pumps, which directly matches this gear pump's primary function of providing hydraulic power transmission. The cast iron body and 250 bar rating are typical specifications for hydraulic pumps classified under this heading.",
          "evidence": [
            {
              "ref": "Chapter 84 Note 2",
              "url": "https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32025R1926",
              "kind": "chapter_note",
              "excerpt": "Subject to the operation of note 3 to Section XVI and subject to note 11 to this chapter, a machine or appliance which answers to a description in one or more of the headings 8401 to 8424 , or heading 8486 and at the same time to a description in one or more of the headings 8425 to 8480 is to be classified under the appropriate heading of the former group or under heading 8486 , as the case may be, and not the latter group.",
              "source_version": "32025R1926"
            }
          ]
        }
      ],
      "missing_attributes": [
        "function"
      ],
      "questions": [
        {
          "why": "Headings within chapter 84 split by the machine's function (pumps, compressors, engines, refrigerating equipment, machine tools, and so on).",
          "fact": "function",
          "question": "Quelle est la fonction de la machine (ex. pomper des liquides, comprimer un gaz, produire une puissance mécanique, découper ou façonner une matière, réfrigérer) ?",
          "discriminates": "heading"
        }
      ]
    },
    "api_version": "v2"
  }
}

In this capture, dataset_versions and sources_and_licences came back empty. We show the dossier as it was returned. The PDF of the same dossier (?format=pdf) came back as a 4,527-byte file.

Catalog classification

Classify a whole catalog with the HS code batch API

POST /v2/customs/classification-batches accepts 1 to 2,000 items as JSON { items: [...] } or as a text/csv body. Each item is classified exactly as a single call would be, kits included. One malformed record never fails the whole job: it gets its own status and error. Send an Idempotency-Key: the same key and the same body within 24 h replays the job (replayed: true) at no extra cost; the same key with a different body is a 409.

curl · captured 2026-09-25
curl -X POST "https://api.legaldata.cleolabs.co/v2/customs/classification-batches" \
  -H "Authorization: Bearer $CLEO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: catalog-import-2026-09-25" \
  -d '{"items":[{"item_id":"A1","description":"Cotton T-shirt, knitted, 100% cotton","country":"GB"},{"item_id":"A2","description":"Stainless steel water bottle 750 ml","country":"GB"}]}'
response · 202
{
  "data": {
    "job_id": "06d435dc-6132-432e-b055-0e80d2790d59",
    "status": "queued",
    "total_items": 2,
    "replayed": false,
    "links": {
      "self": "/v2/customs/classification-batches/06d435dc-6132-432e-b055-0e80d2790d59"
    },
    "ignored_columns": []
  }
}
then
# Job status and a page of items, in submission order
curl -H "Authorization: Bearer $CLEO_API_KEY" \
  "https://api.legaldata.cleolabs.co/v2/customs/classification-batches/06d435dc-6132-432e-b055-0e80d2790d59"

# Every item as CSV (one line per item, position order)
curl -H "Authorization: Bearer $CLEO_API_KEY" -o items.csv \
  "https://api.legaldata.cleolabs.co/v2/customs/classification-batches/06d435dc-6132-432e-b055-0e80d2790d59/items.csv"

# The dossier of item 0 (JSON, or ?format=pdf)
curl -H "Authorization: Bearer $CLEO_API_KEY" \
  "https://api.legaldata.cleolabs.co/v2/customs/classification-batches/06d435dc-6132-432e-b055-0e80d2790d59/items/0/dossier"
CSV header row
item_id,description,country,system,material,composition,use,function,process,presentation,audience

In CSV, write composition as cotton 60%; polyester 40%. A UTF-8 BOM is accepted; a missing item_id column or a ;-delimited file is a 400 naming what is wrong; any other column is ignored and reported in ignored_columns. In items.csv, the code columns are filled only for classified items, questions are joined with |, and cells that could be read as spreadsheet formulas are prefixed with '. The last two columns, review_status and approved_code, carry the human review, and ?approved_only=true keeps only approved items: the file to import into an ERP or a store. When the job ends, the webhooks classification_batch.completed or classification_batch.failed fire to your own subscriptions (see Delivery).

Human review

Approve an HS code as a named reviewer, with locking and a history

The API proposes; a person signs off. On a classification persisted with persist: true, POST /v2/customs/classifications/{id}/review records one decision: decision (approved, rejected or changes_requested), a named reviewer (name or e-mail), an optional comment, an optional approved_code and an optional expected_version. It answers 201 with the decision and the state it leaves: review_status, approved_code, approved_at, review_version. The same body works on one item of a batch with POST /v2/customs/classification-batches/{job_id}/items/{position}/review.

Without approved_code, the approved code is the retained candidate of a classified outcome. A code that is not one of the candidates must be valid in the request’s catalog. rejected and changes_requested clear the approved code. To guard against two reviewers deciding at once, send the review_version you read as expected_version: a mismatch is a 409 version_conflict. Reviews are scoped to the account: every API key of the account sees and reviews the same records.

curl · record a decision
# Approve the retained code of a persisted classification (201)
curl -X POST "https://api.legaldata.cleolabs.co/v2/customs/classifications/$CLASSIFICATION_ID/review" \
  -H "Authorization: Bearer $CLEO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: review-SKU-TS-100-1" \
  -d '{"decision":"approved","reviewer":"broker@example.com","comment":"Knitted, 100 % cotton, men"}'

# The same decision on item 0 of a batch
curl -X POST "https://api.legaldata.cleolabs.co/v2/customs/classification-batches/06d435dc-6132-432e-b055-0e80d2790d59/items/0/review" \
  -H "Authorization: Bearer $CLEO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"decision":"changes_requested","reviewer":"broker@example.com","comment":"Ask the supplier for the fibre split"}'
curl · read it back
# Append-only review history, oldest first
curl -H "Authorization: Bearer $CLEO_API_KEY" \
  "https://api.legaldata.cleolabs.co/v2/customs/classifications/$CLASSIFICATION_ID/reviews"

# The account's persisted classifications, newest first, filtered on review state
curl -H "Authorization: Bearer $CLEO_API_KEY" \
  "https://api.legaldata.cleolabs.co/v2/customs/classifications?review_status=approved&limit=50"

# Only approved batch items, ready to import into an ERP or a store
curl -H "Authorization: Bearer $CLEO_API_KEY" -o approved.csv \
  "https://api.legaldata.cleolabs.co/v2/customs/classification-batches/06d435dc-6132-432e-b055-0e80d2790d59/items.csv?approved_only=true"
Refusals
  • 409 version_conflict — expected_version is not the record’s current review_version, or another decision landed first.
  • 409 review_locked — The record is approved: only changes_requested re-opens it.
  • 409 needs_information — approved on a needs_information outcome: answer the questions and classify again.
  • 409 not_reviewable — approved on an invalid_item or classification_failed outcome.
  • 409 national_line_required — An HS6 code where the request named, or the country uses, a national system.
  • 409 not_ready — A batch item that is still pending.
  • 400 approved_code_required — No approved_code and no retained candidate (status is not classified).
  • 400 approved_code_invalid — A code that is neither a candidate nor valid in the request’s catalog.

GET /v2/customs/classifications/{id}/reviews (and …/items/{position}/reviews) returns the append-only history, oldest first. GET /v2/customs/classifications lists the account’s persisted classifications, newest first, with filters on item_id, status, review_status and creation date, cursor pagination (next_cursor → cursor) and a link to each dossier and review history. The JSON and PDF dossiers show the review state and the latest decision; items.csv?approved_only=true exports only approved batch items, and the connectors write back only approved codes by default. Each review call weighs 1 unit. The MCP tools list_classifications and review_classification expose the same routes to an agent.

Connectors

Write HS codes back to Shopify, Odoo, SAP GTS and WooCommerce

Most of the work is moving codes between systems. The connectors are shipped in @cleo-legal/sdk 0.9.0 as npx @cleo-legal/sdk connectors … (installable from the GitHub release today; npm registry publication pending, see the SDK page). Every command is a dry run unless you pass --apply, and write-back takes only human-approved codes by default (--require-approval):

terminal · @cleo-legal/sdk 0.9.0 from the GitHub release
npm install https://github.com/Cleo-Labs-IA/cleo-customs-examples/releases/download/sdk-v0.9.0/cleo-legal-sdk-0.9.0.tgz
  • Shopify: pull the catalog → batch CSV → apply. It writes harmonizedSystemCode only under --apply, only the approved_code of items a person approved (--no-require-approval falls back to classified outcomes above a confidence floor), re-reads the live code before writing, and never overwrites a different existing code.
  • Odoo, SAP GTS, WooCommerce: CSV profiles with a byte-identical round trip. The SAP GTS profile is marked unverified.

Not yet: the Shopify connector has not been tested against a live Shopify store.

Nomenclature truth

Check an HS code is still valid on a date

A code valid last year may not be valid on the day you ship. GET /v2/customs/codes/validate answers for a code, a system, a country and an as_of date, with the dataset_version it read. Statuses: valid, not_yet_valid, not_in_catalog, unsupported_jurisdiction, stale_dataset, replaced_by, reclassify_required, retired. A replaced code names its successor in replaced_by, following chains up to 5 hops.

curl · Kazakhstan, TN VED · captured 2026-09-25
curl -H "Authorization: Bearer $CLEO_API_KEY" \
  "https://api.legaldata.cleolabs.co/v2/customs/codes/validate?code=8413603100&country=KZ&system=tn_ved&as_of=2026-09-25"
response · 200
{
  "data": {
    "code": "8413603100",
    "system": "tn_ved",
    "country": "KZ",
    "hs6_parent": "841360",
    "dataset_version": {
      "country": "EAEU",
      "system": "tn_ved",
      "source_version": "unversioned-2026-09-23",
      "effective_from": "2026-01-01",
      "effective_to": null
    },
    "hint": null,
    "exists": true,
    "title": {
      "en": "Gear pumps, hydraulic power",
      "ru": "Насосы шестеренные, гидравлические силовые"
    },
    "valid_from": null,
    "status": "valid",
    "effective_to": null,
    "replaced_by": null,
    "replacement_chain": null,
    "chain_note": null
  }
}
curl · a code that is not in the EU catalog
curl -H "Authorization: Bearer $CLEO_API_KEY" \
  "https://api.legaldata.cleolabs.co/v2/customs/codes/validate?code=84136099&country=EU&system=cn8"
response · 200 · trimmed
{
  "data": {
    "code": "84136099",
    "system": "cn8",
    "country": "EU",
    "hs6_parent": "841360",
    "status": "not_in_catalog",
    "exists": false,
    "dataset_version": {
      "country": "EU",
      "system": "cn8",
      "source_version": "unversioned-2026-09-23",
      "effective_from": "2026-01-01",
      "effective_to": null
    }
  }
}

GET /v2/customs/codes/changes lists added, deleted and replaced codes between catalog imports, with an effective_date and a tracked flag. On a system that is not tracked yet, the list is empty and the response says why an empty list does not mean no change:

curl · captured 2026-09-25
curl -H "Authorization: Bearer $CLEO_API_KEY" \
  "https://api.legaldata.cleolabs.co/v2/customs/codes/changes?system=cn8&country=EU&since=2026-01-01"
response · 200 · advisory_disclaimer omitted
{
  "data": [],
  "has_more": false,
  "next_cursor": null,
  "tracked": false,
  "hint": "cn8 changes are not tracked yet; an empty list does not mean no change"
}
HS code lookup

Quick HS code lookup, pinned to the national line

GET /v2/customs/lookup ranks code candidates for a free-text description, without the facts and questions of the classification route. Pin system: without it, most countries rank the global HS6 row first. With system=sacu8, a hydraulic gear pump for South Africa answers at the SACU national line, with the catalog’s source_version.

curl · captured 2026-09-25
curl -H "Authorization: Bearer $CLEO_API_KEY" \
  "https://api.legaldata.cleolabs.co/v2/customs/lookup?description=hydraulic%20gear%20pump%20for%20tractors&country=ZA&system=sacu8&top=3"
response · 200 · trimmed to 2 of 3 candidates
{
  "data": {
    "query": "hydraulic gear pump for tractors",
    "detected_lang": "en",
    "candidates": [
      {
        "code": "84136000",
        "system": "sacu8",
        "country": "SACU",
        "hs6_parent": "841360",
        "title": {
          "en": "- Other rotary positive displacement pumps"
        },
        "confidence": 0.85,
        "rationale": "Code 84136000 covers rotary positive displacement pumps, which accurately describes a hydraulic gear pump's mechanical function and classification under Chapter 84 (machinery). This is the most technically precise classification for the pump as a standalone component.",
        "source_version": "SACU-SARS-2026-08-28-2026-09-25",
        "ambiguous": true
      },
      {
        "code": "87089120",
        "system": "sacu8",
        "country": "SACU",
        "hs6_parent": "870891",
        "title": {
          "en": "--- Parts identifiable for use solely or principally with tractors (excluding road tractors)"
        },
        "confidence": 0.78,
        "rationale": "Code 87089120 specifically identifies parts for tractors, which is appropriate since the product is explicitly described as a tractor hydraulic pump. This recognizes both the tractor application and the component nature of the product.",
        "source_version": "SACU-SARS-2026-08-28-2026-09-25",
        "ambiguous": true
      }
    ],
    "dataset_version": {
      "country": "SACU",
      "system": "sacu8",
      "source_version": "SACU-SARS-2026-08-28-2026-09-25",
      "effective_from": "2026-08-28",
      "effective_to": null
    },
    "classification_status": "ambiguous"
  }
}
Coverage

Customs classification coverage: 249 territories, 112 at the national line

Measured on 25 September 2026 with GET /v2/customs/coverage/countries: 249 territories, 112 national, 137 HS6 only, 26 systems served. Ask it for one country before you promise a market.

National line, by route
  • EU 27 — cn8
  • EAEU 5 — tn_ved: RU, BY, KZ, AM, KG
  • ASEAN 10 — ahtn8
  • CARICOM 13
  • ECOWAS 12
  • EAC 7 — eac8
  • GCC 6
  • SACU 5 — sacu8
  • SICA 5
  • CAN 4
  • MERCOSUR 4 — ncm8
  • CH + LI — ch8
  • Own catalogs — US (hts), CA (ca8/ca10), GB (uk10), NO, JP, AU (au8/au10), NZ, MX, TR, IN, IL, KR

Still HS6 only, among others: China, Hong Kong, Taiwan, Pakistan, Bangladesh, Egypt, Morocco, Tunisia, Algeria, Ukraine, Serbia, Chile, Venezuela, Panama, Iceland, Georgia, Azerbaijan, Uzbekistan. Every catalog carries verified_dataset: false (imported from the official publication, embedded to 100 %, not yet independently re-verified), and the response states the licence basis of each source.

curl · Kenya, national through the EAC
curl -H "Authorization: Bearer $CLEO_API_KEY" \
  "https://api.legaldata.cleolabs.co/v2/customs/coverage/countries?country=KE"
response · 200 · licence_basis shortened
{
  "data": {
    "total": 1,
    "countries": [
      {
        "country": "KE",
        "name": "Kenya",
        "level": "national",
        "via_bloc": "EAC",
        "national_systems": [
          {
            "system": "eac8",
            "source_version": "EAC-CET2022-2026-09-25",
            "effective_from": "2022-07-01",
            "verified_dataset": false,
            "licence_basis": "East African Community, Terms of Use (eac.int/terms), fetched 2026-09-24: …"
          }
        ],
        "hint": null
      }
    ]
  }
}
curl · China, HS6 only
curl -H "Authorization: Bearer $CLEO_API_KEY" \
  "https://api.legaldata.cleolabs.co/v2/customs/coverage/countries?country=CN"
response · 200
{
  "data": {
    "total": 1,
    "countries": [
      {
        "country": "CN",
        "name": "China",
        "level": "hs6_only",
        "via_bloc": null,
        "national_systems": [],
        "hint": "No national catalog is enabled for CN; lookups answer at HS6 (6 digits) only. See GET /v2/customs/coverage/countries."
      }
    ]
  }
}
Classification coverage is not duty coverage

GET /v2/customs/coverage is a separate matrix: 54 countries with duty rows on 25 September 2026 (for example GB: 16,370 duty rows across 97 chapters). A national classification line does not imply a national duty rate. Check both.

GET /v2/customs/coverage · response · 200 · trimmed to GB
{
  "data": {
    "total_countries": 54,
    "countries": [
      {
        "country": "GB",
        "duty_rows": 16370,
        "chapters_covered": 97,
        "code_lengths": [
          10
        ],
        "rate_basis_mix": {
          "national_line": 16370
        },
        "source_tier_mix": {
          "official": 16370
        },
        "sources": [
          "uk-tariff"
        ],
        "verification": {
          "last_verified_at": "2026-09-24T14:57:03.041+00:00",
          "oldest_verified_at": "never",
          "unverified_rows": 15874,
          "pipeline_liveness_cadence": "P3D",
          "next_pipeline_pass_at": "2026-09-27T14:57:03.041Z"
        }
      }
    ]
  }
}
After the code

Import obligations, dual-use and landed cost for the code you chose

A code is the start. Once you have it, read what the destination requires and what the shipment will cost. Duties and landed cost are separate contracts from classification, and they say in the number when a component could not be priced. Specific per-unit duties are not computed, and anti-dumping, countervailing and safeguard values have 0 rows.

Import obligations for a code

A gear pump code for Kazakhstan, government buyer: one legally required item, nothing contractually expected in this capture, and a dual_use_flag to follow up with the dual-use check.

curl · captured 2026-09-25
curl -H "Authorization: Bearer $CLEO_API_KEY" \
  "https://api.legaldata.cleolabs.co/v2/customs/obligations?code=8413603100&country=KZ&context=government&lang=en"
response · 200
{
  "data": {
    "code": "8413603100",
    "country": "KZ",
    "context": "government",
    "legally_required": [
      {
        "type": "certification",
        "title": "EAC Declaration of Conformity",
        "regulation": null,
        "regulation_doc_id": null,
        "standards_id": null,
        "source": "eurasian_economic_commission",
        "mandatory": true,
        "evidence_required": [
          "EAC declaration signed by manufacturer or authorised representative"
        ],
        "url": null,
        "rationale": "TR CU 010 mandates EAC declaration for any machinery placed on EAEU market",
        "inferred": false
      }
    ],
    "contractually_expected": [],
    "dual_use_flag": {
      "applicable": true,
      "checked": true
    }
  }
}

Dual-use export controls

The dual-use check cross-checks the Wassenaar Arrangement list and EU Regulation 2021/821 and returns every matching regime entry, with destination-country overlays. A mass spectrometer for Kazakhstan comes back as a possible_flag on EU 3A201.a, with a recommendation to confirm with the national export control authority.

curl · captured 2026-09-25
curl -H "Authorization: Bearer $CLEO_API_KEY" \
  "https://api.legaldata.cleolabs.co/v2/customs/dual-use-check?description=mass%20spectrometer%20industrial&destination=KZ"
response · 200 · advisory_disclaimer omitted
{
  "data": {
    "applicable": true,
    "confidence": 0.607121087676482,
    "tier": "possible_flag",
    "regimes": [
      {
        "regime": "eu_dual_use",
        "category": "3A201.a",
        "entry_id": "eu-3A201-a",
        "regulation_doc_id": null,
        "url": "https://eur-lex.europa.eu/eli/reg/2021/821/oj",
        "similarity": 0.607121087676482
      }
    ],
    "destination_risk": {
      "country": "KZ",
      "embargoed": false,
      "embargo_regimes": [],
      "additional_approvals_required": [
        "Republic of Kazakhstan export-control approval"
      ]
    },
    "recommendation": "Possible eu_dual_use controls — confidence is moderate. Confirm with the national export control authority before shipment; consider providing power ratings, dimensions, sensor specs, or model codes for a sharper assessment.",
    "detected_lang": "en"
  },
  "coverage_status": "covered"
}

Duty, VAT and excise for a code

Every row states how it was derived (rate_basis), where it was read (source_tier) and how much we trust it (confidence). When a 6-digit query’s national child lines disagree, duty_pct is withheld and resolution.duty_pct_min / duty_pct_max carry the range.

curl · Kazakhstan · captured 2026-09-25
curl -H "Authorization: Bearer $CLEO_API_KEY" \
  "https://api.legaldata.cleolabs.co/v2/customs/duties?code=8413603100&country=KZ&system=tn_ved"
response · 200 · advisory_disclaimer omitted
{
  "data": {
    "code": "8413603100",
    "system": "tn_ved",
    "country": "KZ",
    "duty_pct": 0,
    "duty_specific": null,
    "vat_pct": 16,
    "excise_pct": 0,
    "preferential_origin": null,
    "preferential_rate": null,
    "effective_from": "2026-01-01",
    "effective_to": null,
    "source": "eec_cct_bulk",
    "notes": "EAEU CCT MFN rate (Reshenie Soveta EEK No 54). Raw: 0 EAEU Common Customs Tariff read via alta.ru, not the Eurasian Economic Commission.",
    "clearance_fee_fixed": 37625,
    "clearance_fee_currency": "KZT",
    "rate_basis": "bloc_common_tariff",
    "source_tier": "commercial_portal",
    "source_url": null,
    "confidence": 0.85,
    "verified_at": null,
    "anti_dumping_pct": null,
    "cvd_pct": null,
    "safeguard_pct": null,
    "clearance_fee_pct": 0.1,
    "special_duty_basis": null,
    "special_duty_regulation_id": null,
    "resolution": {
      "matched": "exact",
      "queried_country": "KZ",
      "resolved_country": "KZ",
      "queried_code": "8413603100",
      "resolved_code": "8413603100"
    }
  }
}

A code with no duty row answers with an empty data list and coverage_status: "not_seeded": Cleo holds no row for it, which does not mean the rate is zero.

curl · France, CN8 · captured 2026-09-25
curl -H "Authorization: Bearer $CLEO_API_KEY" \
  "https://api.legaldata.cleolabs.co/v2/customs/duties?code=84136031&country=FR&system=cn8"
response · 200 · advisory_disclaimer omitted
{
  "data": [],
  "coverage_status": "not_seeded",
  "filters": {
    "code": "84136031",
    "country": "FR"
  },
  "next_check_at": null
}

Landed cost

CIF, duty, excise, fees and VAT for the same code, from France to Kazakhstan. Read data_completeness as well as total_is_partial: in this capture total_is_partial is false while components_missing lists anti_dumping, which is not seeded and enters the total at 0.

curl · captured 2026-09-25
curl -X POST "https://api.legaldata.cleolabs.co/v2/customs/landed-cost" \
  -H "Authorization: Bearer $CLEO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"code":"8413603100","origin":"FR","destination":"KZ","fob_usd":25000,"freight_usd":1800}'
response · 200 · trimmed
{
  "data": {
    "code": "8413603100",
    "system": "tn_ved",
    "origin": "FR",
    "destination": "KZ",
    "rates": {
      "base_duty_pct": 0,
      "preferential_rate": null,
      "preferential_origins": null,
      "vat_pct": 16,
      "excise_pct": 0,
      "anti_dumping_pct": 0,
      "cvd_pct": 0,
      "safeguard_pct": 0,
      "clearance_fee_pct": 0.1,
      "clearance_fee_fixed": 37625,
      "clearance_fee_currency": "KZT"
    },
    "breakdown": {
      "cif_usd": 26800,
      "duty_usd": 0,
      "excise_usd": 0,
      "anti_dumping_usd": 0,
      "cvd_usd": 0,
      "safeguard_usd": 0,
      "clearance_fee_usd": 108.87,
      "clearance_fee_fixed_usd": 82.07,
      "vat_usd": 4305.42,
      "total_landed_usd": 31214.29,
      "applied_duty_pct": 0,
      "is_preferential": false,
      "effective_rate_pct": 24.86
    },
    "total_landed_usd": 31214.29,
    "total_is_partial": false,
    "total_excludes": [],
    "data_completeness": {
      "duty": "seeded",
      "preferential": "not_applicable",
      "import_taxes": "seeded",
      "excise": "seeded",
      "anti_dumping": "not_seeded",
      "fx_vintage": "2026-08-28",
      "components_missing": [
        "anti_dumping"
      ]
    }
  }
}
One call

The full decision: code, obligations, duties, landed cost and readiness

POST /v2/compliance/check classifies with the same structured engine as POST /v2/customs/classifications (facts, components, system, as_of) and carries the same code, origin, destination and date into obligations, dual-use, duties and, when you send transaction.fob_usd, landed cost. The answer is in data.decision; advisory_disclaimer is top-level, next to data. A step that fails or times out is marked unavailable; it never fails the whole request. options.persist stores the classification so its dossier and review work as above. It weighs 5 units.

curl
curl -X POST "https://api.legaldata.cleolabs.co/v2/compliance/check" \
  -H "Authorization: Bearer $CLEO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: decision-SKU-TS-100-FR" \
  -d @decision.json   # the body below
decision.json · example request body
{
  "product": {
    "item_id": "SKU-TS-100",
    "description": "Men's knitted T-shirt, 100 % cotton",
    "facts": {
      "process": "knitted",
      "composition": [{ "material": "cotton", "percent": 100 }],
      "audience": "men"
    }
  },
  "origin_country": "BD",
  "destination_country": "FR",
  "transaction": { "fob_usd": 1000 },
  "options": { "persist": true }
}
data.decision
  • inputs — What every step ran on: item_id, description, origin, destination, as_of, system, the code and its classification status.
  • classification — The structured classification (status, candidates with evidence, questions).
  • obligations · dual_use · duties · landed_cost — Each step on the same code. Landed cost only when transaction.fob_usd is given.
  • readiness — ready_for_review, needs_information, needs_review or blocked: the most severe blocker. ready_for_review is the best a machine can say, never “cleared”.
  • blockers — Every reason it is not ready_for_review, most severe first: { code, step, readiness, message }.
  • steps — Per step: ok, unavailable (timed out or failed; the section is null), skipped (no code to run on) or not_requested.
  • provisional_code — Only when the structured classification of a single item timed out or found nothing: the best lookup candidate, used to run obligations, duties and landed cost so you still get an answer. Always flagged provisional (obligations.based_on_provisional_code, product_classification.provisional and code_source); the classification_unavailable blocker and readiness blocked stay. Kits and unsupported or stale catalogs never fall back.
  • persistence — With options.persist: the classification_id whose dossier you can read.
  • limitations — What the inputs could not reach, stated (for example, as_of is not applied to obligations).

Measured live on 28 September 2026: a men’s knitted 100 % cotton T-shirt from Bangladesh to France came back with code 610910 carried into obligations and duties, readiness: needs_review, and landed_cost.total_is_partial: true. Read the blockers before you act on any section: anything but a classified code makes every downstream section provisional. If the classification step runs out of time on a single item, the answer still carries obligations, duties and landed cost computed on a lookup candidate, flagged in decision.provisional_code; worst-case latency is then about 25 s.

Live demo

Duty calculator

Look up the import duty, VAT and excise rate for a code by destination. Calls GET /v2/customs/duties through the playground proxy — sign in first, then attach an API key.

Live demo · production APISign in to the playground first to attach an API key — the call is then proxied securely.

Example: code=8413701000, country=KZ — centrifugal oil pumps, duty-free under EAEU CCT + 12% VAT (the Ethan workflow baseline).

Delivery

Webhooks and MCP tools for customs classification

Three customs webhook events. classification_batch.completed and classification_batch.failed go only to the subscriptions of the key owner that submitted the job; .failed fires at most once per job. customs_nomenclature.changed delivers the same rows as GET /v2/customs/codes/changes, once per import run. Every delivery is signed twice: X-Cleo-Signature (body only, unchanged) and X-Cleo-Signature-V2 with X-Cleo-Timestamp, which binds the time into the signature so you can reject a replay outside a 5-minute window. Payloads and signature verification are on the webhooks page.

The MCP server exposes the same routes to an agent:

classify_customs_itemsubmit_classification_batchget_classification_batchget_classification_dossierget_classification_batch_item_dossierlist_classificationsreview_classificationcustoms_lookupcustoms_obligationscustoms_dutiescustoms_landed_costcustoms_dual_use_checkcustoms_alternativescustoms_reverse_classifyget_coverage
Production

Request ids, safe retries and idempotency

Every response carries an X-Request-Id header. Send your own and it is reused when it is a safe token (letters, digits, ._:-, at most 64 characters); otherwise Cleo generates one. Every v2 JSON error body carries the same value as request_id: quote it to support. A malformed JSON body is a 400.

POST /v2/customs/classifications, POST /v2/compliance/check and both review POSTs accept an optional Idempotency-Key header, scoped to your account and the endpoint, for 24 h. The same key with the same body replays the first successful response with Idempotent-Replayed: true: nothing runs again and no unit is charged. The same key with a different body is a 409 idempotency_conflict; the same key while the first request is still running is a 409 idempotency_in_progress with Retry-After. Only 2xx responses are stored, so a retry after an error runs again. The batch route keeps its own key semantics (it replays the job).

Quota refusals carry a stable error.code, retryable and period; the monthly cap adds Retry-After and resets_at. Browsers can read X-Request-Id, X-RateLimit-*, Retry-After and Idempotent-Replayed (exposed through CORS).

Quota

Customs API request weight: units per call

A call in your plan is one unit. A few endpoints weigh more, because they do more than one classification. An idempotent replay is not charged. Over the monthly cap, the API answers 402 monthly_quota_exhausted with Retry-After and resets_at (the next UTC month); the Sandbox cap is lifetime (free_quota_exhausted, retryable: false). Past the per-minute limit it answers 429 rate_limited with Retry-After. Nothing is charged automatically. See pricing.

EndpointUnits
POST /v2/customs/classifications1 + number of components (max 11)
POST /v2/customs/classification-batchesSum over items of min(1 + components, 11), charged at submission; replays are not charged again
POST /v2/customs/lookup/bulkNumber of descriptions (max 25)
POST /v2/compliance/check5
POST /v2/search/bulkNumber of queries (max 25)
POST /v2/documents/bulkNumber of ids (max 50)
Everything else (lookup, validate, changes, coverage, dossier, history, review, duties, landed cost…)1
Read this before you build

Not yet / not exactly

What the customs classification API does not do today, stated plainly.

  • No US rulings (CROSS) in the evidence. Rulings are EU classification regulations only. For US entries, read the CBP note in the advisory disclaimer.
  • Landed cost is advisory: specific per-unit duties are not computed; anti-dumping, countervailing and safeguard values have 0 rows; there is no DDP collection at checkout.
  • The connectors have never run against a live Shopify store. The SAP GTS profile is marked unverified.
  • Every catalog carries verified_dataset: false: imported from the official publication, embedded to 100 %, not yet independently re-verified. The registry records the licence basis of each source, and the response says both.
  • 137 territories answer at HS6 only, among them China, Hong Kong, Taiwan, Pakistan, Bangladesh, Egypt, Morocco, Tunisia, Algeria, Ukraine, Serbia, Chile, Venezuela, Panama, Iceland, Georgia, Azerbaijan, Uzbekistan. CEMAC has no machine-readable tariff; the response returns a hint.
  • India’s edition is a mosaic of chapter files (89/96 as on 30.06.2024, 6/96 as on 30.06.2025, 1/96 as on 31.03.2024), and its source_version says so.
  • An unpinned lookup (no system) ranks the global HS6 row first for most countries. Pin system for the national line.
  • The order of the HS6 row and the national line in an unpinned classification can change from one call to the next. The French T-shirt captured on 29 September 2026 (printed above) ranks the cn8 line 61091000 first and the HS6 row 610910 second. Pin system when you want the national line.
  • The evidence index, measured when it was built on 25 September 2026, holds 102 HS note blocks (6 GRI, 9 Section Notes, 87 Chapter Notes) from Regulation (EU) 2025/1926. EU classification regulations: 1 indexed that day; the rest is being re-parsed.
  • The rationale text is written by a model. The evidence excerpts are verbatim. Treat them differently.
  • The classified floor depends on the model that answered: 0.9 for Sonnet 5.5, 0.92 for Opus 5.5 and 0.7 for the Haiku fallback. Every response names the model in provenance.reranker and the gate in provenance.acceptance_policy. The question families are not broker-reviewed yet.
  • GET /v2/customs/codes/changes does not track hs6, hs8, cn8, tn_ved, hts, ca8 and ca10 yet. For those, an empty list does not mean no change, and a code their source dropped still answers valid.
  • No photos or documents as input. No rules by product family or category inheritance.
Required notice

Advisory disclaimer

Get started

Run a real check in 2 minutes