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.
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.
02 · User guide
Using the tariff lookup
- 1Select the trade route
Choose the country of origin, export country, and import destination. All country fields are searchable.
- 2Name the product
Use a concise commercial name such as “T-shirt,” “ceramic mug,” or “lithium-ion battery pack.”
- 3Add classification facts
Describe material composition, construction, intended use or wearer, processing, dimensions, packaging, and distinctive features.
- 4Enter 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.
- 5Add 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.
- 6Choose 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.
- 7Choose 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. - 8Run 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_codeThe international six-digit Harmonized System subheading suggested by the model.
destination_tariff_codeThe 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_codeThe selected 10-digit UK import commodity code. Database mode accepts only records marked declarable in the active UK release.
eu_cn_codeThe selected and verified 8-digit EU Combined Nomenclature code. It is not a complete 10-digit TARIC measures determination.
br_ncm_codeThe 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_codeThe 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_codeThe 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_codeThe 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_verifiedConfirms the exact code exists in the active supported official dataset. It does not establish legal classification.
declaration_readyIndicates whether supplied facts can distinguish the required national suffix. It is not customs approval.
missing_informationSpecific additional facts needed to resolve a classification branch.
tax_componentsEstimated charges with type, rate, taxable base, amount, and calculation explanation.
total_taxesThe server-calculated sum of every tax-component amount. PHP recalculates this instead of trusting model arithmetic.
customs_valuationConverted product value, freight, insurance, other valuation adjustments, and PHP-recalculated CIF customs value in the dispatch-country currency.
input_declared_value / input_currencyThe product value and currency submitted by the user before conversion.
declared_value / currencyThe product value converted to the dispatch country’s official currency. Tax components use the same three-letter currency code.
ordered_resultThe 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_summaryA backward-compatible alias of ordered_result for existing API consumers.
request_costModel calls, token usage, and estimated USD token cost. Taxes, credits, negotiated prices, and data-provider fees are excluded.
result_languageThe requested two-letter output-language code. Human-readable descriptions, charge names, calculations, methodology, caveats, and missing-information text follow this language.
cache.hitWhen true, a stored result was reused and no OpenAI request was made for this lookup.
cache.mode / cache.bypassedReports the saved-result lookup method and whether the tariff_lookups cache was skipped.
tariff_sourceReports 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
| System | Resolution | Live lookup | Current scope |
|---|---|---|---|
| HTSUS | 10 digits | U.S. imports | Nomenclature, descriptions, and general duty-rate text |
| Swiss Tares | 8 digits | Swiss imports | Tariff-number structure and descriptions |
| UK Trade Tariff | 10 digits | UK imports | Declarable commodity nomenclature and descriptions; duty measures are not imported |
| EU CN | 8 digits | EU member-state imports | Annual CN verification; not daily 10-digit TARIC measures |
| Brazil NCM | 8 digits | Brazilian imports | Official NCM nomenclature and descriptions; TEC duty rates and II/IPI/PIS/COFINS/ICMS taxes are not imported |
| Canada Customs Tariff | 10 digits | Canadian imports | Full CBSA nomenclature, hierarchy, and Most-Favoured-Nation (MFN) duty-rate text |
| Australia Working Tariff | 8 digits | AI suggestion | No synchronized database (no official bulk export exists); format and classification rules only |
| Japan statistical code | 9 digits | AI suggestion | No synchronized database (no official bulk export exists); format and classification rules only |
| Other destinations | 6-digit HS | AI suggestion | No local destination-specific verification |
Configure PostgreSQL and synchronize datasets to show active release counts here.
05 · Technical design
Request architecture
- The browser posts JSON to
/api/lookup.php. ShipmentValidatornormalizes and validates every input field.- In Saved results first mode,
LookupRepositorychecks PostgreSQL for an identical, unexpired request hash. New AI request skips this result-cache check. - On a miss,
OpenAiTariffServiceturns the form into a short question-and-answer shipment prompt, then callsPOST /v1/responsesusing strict JSON Schema Structured Outputs. - When Tariff database is selected for U.S., Swiss, UK, EU, Brazilian, or Canadian imports, candidates are read only from the corresponding active
tariff_codesrelease. 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. - The code is verified against the active release. An unverified code is marked not declaration-ready.
- 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.
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
| Variable | Required | Purpose |
|---|---|---|
OPENAI_API_KEY | Yes | Server-side credential for OpenAI requests. |
OPENAI_MODEL | No | Model identifier; defaults to gpt-4o-mini. |
OPENAI_*_COST_PER_1M | No | Input, cached-input, and output pricing overrides. |
DB_DSN | Recommended | PDO PostgreSQL DSN. Empty disables persistence and cache. |
DB_USER / DB_PASSWORD | With DB | Database credentials. |
CACHE_TTL_SECONDS | No | Cache freshness window; default 86,400 seconds. Use 0 to disable. |
CACHE_VERSION | No | Change after prompt or result-rule changes to invalidate old keys. |
TARIFF_DATA_DIR | No | Downloaded source directory; defaults to storage/tariff-source. |
MDB_EXPORT_BIN | No | Path to the mdbtools mdb-export binary used only by the Canadian sync; defaults to mdb-export on PATH. |
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, orzh; defaults toen. - Lookup mode: optional
database_firstorai_only; defaults todatabase_first. - Tariff source mode: optional
tariff_databaseorai_only; defaults totariff_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
}
}
}
| Status | Meaning |
|---|---|
| 200 | Successful lookup or cache hit. |
| 405 | Method other than POST. |
| 422 | Invalid JSON or input validation failure. |
| 502 | OpenAI, 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_lookupsRequest JSON, result JSON, request hash, HS code, total taxes, currency, and creation time.
tariff_dataset_releasesImported versions with system, release key, official source, metadata, active status, and import time.
tariff_codesCodes, 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
| Option | Database system | Downloaded content |
|---|---|---|
us | HTSUS | USITC JSON export with hierarchy and general-rate text. |
ch | SWISS_TARES | FOCBS tariff-number structure CSV. |
uk | UK_TARIFF | DBT commodities and descriptions CSV tables joined during parsing. |
eu | EU_CN | English 2026 Combined Nomenclature spreadsheet. |
br | NCM_BR | Portal Único Siscomex public NCM nomenclature JSON export. |
ca | CA_TARIFF | CBSA Customs Tariff Access database, converted to CSV with mdbtools. |
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
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_VERSIONafter 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, andno-storeheaders. - 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_codeandjp_tariff_coderesults 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.
Verify the code, effective date, origin treatment, customs value, and measures with the destination authority, a licensed broker, or a binding-ruling process.