Pakajo Tariff Atlas

Application handbook

Tariff Atlas
documentation.

How to use the lookup, understand its output, operate the API, manage PostgreSQL, and synchronize official tariff datasets.

PHP 8+PostgreSQLOpenAI Responses API Documentation reflects the current application code.

01 · Overview

What the application does

Pakajo Tariff Atlas accepts shipment and product facts, asks an OpenAI model for a structured customs classification and tax estimate, verifies supported destination tariff numbers against locally synchronized official data, and returns a predictable JSON result.

Decision support, not a ruling

The result is an estimate. A code-existence check does not prove that a code is correct for a product, and the application cannot issue a binding customs decision.

ClassificationInternational 6-digit HS suggestion plus supported destination extensions.
Tax estimateItemized duty, VAT/GST, excise, processing fees, and other supported charges.
VerificationDatabase checks against active U.S., Swiss, UK, EU, Brazilian, and Canadian nomenclature releases. Australia and Japan use AI-guided format rules with no synchronized database.
Cost visibilityActual token usage and estimated OpenAI cost for each request.
Result cachingIdentical recent requests can avoid another model call.
Audit dataValidated request and result JSON can be stored in PostgreSQL.

02 · User guide

Using the tariff lookup

  1. 1
    Select the trade route

    Choose the country of origin, export country, and import destination. All country fields are searchable.

  2. 2
    Name the product

    Use a concise commercial name such as “T-shirt,” “ceramic mug,” or “lithium-ion battery pack.”

  3. 3
    Add classification facts

    Describe material composition, construction, intended use or wearer, processing, dimensions, packaging, and distinctive features.

  4. 4
    Enter value, input currency, weight, and result language

    Enter the product value in its current currency. The result converts customs-related amounts into the official currency of the dispatch country and states its exchange-rate assumption. Weight is entered in grams.

  5. 5
    Add optional freight, insurance, and region

    Freight and insurance cost use the product's input currency and are used directly in the customs value (CIF) calculation instead of being estimated. Import state/region only matters for destinations with region-specific taxes, such as Brazilian state ICMS; leave any of these blank to let the model assume a reasonable value.

  6. 6
    Choose saved-result behavior

    The switch starts off for a new AI request. Turn on Saved results first to reuse a matching recent result and save OpenAI cost.

  7. 7
    Choose the tariff source

    The switch starts off in AI only mode. Turn on Tariff database to constrain and verify U.S., Swiss, UK, EU, Brazilian, and Canadian codes through tariff_codes. Australia and Japan have no synchronized database, so this switch has no effect on those destinations.

  8. 8
    Run and review

    Check declaration readiness, missing information, confidence, calculations, caveats, and official verification links.

Good product description

Men's black short-sleeve crew-neck T-shirt;
100% cotton knitted fabric; hemmed sleeves and bottom;
no pockets, trim, embroidery, or protective features.

Avoid vague descriptions such as “clothing,” “sample,” “part,” or “gift.” They do not contain enough facts for reliable classification.

03 · Result guide

Understanding the result

hs_code

The international six-digit Harmonized System subheading suggested by the model.

destination_tariff_code

The destination code: U.S. 10-digit HTSUS, Swiss 8-digit Tares, UK 10-digit commodity code, EU 8-digit CN, Brazilian 8-digit NCM, Canadian 10-digit tariff number, Australian 8-digit Working Tariff classification, Japanese 9-digit statistical code, or six-digit HS elsewhere.

uk_tariff_code

The selected 10-digit UK import commodity code. Database mode accepts only records marked declarable in the active UK release.

eu_cn_code

The selected and verified 8-digit EU Combined Nomenclature code. It is not a complete 10-digit TARIC measures determination.

br_ncm_code

The selected and verified 8-digit Brazilian NCM (Nomenclatura Comum do Mercosul) code, formatted XXXX.XX.XX. The first six digits are the international HS subheading; TEC import-duty rates and other Brazilian taxes (II, IPI, PIS/COFINS-Importação, ICMS) are not imported and remain AI-estimated.

ca_tariff_code

The selected and verified 10-digit Canadian Customs Tariff number, formatted XXXX.XX.XX.XX (an 8-digit tariff item plus a 2-digit statistical suffix). official_general_duty_rate carries the CBSA Most-Favoured-Nation (MFN) rate for the tariff item.

