REST · MCP · GPT · OpenAPI 3.1.0

The reference layer, documented

24 REST endpoints · 25 MCP tools — every response carries its _source: the authority, the dataset edition, and where it came from.

> tools/call adr_lookup { "un_number": "1203" } { "count": 1, "results": [ { "verified": false, "verification": { "status": "unverified", "basis": "dataset", "reason": "No row-level audit. Transcribed from the UNECE ADR 2025 Table A and held by the dataset-level checks only (56 anchor rows carry a row-level audit)." }, "un_number": "1203", "proper_shipping_name": "MOTOR SPIRIT or GASOLINE or PETROL", "class": "3", "classification_code": "F1", "packing_group": "II", "labels": "3", "special_provisions": "243 534 664", "limited_quantity": "1 L", "excepted_quantity": "E2", "transport_category": "2", "tunnel_restriction_code": "(D/E)", "hazard_identification_number": "33", "variant_index": 0, "variant_count": 1 } ], "meta": { "source": "ADR 2025 (UNECE, ECE/TRANS/352, applicable 1 Jan 2025); given legal effect by EU Directive 2008/68/EC (consolidated). Factual compilation, best-effort — not legal advice, not a regulatory authority.", "edition": "ADR 2025", "entries": 2939 }, "_source": { "type": "reference", "authority": "UNECE", "dataset": "ADR 2025", "source_url": "https://unece.org/adr-2025-files", "data_vintage": "ADR 2025", "licence": "factual compilation, no named open licence", "retrieved_via": "freightutils.com", "dataset_edition": "ADR 2025", "authority_current_edition": "ADR 2025", "checked_at": "2026-10-15" } }
npx freightutils-mcp
Claude DesktopCursorChatGPTn8nZapierMake
Open Playground →📥 Download OpenAPI 3.1.0 Spec (JSON)📥 Download Postman Collection

Compatible with Swagger and Postman import

Overview

The FreightUtils API is the neutral freight reference layer for AI agents — source-backed dangerous-goods, customs, location and freight-calculation data an agent can call and cite, from primary sources (ADR 2025 / UNECE, HS 2022 / WCO, IATA-regulated airline prefixes). Neutral by design: no freight to sell and no carrier to push — a lookup returns what the named source says, with the citation attached so an agent can inspect it rather than take our word for it. It is a stateless REST API; every calculator on this site has a corresponding endpoint. No authentication is required. Responses are JSON. CORS is enabled for all origins.

Base URL: https://www.freightutils.com/api
Download OpenAPI 3.1.0 Spec (JSON)

Reliability & Support

FreightUtils APIs are hosted on Vercel's global edge network with automatic SSL and CDN caching.

Status
APIs are actively maintained and monitored.
Support
contact@freightutils.com — corrections and API issues typically addressed within 2 business days.
Versioning
Reference endpoints (ADR, HS, airlines) include a meta object with data source and edition information. Breaking changes will be announced via the API docs page.
Open Access Tier
No authentication required for casual use. Rate limit: 25 requests per day per IP. Free API keys available for 100/day.
Commercial Access
For production integrations with higher limits, see Pro pricing or contact us directly.
Free API Key
Get 100 requests/day — no credit card required
Enter your email and we'll send you an API key instantly.
Anonymous: 25/day · Free key: 100/day · Pro: 50,000/month
Need higher limits? See our plans →

MCP Server — AI Agent Integration

The full MCP server reference — all 25 tools, the response envelope, rate limits and directory listings — lives on its own page.

MCP server docs →

FreightUtils is the neutral freight reference layer for AI agents, available as a Model Context Protocol (MCP) server — direct, citable access to all 25 MCP tools (24 REST-backed + get_subscribe_link) from named primary sources, with no freight to sell and no carrier to push.

Install via npm
npx freightutils-mcp
Or connect via URL
https://www.freightutils.com/api/mcp

In Claude — claude.ai, the desktop and mobile apps, Cowork — add FreightUtils from Claude's connector directory, or as a custom connector with the URL above (how). No config file needed.

Claude Desktop Configuration
Add to your claude_desktop_config.json:
{ "mcpServers": { "freightutils": { "command": "npx", "args": ["freightutils-mcp"] } } }
First-time setup verification

After saving the config, fully quit and relaunch your MCP client (Claude Desktop, Cursor, Cline). MCP servers are only loaded at client startup — editing the config in a running session does nothing until restart.

Confirm the FreightUtils MCP surface is reachable before asking your agent any freight question:

curl https://www.freightutils.com/api/mcp/health

A 200 response with "status":"ok" and "tools_registered":25 means the remote MCP surface is live. The endpoint is callable by your agent too — Claude/Cursor/Cline can hit it for a self-diagnostic without you having to open a terminal.

