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.
What the tools change in an AI model’s answers is measured on the eval results page: the same freight reference questions, asked alone and with the tools.
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. A failed identifier carries failure: a reason an agent can branch on (INVALID_CHECK_DIGIT, WRONG_LENGTH, BAD_CHARSET), a message, the expected format and an example; an AWB whose airline cannot be named carries details.airline_failure (UNKNOWN_PREFIX or AMBIGUOUS), and a type the validator does not check answers 400 with UNSUPPORTED_TYPE. /api/resolve runs the same check on an AWB, container or IMO number as it resolves (validation), and says why it could not give one answer (failure: NO_MATCH or AMBIGUOUS). Structural only — a valid check digit means well-formed, not that the entity exists. The freightutils-mcp package types failure on validate and validation / failure on resolve_reference from 2.27.0.
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. By default only airports with scheduled airline service and an IATA code are ranked (no heliports, no closed fields) — the airports a shipment can fly from; include_all=true ranks every type except closed fields, and a type filters on that type. The answer states distance_basis (great_circle) and the filter applied. OurAirports rows titled “[Duplicate]” are never ranked (they answer their exact code on /api/airports). Does NOT geocode place names or compute routes — pass coordinates you already hold. MCP: nearest_airport takes include_all on the hosted server and in the freightutils-mcp package from 2.27.0.
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) | — |
include_all | boolean | No | true: every airport type except closed fields; default: scheduled airports with an IATA code only | false |
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), and every entry carries all 20 Table A columns as codes: (8)–(14) packing and tank, (16)–(19) special provisions for carriage, beside the identification, classification, quantity, transport-category and hazard-identification columns. 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 |
include_codes | string | none | compact | full — adds resolved_codes[] beside results: what each code in columns (5)–(20) means, once per answer, with the rows that carry it (see the ADR code registry). Default compact for un, none for search, class and un_numbers batches. | — |
Provide exactly one parameter per request (include_codes may go beside any of them). 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.
What null means here: a Table A cell that is empty for the row (no special provision, no limited quantity, no tunnel code, no packing group) is null, never a guess and never 0; on the scope-flagged rows above, the cells the scope remark replaces are null and the remark is in table_a_remark.
Search by substance name:
Filter by hazard class:
What each code printed in ADR 2025 Table A means, in FreightUtils’ own words, written from the free UNECE publication (ECE/TRANS/352) and cited to its volume, section and page. Seventeen families: label models (column 5), special provisions (6), packing instructions (8), special packing provisions (9a), mixed packing provisions (9b), portable tank instructions and bulk container codes (10), portable tank special provisions (11), tank codes (12, decoded part by part), tank special provisions TU/TC/TE/TA/TT/TM (13), vehicle designations (14), carriage provisions V (16), VC and AP (17), CV (18) and S (19), and hazard identification numbers (20). Column (15) comes from the 1.1.3.6.3 transport-category table and the 8.6.4 tunnel-code table.
| Call | Returns |
|---|---|
GET /api/adr/codes | The families: name, Table A column, section, codes in use, codes reviewed. |
GET /api/adr/codes?family=sp&page=1 | One family’s codes, most-carried first, 100 per page: code, summary, section, row_count, verification_status, url. |
GET /api/adr/codes/{family}/{code} | One record: section, source (volume, page), title (the provision’s own heading, the only quoted text), summary, meaning, parameters (limits, conditions, permitted packagings, tank-code parts, digits), applies_to, references, row_count, verification_status. A code the family does not hold is a 404 naming the family’s count. Codes with a slash or brackets also answer by slug: EX-III, PxBN-M, L4BN-plus. |
GET /api/adr?un=1203&include_codes=compact|full|none | resolved_codes[] beside results, one entry per distinct code in the answer: code, family, columns (the Table A columns it is printed in), rows (the indexes of results[] that carry it), summary, section, verification_status. full adds url, title, meaning, parameters, applies_to, references. A code’s page is https://www.freightutils.com/adr/codes/{family}/{code} (transport category: /adr-calculator; tunnel code: /adr/tunnel-codes). A code string two families hold is one entry per family. The rows keep the code strings as Table A prints them. |
Two of UN 1263’s resolved codes (compact) — six rows, each code served once:
verification_status: paraphrase_unreviewed — written from the source, not yet reviewed; machine_reviewed — two independent machine readings of the UNECE text, compared with the entry, agreed with it (not a person's review); contested — a machine reading disagreed with part of it and the owner adjudicates; owner_reviewed — reviewed against the source by the founder; dg_reviewed — also reviewed by an independent dangerous-goods safety adviser; needs_fix — a review found a correction is due. Unreviewed entries are served as such, never hidden and never labelled otherwise; entries from the two owner tables (transport category, tunnel code) say verified. A review is recorded by role and date, and the status is derived from that record alone.
The verbatim rule: only title is the publication’s wording (at most twelve words). The build fails if any summary, meaning, note or parameter text shares ten consecutive words with ADR 2025. Numbers, units and code lists are given exactly as ADR states them.
Statements: /api/adr?un=1203&include=statements (MCP include_statements: true) adds statements[] to every row — tunnel, transport category, labels, packing, tanks and carriage — each {topic, text, basis[], codes[], verification_status}. The text is assembled from the code meanings above and the 8.6.4 and 1.1.3.6.3 tables, never written separately (a build check proves it); its status is the lowest of the codes it draws on, and verified when it draws on those two tables only. The UN pages show the same statements under “In plain words”.
MCP (the hosted server, and the freightutils-mcp package from 2.25.0): adr_lookup takes the same include_codes argument; resolve_reference resolves a code such as TU9 or S20 to its record, lists every family that holds an ambiguous one (336 is both a special provision and a hazard identification number), and answers a well-formed code no Table A row carries (P999) as pattern_only.
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) |
resolved_subtotal_points | number | Points of the lines that resolved (state COUNTED) |
threshold_exceeded_by_resolved_items | boolean | The resolved lines alone exceed 1,000 — unresolved lines can only add points, so the load is not exempt whatever they resolve to |
verdict_basis | string | all_items (every line resolved), resolved_items_alone (settled by the resolved lines while others are BLOCKED) or none (no verdict) |
unresolved_item_indexes | number[] | The BLOCKED lines, by their index in the request |
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 |
prefix_shared / prefix_holders | boolean / string[] | Every record with an AWB prefix: whether another record holds the same prefix, and every holder's slug (verified first, ceased last). A shared prefix alone does not name the issuing carrier |
operational_status | string | Cargo records: active · ceased · unknown — whether the carrier operates, a different thing from status (the identifiers' verification). active needs the carrier's own page or its ICAO designator listed for it in the current FAA Order JO 7340.2; ceased needs the carrier's own page, or the designator withdrawn by the FAA together with a dead former domain. Wikidata alone leaves unknown |
operational_status_basis | string | carrier_published · faa_designator_current · domain_fetch_check · wikidata_dissolved; the sources are in field_provenance.operational_status |
restrictions | object[] | Where the EU Air Safety List (Annex A ban / Annex B restrictions) or the UK Air Safety List names the carrier: list, annex, scope, regulation and date, source URL, access date, sha256, and whether it matched on the ICAO designator or on name and state. A flag with its source, never a judgement |
cargo_website / tracking_url_template / dg_acceptance_url | string | Carrier-published only, each answering HTTP 200 when read. The tracking template carries {prefix} / {serial} placeholders. Absent where no carrier page qualifies |
code_history | object[] | Dated designator changes, only where a source gives the date (Wikidata end-time qualifiers, marked second-class) |
chargeable_weight_ref | string | Slug of the carrier's volumetric-divisor page, /chargeable-weight/{slug} — linked, not copied |
airline_lookup on MCP carries the same fields; the freightutils-mcp package types them from 2.27.0.
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.
Only an ad valorem (percentage) duty is computed. Every answer carries duty_status, duty_form and duty_expression (HMRC’s own wording) and lists in not_applied what the figures never include. When HMRC states a specific or compound duty — white sugar 1701991000 is 35.00 GBP / 100 kg — the answer is duty_status: "unsupported" with the expression quoted and the duty, VAT and landed-cost figures null, never 0. If HMRC does not answer, the API returns 503 upstream_unavailable (retryable), never “not found”.
Search and look up UN/LOCODE transport locations — 116,086+ seaports, airports, rail terminals, inland depots, and border crossings worldwide. Responses are cached for 24 hours.
One record per code. A code UNECE lists under two names (a bilingual name in both orders) carries the other in alternate_names. change_indicator is UNECE’s mark for this issue (+ added, # name changed, X to be removed), and an entry marked X has entry_status: "deleted" and a warning. UNECE leaves its IATA column blank when the IATA code equals the location code; iata_code_basis says whether the code came from UNECE, was confirmed against OurAirports, or is not-confirmed (null means unknown, not none).
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:
The consignment object
One typed description of a load that the checks can read: lines[] (each with description, un_number, packing_group, variant_index, quantity { value, unit, basis } — the dangerous-goods quantity of the whole line —, packages { count, type, gross_kg } — gross_kg is all the line’s packages together —, hs_code, value { amount, currency }, dims of one package and stackable), journey { origin, destination, date, tunnels_on_route, mode } (places are UN/LOCODEs or ISO country codes, never addresses), vehicle { type, max_mass_t, axles } and options. Only lines[] is required. Parties are not part of version 1. JSON Schema: /schema/consignment-object.v1.json.
Send it as { "consignment": { ... } } to POST /api/consignment, POST /api/shipment/summary or POST /api/adr-calculator (on MCP, hosted and freightutils-mcp from 2.25.0: the consignment argument of consignment_calculator, shipment_summary and adr_exemption_calculator), alone. The object is mapped onto the tool’s own request, so the answer is the one the equivalent request in the tool’s own shape gives. If the object lacks an input the check needs, the answer is a 400 MISSING_INPUT that names it by path in missing_inputs (for example lines[1].dims); fields the tool does not read are listed in normalized_input.consignment.not_read. POST /api/validate with { "type": "consignment", "consignment": { ... } } says whether an object is valid and which of the three checks it carries the inputs for.
MCP scopes
An agent that only needs part of FreightUtils can connect to a scope and load fewer tool definitions: https://www.freightutils.com/api/mcp/dg (dangerous goods: adr_lookup, adr_exemption_calculator, adr_lq_eq_check, validate, resolve_reference), /api/mcp/customs (hs_code_lookup, uk_duty_calculator, ics2_check, incoterms_lookup, validate) and /api/mcp/ops (the freight calculators, containers, ULDs, vehicles, UN/LOCODEs, airports, airlines, validate, resolve_reference). /api/mcp is unchanged and serves every tool. A scope is a filter, not a fork: the same tools, names, schemas and answers, and the same key and limits. Scopes leave out get_subscribe_link (its link is in every limit answer already); serverInfo.name names the scope (freightutils-mcp-dg, …). The dangerous-goods scope’s definitions are about a quarter of the full list’s size. The freightutils-mcp package serves the same lists over stdio from 2.25.0: npx freightutils-mcp --scope dg (or customs, ops; also the FREIGHTUTILS_SCOPE environment variable).
What null means on a calculator: the figure was not computed because an input it needs was missing — the inputs are listed in missing_inputs under envelope 1.2, by the names the call lacked (never one it sent, never an optional one). A null is never a zero.
Batch inputs
Six tools take many items in one call: /api/adr?un_numbers=, /api/unlocode?codes=, /api/airports?codes= (3 letters IATA, 4 ICAO), /api/airlines?codes= (2 characters IATA, 3 letters ICAO, 3 digits AWB prefix) and /api/hs?codes= — up to 50, comma-separated — and POST /api/duty with { "lines": [ … ] }, up to 10 (the UK Trade Tariff is read live per line, and each line gets its own receipt). On MCP (the hosted server, and the freightutils-mcp package from 2.25.0) the same arguments are arrays: un_numbers, codes, lines. Send the batch argument alone.
Each item is answered by the same handler as its single call, so it gives the same answer. The batch answer lists results[] in input order, each with its own status (ok, not_found, ambiguous, unsupported or partial), its result, edition and as-of date. The batch status is ok only when every item is; otherwise it is partial, with not_found and unresolved naming the items by index. Counting: a batch counts as one call against the daily limit, and as one call per item against the Pro monthly allowance.
Check receipts
Receipts are issued for calls made with an API key — a free key is enough (POST /api/keys/register; see pricing). An answer of the six check tools — the ADR 1.1.3.6 calculator, the LQ/EQ check, the UK duty calculator, the consignment calculator, the shipment summary and the ICS2 check — to a keyed call carries a check_id (128 random bits) and a receipt_url: in the X-FreightUtils-Check-Id and X-FreightUtils-Receipt headers on every REST answer, in _meta.check_id / _meta.receipt_url under ?envelope=1.2, and in structuredContent._meta on the hosted MCP and in the freightutils-mcp package (typed in its output schema from 2.25.0). Lookups and page loads carry none, and neither does a refusal of the input (no check ran). An answer of a check is never cached.
A check called without a key gets the same answer and nothing is stored: there is no check_id and no record to retrieve. The answer carries the hash its receipt would have had — in the X-FreightUtils-Receipt-Hash header on every REST form, and in _meta.receipt under ?envelope=1.2 and on the hosted MCP: stored: false, reason: "key_required", schema_version, receipt_hash and the hashed fields the answer does not otherwise carry (created_at, tool, inputs, dataset_version, engine_version and result, the receipt’s snapshot of the answer). To recompute it, take those six with the envelope’s edition and as_of and, when present, _meta.edition_basis; write that object as canonical JSON (object keys sorted at every level, no whitespace, a missing edition_basis left out) and take its SHA-256. A duty batch answered without a key carries one on each line. The freightutils-mcp package types _meta.receipt in its output schema from 2.26.0.
GET /api/receipts/{check_id} returns the record for 90 days (ADR 5.4.4 keeps the transport document three months): the inputs the check consumed, with units and defaults explicit, the edition and as-of date, the hash of each data file read and the engine version, the rules applied with their section and table, the result with its warnings and anything unresolved, and a fixed not_proven sentence — the receipt does not prove the inputs were true, the goods correctly described, the load as calculated, or every legal requirement checked. receipt_hash is a SHA-256 over the canonical JSON of the created time, tool, inputs, edition, as-of date, edition basis, data hashes, engine version and result, so a copy can be checked. ?format=text gives a flat summary. A receipt never holds a name, an address, an email, an API key, an IP address or free text over 200 characters; redacted_fields says what was cut. A keyed check’s receipt is read with the same key or a Pro key of the same account; receipts stored before 30 October 2026 for calls made without a key stay readable by their id alone until they expire, and none is rewritten. Verification by id is free on every tier; receipt fetches count against the daily allowance like the lookups.
Which edition a check rested on. Every ?envelope=1.2 answer of the six checks, and its receipt, carries edition and as_of (the date of the answer) for the dataset the check read, named as the lookups name it: the ADR checks — and the consignment calculator and shipment summary when a line carries a UN number — give ADR 2025 with the dates it applies, exactly as adr_lookup gives them (from and until, under the transitional provision); the duty calculator gives the UK Trade Tariff as a live source, applying from the date of the answer; the ICS2 check gives the version of the stop-word list. A check that read no regulatory dataset gives edition: null with _meta.edition_basis “no regulatory dataset read”, so a null is never unexplained. Receipts issued from 30 October 2026 are receipt.v2 (the as-of date and edition basis are inside the hash); earlier ones stay receipt.v1 and are served as stored. The X-FreightUtils-Receipt-Schema header says which.
ADR editions and as_of
The five tools that read ADR data — /api/adr (adr_lookup), /api/adr-calculator (adr_exemption_calculator), /api/adr/lq-check (adr_lq_eq_check), /api/consignment (consignment_calculator) and /api/shipment/summary (shipment_summary) — take edition ("2025" or "2027") and as_of (YYYY-MM-DD), as query parameters on a GET, in the JSON body on a POST, and as arguments on the hosted MCP server and in the freightutils-mcp package from 2.26.0. edition wins when given; otherwise as_of — today, UTC, when omitted — picks the edition by the legal timeline:
| as_of | Answer without edition | With edition |
|---|---|---|
| before 1 January 2027 | ADR 2025, exactly as before | 2027: ADR 2027 with an EDITION_NOT_YET_APPLICABLE warning |
| 1 January 2027 to 30 June 2027 | ADR 2027, with an EDITION_TRANSITIONAL notice: ADR 2025 may still be applied (1.6.1.1) | 2025: ADR 2025, with the same notice |
| from the day after | ADR 2027 | 2025: refused, EDITION_UNSUPPORTED (status unsupported, HTTP 422) |
A caller who sends neither gets the same answer as before until ADR 2027 applies. An answer on a named edition or date carries adr_edition (edition, name, as_of, what chose it, the dates it applies); the ?envelope=1.2 answer names it in edition and echoes as_of, and a check’s receipt carries both under its hash. ADR 2027 is built from the ADR 2025 table with every amendment of ECE/TRANS/WP.15/274, Add.1 and Corr.1 applied (methodology). Under ADR 2027 a deleted entry (UN 2941) answers not_found with the reason, and the code meanings of the provisions ADR 2027 amends say amended_in_2027 — the meaning given is the ADR 2025 text.
Transitional measures. ADR 2027 deletes note a of 1.1.3.6.3, so UN 0081, 0082, 0084, 0241, 0331, 0332, 0482, 1005 and 1017 count ×50 with a 20 kg maximum. Under edition=2027 the 1.1.3.6 answer (and the shipment summary’s adrFlags) carries transitional_1_6_1_61 beside the verdict: the ADR 2025 note-a basis (×20, 50 kg) with points_on_this_basis and exempt_on_this_basis, which 1.6.1.61 allows until 31 December 2030. The verdict is always the ADR 2027 rule; both bases are shown. An ADR 2027 lookup of UN 2372 carries transitional_notes[]: its tank code changes from LGBF to L4BH, and 1.6.3.64 / 1.6.4.70 let a tank built before the date ADR 2025 stops applying keep the old code until 31 December 2030.
variant_index under ADR 2027. UN 2348 and UN 2862 gain a packing group II row in ADR 2027, listed before the packing group III row, so the PG III row is variant_index 1 under ADR 2027 (0 under ADR 2025). A call that sends packing_group is unaffected; only a call that pins a bare variant_index for these two UN numbers reads the other row — send packing_group.
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.
Envelope 1.2 — status, edition and as-of date. Add ?envelope=1.2 (query parameter only) and the envelope gains four fields beside ok: status (ok, partial, not_found, unsupported or ambiguous), missing_inputs (the input names a partial answer needs; empty otherwise), edition (the edition the answer is served from, with the dates it applies — ADR 2025 from 1 January 2025, and it may still be applied until 30 June 2027; null for a computed answer) and as_of (the served dataset’s audited date). ok is unchanged — true for every delivered answer, including a search that matched nothing — and no value in the answer changes. ?envelope=1 and ?envelope=1.1 are unchanged; the MCP tools (the hosted server and the freightutils-mcp package from 2.24.0) answer with 1.2. Schema: /schemas/response-envelope.v1.2.json.
Anonymous answers say so. A call made without an API key carries an X-FreightUtils-Identify response header — one sentence giving the anonymous limit (25/day per address), what a free key changes (100/day, and notice of changes) and how to label your calls. The same sentence is in the body as _meta.identify only under ?envelope=1.2; the plain body and the 1 and 1.1 envelopes never carry it. On the hosted MCP server it is in the tool result’s _meta plus one extra text item. Keyed calls never carry it. To be recognisable in our usage figures without a key, add the optional ?client=<your-app> parameter or an X-FreightUtils-Client: <your-app> header (up to 40 characters of letters, digits, ., _ and -). It changes no answer.
On the hosted MCP server (/api/mcp) the envelope is returned as structuredContent (validating against each tool’s output schema) and serves v1.2 unconditionally — status, missing inputs, edition and as-of on every answer, a refusal included; 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", "1.1" or "1.2") 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.