au_tariff_code

The model's 8-digit Australian Working Tariff classification (Customs Tariff Act 1995, Schedule 3), formatted XXXX.XX.XX. Not the AHECC export classification. No official dataset is synchronized for Australia, so this code is never database-verified; a further 2-digit ABS statistical code is required for the actual import declaration.

jp_tariff_code

The model's 9-digit Japan statistical code for import, formatted XXXX.XX-XXX (6-digit HS plus a 3-digit Japan-specific suffix). No official dataset is synchronized for Japan, so this code is never database-verified.

destination_tariff_verified

Confirms the exact code exists in the active supported official dataset. It does not establish legal classification.

declaration_ready

Indicates whether supplied facts can distinguish the required national suffix. It is not customs approval.

missing_information

Specific additional facts needed to resolve a classification branch.

tax_components

Estimated charges with type, rate, taxable base, amount, and calculation explanation.

total_taxes

The server-calculated sum of every tax-component amount. PHP recalculates this instead of trusting model arithmetic.

customs_valuation

Converted product value, freight, insurance, other valuation adjustments, and PHP-recalculated CIF customs value in the dispatch-country currency.

input_declared_value / input_currency

The product value and currency submitted by the user before conversion.

declared_value / currency

The product value converted to the dispatch country’s official currency. Tax components use the same three-letter currency code.

ordered_result

The server-built primary output in display order: detailed code, tariff-code type, digit count, full description, basic and additional duty percentages, total duty percentage, VAT/GST percentage, CIF, duty amount, VAT amount, and total import charges. Duty percentages use CIF as the denominator.

final_summary

A backward-compatible alias of ordered_result for existing API consumers.

request_cost

Model calls, token usage, and estimated USD token cost. Taxes, credits, negotiated prices, and data-provider fees are excluded.

result_language

The requested two-letter output-language code. Human-readable descriptions, charge names, calculations, methodology, caveats, and missing-information text follow this language.

cache.hit

When true, a stored result was reused and no OpenAI request was made for this lookup.

cache.mode / cache.bypassed

Reports the saved-result lookup method and whether the tariff_lookups cache was skipped.

tariff_source

Reports whether tariff_codes was queried and whether the destination code was verified. In ai_only mode, the code is explicitly unverified.

04 · Coverage

Tariff systems and data status

SystemResolutionLive lookupCurrent scope
HTSUS10 digitsU.S. importsNomenclature, descriptions, and general duty-rate text
Swiss Tares8 digitsSwiss importsTariff-number structure and descriptions
UK Trade Tariff10 digitsUK importsDeclarable commodity nomenclature and descriptions; duty measures are not imported
EU CN8 digitsEU member-state importsAnnual CN verification; not daily 10-digit TARIC measures
Brazil NCM8 digitsBrazilian importsOfficial NCM nomenclature and descriptions; TEC duty rates and II/IPI/PIS/COFINS/ICMS taxes are not imported
Canada Customs Tariff10 digitsCanadian importsFull CBSA nomenclature, hierarchy, and Most-Favoured-Nation (MFN) duty-rate text
Australia Working Tariff8 digitsAI suggestionNo synchronized database (no official bulk export exists); format and classification rules only
Japan statistical code9 digitsAI suggestionNo synchronized database (no official bulk export exists); format and classification rules only
Other destinations6-digit HSAI suggestionNo local destination-specific verification
Live status unavailable

Configure PostgreSQL and synchronize datasets to show active release counts here.

05 · Technical design

Request architecture

Browser formHTML, CSS, jQuery
PHP endpointValidation and cache
OpenAIStructured classification
Official dataCode verification
JSON resultTax total and cost
  1. The browser posts JSON to /api/lookup.php.
  2. ShipmentValidator normalizes and validates every input field.
  3. In Saved results first mode, LookupRepository checks PostgreSQL for an identical, unexpired request hash. New AI request skips this result-cache check.
  4. On a miss, OpenAiTariffService turns the form into a short question-and-answer shipment prompt, then calls POST /v1/responses using strict JSON Schema Structured Outputs.
  5. When Tariff database is selected for U.S., Swiss, UK, EU, Brazilian, or Canadian imports, candidates are read only from the corresponding active tariff_codes release. A constrained model call selects from those official candidates. In tariff-source AI only mode, these database queries and verification are skipped. Australian and Japanese imports have no synchronized database at all, so the model's own code is always returned unverified.
  6. The code is verified against the active release. An unverified code is marked not declaration-ready.
  7. PHP recalculates total taxes, adds verification links and cost data, stores the result, and returns JSON.

