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.
Reliability & Support
FreightUtils APIs are hosted on Vercel's global edge network with automatic SSL and CDN caching.
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.
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_config.json: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:
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.
mcp.json file (Settings → MCP). Same shape as Claude Desktop:| Symptom | Likely cause | Fix |
|---|---|---|
| Tools not appearing in the client | MCP client wasn’t restarted after the config edit | Fully 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 logs | npx not on PATH, or node version older than 18 | Install 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 exceeded | Get 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": true | Bad 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 agent | No CLI access during a conversation | Ask 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. |
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 ↓
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
| Mode | Calculations Included |
|---|---|
road | CBM, LDM, pallet spaces, trailer utilisation, road chargeable weight (1 LDM = 1,750 kg), vehicle suggestion |
air | CBM, volumetric weight (1 CBM = 167 kg), air chargeable weight |
sea | CBM, revenue tonnes (W/M at 1 CBM = 1,000 kg), container suggestion |
multimodal | All 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:
Example Response
Pro-tier endpoint. Free access: 25 requests/day anonymous, 100/day with a free API key. Subscribe for higher limits.
Calculate the cubic metre (CBM) volume of a shipment. Returns total CBM plus equivalents in cubic feet, litres, and cubic inches.
Parameters
| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
l | number | Yes | Length of one piece in centimetres | — |
w | number | Yes | Width of one piece in centimetres | — |
h | number | Yes | Height of one piece in centimetres | — |
pcs | integer | No | Number of identical pieces — a whole number; 2.5 or 1e+21 is refused with 400, never rounded | 1 |
Example Request
5 boxes, 120×80×100 cm each:
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
| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
mass | number | Yes | Shipment mass (in mass_unit) | — |
mass_unit | kg | tonnes | No | Unit for mass | kg |
distance_km | number | Yes | Transport distance in km (caller-provided; not routed) | — |
mode | enum | Yes | road | rail | sea | air | inland_waterway | — |
sub_mode | string | No | Vehicle class (e.g. articulated, container ship) | representative |
region | uk | us | fr | No | Factor source: uk=DEFRA, us=EPA, fr=ADEME | per-mode |
basis | wtw | ttw | No | Well-to-wheel or tank-to-wheel | wtw |
Example Request
10 tonnes by road, 500 km, UK (DEFRA), well-to-wheel:
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
| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
text | string | No* | Arbitrary string to scan for container / AWB / IMO identifiers (parse mode) | — |
value | string | No* | A single identifier to validate (typed mode); requires type | — |
type | container | awb | imo | No* | Identifier type for value | — |
*Provide text (parse mode) OR value+type (typed mode).
Example Request
Parse a mixed string:
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
| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
description | string | Yes | The goods description to check against the EU ICS2 stop-words list | — |
Example Request
A vague standalone description:
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
| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
iata | string | One of | Exact 3-letter IATA code (e.g. LHR) | — |
icao | string | One of | Exact 4-character ICAO code (e.g. EGLL) | — |
q | string | One of | Name / city / municipality search (min 2 chars) | — |
type | string | No | Filter: large_airport, medium_airport, small_airport, heliport, closed, seaplane_base | — |
Example Request
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
| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
lat | number | Yes | Latitude in decimal degrees (-90 to 90) | — |
lon | number | Yes | Longitude in decimal degrees (-180 to 180) | — |
radius_km | number | No | Maximum distance in kilometres | — |
max_results | integer | No | Results to return (1–50) | 10 |
type | string | No | Filter by airport type (e.g. large_airport) | — |
Example Request
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
| Parameter | Type | Required | Description |
|---|---|---|---|
q | string | Yes | One freight identifier — e.g. 176, UN1845, NLRTM, FOB, 22G1, MSKU1100810, 090111, D/E |
Example Request
Response Fields (per candidate)
| Field | Type | Description |
|---|---|---|
entity_type | string | What the candidate IS — dangerous_good, airline, airport, location, hs_classification, incoterm, container_type, container_equipment, uld_type, tunnel_restriction |
identifier_type | string | Which 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 |
record | object | null | Core fields from the matching dataset; null for a pattern-only match (e.g. an ISO type code not in the served set) |
verification_status | string | Normalized across datasets — active_verified, verified, computed, partial, pending_verification, unchecked, unverified, historical, pattern_only |
verification_basis | string | record = from this record’s own provenance; dataset = the dataset-level claim (no record-level signal exists) |
rank_basis | string | Why the candidate sits where it does — record hit > verification status > static dataset prior > stable tiebreak |
canonical_url / api_url | string? | Where to go deeper — the record’s page and the dataset’s REST query; omitted when none exists |
citation | object | The matching dataset’s own citation for this record — authority stays attached to the record it describes |
Calculate air freight chargeable weight — whichever is higher between actual gross weight and volumetric (dimensional) weight. Supports custom volumetric factors for all carriers.
Parameters
| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
l | number | Yes | Length of one piece in centimetres | — |
w | number | Yes | Width of one piece in centimetres | — |
h | number | Yes | Height of one piece in centimetres | — |
gw | number | Yes | Total gross weight of all pieces in kg | — |
pcs | integer | No | Number of identical pieces — a whole number; 2.5 or 1e+21 is refused with 400, never rounded | 1 |
factor | integer | No | Volumetric divisor, a whole number: 6000 (IATA standard), 5000 (express carriers) | 6000 |
mode | string | No | air 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:
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
| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
pl | number | Yes | Pallet length in centimetres | — |
pw | number | Yes | Pallet width in centimetres | — |
pmh | number | Yes | Maximum total stack height in centimetres (floor to top of cargo) | — |
bl | number | Yes | Box length in centimetres | — |
bw | number | Yes | Box width in centimetres | — |
bh | number | Yes | Box height in centimetres | — |
ph | number | No | Pallet board/deck height in cm — deducted from usable height | 15 |
bwt | number | No | Weight per box in kg — enables weight constraint calculation | — |
mpw | number | No | Maximum pallet payload weight in kg — caps result if weight exceeded | — |
rotate | boolean | No | Allow 90° rotation of boxes for best fit. Pass false to disable. | true |
Example Request
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
| Parameter | Type | Description | Max results |
|---|---|---|---|
un | string | Exact UN number lookup. Accepts 1203, UN1203, UN 1203 or 01203. | 1 |
search | string | Case-insensitive partial match on the proper shipping name. Min 2 characters. Also accepts q as an alias. | 50 |
class | string | Filter 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 / limit | integer | Paging 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:
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:
Filter by hazard class:
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
| Parameter | Type | Required | Description |
|---|---|---|---|
un | string | Yes | UN 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 |
qty | number | Yes | 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. |
unit | string | No | L 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. |
basis | string | No | net 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:
POST — Multi-Substance Load
Request body:
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
| Field | Type | Description |
|---|---|---|
items[].state | string | The 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_id | boolean? | 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_group | string | null | Resolved ADR packing group for the row (echoed for traceability) |
variant_index | number | null | Resolved ADR Table A variant index for the row (null on a withheld line) |
transport_category | string | null | ADR transport category (0–4); null on a withheld line |
multiplier | number | null | Points multiplier for the category (null for cat 0, out-of-scope rows and withheld lines) |
points | number | null | quantity × multiplier |
items[].withheld | boolean? | 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_variants | object[]? | 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_points | number | null | Sum of all substance points; null while any line is withheld or declared on a basis 1.1.3.6.3 does not count |
exempt | boolean | null | true 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_required | boolean? | true when at least one line is withheld — candidates[] then lists the withheld lines’ rows, each tagged with item_index and un_number |
blocking_errors | object[]? | 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_zero | boolean | true if any substance is transport category 0 |
has_quantity_exceedance | boolean | true if any substance exceeds its per-category max quantity |
warnings | string[] | Human-readable warning messages for limit violations and out-of-scope exclusions |
not_subject_to_adr | boolean? | 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_ref | string? | ADR section whose carriage conditions still apply to a not-subject load (e.g. 5.5.3 for UN 1845 dry ice) |
conditions | object[]? | Verbatim ADR 2025 conditions for conditions_ref ({ref, heading, requirement, note?}) |
carriage_prohibited | boolean? | 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.
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
| Field | Type | Required | Description |
|---|---|---|---|
mode | string | No | lq (default) or eq |
items | array | Yes | 1–20 items to check |
items[].un_number | string | Yes | UN number (e.g. 1203) |
items[].quantity | number | Yes | Quantity per inner packaging |
items[].unit | string | No | ml, L (default), g, or kg |
items[].inner_packaging_qty | number | No | Inner 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_group | string | No | I/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_index | number | No | ADR 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:
Response Fields
| Field | Type | Description |
|---|---|---|
mode | string | lq or eq |
overall_status | string | qualifies, 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[] | array | Per-item results with substance info, limits, and pass/fail |
items[].status | string | within_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_variants | object[]? | 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 |
summary | object | Counts of qualifying, exceeding, and not-permitted items, plus not_subject, inconclusive and withheld when non-zero |
human_review_required | boolean? | true when at least one line is withheld — candidates[] then lists the withheld lines’ rows, each tagged with item_index and un_number |
references | object | ADR chapter and table references |
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
| Parameter | Type | Description | Match |
|---|---|---|---|
q | string | General 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 |
iata | string | IATA 2-letter code (e.g. EK) | Exact |
icao | string | ICAO 3-letter code (e.g. UAE) | Exact |
prefix | string | AWB 3-digit prefix (e.g. 176) | Exact |
country | string | Country name (e.g. Germany) | Partial |
view / offset / limit | string / number | Opt-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:
IATA code lookup:
Name search:
Response Fields
| Field | Type | Description |
|---|---|---|
slug | string | URL-friendly identifier |
airline_name | string | Official airline name |
iata_code | string | null | 2-character IATA designator |
icao_code | string | null | 3-character ICAO designator |
awb_prefix | string[] | null | 3-digit AWB prefix(es) — array, some airlines have multiple |
callsign | string | null | Radio callsign for ATC communication |
country | string | null | Country of registration |
has_cargo | boolean | true if airline has AWB prefix(es) |
aliases | string[] | null | Alternative names (e.g. cargo division name) |
status | string | active_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_at | string | Date the status was last established |
data_note | string | Plain-language caveat about this record's evidence, where one applies |
field_provenance | object | Per-FIELD source citations — which page confirms which field. Aggregators score zero and never appear |
sources | object[] | Record-level union of the per-field citations (with access dates) |
audited_at | string | Audited records only — date of the audit |
decision_rationale | string | Audited records only — why the published values were accepted |
verified | boolean | Deprecated — 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.
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
| Parameter | Type | Description |
|---|---|---|
code | string | INCOTERM code (e.g. FOB, CIF, DDP). DAT, which Incoterms 2020 replaced, answers 404 with replaced_by: "DPU" and the ICC source. |
category | string | Filter by transport mode: any_mode or sea_only |
view / offset / limit | string / number | Opt-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:
Filter by transport category:
All terms:
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
| Parameter | Type | Required | Description |
|---|---|---|---|
type | string | No | Container 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 / limit | string / number | No | Opt-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. |
l | number | No | Item length in cm (requires type + w + h) |
w | number | No | Item width in cm |
h | number | No | Item height in cm |
wt | number | No | Weight of one item in kg, 0 or more |
qty | integer | No | Number of items — a whole number of 1 or more |
Example Requests
List all containers:
Single container specs:
Loading calculation — how many 60×40×40cm boxes fit in a 40ft HC:
Convert between freight-relevant units — weights, volumes, lengths, and freight-specific conversions (CBM to chargeable weight, CBM to freight tonnes).
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
value | number | Yes | The number to convert |
from | string | Yes | Source unit code |
to | string | Yes | Target unit code |
Supported Unit Codes
| Group | Codes |
|---|---|
| Weight | kg, lbs, oz, tonnes, short_tons, long_tons |
| Volume | cbm, cuft, cuin, litres, gal_us, gal_uk |
| Length | cm, inches, m, feet, mm |
| Freight | chargeable_kg (target only, from=cbm), freight_tonnes (target only, from=cbm) |
Example Requests
Standard conversion:
CBM to chargeable weight (IATA 6000 divisor):
CBM to freight tonnes (W/M rule):
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
| Parameter | Type | Description | Max results |
|---|---|---|---|
q | string | Case-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 |
code | string | Exact 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 |
section | string | Browse 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:
Code lookup with ancestors:
Browse section:
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)
| Field | Type | Required | Description |
|---|---|---|---|
mode | string | No | road (default), air, or sea |
lines | array | Yes* | 1–50 canonical line objects (see below). Preferred. |
items | array | Yes* | Legacy flat alias (dimensions in cm, weight in kg). *Provide lines or items. |
options | object | No | air_volumetric_divisor (default 6000), container_number, awb_number |
Canonical Line Object (lines[])
| Field | Type | Required | Description |
|---|---|---|---|
quantity | integer | Yes | Number of identical pieces |
dims | object | Yes | { l, w, h, unit } — unit one of mm, cm, m, in |
weight | object | Yes | { value, unit } — unit one of kg, g, t, lb |
stackable | boolean | No | Stack two-high (halves loading-metre footprint) |
hs_code | string | No | HS commodity code (6–10 digits) |
un_number | string | No | UN number — triggers the dangerous-goods reference flag |
description | string | No | Item label |
Example Request
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)
| Field | Type | Required | Description |
|---|---|---|---|
commodity_code | string | Yes | UK 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_country | string | Yes | ISO 2-letter country code (e.g. CN, DE) |
customs_value | number | Yes | Goods value in GBP, greater than 0 |
freight_cost | number | No | Freight cost in GBP, 0 or more (added to CIF value; a negative cost is refused with 400) |
insurance_cost | number | No | Insurance cost in GBP, 0 or more (added to CIF value; a negative cost is refused with 400) |
incoterm | string | No | INCOTERM (e.g. FOB, CIF, EXW) |
Example Request
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.
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
| Parameter | Type | Description |
|---|---|---|
code | string | Exact UN/LOCODE lookup (e.g. GBLHR, NLRTM) |
q | string | Search by name (e.g. rotterdam, heathrow) |
country | string | Filter by country code (e.g. GB, NL) |
function | string | Filter by function: port, airport, rail, road, icd, border |
limit | integer | Max 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:
Exact code lookup:
Filter by country and function:
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
| Parameter | Type | Description |
|---|---|---|
type | string | ULD code or slug (e.g. AKE, PMC). Omit to list all. Alias: code. |
code | string | Alias for type — accepted because the response field is named code |
category | string | Filter: container, pallet, or special |
deck | string | Filter by deck: lower or main |
view / offset / limit | string / number | Opt-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:
Filter by category:
All ULD types:
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
| Parameter | Type | Description |
|---|---|---|
slug | string | Vehicle slug (e.g. standard-curtainsider). Omit to list all. |
category | string | Filter: articulated, rigid, or van |
region | string | Filter: EU or US |
view / offset / limit | string / number | Opt-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:
Filter by category:
Filter by region:
HTTP Status Codes
| Code | Meaning |
|---|---|
| 200 | Success — calculation result returned as JSON |
| 400 | Bad Request — missing or invalid parameters. Check the error message in the response. |
| 404 | Not Found — no results for the given query (airlines and ADR endpoints) |
| 405 | Method Not Allowed — only GET (or POST for /api/adr-calculator, /api/adr/lq-check, /api/shipment/summary) is supported |
| 500 | Internal 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:
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:
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.
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.
| basis | Meaning | Typical level |
|---|---|---|
deterministic | Pure computation (CBM, chargeable weight, LDM, conversions). | high |
provenance | Reference-data lookup with an audit status. | high (verified) / medium (provenance pending) |
match_quality | Fuzzy / ranked search — carries a score. | from the score |
freshness | Computed 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 []).
| code | Meaning |
|---|---|
RATE_LIMITED | Quota exceeded (maps to the 429 + Retry-After). |
MISSING_INPUT | A required parameter was absent. |
INVALID_INPUT | A parameter was present but malformed. |
NOT_FOUND | An exact lookup (code / id) matched nothing. |
NO_MATCH | A search / ranked query returned nothing. |
FUZZY_BEST_MATCH | Advisory: the top result is a best-effort fuzzy match, not an exact hit. |
PROVENANCE_PENDING | Advisory: this dataset’s provenance is pending independent verification. |
METHOD_NOT_ALLOWED | Wrong 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):
Blocking error with a recovery hint (GET /api/adr?un=9999&envelope=1 → 404):
Platform Commitments
Five pages that spell out what you can rely on:
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.