Guide · Python · SDK · Trade Compliance

Logistics & Shipping Validation

Four validators, one shipping workflow. Here's how to validate a container code, HS classification code, UN/LOCODE port, and EORI number in Python — using asyncio.gather for parallel validation before submitting to customs.

1. The shipping validation problem

International shipments involve four distinct identifier types — each governed by a different standard, each validated by a different authority. A single invalid identifier can stall a shipment at customs, trigger a port rejection, or result in a customs fine.

The solution: validate all four identifiers in parallel before the booking is submitted to the carrier or customs system — catching errors at data entry, not at the port.

ℹ️Python's asyncio.gather makes it straightforward to run all four validation calls concurrently. The total latency equals the slowest single call, not the sum of all four.

2. The four identifiers

ValidatorWhat it validatesWhen to useAPI endpoint
Container CodeISO 6346 owner code + serial + mod-11Tracking intermodal containersGET /v0/container-code
HS CodeWCO Harmonized System product classificationCustoms declarationsGET /v0/hs-code
UN/LOCODE5-char UN port/place code + location lookupPort of loading/dischargeGET /v0/locode
EORIEU Economic Operators RegistrationEU customs filingsGET /v0/eori

3. Step 1: Validate the container code

ISO 6346 container codes consist of a 3-letter owner code, 1-letter category identifier, 6-digit serial number, and a mod-11 check digit.

Example response

{
  "valid": true,
  "ownerCode": "MSC",
  "categoryCode": "U",
  "categoryName": "Freight container",
  "serialNumber": "305638",
  "checkDigit": "3"
}

What to check in your code

1

valid is True — owner code, serial, and mod-11 check digit all pass

2

category_code — verify container type matches cargo (R = reefer for refrigerated goods)


4. Step 2: Validate the HS code

HS codes have hierarchical depth: 2-digit chapters, 4-digit headings, 6-digit subheadings. Customs declarations require 6-digit subheadings.

Example response

{
  "valid": true,
  "code": "847130",
  "level": "subheading",
  "description": "Portable automatic data-processing machines",
  "formatted": "8471.30",
  "chapter": {
    "code": "84",
    "description": "Nuclear reactors, boilers, machinery and mechanical appliances; parts thereof"
  }
}

What to check

1

valid is True — code exists in WCO HS nomenclature

2

level == "subheading" — 6-digit depth required for customs declarations


5. Step 3: Validate the UN/LOCODE

Example response

{
  "valid": true,
  "found": true,
  "locode": "DEHAM",
  "country": "DE",
  "location": "HAM",
  "name": "Hamburg",
  "nameAscii": "Hamburg",
  "subdivision": "HH",
  "functions": ["port", "rail", "road"],
  "coordinates": "5333N 00958E"
}

What to check

1

found is True — location exists in UN/LOCODE database

2

"port" in functions — location supports the required transport mode


6. Step 4: Validate the EORI number

Example response

{
  "valid": true,
  "countryCode": "DE",
  "country": "Germany",
  "identifier": "123456789012345",
  "formatted": "DE123456789012345"
}

7. Putting it all together — parallel validation

Install with pip install isvalid-sdk for the async SDK, or pip install requests for synchronous HTTP calls with ThreadPoolExecutor.

import asyncio
import os
from isvalid_sdk import IsValidConfig, create_client

iv = create_client(IsValidConfig(api_key=os.environ["ISVALID_API_KEY"]))


async def validate_shipment(
    container_code: str,
    hs_code: str,
    locode: str,
    eori_number: str,
) -> dict:
    container, hs, loc, eori = await asyncio.gather(
        iv.container_code(container_code),
        iv.hs_code(hs_code),
        iv.locode(locode),
        iv.eori(eori_number),
    )

    return {
        "container": {
            "valid": container["valid"],
            "owner_code": container.get("ownerCode"),
            "category_name": container.get("categoryName"),
        },
        "hs": {
            "valid": hs["valid"],
            "level": hs.get("level"),
            "description": hs.get("description"),
            "formatted": hs.get("formatted"),
        },
        "locode": {
            "valid": loc["valid"],
            "found": loc.get("found"),
            "name": loc.get("name"),
            "functions": loc.get("functions"),
        },
        "eori": {
            "valid": eori["valid"],
            "country_code": eori.get("countryCode"),
            "formatted": eori.get("formatted"),
        },
    }


# ── Example: Hamburg port, electronics, MSC container, German EORI ─────────
result = asyncio.run(validate_shipment(
    container_code="MSCU3056383",
    hs_code="847130",
    locode="DEHAM",
    eori_number="DE123456789012345",
))

print("Container:", f"✓ {result['container']['owner_code']} / {result['container']['category_name']}"
      if result["container"]["valid"] else "✗ invalid")
print("HS Code  :", f"✓ {result['hs']['formatted']} — {result['hs']['description']}"
      if result["hs"]["valid"] else "✗ invalid")
print("LOCODE   :", f"✓ {result['locode']['name']}"
      if result["locode"]["found"] else "✗ not found")
print("EORI     :", f"✓ {result['eori']['formatted']}"
      if result["eori"]["valid"] else "✗ invalid")

if result["hs"]["valid"] and result["hs"]["level"] != "subheading":
    print(f"⚠ HS code is at {result['hs']['level']} level — customs requires 6-digit subheading")
asyncio.gather runs all four validation coroutines concurrently. The total latency equals the slowest single call — not the sum of all four.

8. cURL examples

Validate a container code:

curl -H "Authorization: Bearer YOUR_API_KEY" \
  "https://api.isvalid.dev/v0/container-code?value=MSCU3056383"

Validate an HS code:

curl -H "Authorization: Bearer YOUR_API_KEY" \
  "https://api.isvalid.dev/v0/hs-code?value=847130"

Validate a UN/LOCODE:

curl -H "Authorization: Bearer YOUR_API_KEY" \
  "https://api.isvalid.dev/v0/locode?value=DEHAM"

Validate an EORI number:

curl -H "Authorization: Bearer YOUR_API_KEY" \
  "https://api.isvalid.dev/v0/eori?value=DE123456789012345"

9. Handling edge cases

Normalize container code formats

Container codes may arrive as "MSCU 305638-3" — normalize before validating.

def normalize_container_code(raw: str) -> str:
    return raw.replace(" ", "").replace("-", "").upper()

normalized = normalize_container_code("MSCU 305638-3")  # → "MSCU3056383"

HS code level enforcement

Require 6-digit subheading depth for customs declarations.

hs = await iv.hs_code("8471")
if hs["valid"] and hs.get("level") != "subheading":
    raise ValueError(
        f"HS code at {hs['level']} level — customs requires 6-digit subheading"
    )

LOCODE not found in database

Valid format but not in UN/LOCODE registry — often an obsolete or mistyped code.

loc = await iv.locode("DEABC")
if loc["valid"] and not loc.get("found"):
    return {
        "status": "unknown_locode",
        "reason": "Location code not found in UN/LOCODE database",
    }

10. Summary checklist

Validate container codes before booking — prevents terminal rejections
Check HS code level — require 6-digit subheading for customs declarations
Confirm LOCODE found is True — unknown ports cause routing failures
Validate EORI before customs filing — invalid EORI triggers delays and fines
Do not accept free-text port names — always validate a UN/LOCODE
Do not use 4-digit HS headings for import declarations — require full 6-digit codes

See also

Validate shipping documents before filing

Free tier includes 100 API calls per day. No credit card required. Container code, HS code, LOCODE, and EORI validation all included.