The Structured Outputs schema is sent under text.format, matching the current OpenAI Responses API guidance.

06 · Installation

Local setup

Use PHP 8+ with cURL, PDO PostgreSQL, JSON, mbstring, XMLReader, SimpleXML, and Zip support. PostgreSQL is recommended for caching and official datasets.

Canadian sync needs mdbtools

The CBSA does not publish a CSV or JSON export; its only official machine-readable format is a Microsoft Access database. Synchronizing Canada shells out to the mdb-export binary from mdbtools (brew install mdbtools or apt install mdbtools) to convert it to CSV first. Set MDB_EXPORT_BIN if it is not on the system PATH. No other destination requires this.

1. Create configuration

cp .env.example .env

2. Add the server-side API key

OPENAI_API_KEY=your_server_side_key
OPENAI_MODEL=gpt-4o-mini

3. Apply the database schema

psql -d customs_tariff -f database/schema.sql

4. Import official data

php bin/sync-tariffs.php --system=all

5. Start locally

php -S localhost:8080 -t public

Open http://localhost:8080. The production web-server document root should be the public/ directory.

07 · Configuration

Environment variables

VariableRequiredPurpose
OPENAI_API_KEYYesServer-side credential for OpenAI requests.
OPENAI_MODELNoModel identifier; defaults to gpt-4o-mini.
OPENAI_*_COST_PER_1MNoInput, cached-input, and output pricing overrides.
DB_DSNRecommendedPDO PostgreSQL DSN. Empty disables persistence and cache.
DB_USER / DB_PASSWORDWith DBDatabase credentials.
CACHE_TTL_SECONDSNoCache freshness window; default 86,400 seconds. Use 0 to disable.
CACHE_VERSIONNoChange after prompt or result-rule changes to invalidate old keys.
TARIFF_DATA_DIRNoDownloaded source directory; defaults to storage/tariff-source.
MDB_EXPORT_BINNoPath to the mdbtools mdb-export binary used only by the Canadian sync; defaults to mdb-export on PATH.
Never expose the API key

Keep it out of JavaScript, HTML, source control, screenshots, logs, and client-visible errors.

08 · HTTP API

Lookup endpoint

POST /api/lookup.php using Content-Type: application/json.

Request

{
  "origin_country": "China",
  "export_country": "Ghana",
  "import_country": "United States",
  "product": "T-Shirt",
  "description": "100% cotton knitted men's T-shirt",
  "value": 20.00,
  "currency": "EUR",
  "weight_g": 200,
  "import_region": "",
  "freight_cost": 5.00,
  "insurance_cost": 1.50,
  "result_language": "en",
  "lookup_mode": "database_first",
  "tariff_source_mode": "tariff_database"
}

Validation limits

  • Country fields: required, maximum 100 characters.
  • Product: required, maximum 150 characters.
  • Description: required, maximum 500 characters.
  • Value: greater than zero and at most 1,000,000,000.
  • Weight: positive integer grams and at most 100,000,000.
  • Product value currency: exactly three alphabetic ISO-style characters after normalization. This is the input currency before conversion.
  • Import region: optional, maximum 100 characters. A destination state/province/region name, only used where the destination applies region-specific taxes (for example Brazilian state ICMS).
  • Freight cost / insurance cost: optional, zero or greater, at most 1,000,000,000, in the same currency as value. Omit or send an empty string to let the model estimate instead of supplying a shipper-provided figure.
  • Result language: optional de, en, es, fr, it, cs, nl, pl, pt, or zh; defaults to en.
  • Lookup mode: optional database_first or ai_only; defaults to database_first.
  • Tariff source mode: optional tariff_database or ai_only; defaults to tariff_database.

Response envelope