Cursor Configuration
Cursor uses an mcp.json file (Settings → MCP). Same shape as Claude Desktop:
{ "mcpServers": { "freightutils": { "command": "npx", "args": ["-y", "freightutils-mcp"] } } }
Cline Configuration
Cline (VS Code extension): open the MCP server settings panel, click "Edit MCP Settings", add:
{ "mcpServers": { "freightutils": { "command": "npx", "args": ["-y", "freightutils-mcp"], "disabled": false, "autoApprove": [] } } }
Troubleshooting
SymptomLikely causeFix
Tools not appearing in the clientMCP client wasn’t restarted after the config editFully quit (Cmd+Q on macOS / right-click → Quit on Windows tray) and relaunch. Don’t just close the window.
"Server failed to start" / spawn error in client logsnpx not on PATH, or node version older than 18Install Node.js 18+ from nodejs.org. On macOS, an absolute path in the config (e.g. "/opt/homebrew/bin/npx") avoids PATH issues for GUI-launched clients.
Tool calls return HTTP 429 / "rate_limited"Anonymous IP cap of 25 requests/day exceededGet a free API key from the signup form below (100/day) or upgrade to Pro (50,000/month). The freightutils-mcp npm package passes the key through on every call (since v2.3.0) — set the FREIGHTUTILS_API_KEY env var in your MCP client config. On the remote URL (https://www.freightutils.com/api/mcp), send it as an X-API-Key header.
Specific tool returns "isError": trueBad input shape (snake_case vs camelCase, missing required field) or unknown lookup key (UN number / HS code / AWB prefix not in the dataset)The error message in the tool response names the field. Verify against the schema at /api-docs or call the corresponding playground endpoint directly to confirm the input shape.
Want to verify the surface from inside an agentNo CLI access during a conversationAsk the agent to fetch /api/mcp/health. The endpoint is public, returns the server version + registered tool count + transport URLs in a single JSON, and is rate-limit exempt so the diagnostic always works.
What AI agents can do
“Calculate loading metres for 20 Euro pallets on an artic trailer”
“Look up UN 1203 and check if 200 litres qualifies for ADR exemption”
“What's the chargeable weight for 2 boxes 120×80×100cm at 500kg?”
“Find the airline with AWB prefix 176”
“How many boxes 40×30×25cm fit on a Euro pallet?”

Get Started in 2 Minutes

// FreightUtils API — JavaScript example
// Calculate loading metres for 10 Euro pallets

const response = await fetch(
  'https://www.freightutils.com/api/ldm?pallet=euro&qty=10'
);
const data = await response.json();

console.log(`LDM: ${data.ldm}`);
console.log(`Utilisation: ${data.utilisation_percent}%`);
console.log(`Fits: ${data.fits}`);

All endpoints work the same way. No auth, no signup. Full reference below ↓

POST/api/shipment/summaryShipment Summary (Composite)Try it →
Flagship Endpoint

Composite endpoint that chains CBM, chargeable weight, LDM, ADR compliance, and UK duty/VAT estimation into a single call. Accepts a unified Shipment object and returns comprehensive results based on transport mode.

Mode Parameter

ModeCalculations Included
roadCBM, LDM, pallet spaces, trailer utilisation, road chargeable weight (1 LDM = 1,750 kg), vehicle suggestion
airCBM, volumetric weight (1 CBM = 167 kg), air chargeable weight
seaCBM, revenue tonnes (W/M at 1 CBM = 1,000 kg), container suggestion
multimodalAll of the above — road, air, and sea calculations combined

Unknown fields are refused, never ignored. A key the summary does not read — in the body, on an item, in origin / destination or in options — is refused with HTTP 400 naming it, and so is one field sent under two names (un_number and unNumber). Dropped, a line’s un: "1051" made it an ordinary line and the dangerous-goods check never saw a transport category 0 substance.

Per piece or per line. weight and customs_value are per item — each is multiplied by the line’s quantity (the response echoes customs.customsValueBasis and customs.customsValueTotal). adr_quantity is the total for the whole line, all pieces together, and is never multiplied (compliance.adrFlags.adrQuantityBasis: "line_total"). The suggested vehicle must hold the load by its own record — length, payload and pallet capacity — and trailerUtilisation is of that vehicle (utilisationBasis names it); the suggested container is the smallest dry box whose record holds the volume and payload.

Money is 0 or more. freight_cost, insurance_cost and each item’s customs_value must be a finite number of 0 or more; left out, null or blank means none. A negative, NaN or infinite value is refused with HTTP 400 naming the field — a negative freight cost used to come off the CIF value and the UK duty estimate with it.

Example Request

Mixed 3-item road shipment with a DG item and HS code:

curl -X POST "https://www.freightutils.com/api/shipment/summary" \ -H "Content-Type: application/json" \ -d '{ "mode": "road", "origin": { "country": "DE", "locode": "DEHAM" }, "destination": { "country": "GB", "locode": "GBFXT" }, "incoterm": "CIF", "freight_cost": 850, "insurance_cost": 120, "items": [ { "description": "Machine parts on Euro pallets", "length": 120, "width": 80, "height": 110, "weight": 480, "quantity": 6, "stackable": false, "pallet_type": "euro", "hsCode": "8479899790", "customs_value": 2000 }, { "description": "Cleaning solvent (DG)", "length": 60, "width": 40, "height": 50, "weight": 25, "quantity": 4, "unNumber": "1993" }, { "description": "Spare filters", "length": 40, "width": 30, "height": 20, "weight": 8, "quantity": 10, "stackable": true } ] }'

Example Response

{ "mode": "road", "itemCount": 3, "totals": { "pieces": 20, "grossWeight": 3060, "volumeCBM": 7.06, "chargeableWeight": 5337.5, "billingBasis": "volume" }, "modeSpecific": { "loadingMetres": 3.05, "palletSpaces": 15, "palletRows": 4, "trailerUtilisation": 33.52, "utilisationBasis": "18t Rigid (9.1 m load length)", "suggestedVehicle": "18t Rigid", "suggestedVehicleSlug": "18t-rigid", "chargeableWeightRoad": 5337.5 }, "compliance": { "hasDangerousGoods": true, "adrFlags": { "unNumbers": [ "1993" ], "totalPoints": null, "exemptionApplicable": null, "pointsBasis": "not_calculated", "adrQuantityBasis": "line_total", "pointsNote": "ADR 1.1.3.6 points are not calculated from a shipment summary: the only quantity here is gross package mass. Quantity on the ADR 1.1.3.6.3 basis: net kg for solids and liquefied, refrigerated or dissolved gases; litres for liquids; receptacle water capacity in litres for compressed or adsorbed gases and chemicals under pressure; articles as the mass of the articles without their packagings, in kg. Call adr_exemption_calculator with the quantity on that basis, and with the packing group or variant index for a multi-variant UN.", "variantAmbiguous": [ "1993" ], "outOfScope": [] } }, "customs": { "hsCodesPresent": true, "canEstimateUkDuty": true, "customsValueBasis": "per_item", "customsValueTotal": 12000, "dutyEstimate": { "cifValue": 12000, "dutyRate": "0.0%", "dutyAmount": 0, "vatRate": "20.0%", "vatAmount": 2400, "totalTaxes": 2400 } }, "warnings": [ "Dangerous goods detected — verify ADR compliance before transport", "Multiple ADR Table A variants for UN 1993 — packing group, transport category and limited quantity differ by variant; pin one before relying on any ADR verdict" ], "disclaimer": "Estimate only. Verify with your carrier, customs broker, or freight forwarder.", "dataVersion": { "duty": "GOV.UK Trade Tariff, fetched 2026-10-08", "adr": "UNECE ADR 2025", "hs": "WCO HS 2022", "vehicle": "vehicles-ref.json (17 records), audited 2026-05-16", "ldm": "loading-metre convention divisor 2.4 m (basis: ldm-convention)" } }

Pro-tier endpoint. Free access: 25 requests/day anonymous, 100/day with a free API key. Subscribe for higher limits.

GET/api/ldmLoading Metres CalculatorTry it →

Calculate the loading metres (LDM) required for a consignment on a UK/EU road freight trailer.

Parameters

ParameterTypeRequiredDescriptionDefault
lengthnumberYes*Pallet length in millimetres—
widthnumberYes*Pallet width in millimetres—
palletstringNo*Preset: euro, uk, half, quarter. Replaces length/width.—
qtyintegerNoNumber of pallets — a whole number; 2.5 or 1e+21 is refused with 400, never rounded1
stackablebooleanNoWhether pallets can be stackedfalse
stack2 or 3NoMax stack height (used when stackable=true)2
weightnumberNoWeight per pallet in kgnull
vehiclestringNoartic, rigid10 (DEPRECATED — removal in 3.0.0; still answers, returns a warning; use custom or artic), rigid75, luton, us53 (53ft US/Canada), us48 (48ft US), custom. Any other value is refused with 400.artic
vehicle_lengthnumberNoThe custom vehicle’s load length in metres, greater than 0 (required when vehicle=custom — fits and utilisation are measured against it)—

* Either pallet (preset ID) or both length and width must be provided.

Example Requests

12 Euro pallets on an artic:

GET /api/ldm?pallet=euro&qty=12&vehicle=artic

Custom dimensions, stackable, with weight check:

GET /api/ldm?length=1200&width=1000&qty=5&stackable=true&stack=2&weight=300&vehicle=rigid75

cURL example:

curl "https://www.freightutils.com/api/ldm?pallet=euro&qty=12&vehicle=artic"

US 53ft trailer:

curl "https://www.freightutils.com/api/ldm?pallet=euro&qty=20&vehicle=us53"

Response

{ "ldm": 4.8, "vehicle": { "name": "13.6m Artic Trailer", "length_m": 13.6, "max_payload_kg": 24000 }, "utilisation_percent": 35.29, "pallet_spaces": { "used": 12, "available": 33, "basis": "euro_floor_positions", "floor_positions_basis": "record_floor_count", "floor_positions_note": "13.6m Artic Trailer holds 33 Euro (1200 × 800) floor positions (its record's count, basis floor-positions).", "record_count": 33, "record_count_basis": "floor-positions" }, "total_weight_kg": null, "payload_checked": false, "fits": true, "warnings": [ { "type": "info", "message": "Payload not checked: no weight given. 13.6m Artic Trailer's payload is 24,000 kg — give a weight per pallet to check it." } ], "meta": { "inputs": { "length_mm": 1200, "width_mm": 800, "qty": 12, "stackable": false, "stack_factor": 2, "weight_per_pallet_kg": null, "vehicle": "artic" } } }
GET/api/cbmCubic Metres CalculatorTry it →

Calculate the cubic metre (CBM) volume of a shipment. Returns total CBM plus equivalents in cubic feet, litres, and cubic inches.

Parameters

ParameterTypeRequiredDescriptionDefault
lnumberYesLength of one piece in centimetres—
wnumberYesWidth of one piece in centimetres—
hnumberYesHeight of one piece in centimetres—
pcsintegerNoNumber of identical pieces — a whole number; 2.5 or 1e+21 is refused with 400, never rounded1

Example Request

5 boxes, 120×80×100 cm each:

curl "https://www.freightutils.com/api/cbm?l=120&w=80&h=100&pcs=5"
{ "cbm_per_piece": 0.96, "total_cbm": 4.8, "total_volume_m3": 4.8, "cubic_feet": 169.5106, "litres": 4800, "cubic_inches": 292913.8, "pieces": 5, "meta": { "inputs": { "length_cm": 120, "width_cm": 80, "height_cm": 100, "pieces": 5 } } }
GET/api/emissionsFreight Emissions (ISO 14083 / GLEC)Try it →

Estimate freight transport CO2e using the ISO 14083 / GLEC distance-based method: emissions = mass × distance × an open emission-intensity factor (kgCO2e/tonne-km). Provide actual gross mass, not chargeable/volumetric weight (a common air-freight mistake — see mass_basis). Returns well-to-wheel and tank-to-wheel emissions, the exact factor used, and a _source citing both the method and the specific open factor (DEFRA / EPA / ADEME). Each result also carries empty_running (the fleet-average factor already includes average empty running — don't double-count an empty return), representativeness (sea/air = low, high real-world variance), a human-readable summary, and _source.factor.last_verified. You provide the distance — this endpoint does not route. Best-effort estimate, not a verified carbon report; an unknown mode/sub_mode/region returns available:false with the covered options.

Parameters

ParameterTypeRequiredDescriptionDefault
massnumberYesShipment mass (in mass_unit)—
mass_unitkg | tonnesNoUnit for masskg
distance_kmnumberYesTransport distance in km (caller-provided; not routed)—
modeenumYesroad | rail | sea | air | inland_waterway—
sub_modestringNoVehicle class (e.g. articulated, container ship)representative
regionuk | us | frNoFactor source: uk=DEFRA, us=EPA, fr=ADEMEper-mode
basiswtw | ttwNoWell-to-wheel or tank-to-wheelwtw

Example Request

10 tonnes by road, 500 km, UK (DEFRA), well-to-wheel:

curl "https://www.freightutils.com/api/emissions?mass=10&mass_unit=tonnes&distance_km=500&mode=road&region=uk&basis=wtw"
{ "available": true, "tonne_km": 5000, "factor": { "authority": "UK DEFRA / DESNZ — Greenhouse gas reporting: conversion factors", "edition": "2026", "region": "uk", "unit": "kgCO2e/tonne-km", "wtw": 0.12715, "ttw": 0.10356 }, "emissions": { "wtw_kgco2e": 635.75, "ttw_kgco2e": 517.8, "primary_kgco2e": 635.75, "basis_used": "wtw" }, "mass_basis": "actual_gross_mass", "empty_running": "sector_average_included", "representativeness": "medium", "confidence": "medium — fleet-average factor representative for the mode", "summary": "635.75 kgCO2e WTW for 10 t × 500 km by road (UK DEFRA 2026 fleet-average factor). Uses ACTUAL GROSS MASS, not chargeable/volumetric weight. Fleet-average empty running is already included — do not add a separate empty-return leg.", "methodology": "ISO 14083: 2023 / GLEC Framework v3.2", "_source": { "type": "methodology", "standard": "ISO 14083: 2023 / GLEC Framework v3.2", "source_url": "https://www.freightutils.com/methodology", "computed_by": "freightutils.com", "factor": { "authority": "UK DEFRA / DESNZ — Greenhouse gas reporting: conversion factors", "edition": "2026", "value": 0.12715, "unit": "kgCO2e/tonne-km", "basis": "wtw", "last_verified": "2026-06-26", "licence": "Open Government Licence v3.0" } } }
GET/api/validateIdentifier Validator (ISO 6346 / AWB / IMO)Try it →

Parse an arbitrary string (e.g. a booking line or email) to find and validate every freight identifier in it — shipping container (ISO 6346), air waybill (IATA modulus-7) and IMO ship number — or validate a single identifier by type. Returns per identifier: type, normalised form, valid (pass/fail), expected vs actual check digit, details (container owner/category; the AWB airline, or airline_candidates when the prefix is held by more than one record; IMO number) and a _source. Structural only — a valid check digit means well-formed, not that the entity exists.

Parameters

ParameterTypeRequiredDescriptionDefault
textstringNo*Arbitrary string to scan for container / AWB / IMO identifiers (parse mode)—
valuestringNo*A single identifier to validate (typed mode); requires type—
typecontainer | awb | imoNo*Identifier type for value—

*Provide text (parse mode) OR value+type (typed mode).

Example Request

Parse a mixed string:

curl "https://www.freightutils.com/api/validate?text=Box%20CSQU3054383%20on%20IMO9074729"
{ "found": [ { "type": "container", "raw": "CSQU3054383", "normalised": "CSQU3054383", "valid": true, "check_digit": { "expected": 3, "actual": 3 }, "details": { "owner_prefix": "CSQ", "equipment_category": "U", "equipment_category_label": "Freight container", "serial": "305438" }, "_source": { "type": "methodology", "standard": "ISO 6346", "authority": "ISO", "computed_by": "freightutils.com" } }, { "type": "imo", "raw": "IMO9074729", "normalised": "IMO9074729", "valid": true, "check_digit": { "expected": 9, "actual": 9 }, "details": { "number": "9074729" }, "_source": { "type": "methodology", "standard": "IMO Ship Identification Number Scheme", "authority": "IMO", "computed_by": "freightutils.com" } } ], "disclaimer": "Structural validation only — a valid check digit means the identifier is well-formed, not that the container/shipment/vessel exists or is active." }
GET/api/ics2-checkICS2 Stop-Words Checker (EU goods descriptions)Try it →

Check a goods description against the official EU ICS2 stop-words list (unacceptable/vague terms for entry summary declarations). Returns the flagged terms — each with a note on whether it is the standalone description (automatic ENS rejection) or embedded (make the description more specific) — a clean boolean, a caveat, and a _source citing the EU list + legal basis. Reference only: not an ENS filing, not a compliance determination; the list is non-exhaustive and clean does not guarantee acceptance. No accepted/rejected verdict. The submitted description is not persisted or logged (response is no-store).

Parameters

ParameterTypeRequiredDescriptionDefault
descriptionstringYesThe goods description to check against the EU ICS2 stop-words list—

Example Request

A vague standalone description:

curl "https://www.freightutils.com/api/ics2-check?description=general%20cargo"
{ "description": "general cargo", "flagged": [ { "term": "General cargo", "note": "Used as the entire goods description — a listed stop-word on its own is an automatic ENS rejection. Replace it with a specific product description..." } ], "clean": false, "caveat": "The EU ICS2 stop-words list is non-exhaustive ... a \"clean\" result does NOT mean the declaration will be accepted ...", "_source": { "type": "reference", "authority": "European Commission DG TAXUD — EU ICS2 (Advance Cargo Information System)", "legal_basis": "Commission Delegated Regulation (EU) 2015/2446, Annex B, data element 18 05 000 000 (Description of goods)", "list_in_force": "2026-05-04", "non_exhaustive": true, "retrieved_via": "freightutils.com" }, "disclaimer": "Reference check only. ... not an ENS filing, not a customs-compliance determination, and not legal advice ..." }
GET/api/airportsAirport Code Lookup (IATA / ICAO / name)Try it →

Look up an airport by IATA code (3 letters), ICAO code (4 chars), or free-text name / city search. Provide one of iata, icao or q. Returns the full record (both codes, name, type, municipality, region, country, coordinates, elevation); ambiguous name searches return ranked candidates. Optional type filter. Data: OurAirports (public domain), cross-checked vs OpenFlights + Wikidata. Reference only — not for navigation; verify current codes with IATA/ICAO.

Parameters

ParameterTypeRequiredDescriptionDefault
iatastringOne ofExact 3-letter IATA code (e.g. LHR)—
icaostringOne ofExact 4-character ICAO code (e.g. EGLL)—
qstringOne ofName / city / municipality search (min 2 chars)—
typestringNoFilter: large_airport, medium_airport, small_airport, heliport, closed, seaplane_base—

Example Request

curl "https://www.freightutils.com/api/airports?q=heathrow"
{ "count": 1, "results": [ { "ident": "EGLL", "iata_code": "LHR", "name": "London Heathrow Airport", "type": "large_airport", "municipality": "London", "region": "England", "country": "GB", "country_name": "United Kingdom", "latitude": 51.470748, "longitude": -0.459909, "elevation_ft": 83 } ], "disclaimer": "Airport reference data from OurAirports (public domain) ... not for navigation; verify current IATA / ICAO codes with the airport authority.", "_source": { "type": "reference", "authority": "OurAirports (community-maintained open data)", "dataset": "OurAirports 2026-06-24", "source_url": "https://ourairports.com/data/", "licence": "Public Domain (OurAirports — dedicated to the public domain)", "retrieved_via": "freightutils.com" } }
GET/api/nearest-airportNearest airports to a coordinateTry it →

Find the airports nearest to a caller-provided latitude/longitude, sorted by great-circle (haversine) distance with distance_km on each result. Coordinates are input only — never stored or logged. Optional radius_km, max_results (1–50, default 10) and type filter. Does NOT geocode place names or compute routes — pass coordinates you already hold.

Parameters

ParameterTypeRequiredDescriptionDefault
latnumberYesLatitude in decimal degrees (-90 to 90)—
lonnumberYesLongitude in decimal degrees (-180 to 180)—
radius_kmnumberNoMaximum distance in kilometres—
max_resultsintegerNoResults to return (1–50)10
typestringNoFilter by airport type (e.g. large_airport)—

Example Request

curl "https://www.freightutils.com/api/nearest-airport?lat=51.47&lon=-0.46&max_results=3&type=large_airport"
{ "count": 3, "results": [ { "ident": "EGLL", "iata_code": "LHR", "name": "London Heathrow Airport", "type": "large_airport", "country": "GB", "latitude": 51.470748, "longitude": -0.459909, "distance_km": 0.1 }, { "ident": "EGKK", "iata_code": "LGW", "name": "London Gatwick Airport", "type": "large_airport", "country": "GB", "latitude": 51.148102, "longitude": -0.190278, "distance_km": 40.6 }, { "ident": "EGGW", "iata_code": "LTN", "name": "London Luton Airport", "type": "large_airport", "country": "GB", "latitude": 51.874699, "longitude": -0.368333, "distance_km": 45.4 } ], "disclaimer": "Airport reference data from OurAirports (public domain) ... coordinates are the airport reference point, not for navigation.", "_source": { "type": "reference", "authority": "OurAirports (community-maintained open data)", "dataset": "OurAirports 2026-06-24", "licence": "Public Domain (OurAirports — dedicated to the public domain)", "retrieved_via": "freightutils.com" } }
GET/api/resolveResolve any freight identifierTry it →

The agent front door: one identifier-ish string in, typed, ranked, cited candidates out — without needing to know which lookup endpoint fits. Thirteen grammars all run on every query: UN numbers, AWB prefixes, airline IATA/ICAO, airport IATA/ICAO, UN/LOCODE, ISO 6346 container numbers (check digit computed), HS codes, Incoterms, ADR tunnel codes, ULD serials and ISO container size/type codes. Ambiguity is the product: colliding grammars return multiple candidates with an inspectable rank_basis on each, never a silent guess. Zero candidates is a validcount: 0 result — never an error. v1 resolves a single token (max 32 chars); free-text extraction is out of scope. This endpoint is envelope v1.1 native — responses are always enveloped; pass ?envelope=1 for the byte-stable v1 envelope.

Parameters

ParameterTypeRequiredDescription
qstringYesOne freight identifier — e.g. 176, UN1845, NLRTM, FOB, 22G1, MSKU1100810, 090111, D/E

Example Request

curl "https://www.freightutils.com/api/resolve?q=LHR"
{ "ok": true, "envelope_version": "1.1", "result": { "query": "LHR", "normalized": "LHR", "count": 2, "candidates": [ { "entity_type": "airport", "identifier_type": "airport_iata", "value_normalized": "LHR", "summary": "London Heathrow Airport — London, United Kingdom", "record": { "ident": "EGLL", "iata_code": "LHR", "name": "London Heathrow Airport", "type": "large_airport" }, "verification_status": "verified", "verification_basis": "dataset", "rank_basis": "dataset record hit · verification verified (dataset-level) · static prior airports #6 of 10 · tiebreak airport_iata/LHR", "canonical_url": "/airports", "api_url": "/api/airports?iata=LHR", "citation": { "text": "London Heathrow Airport (LHR) — OurAirports open data ..." } }, { "entity_type": "airline", "identifier_type": "airline_icao", "value_normalized": "LHR", "summary": "ICAO LHR — Al Ahram Aviation — Egypt", "record": { "airline_name": "Al Ahram Aviation", "icao_code": "LHR" }, "verification_status": "unchecked", "verification_basis": "record", "rank_basis": "dataset record hit · verification unchecked (record-level) · static prior airlines #1 of 10 · tiebreak airline_icao/LHR", "canonical_url": "/airlines/al-ahram-aviation", "api_url": "/api/airlines?icao=LHR", "citation": { "text": "ICAO LHR — Al Ahram Aviation ..." } } ] }, "confidence": { "level": "high", "basis": "deterministic" }, "citation": { "text": "\"LHR\" resolved to 2 candidates — top: airport_iata LHR, London Heathrow Airport ..." } }

Response Fields (per candidate)

FieldTypeDescription
entity_typestringWhat the candidate IS — dangerous_good, airline, airport, location, hs_classification, incoterm, container_type, container_equipment, uld_type, tunnel_restriction
identifier_typestringWhich grammar matched — un_number, awb_prefix, airline_iata, airline_icao, airport_iata, airport_icao, unlocode, container_number, hs_code, incoterm, tunnel_code, uld_type, iso_container_type
recordobject | nullCore fields from the matching dataset; null for a pattern-only match (e.g. an ISO type code not in the served set)
verification_statusstringNormalized across datasets — active_verified, verified, computed, partial, pending_verification, unchecked, unverified, historical, pattern_only
verification_basisstringrecord = from this record’s own provenance; dataset = the dataset-level claim (no record-level signal exists)
rank_basisstringWhy the candidate sits where it does — record hit > verification status > static dataset prior > stable tiebreak
canonical_url / api_urlstring?Where to go deeper — the record’s page and the dataset’s REST query; omitted when none exists
citationobjectThe matching dataset’s own citation for this record — authority stays attached to the record it describes
GET/api/chargeable-weightAir Freight Chargeable WeightTry it →

Calculate air freight chargeable weight — whichever is higher between actual gross weight and volumetric (dimensional) weight. Supports custom volumetric factors for all carriers.

Parameters

ParameterTypeRequiredDescriptionDefault
lnumberYesLength of one piece in centimetres—
wnumberYesWidth of one piece in centimetres—
hnumberYesHeight of one piece in centimetres—
gwnumberYesTotal gross weight of all pieces in kg—
pcsintegerNoNumber of identical pieces — a whole number; 2.5 or 1e+21 is refused with 400, never rounded1
factorintegerNoVolumetric divisor, a whole number: 6000 (IATA standard), 5000 (express carriers)6000
modestringNoair or sea (W/M, 1 CBM = 1 revenue tonne), in any case. Any other value is refused with 400 — never computed as air.air

Example Request

2 pieces, 120×80×100 cm, 500 kg total, IATA factor:

curl "https://www.freightutils.com/api/chargeable-weight?l=120&w=80&h=100&gw=500&pcs=2&factor=6000"
{ "chargeable_weight_kg": 500, "basis": "actual", "gross_weight_kg": 500, "volumetric_weight_kg": 320, "volumetric_weight_per_piece_kg": 160, "cbm": 1.92, "ratio": 3.84, "factor": 6000, "pieces": 2, "meta": { "inputs": { "length_cm": 120, "width_cm": 80, "height_cm": 100, "gross_weight_kg": 500, "pieces": 2, "factor": 6000 } } }
GET/api/palletPallet Box Fitting CalculatorTry it →

Calculate how many boxes fit on a pallet using a layer-based algorithm. Returns boxes per layer, number of layers, total boxes, orientation used, and volume/weight analysis. Optional weight constraint caps the result at the pallet's maximum payload.

Parameters

ParameterTypeRequiredDescriptionDefault
plnumberYesPallet length in centimetres—
pwnumberYesPallet width in centimetres—
pmhnumberYesMaximum total stack height in centimetres (floor to top of cargo)—
blnumberYesBox length in centimetres—
bwnumberYesBox width in centimetres—
bhnumberYesBox height in centimetres—
phnumberNoPallet board/deck height in cm — deducted from usable height15
bwtnumberNoWeight per box in kg — enables weight constraint calculation—
mpwnumberNoMaximum pallet payload weight in kg — caps result if weight exceeded—
rotatebooleanNoAllow 90° rotation of boxes for best fit. Pass false to disable.true

Example Request

curl "https://www.freightutils.com/api/pallet?pl=120&pw=80&pmh=220&bl=40&bw=30&bh=25&bwt=5&mpw=1500"
{ "boxes_per_layer": 8, "layers": 8, "total_boxes": 64, "orientation": "rotated", "boxes_per_row": 4, "boxes_per_col": 2, "usable_height_cm": 205, "utilisation_percent": 100, "total_box_volume_cbm": 1.92, "pallet_volume_cbm": 1.968, "wasted_space_cbm": 0.048, "weight_limited": false, "total_weight_kg": 320, "remaining_weight_capacity_kg": 1180, "volume_utilisation_percent": 97.6, "max_boxes_by_weight": 300, "notes": [], "meta": { "inputs": { "pallet_length_cm": 120, "pallet_width_cm": 80, "pallet_max_height_cm": 220, "pallet_height_cm": 15, "box_length_cm": 40, "box_width_cm": 30, "box_height_cm": 25, "box_weight_kg": 5, "max_payload_weight_kg": 1500, "allow_rotation": true } } }
GET/api/adrADR 2025 Dangerous Goods LookupTry it →

Look up ADR 2025 dangerous goods by UN number, search by substance name, or filter by hazard class. The dataset contains 2,939 entries from the ADR 2025 Dangerous Goods List (Table A). Responses are cached for 1 hour (s-maxage=3600).

Query Modes

ParameterTypeDescriptionMax results
unstringExact UN number lookup. Accepts 1203, UN1203, UN 1203 or 01203.1
searchstringCase-insensitive partial match on the proper shipping name. Min 2 characters. Also accepts q as an alias.50
classstringFilter by ADR hazard class, Table A column (3a) (e.g. 3, 6.1). Explosives are class 1: the division and compatibility group are in classification_code (e.g. 1.4S), so 1.4 matches no row.100
offset / limitintegerPaging for search and class. Every list answer carries total, truncated and next_offset (class 8 is 368 rows); count is the rows on this page. A search with no hits is a 404 with suggestions — the closest proper shipping names in Table A (“acetne” → ACETONE, UN 1090).0 / 50 or 100

Provide exactly one parameter per request. Omitting all parameters returns a 400 with usage hints.

Example Requests

Exact UN number lookup:

curl "https://www.freightutils.com/api/adr?un=1203"
{ "count": 1, "results": [ { "un_number": "1203", "proper_shipping_name": "MOTOR SPIRIT or GASOLINE or PETROL", "class": "3", "classification_code": "F1", "packing_group": "II", "labels": "3", "special_provisions": "243 534 664", "limited_quantity": "1 L", "excepted_quantity": "E2", "transport_category": "2", "tunnel_restriction_code": "(D/E)", "hazard_identification_number": "33", "variant_index": 0, "variant_count": 1, "verified": false, "verification": { "status": "unverified", "basis": "dataset", "reason": "No row-level audit. Transcribed from the UNECE ADR 2025 Table A and held by the dataset-level checks only (56 anchor rows carry a row-level audit)." } } ], "meta": { "source": "ADR 2025 (UNECE, ECE/TRANS/352); given legal effect by EU Directive 2008/68/EC (consolidated)", "edition": "ADR 2025", "entries": 2939 } }

Per-row verification: every row carries verified and a verification block, always. verified: true (status verified, basis row) only where the row itself was audited field by field against the purchased UNECE Table A — then audited_at, the cited source count and the row’s sources[] ride along (56 anchor rows today, e.g. UN 1845, UN 1789). Every other row says verified: false with the reason no row-level audit: it was transcribed from the same Table A and is held by the dataset-level checks only. The envelope’s _source.provenance_status is the dataset-level claim; the row field is the row’s own. Silence is never verification.

Scope-flagged rows: 28 Table A entries carry a scope remark instead of a packing group. Those return packing_group: null plus not_subject_to_adr: true(with conditions_ref, e.g. "5.5.3" for UN 1845 dry ice) orcarriage_prohibited: true, and table_a_remark preserves the verbatim Table A text.

Search by substance name:

curl "https://www.freightutils.com/api/adr?search=acetone"
{ "count": 3, "results": [ ... ] }

Filter by hazard class:

curl "https://www.freightutils.com/api/adr?class=3"
{ "count": 50, "results": [ ... ] }
GETPOST/api/adr-calculatorADR 1.1.3.6 Exemption CalculatorTry it →

Calculate whether the ADR 1.1.3.6 small load exemption applies to dangerous goods carried in packages (1.1.3.6.2 — not goods in bulk or in tanks; the calculator takes no packaging input). Supports single-substance GET queries and multi-substance POST requests. Checks the 1.1.3.6.4 points total (1,000) and the 1.1.3.6.3 maximum for each transport category — a total on the vehicle when every line is in the same category. Every line comes back with a state naming its route: COUNTED, AIR_ONLY_ID, NOT_SUBJECT_TO_ADR or BLOCKED.

GET — Single Substance

ParameterTypeRequiredDescription
unstringYesUN number (e.g. 1203) — or an ID-prefixed air-only number (e.g. ID8000), which has no ADR Table A entry: its line is routed AIR_ONLY_ID and nothing is counted for it under 1.1.3.6 — the ADR answer for limited-quantity packages to the ICAO Technical Instructions (3.4.9, 3.4.10, 1.1.3.6.5), which the line’s warning states
qtynumberYesQuantity on the ADR 1.1.3.6.3 basis: net kg for solids and liquefied, refrigerated or dissolved gases; litres for liquids; receptacle water capacity in litres for compressed or adsorbed gases and chemicals under pressure; articles as the mass of the articles without their packagings, in kg.
unitstringNoL or kg. Omit it and qty is taken as already on the 1.1.3.6.3 basis (unchanged behaviour). Supply it and it is checked against the dimension 1.1.3.6.3 counts for that row; a mismatch returns points: null, total_points: null, exempt: null and items[].basis_mismatch: true with a warning naming the right dimension — never a total computed from the wrong quantity. A row whose Table A column (15) carries no transport category (–, or See SP 671 on the kit entry UN 3316) returns the same null total with items[].category_unresolved: true and a warning naming the column value.
basisstringNonet or gross. 1.1.3.6.3 counts the goods, never the packaging, so gross withholds the verdict in any unit.

Example — 200 litres of petrol:

curl "https://www.freightutils.com/api/adr-calculator?un=1203&qty=200"
{ "items": [ { "un_number": "1203", "state": "COUNTED", "proper_shipping_name": "MOTOR SPIRIT or GASOLINE or PETROL", "class": "3", "transport_category": "2", "quantity": 200, "multiplier": 3, "points": 600 } ], "total_points": 600, "threshold": 1000, "exempt": true, "has_category_zero": false, "has_quantity_exceedance": false, "warnings": [], "message": "1.1.3.6 exemption applies" }

POST — Multi-Substance Load

Request body:

POST /api/adr-calculator Content-Type: application/json { "items": [ { "un_number": "1203", "quantity": 200 }, { "un_number": "1090", "quantity": 50 } ] }

Response structure is identical to the GET endpoint, with multiple items in the array.

Inputs are never coerced. Every quantity must be a JSON number greater than 0 and at most 1,000,000,000 (GET qty: digits, e.g. 200,0.5 or 5e-7). Text such as "300 kg", null,0 or a negative value is refused with HTTP 400 naming the value received — one bad line refuses the whole request, and no points are ever computed from it. unit accepts onlyL or kg, basis only net or gross,packing_group only I/II/III, andvariant_index only a whole number (a blank or false no longer means row 0); a GET parameter sent twice is refused. When every line is in the same transport category, the column (3) maximum is checked against the total on the vehicle (1.1.3.6.3), not line by line. Every field of a line — un_number, quantity, packing_group, variant_index, unit, basis — goes on that line in a POST: sent at the top level beside items[] it is refused with HTTP 400 naming the field (a substance as an item of its own) — never dropped, and never applied to a line by guess.

Unknown fields are refused, never ignored. A key the calculator does not read — on an item, beside items[], or as a GET parameter — is refused with HTTP 400 that names it, suggests the accepted name it most likely meant and lists the accepted ones. Field names are exact: quantity_basis (the name the answer echoes), Unit or UN_NUMBER used to be dropped without a word, so a declaration under the wrong name — gross, kilograms, a second substance — changed nothing and the load could read exempt. Accepted on an item: un_number, quantity, packing_group, variant_index, unit, basis; GET parameters: un, qty, unit, basis, packing_group, variant_index, plus envelope. Send the API key in the X-API-Key header. The same rule holds on /api/adr/lq-check, /api/shipment/summary and the MCP tools.

Multi-variant UN numbers: some UN numbers have more than one ADR Table A row (different packing groups / concentration bands with different transport categories — e.g. UN 1789 PG II vs PG III). Pass an optional packing_group(I/II/III, GET query or per item) or variant_index(from /api/adr) to pin one row. Without one, the response is HTTP 200 with blocking_errors[AMBIGUOUS_UN_VARIANT], human_review_required: true and a candidates[] list (no verdict) rather than a silently-guessed row — the code is in the plain body as well as the envelope. Its recovery names the line’s item_index and the fields that choose a row (packing_group or variant_index), and never chooses one itself: its params repeat your call without a row, so sent unchanged they are asked again. Pick the row from candidates[] by the goodsrow from candidates[] by the goods’ own packing group or concentration, never byrsquo; own packing group, concentration or description, never by its position in the list. A packing_group that still leaves several rows is enough when those rows agree on class, transport category, scope and the 1.1.3.6.3 counted dimension (UN 1790 PG I: two concentration bands, both category 1) — the verdict is returned with items[].equivalent_variants and avariant_note saying which rows it holds for; only when the rows disagree(UN 2215 PG III: MOLTEN is category 0, the solid category 3) does it still ask for variant_index, and then candidates[] lists only that packing group’s rows. In a mixed load, an unpinned multi-variant line is withheld on its own (items[].withheld: true, points null) while every other line keeps its points; total_points is null until it is pinned, because 1.1.3.6.4 sums every line, and exempt is null too unless the resolved lines alone already disqualify the load — a “CARRIAGE PROHIBITED” entry, a category 0 entry, a per-substance maximum exceeded, or a partial sum already over 1,000 — in which case exempt is false with the usual message (and carriage_prohibited: true where that is the reason), since no packing group can undo those. candidates[] carry expected_unit, the 1.1.3.6.3 counted dimension — for UN 3375 PG II (liquid in L, solid in kg) that is the only field on which the rows differ. Only a load in which every line is ambiguous returns the no-items candidates[] shape.

Response Fields

FieldTypeDescription
items[].statestringThe line’s route, in the document check’s words: COUNTED (evaluated on one Table A row — its points, or a category 0 / carriage-prohibited row that settles the load), AIR_ONLY_ID (an ID-prefixed number: no Table A entry, nothing counted — the limited-quantity condition is in the line’s warning), NOT_SUBJECT_TO_ADR, or BLOCKED (no points from the line as sent — withheld, basis mismatch, no transport category; its own flag says which)
items[].air_only_idboolean?true on an ID-prefixed air-only line: un_number is the ID number (e.g. ID8000), the row fields are null and the line never enters the points math — never not_subject_to_adr
packing_groupstring | nullResolved ADR packing group for the row (echoed for traceability)
variant_indexnumber | nullResolved ADR Table A variant index for the row (null on a withheld line)
transport_categorystring | nullADR transport category (0–4); null on a withheld line
multipliernumber | nullPoints multiplier for the category (null for cat 0, out-of-scope rows and withheld lines)
pointsnumber | nullquantity × multiplier
items[].withheldboolean?true when this line’s UN resolves to more than one Table A row and nothing pinned one — its row fields are null (never the first row’s), its candidate rows sit under candidates[] tagged with its item_index
items[].equivalent_variantsobject[]?Present when the supplied packing_group left several rows that agree on every field the verdict uses: the rows (variant_index, proper_shipping_name) this verdict holds for; variant_note says so in words and which row’s fields are shown
total_pointsnumber | nullSum of all substance points; null while any line is withheld or declared on a basis 1.1.3.6.3 does not count
exemptboolean | nulltrue if total ≤ 1,000 AND no cat 0 AND no quantity exceedance; null when no load verdict was reached (never false as a stand-in)
human_review_requiredboolean?true when at least one line is withheld — candidates[] then lists the withheld lines’ rows, each tagged with item_index and un_number
blocking_errorsobject[]?Only when every line is ambiguous (no verdict, no items): [{ code: "AMBIGUOUS_UN_VARIANT", message, recovery }] — the same array the envelope carries, in the plain body too. recovery.params never pick a row: sent unchanged they return this code again
has_category_zerobooleantrue if any substance is transport category 0
has_quantity_exceedancebooleantrue if any substance exceeds its per-category max quantity
warningsstring[]Human-readable warning messages for limit violations and out-of-scope exclusions
not_subject_to_adrboolean?true when EVERY item is listed “NOT SUBJECT TO ADR” in Table A — the 1.1.3.6 math is not applied (there is nothing to be exempt from)
conditions_refstring?ADR section whose carriage conditions still apply to a not-subject load (e.g. 5.5.3 for UN 1845 dry ice)
conditionsobject[]?Verbatim ADR 2025 conditions for conditions_ref ({ref, heading, requirement, note?})
carriage_prohibitedboolean?true when the load contains a “CARRIAGE PROHIBITED” Table A entry — exempt is false; the load may not be carried by road under ADR

Scope verdicts: 28 ADR 2025 Table A rows carry a scope remark instead of a packing group. A load of such entries never enters the 1.1.3.6 points math — e.g.?un=1845&qty=100 (dry ice) returns not_subject_to_adr: true withmessage “Not subject to ADR (road). Section 5.5.3 applies: …” and the verbatim 5.5.3 conditions, while a “CARRIAGE PROHIBITED” entry (e.g. UN 0020) returns exempt: false with carriage_prohibited: true. An ID-prefixed air-only number (e.g. ID 8000, consumer commodity) has no ADR Table A entry and so gives no transport category: its line is AIR_ONLY_ID and adds nothing to the points, with a warning stating the condition that rests on — it is the ADR answer when the packages are limited quantities to the ICAO Technical Instructions (3.4.9 and 3.4.10 deem them to meet 3.4.1–3.4.4, and goods exempted under 1.1.3.4.2 are not taken into account by 1.1.3.6.5); goods not in such packages must be entered under their own UN numbers. A load of nothing else scores 0 with its own message — never “1.1.3.6 exemption applies”, and never “not subject to ADR”, which is a Table A remark an ID number does not have. Only the ID prefix makes a line air-only.

POST/api/adr/lq-checkADR Limited & Excepted Quantity CheckerTry it →

Check whether dangerous goods qualify for ADR Limited Quantity (Chapter 3.4) or Excepted Quantity (Chapter 3.5) concessions. Accepts up to 20 items per request and returns per-item pass/fail status against ADR Table A limits.

Request Body

FieldTypeRequiredDescription
modestringNolq (default) or eq
itemsarrayYes1–20 items to check
items[].un_numberstringYesUN number (e.g. 1203)
items[].quantitynumberYesQuantity per inner packaging
items[].unitstringNoml, L (default), g, or kg
items[].inner_packaging_qtynumberNoInner packagings in one outer packaging (EQ mode only) — a whole number of 1 or more; omitted means 1. 0, -20 or 16.5 is refused with 400.
items[].packing_groupstringNoI/II/III — disambiguates a UN number with more than one ADR Table A row (e.g. UN 1789). Ignored for single-row UN numbers.
items[].variant_indexnumberNoADR Table A variant index (from /api/adr) — pins one row when variants share a packing group (concentration bands).

A UN number that resolves to more than one ADR Table A row without a packing_group/variant_index returns HTTP 200 with human_review_required: true and a candidates[] list (no verdict) rather than checking a silently-guessed packing group. A packing_group that still leaves several rows is enough when those rows agree on class, column (7a), column (7b) and scope (UN 1790 PG I: two concentration bands, both LQ 0 / E0) — the item is answered with equivalent_variants and a variant_note; only when the rows disagree does it still ask for variant_index, listing only that packing group’s rows. In a batch, an unpinned multi-variant line is withheld on its own(status: "withheld", row fields null) while the other lines are answered;overall_status can never read qualifies while a line is withheld, andcandidates[] carries that line’s rows tagged with its item_index. Only a batch in which every line is ambiguous returns the no-items shape; it then carries blocking_errors[AMBIGUOUS_UN_VARIANT], one per line, in the plain body as well as the envelope, whose recovery names the line’s item_index and the fields that choose a row and never chooses one — its params, sent unchanged, are asked again.

Unknown fields are refused, never ignored. Only the fields above are read; any other key — on an item (inner_packagings, units), at the top level (Mode, or inner_packaging_qty beside items[]) or in the query string — is refused with HTTP 400 naming it and suggesting the accepted name. Dropped, a count of 20 inner packagings fell back to one and an EQ check read qualifies where 20 × 30 ml is over E2’s 500 ml per outer packaging.

Example — check LQ for 0.5 L of petrol:

curl -X POST https://www.freightutils.com/api/adr/lq-check \ -H "Content-Type: application/json" \ -d '{"mode":"lq","items":[{"un_number":"1203","quantity":0.5,"unit":"L"}]}'
{ "mode": "lq", "overall_status": "qualifies", "items": [ { "un_number": "1203", "substance": "MOTOR SPIRIT or GASOLINE or PETROL", "class": "3", "packing_group": "II", "lq_limit": "1 L", "lq_limit_value": 1, "lq_limit_unit": "L", "eq_code": "E2", "quantity_entered": 0.5, "unit_entered": "L", "status": "within_limit", "reason": "0.5 L is within the LQ limit of 1 L per inner packaging" } ], "summary": { "total_items": 1, "qualifying": 1, "exceeding": 0, "not_permitted": 0 }, "references": { "adr_chapter": "3.4", "table": "3.2 Column 7a" } }

Response Fields

FieldTypeDescription
modestringlq or eq
overall_statusstringqualifies, does_not_qualify, partial, not_applicable (every item is listed “NOT SUBJECT TO ADR” in Table A — LQ/EQ provisions do not apply), or inconclusive (nothing in the batch could be compared). A batch holding any inconclusive or withheld item never reads qualifies.
items[]arrayPer-item results with substance info, limits, and pass/fail
items[].statusstringwithin_limit, exceeds_limit, not_permitted, not_subject (row is outside ADR scope — neither a pass nor a fail; scope rows also carry not_subject_to_adr / carriage_prohibited flags), inconclusive — Table A column (7a) states the limit in one dimension, and the quantity was sent in the other family (a mass against a volume limit, or the reverse). ADR states no density, so no verdict is given; re-send the quantity in the unit named by lq_limit_unit — or withheld: the line’s UN resolves to more than one Table A row and nothing pinned one. A withheld line carries withheld: true, an ambiguity_reason and null row fields (never the first row’s); its candidate rows are under candidates[] tagged with its item_index.
items[].equivalent_variantsobject[]?Present when the supplied packing_group left several rows that agree on class, column (7a), column (7b) and scope: the rows (variant_index, proper_shipping_name) this verdict holds for; variant_note says so in words and which row’s fields are shown
summaryobjectCounts of qualifying, exceeding, and not-permitted items, plus not_subject, inconclusive and withheld when non-zero
human_review_requiredboolean?true when at least one line is withheld — candidates[] then lists the withheld lines’ rows, each tagged with item_index and un_number
referencesobjectADR chapter and table references
GET/api/airlinesAirline Codes & AWB Prefix LookupTry it →

Search airlines by name, IATA code, ICAO code, AWB prefix, or country. The dataset contains6,356 airlines including 392 cargo airlines with AWB prefixes.

Query Modes

ParameterTypeDescriptionMatch
qstringGeneral search — matches name, codes, prefix, country. Smart: 2–3 digits match prefix only, 2–3 letters match IATA/ICAO only, 4+ chars search all fields.Smart
iatastringIATA 2-letter code (e.g. EK)Exact
icaostringICAO 3-letter code (e.g. UAE)Exact
prefixstringAWB 3-digit prefix (e.g. 176)Exact
countrystringCountry name (e.g. Germany)Partial
view / offset / limitstring / numberOpt-in paging: view=summary returns summary rows (the record’s identifying fields) with total, truncated, next_offset and a detail_hint; offset / limit page them. Without view the full list is returned as before. The MCP tool returns summary pages by default (full: true for full records). Applies to q and country lists.—

Example Requests

AWB prefix lookup:

curl "https://www.freightutils.com/api/airlines?prefix=176"
{ "count": 1, "results": [ { "slug": "emirates", "airline_name": "Emirates", "iata_code": "EK", "icao_code": "UAE", "awb_prefix": ["176"], "callsign": "EMIRATES", "country": "United Arab Emirates", "has_cargo": true, "aliases": ["Emirates SkyCargo"], "verified": false, "status": "unverified", "status_checked_at": "2026-08-11", "audited_at": "2026-05-13", "field_provenance": { "iata_code": [ { "url": "https://www.iata.org/en/publications/directories/code-search/?airline.search=EK", "publisher": "IATA (free code search, tie-break spot check)", "accessed_at": "2026-08-11" } ], "icao_code": [ { "url": "https://www.faa.gov/air_traffic/publications/atpubs/cnt_html/chap3_section_2.html", "publisher": "FAA Order JO 7340.2P Chg 3 (eff 2026-07-09), Chapter 3", "accessed_at": "2026-08-11" } ], "callsign": [ { "url": "https://www.faa.gov/air_traffic/publications/atpubs/cnt_html/chap3_section_2.html", "publisher": "FAA Order JO 7340.2P Chg 3 (eff 2026-07-09), Chapter 3", "accessed_at": "2026-08-11" } ], "operating_status": [ { "url": "https://www.skycargo.com/", "publisher": "Emirates SkyCargo", "accessed_at": "2026-08-11" } ] }, "sources": [ { "url": "https://www.faa.gov/air_traffic/publications/atpubs/cnt_html/chap3_section_2.html", "accessed_at": "2026-08-11" }, { "url": "https://www.iata.org/en/publications/directories/code-search/?airline.search=EK", "accessed_at": "2026-08-11" }, { "url": "https://www.skycargo.com/", "accessed_at": "2026-08-11" } ], "data_note": "Audited in the 2026-05-13 anchor pass, but awb_prefix carries no per-field carrier-published citation and the anchor sources include aggregators (which score zero). Held at unverified until a later wave re-sources it." } ], "meta": { "source": "Compiled by FreightUtils from public sources. Verified records cite carrier-published pages and FAA Order JO 7340.2 field by field (field_provenance), with IATA's public code search as a tie-break only; the rest are pending verification. IATA and ICAO assign these codes, but this dataset is not published or endorsed by IATA or ICAO.", "airlines": 6356, "last_verified": "2026-05-13" }, "_source": { "type": "reference", "authority": "FreightUtils airline code compilation (public sources)", "provenance_status": "pending-verification" } }

IATA code lookup:

curl "https://www.freightutils.com/api/airlines?iata=EK"

Name search:

curl "https://www.freightutils.com/api/airlines?q=emirates"

Response Fields

FieldTypeDescription
slugstringURL-friendly identifier
airline_namestringOfficial airline name
iata_codestring | null2-character IATA designator
icao_codestring | null3-character ICAO designator
awb_prefixstring[] | null3-digit AWB prefix(es) — array, some airlines have multiple
callsignstring | nullRadio callsign for ATC communication
countrystring | nullCountry of registration
has_cargobooleantrue if airline has AWB prefix(es)
aliasesstring[] | nullAlternative names (e.g. cargo division name)
statusstringactive_verified · historical · unverified — see the note below. Absent means never audited; absence is a claim of its own and is never coerced to a value
status_checked_atstringDate the status was last established
data_notestringPlain-language caveat about this record's evidence, where one applies
field_provenanceobjectPer-FIELD source citations — which page confirms which field. Aggregators score zero and never appear
sourcesobject[]Record-level union of the per-field citations (with access dates)
audited_atstringAudited records only — date of the audit
decision_rationalestringAudited records only — why the published values were accepted
verifiedbooleanDeprecated — computed mirror of status === 'active_verified', kept for one release. Absent (not false) on never-audited records. Read status

Verification status, and what it changes in the envelope

The airline dataset is still pending-verification, but individual records now carry their own evidence. active_verified means every identifier this API serves for that record — IATA code, ICAO code, AWB prefix — was confirmed against a page the carrier itself publishes (ICAO designator and callsign against FAA Order JO 7340.2), with the exact URL and access date in field_provenance. Aggregator and directory sites are scored at zero and never appear as evidence.historical means the carrier demonstrably ceased operating; the codes are kept as historical fact and may since have been reassigned. unverified means we looked and the evidence did not clear that bar — the honest default, not a slur on the record.

This is load-bearing in the envelope: an answer whose records are all active_verified is emitted at confidence.level: "high" with no advisory. Any other mix — one unverified record, one never-audited record, or an empty result — falls back to medium plus the PROVENANCE_PENDING warning. The lift is earned per answer and derived from the records in it, so it can never claim more than the evidence behind it.

Responses also carry a top-level _source citability envelope and a meta block whose airlines count is derived from the dataset at build time — never hand-typed.

GET/api/incotermsINCOTERMS 2020 LookupTry it →

Look up INCOTERMS 2020 trade terms. Returns all 11 terms by default, or filter by code or transport category. Each term includes seller/buyer responsibilities, risk and cost transfer points, insurance obligations, and practical guidance.

Parameters

ParameterTypeDescription
codestringINCOTERM code (e.g. FOB, CIF, DDP). DAT, which Incoterms 2020 replaced, answers 404 with replaced_by: "DPU" and the ICC source.
categorystringFilter by transport mode: any_mode or sea_only
view / offset / limitstring / numberOpt-in paging: view=summary returns summary rows (the record’s identifying fields) with total, truncated, next_offset and a detail_hint; offset / limit page them. Without view the full list is returned as before. The MCP tool returns summary pages by default (full: true for full records).

Omit all parameters to return all 11 INCOTERMS 2020 terms.

Example Requests

Single term lookup:

curl "https://www.freightutils.com/api/incoterms?code=FOB"
{ "code": "FOB", "name": "Free on Board", "slug": "fob-free-on-board", "category": "sea_only", "summary": "Seller delivers goods on board the vessel at port of shipment. One of the most commonly used terms.", "seller_responsibility": "Deliver goods on board the vessel at named port. Export clearance. Loading costs.", "buyer_responsibility": "Main sea freight, insurance, import clearance, duties.", "risk_transfer": "When goods are on board the vessel at port of shipment.", "cost_transfer": "At port of shipment, once loaded on board.", "insurance": "No obligation on either party.", "export_clearance": "Seller.", "import_clearance": "Buyer.", "best_for": "Sea freight where buyer wants to arrange their own shipping and insurance. Very commonly used in international trade.", "watch_out": "Sea and inland waterway ONLY. Despite being widely used, FOB is technically incorrect for containerised cargo..." }

Filter by transport category:

curl "https://www.freightutils.com/api/incoterms?category=sea_only"

All terms:

curl "https://www.freightutils.com/api/incoterms"
GET/api/containersContainer Capacity ReferenceTry it →

Shipping container specifications — internal/external dimensions, weights, door openings, and pallet capacity for all 10 standard ISO container types. Optionally calculate how many items fit in a specific container.

Query Modes

ParameterTypeRequiredDescription
typestringNoContainer slug (e.g. 20ft-standard, 40ft-high-cube), in any case; the ISO size-type code (45G1) and the short form (40HC) are read too. A miss answers 404 with the closest slugs from the records. Omit to list all.
view / offset / limitstring / numberNoOpt-in paging: view=summary returns summary rows (the record’s identifying fields) with total, truncated, next_offset and a detail_hint; offset / limit page them. Without view the full list is returned as before. The MCP tool returns summary pages by default (full: true for full records). Applies when type is omitted.
lnumberNoItem length in cm (requires type + w + h)
wnumberNoItem width in cm
hnumberNoItem height in cm
wtnumberNoWeight of one item in kg, 0 or more
qtyintegerNoNumber of items — a whole number of 1 or more

Example Requests

List all containers:

curl "https://www.freightutils.com/api/containers"

Single container specs:

curl "https://www.freightutils.com/api/containers?type=40ft-high-cube"

Loading calculation — how many 60×40×40cm boxes fit in a 40ft HC:

curl "https://www.freightutils.com/api/containers?type=40ft-high-cube&l=60&w=40&h=40&wt=15&qty=500"
{ "container": { "name": "40ft High Cube", "slug": "40ft-high-cube", ... }, "loading": { "fits_lengthwise": 20, "fits_widthwise": 5, "fits_height": 6, "max_items": 600, "requested_qty": 500, "fits": true, "total_weight_kg": 7500, "within_payload": true, "utilisation_percent": 63.2 } }
GET/api/convertUnit ConverterTry it →

Convert between freight-relevant units — weights, volumes, lengths, and freight-specific conversions (CBM to chargeable weight, CBM to freight tonnes).

Parameters

ParameterTypeRequiredDescription
valuenumberYesThe number to convert
fromstringYesSource unit code
tostringYesTarget unit code

Supported Unit Codes

GroupCodes
Weightkg, lbs, oz, tonnes, short_tons, long_tons
Volumecbm, cuft, cuin, litres, gal_us, gal_uk
Lengthcm, inches, m, feet, mm
Freightchargeable_kg (target only, from=cbm), freight_tonnes (target only, from=cbm)

Example Requests

Standard conversion:

curl "https://www.freightutils.com/api/convert?value=100&from=kg&to=lbs"
{ "input": { "value": 100, "unit": "kg", "name": "Kilograms" }, "result": { "value": 220.462442, "unit": "lbs", "name": "Pounds" }, "formula": "Kilograms × 2.204624 = Pounds" }

CBM to chargeable weight (IATA 6000 divisor):

curl "https://www.freightutils.com/api/convert?value=10&from=cbm&to=chargeable_kg"
{ "input": { "value": 10, "unit": "cbm", "name": "Cubic Metres" }, "result": { "value": 1666.7, "unit": "chargeable_kg", "name": "Chargeable Weight (kg)" }, "formula": "Cubic Metres × 166.67 = Chargeable Weight (kg)", "note": "IATA volumetric weight: 1 CBM = 166.67 kg (divisor 6000)..." }

CBM to freight tonnes (W/M rule):

curl "https://www.freightutils.com/api/convert?value=5&from=cbm&to=freight_tonnes"
GET/api/hsHS Code LookupTry it →

Search and browse Harmonized System (HS 2022) commodity codes. Supports text search by product description, exact code lookup with ancestor chain, and section browsing. Covers all 6,937 codes across 21 sections and 97 chapters.

Query Modes

ParameterTypeDescriptionMax results
qstringCase-insensitive search on descriptions and codes. Min 2 characters. When no HS 2022 description contains the words, the UK Trade Tariff search references (HMRC) are asked, and the codes they name come back with matched_via (“laptop” → 847130; the WCO text says “portable automatic data-processing machines”).50
codestringExact HS code lookup (2, 4, or 6 digit), with or without dots or spaces (8471.30 = 847130). Returns full details with ancestor chain and children.1
sectionstringBrowse by section (Roman numeral, e.g. II). Returns all chapters in that section.All

Provide exactly one parameter per request. Omitting all parameters returns a 400 with usage hints.

Example Requests

Search by description:

curl "https://www.freightutils.com/api/hs?q=coffee"
{ "query": "coffee", "results": [ { "hscode": "0901", "description": "Coffee, whether or not roasted...", "level": 4, "section": "II", "parent": "09" } ], "count": 12 }

Code lookup with ancestors:

curl "https://www.freightutils.com/api/hs?code=090111"
{ "hscode": "090111", "description": "Coffee; not roasted, not decaffeinated", "level": 6, "section": "II", "parent": "0901", "ancestors": [ { "hscode": "09", "description": "Coffee, tea, mate and spices", "level": 2 }, { "hscode": "0901", "description": "Coffee, whether or not roasted...", "level": 4 } ], "children": [], "sectionName": "Vegetable products" }

Browse section:

curl "https://www.freightutils.com/api/hs?section=II"
POST/api/consignmentMulti-Item Consignment CalculatorTry it →

Calculate total CBM, loading metres (LDM), volumetric and mode-specific chargeable weight across a multi-item mixed consignment — per-line and grand totals, plus objective advisory flags (implausible density, mode/option mismatch, dangerous-goods presence by UN number against the ADR 2025 reference, and ISO 6346 container / IATA AWB check-digit validity). Supports sea, air, and road modes. Canonical request/response schema: consignment.v1.json.

Best-effort deterministic calculation and reference data only. Verify all inputs. Not regulatory, customs, or dangerous-goods compliance advice — you remain responsible for classification, documentation and carrier acceptance.

Request Body (JSON)

FieldTypeRequiredDescription
modestringNoroad (default), air, or sea
linesarrayYes*1–50 canonical line objects (see below). Preferred.
itemsarrayYes*Legacy flat alias (dimensions in cm, weight in kg). *Provide lines or items.
optionsobjectNoair_volumetric_divisor (default 6000), container_number, awb_number

Canonical Line Object (lines[])

FieldTypeRequiredDescription
quantityintegerYesNumber of identical pieces
dimsobjectYes{ l, w, h, unit } — unit one of mm, cm, m, in
weightobjectYes{ value, unit } — unit one of kg, g, t, lb
stackablebooleanNoStack two-high (halves loading-metre footprint)
hs_codestringNoHS commodity code (6–10 digits)
un_numberstringNoUN number — triggers the dangerous-goods reference flag
descriptionstringNoItem label

Example Request

curl -X POST "https://www.freightutils.com/api/consignment" \ -H "Content-Type: application/json" \ -d '{ "mode": "air", "lines": [ { "quantity": 4, "dims": { "l": 120, "w": 80, "h": 100, "unit": "cm" }, "weight": { "value": 300, "unit": "kg" } }, { "quantity": 10, "dims": { "l": 60, "w": 40, "h": 50, "unit": "cm" }, "weight": { "value": 15, "unit": "kg" } } ]}'
{ "schema_version": "consignment.v1", "mode": "air", "air_volumetric_divisor": 6000, "per_line": [ /* cbm, gross_weight_kg, volumetric_weight_kg, ldm, chargeable_weight_kg, ... per line */ ], "totals": { "cbm": 5.04, "gross_weight_kg": 1350, "volumetric_weight_kg": 840, "chargeable_weight_kg": 1350, "billing_basis": "weight", "line_count": 2, "piece_count": 14 }, "flags": [], "disclaimer": "Best-effort deterministic calculation and reference data only. ..." }
POST/api/dutyUK Import Duty & VAT EstimatorTry it →

Estimate UK import duty and VAT for a commodity code using live GOV.UK Trade Tariff data. Accepts customs value, origin country, freight/insurance costs, and INCOTERM for CIF adjustment.

Request Body (JSON)

FieldTypeRequiredDescription
commodity_codestringYesUK commodity code — a declarable 10-digit code (spaces allowed). A 6- or 8-digit code is refused with 400 and declarable_codes, the 10-digit codes beneath it in the UK Trade Tariff — it is never padded with zeros to a code you did not send. origin_country must be ISO 3166-1 alpha-2.
origin_countrystringYesISO 2-letter country code (e.g. CN, DE)
customs_valuenumberYesGoods value in GBP, greater than 0
freight_costnumberNoFreight cost in GBP, 0 or more (added to CIF value; a negative cost is refused with 400)
insurance_costnumberNoInsurance cost in GBP, 0 or more (added to CIF value; a negative cost is refused with 400)
incotermstringNoINCOTERM (e.g. FOB, CIF, EXW)

Example Request

curl -X POST "https://www.freightutils.com/api/duty" \ -H "Content-Type: application/json" \ -d '{ "commodity_code": "6109100010", "origin_country": "CN", "customs_value": 10000, "freight_cost": 500, "insurance_cost": 50, "incoterm": "FOB" }'
{ "commodity_code": "6109100010", "commodity_description": "T-shirts", "origin_country": "CN", "origin_country_name": "China", "cif_value": 10550, "duty_rate": "12.0%", "duty_rate_percent": 12, "duty_amount": 1266, "vat_rate": "20.0%", "vat_rate_percent": 20, "vat_amount": 2363.2, "total_import_taxes": 3629.2, "total_landed_cost": 14179.2, "warnings": [ { "type": "info", "message": "Preferential rates may be available under a trade agreement. Check GOV.UK Trade Tariff for details." } ], "source": "GOV.UK Trade Tariff API", "disclaimer": "Estimate only. Not for customs declaration purposes. Verify with HMRC or a licensed customs broker.", "_source": { "type": "reference", "authority": "HM Revenue & Customs (GOV.UK Trade Tariff)", "…": "…" } }

A response from production on 2026-10-08 (live rates move). Send 610910 instead and the answer is a 400 naming 6109100010 (T-shirts) and 6109100090 (Other) in declarable_codes.

GET/api/unlocodeUN/LOCODE LookupTry it →

Search and look up UN/LOCODE transport locations — 116,232+ seaports, airports, rail terminals, inland depots, and border crossings worldwide. Responses are cached for 24 hours.

Query Modes

ParameterTypeDescription
codestringExact UN/LOCODE lookup (e.g. GBLHR, NLRTM)
qstringSearch by name (e.g. rotterdam, heathrow)
countrystringFilter by country code (e.g. GB, NL)
functionstringFilter by function: port, airport, rail, road, icd, border
limitintegerMax results (1–100, default: 20; values outside are clamped, text that is not a whole number is refused with 400)

Example Requests

Search by name:

curl "https://www.freightutils.com/api/unlocode?q=rotterdam"
{ "query": "rotterdam", "count": 3, "results": [ { "locode": "NLRTM", "name": "Rotterdam", "country": "NL", "subdivision": "ZH", "functions": ["port", "rail", "road"], "coordinates": { "lat": 51.92, "lon": 4.48 } } ], "meta": { "source": "UNECE UN/LOCODE 2025-1 — UN/LOCODE © UNECE, CC BY 4.0 (as UNECE states for UN/CEFACT standards)", "total_entries": 116232 } }

Exact code lookup:

curl "https://www.freightutils.com/api/unlocode?code=GBLHR"

Filter by country and function:

curl "https://www.freightutils.com/api/unlocode?country=GB&function=port&limit=10"
GET/api/uldAir Freight ULD TypesTry it →

Look up air freight Unit Load Device (ULD) specifications. 16 types including LD3 (AKE), PMC main deck pallet, temperature-controlled containers, and more. Returns dimensions, weights, volume, and aircraft compatibility.

Parameters

ParameterTypeDescription
typestringULD code or slug (e.g. AKE, PMC). Omit to list all. Alias: code.
codestringAlias for type — accepted because the response field is named code
categorystringFilter: container, pallet, or special
deckstringFilter by deck: lower or main
view / offset / limitstring / numberOpt-in paging: view=summary returns summary rows (the record’s identifying fields) with total, truncated, next_offset and a detail_hint; offset / limit page them. Without view the full list is returned as before. The MCP tool returns summary pages by default (full: true for full records). Applies when type is omitted.

An unrecognised query parameter returns 400 naming it, rather than being ignored — a filter this endpoint does not understand must never come back looking like a successful unfiltered query.

Example Requests

Single ULD lookup:

curl "https://www.freightutils.com/api/uld?type=AKE"

Filter by category:

curl "https://www.freightutils.com/api/uld?category=pallet"

All ULD types:

curl "https://www.freightutils.com/api/uld"
GET/api/vehiclesVehicle & Trailer TypesTry it →

Look up road freight vehicle and trailer specifications. 17 types covering articulated trailers, rigid trucks, and vans. Returns internal dimensions, payload limits, pallet capacity, and features.

Values carry their own semantics rather than leaving you to infer them. internal_dimensions.height is null on open-deck vehicles (flatbed, low-loader) — never 0, which would make a length × width × height calculation return zero volume. height_basis says what a null means (unbounded-open-top — no roof, so no constraint exists; varies-by-configuration; or unverified — a constraint exists but no trustworthy figure is held), and open_top states the physical fact separately. pallet_capacity_basis says whether a pallet count is floor geometry (floor-positions), capped by payload (weight-limited), an industry-convention figure, or unverified — which matters, because a floor-derived count and a payload-capped count are very different answers. max_pallet_height_mm gives the per-deck clearance on multi-deck vehicles and is null elsewhere, where the internal height is the usable clearance.

Parameters

ParameterTypeDescription
slugstringVehicle slug (e.g. standard-curtainsider). Omit to list all.
categorystringFilter: articulated, rigid, or van
regionstringFilter: EU or US
view / offset / limitstring / numberOpt-in paging: view=summary returns summary rows (the record’s identifying fields) with total, truncated, next_offset and a detail_hint; offset / limit page them. Without view the full list is returned as before. The MCP tool returns summary pages by default (full: true for full records). Applies when slug is omitted.

Example Requests

Single vehicle lookup:

curl "https://www.freightutils.com/api/vehicles?slug=standard-curtainsider"

Filter by category:

curl "https://www.freightutils.com/api/vehicles?category=articulated"

Filter by region:

curl "https://www.freightutils.com/api/vehicles?region=EU"

HTTP Status Codes

CodeMeaning
200Success — calculation result returned as JSON
400Bad Request — missing or invalid parameters. Check the error message in the response.
404Not Found — no results for the given query (airlines and ADR endpoints)
405Method Not Allowed — only GET (or POST for /api/adr-calculator, /api/adr/lq-check, /api/shipment/summary) is supported
500Internal Server Error — unexpected error, please report via GitHub

Field Naming

All endpoints use snake_case field names in responses (e.g. internal_length_cm, max_gross_kg). POST request bodies on /api/duty and /api/consignment accept either casing for backwards compatibility — snake_case is the documented form.

Rate Limiting

The API is free to use. Anonymous rate limit: 25 requests per day per IP. Free API key: 100 requests per day. Pro: 50,000 requests per month.

Error Responses

All endpoints return standard HTTP error codes with a descriptive JSON error message:

400Bad Request— Missing or invalid parameters
{"error": "Missing required parameter: l (length in cm)"}
400Bad Request— A near miss of a real parameter (another case or separator, a plural, or the MCP argument name) is refused, naming the right one; other unknown query parameters such as cache-busters are ignored. JSON bodies refuse every key they do not read.
{"error": "Unknown query parameter \"quantity\" (did you mean \"qty\"?) — not read, so the answer would have ignored it. Accepted parameters: pallet, length, width, qty, stackable, stack, weight, vehicle, vehicle_length.", "unknown_parameters": ["quantity"]}
404Not Found— Resource does not exist
{"error": "No ADR entry found for UN number 9999"}
500Server Error— Unexpected error
{"error": "Internal server error"}

Response Envelope (agent-facing, opt-in)

Every tool can also return a v1 response envelope — a thin, agent-facing wrapper that keeps the answer under result and adds the four things an autonomous agent needs to use and cite it safely: how much to trust it (confidence), where it came from(_source), a ready-to-quote line (citation), and — when relevant —what was corrected (normalized_input), advisories (warnings),blocking errors with a recovery hint (blocking_errors), and time-boxed validity(validity). It is fully described by the JSON Schema at /schemas/response-envelope.v1.json.

The envelope is opt-in. The flat legacy body (the answer at the top level) stays the default and is byte-unchanged, so existing REST consumers and the freightutils-mcp npm package are unaffected. Request the envelope explicitly with either ?envelope=1 or an Accept header:

curl "https://www.freightutils.com/api/airports?q=heathrow&envelope=1"
curl -H "Accept: application/vnd.freightutils.v1+json" "https://www.freightutils.com/api/airports?q=heathrow"

Cacheable responses carry Vary: Accept, so a shared cache keeps the two shapes apart. Behind a cache or proxy of your own that ignores Vary, use ?envelope=1: the shape is then part of the URL.

Envelope v1.1 — the freshness revision (additive, negotiated). Request it with ?envelope=1.1 (query parameter only). On editioned reference datasets the _source block gains dataset_edition (what this response is served from), authority_current_edition (the authority’s current in-force edition at last check) and checked_at (when the two were last compared) — and whenever the served edition trails the authority’s, the response carries an EDITION_LAG warning, advisory and never blocking. Continuous datasets (live UK duty, airport snapshots) legitimately omit the three fields. ?envelope=1 remains byte-stable v1. Schema: /schemas/response-envelope.v1.1.json.

curl "https://www.freightutils.com/api/unlocode?code=GBLHR&envelope=1.1"

On the hosted MCP server (/api/mcp) the envelope is returned as structuredContent (validating against each tool’s output schema) and serves v1.1 unconditionally — its declared schema updates with each deploy — while content[0].text keeps the flat legacy JSON so text-parsing clients are unaffected. envelope_version ("1" or "1.1") lets any consumer detect the shape.

Confidence model

confidence.level is high / medium / low, and confidence.basissays why. A numeric score (0–1) is present only when the basis ismatch_quality.

basisMeaningTypical level
deterministicPure computation (CBM, chargeable weight, LDM, conversions).high
provenanceReference-data lookup with an audit status.high (verified) / medium (provenance pending)
match_qualityFuzzy / ranked search — carries a score.from the score
freshnessComputed over live, time-sensitive data (e.g. UK duty rates).high

Warnings & blocking errors

warnings are non-blocking advisories; blocking_errors mean no answer was produced (ok: false) and each carries a recovery hint (an action, and often atool + params to retry with). Both use a stable UPPER_SNAKE code taxonomy; the keys are omitted entirely when empty (never []).

codeMeaning
RATE_LIMITEDQuota exceeded (maps to the 429 + Retry-After).
MISSING_INPUTA required parameter was absent.
INVALID_INPUTA parameter was present but malformed.
NOT_FOUNDAn exact lookup (code / id) matched nothing.
NO_MATCHA search / ranked query returned nothing.
FUZZY_BEST_MATCHAdvisory: the top result is a best-effort fuzzy match, not an exact hit.
PROVENANCE_PENDINGAdvisory: this dataset’s provenance is pending independent verification.
METHOD_NOT_ALLOWEDWrong HTTP method for the endpoint (e.g. GET on a POST-only tool).

Provenance, citation & validity

_source is the canonical provenance (name, checked date orrequest-time, and provenance_status: verified / pending-verification /computed / live). citation.text is a ready-to-quote line, with an optionalqualifier hedge for fuzzy or not-legal-advice answers. validity appears only on time-boxed tools (regulatory editions, live rates) and carries effective_from / effective_to /as_of.

Fuzzy best-match (GET /api/airports?q=heathrow&envelope=1):

{ "envelope_version": "1", "ok": true, "result": { "count": 2, "results": [ { "name": "Heathrow Airport", "iata_code": "LHR" } ] }, "confidence": { "level": "medium", "basis": "match_quality", "score": 0.66 }, "normalized_input": { "query": "heathrow", "normalized_query": "heathrow", "matched_field": "name" }, "warnings": [ { "code": "FUZZY_BEST_MATCH", "message": "Best of 2 ranked match(es) — matched on name." } ], "_source": { "name": "OurAirports (public domain)", "checked": "2026-06-24", "provenance_status": "verified", "source_url": "https://ourairports.com/data/" }, "citation": { "text": "Heathrow Airport (LHR) — OurAirports.", "qualifier": "Fuzzy best match for \"heathrow\"; verify the code." } }

Blocking error with a recovery hint (GET /api/adr?un=9999&envelope=1 → 404):

{ "envelope_version": "1", "ok": false, "result": {}, "confidence": { "level": "high", "basis": "provenance" }, "blocking_errors": [ { "code": "NOT_FOUND", "message": "UN number 9999 not found in the ADR 2025 dataset.", "recovery": { "action": "Check the UN number and retry, or search by name.", "tool": "adr_lookup", "params": { "un": "1203" } } } ], "_source": { "name": "UNECE (ADR 2025)", "checked": "2026-05-13", "provenance_status": "verified" }, "citation": { "text": "UN 9999 is not listed in UNECE ADR 2025." } }

Platform Commitments

Five pages that spell out what you can rely on:

Changelog
Every release, with an RSS feed.
Status
Current health plus 24h and 7d uptime.
Roadmap
Shipped, in progress, and what’s next.
Versioning Policy
Breaking-change contract and semver.
Deprecation Policy
3-month minimum notice with migration guides.

Source Code & Issue Reporting

The FreightUtils MCP server is open source. Report bugs, request features, or contribute on GitHub: github.com/SoapyRED/freightutils-mcp. For data corrections or API support, email contact@freightutils.com.