{
  "success": true,
  "data": {
    "hs_code": "610910",
    "destination_tariff_system": "HTSUS",
    "destination_tariff_code": "6109.10.00.12",
    "destination_tariff_verified": true,
    "uk_tariff_code": null,
    "eu_cn_code": null,
    "br_ncm_code": null,
    "ca_tariff_code": null,
    "au_tariff_code": null,
    "jp_tariff_code": null,
    "declaration_ready": true,
    "input_declared_value": 20,
    "input_currency": "EUR",
    "declared_value": 330,
    "customs_valuation": {
      "product_value": 330,
      "freight": 82.5,
      "insurance": 24.75,
      "other_adjustments": 0,
      "customs_value_cif": 437.25,
      "currency": "GHS"
    },
    "tax_components": [
      {
        "name": "Basic customs duty",
        "type": "basic_customs_duty",
        "rate_percentage": 16.5,
        "taxable_base": 437.25,
        "amount": 72.15,
        "currency": "GHS",
        "calculation": "16.5% of GHS 437.25"
      }
    ],
    "total_taxes": 72.15,
    "currency": "GHS",
    "result_language": "en",
    "result_language_name": "English",
    "verification_links": [],
    "tariff_source": {
      "mode": "tariff_database",
      "tariff_codes_queried": true,
      "verified": true
    },
    "ordered_result": {
      "detailed_tariff_code": "6109.10.00.12",
      "tariff_code_type": "HTSUS",
      "number_of_digits": 10,
      "full_tariff_description": "Cotton T-shirts, knitted or crocheted",
      "basic_customs_duty_percentage": 16.5,
      "additional_duties_percentage": 0,
      "total_customs_duty_percentage": 16.5,
      "import_vat_gst_percentage": null,
      "customs_value_cif": 437.25,
      "customs_duty_amount": 72.15,
      "import_vat_amount": 0,
      "total_import_charges": 72.15,
      "currency": "GHS"
    },
    "request_cost": {},
    "cache": {
      "hit": false,
      "mode": "database_first",
      "bypassed": false
    }
  }
}
StatusMeaning
200Successful lookup or cache hit.
405Method other than POST.
422Invalid JSON or input validation failure.
502OpenAI, downstream, or unexpected processing failure.

Responses include X-Tariff-Cache: HIT, MISS, or BYPASS and Cache-Control: no-store. BYPASS means ai_only intentionally skipped the saved-result cache.

09 · PostgreSQL

Database model

tariff_lookups

Request JSON, result JSON, request hash, HS code, total taxes, currency, and creation time.

tariff_dataset_releases

Imported versions with system, release key, official source, metadata, active status, and import time.

tariff_codes

Codes, display format, hierarchy depth, descriptions, rate text, validity dates, and source metadata.

Each import runs in a transaction. A release becomes active only after every row is inserted. A failed import rolls back and leaves the prior active release available.

Apply or update the schema

psql -d customs_tariff -f database/schema.sql

The schema uses safe create and migration statements, so it can be reapplied after application updates.

10 · Tariff synchronization

Synchronizing official tariff data

Run synchronization from the project root with PostgreSQL configured. The command downloads each source, calculates a SHA-256 release key, normalizes rows, and atomically activates the completed release.

Synchronize everything

php bin/sync-tariffs.php --system=all

Synchronize one system

php bin/sync-tariffs.php --system=us
php bin/sync-tariffs.php --system=ch
php bin/sync-tariffs.php --system=uk
php bin/sync-tariffs.php --system=eu
php bin/sync-tariffs.php --system=br
php bin/sync-tariffs.php --system=ca
OptionDatabase systemDownloaded content
usHTSUSUSITC JSON export with hierarchy and general-rate text.
chSWISS_TARESFOCBS tariff-number structure CSV.
ukUK_TARIFFDBT commodities and descriptions CSV tables joined during parsing.
euEU_CNEnglish 2026 Combined Nomenclature spreadsheet.
brNCM_BRPortal Único Siscomex public NCM nomenclature JSON export.
caCA_TARIFFCBSA Customs Tariff Access database, converted to CSV with mdbtools.
No sync command for Australia or Japan

Neither ABF nor Japan Customs publishes a bulk downloadable tariff file — only individual chapter web pages with no official export. These two destinations use AI-guided format rules and official verification links only; there is no --system=au or --system=jp option and no tariff_codes release for either.

Import downloaded files

php bin/sync-tariffs.php --system=us \
  --us-file=/data/usitc.json

php bin/sync-tariffs.php --system=ch \
  --swiss-file=/data/TN_STRUCTURE_v1.csv

php bin/sync-tariffs.php --system=uk \
  --uk-file=/data/commodities.csv \
  --uk-descriptions-file=/data/commodity-descriptions.csv

php bin/sync-tariffs.php --system=eu \
  --eu-file=/data/CNofftextE_2026.xlsx

php bin/sync-tariffs.php --system=br \
  --br-file=/data/ncm.json

php bin/sync-tariffs.php --system=ca \
  --ca-file=/data/ca-tariff.csv

--ca-file expects an already-converted CSV (the output of mdb-export <file>.accdb TPHS), not the raw .accdb or the CBSA zip.

Suggested schedule

A daily attempt is appropriate. Identical files reuse the existing release instead of duplicating rows.

# Every day at 03:20 server time
20 3 * * * cd /path/to/customs_tariff_lookup && \
/usr/bin/php bin/sync-tariffs.php --system=all \
>> /var/log/tariff-sync.log 2>&1
Production scheduling

Use absolute paths, protect logs, monitor non-zero exit codes, and run under the same operating-system user used by the scheduler.

Expected output

HTSUS: imported 29725 rows (sha256-…)
SWISS_TARES: already current 14603 rows (sha256-…)
UK_TARIFF: imported 67477 rows (sha256-…)
EU_CN: imported 12583 rows (2026-sha256-…)
NCM_BR: imported 10515 rows (sha256-…)
CA_TARIFF: imported 10973 rows (sha256-…)

11 · Operations

Testing and maintenance

Application tests

php -d zend.assertions=1 -d assert.exception=1 tests/run.php

PHP syntax check

find . -name '*.php' -not -path './vendor/*' -exec php -l {} \;

Routine checks

  • Confirm the six expected active releases (U.S., Swiss, UK, EU, Brazil, Canada) and their import dates.
  • Run known U.S., Swiss, UK, EU, Brazilian, and Canadian samples and confirm destination verification.
  • Check cache headers and confirm cache hits report zero current-request cost.
  • Compare configured cost rates with current model pricing.
  • Change CACHE_VERSION after material prompt, schema, or rule changes.

12 · Security

Security and data handling

  • The API key remains server-side and is sent only to the OpenAI API authorization header.
  • Inputs are length-limited and normalized; product fields are treated as data, not model instructions.
  • The endpoint sets JSON content type, nosniff, and no-store headers.
  • PostgreSQL access uses prepared statements and JSONB.
  • Rendered verification links accept HTTPS and open with noopener noreferrer.
  • Production should enforce HTTPS, restrict database access, rotate secrets, and apply appropriate retention and privacy rules.

13 · Sources

Official and technical sources

14 · Limitations

What the application does not guarantee

  • A code existing in a schedule does not establish that it is legally correct for the goods.
  • Estimates may omit freight, insurance, valuation adjustments, de minimis rules, quotas, trade remedies, preferential origin, excise, or destination fees.
  • EU CN is annual 8-digit nomenclature. EU imports can require daily 10-digit TARIC codes and additional measures.
  • The stored UK dataset currently covers codes and descriptions; duty-measure integration is pending.
  • The UK destination currently follows the Great Britain tariff dataset. Northern Ireland can require the separate NI Online Tariff and additional route-specific treatment.
  • The stored Brazil NCM dataset currently covers codes and descriptions only. TEC import-duty rates and Brazilian taxes (Imposto de Importação, IPI, PIS/COFINS-Importação, state ICMS) are not imported and remain AI-estimated; confirm them with the Receita Federal simulator before filing.
  • Australia and Japan have no synchronized database at all, because neither ABF nor Japan Customs publishes a bulk downloadable tariff file. The au_tariff_code and jp_tariff_code results are always the model's own unverified guess, formatted per the classification rules in the system prompt, regardless of the tariff-source-mode switch.
  • Destinations outside the U.S., Switzerland, UK, EU, Brazil, and Canada currently receive an unverified six-digit HS suggestion.
  • WCO and WTO bulk datasets are not redistributed; their licensing must be resolved before importing protected material.
Before customs filing

Verify the code, effective date, origin treatment, customs value, and measures with the destination authority, a licensed broker, or a binding-ruling process.