Rate Estimate API v1
By requesting a key or using this API you agree to the Freightos Data Terms & Conditions.
Free use is limited to 10 requests per day. To request a key or a higher limit, contact [email protected].
Send a lane and a load, get the average market rate and transit time back in one synchronous response, broken down by transport mode.
POST https://ship.freightos.com/api/rate-estimate/v1/searchYour API key is issued for one environment. A production key works against ship.freightos.com;
a test key works against the test host you were given, and the two are not interchangeable.
What it returns
A market rate service, not a booking service.
You get the mean price and transit time for your lane and load, one set per transport mode. Not the individual quotes, and nothing you can book against. The lane and load come back alongside the averages, so you can read the numbers without keeping your original request to hand.
Averages are never taken across modes. An air rate and an ocean rate answer different questions, and a mean of the two is not a rate anyone pays.
One request, one response. No polling.
Quick start
Headers
| Header | Status | Notes |
|---|---|---|
apiKey |
Required | Your Freightos API key. Valid on one environment only. A production key will not work against a test host. |
Content-Type |
Required | application/json |
Every request is recorded, including the ones you send while integrating. There is no dry-run mode.
Smallest request that works
{
"shipment": {
"origin": { "type": "seaport", "code": "USNYC" },
"destination": { "type": "seaport", "code": "GBFXT" },
"load": {
"packages": [
{ "packagingType": "container40", "quantity": 1 }
]
}
}
}
Scalars are plain values. Measurements and money are objects, because they carry a unit or a currency next to the number.
Request
Required
| Field | Type | Notes |
|---|---|---|
shipment.origin | object | See Locations. |
origin.type | string | seaport, airport, address or terminal. |
shipment.destination | object | Same shape as origin. |
destination.type | string | As above. |
shipment.load.packages | array | At least one entry. |
packagingType | string | container20, container40, container40HC, pallets, boxes and others. |
quantity | integer | How many units of this packaging. |
Optional
Each one changes the averages you get back. None of them is decoration.
| Field | Type | Effect |
|---|---|---|
shipment.pickupDate | string | ISO 8601. Defaults to today. Cannot be in the past. Filters rates by validity. |
shipment.direction | string | Export or Import. |
shipment.transportMode | string | Limits which modes we quote. Omit it and you get an entry for every mode that applies, which is usually what you want. |
shipment.insuranceValue | money | Requests insurance for that value, and prices a premium into the averages wherever the seller offers it. Must be greater than zero. See What rates cover. |
customer.id | string | Business key of the account. Only read when name is present too. |
customer.name | string | Account name. Required alongside id. |
load.declaredValue | money | Commercial value of the goods. Send neither this nor insuranceValue and we default it to 1, so searchUrl opens without the page asking for a goods value. |
pricePreference.currency | string | Currency to price in, for example USD. |
pricePreference.includeOriginPortCharges | boolean | Prices the origin leg. Set explicitly for address-to-address. |
pricePreference.includeDestinationPortCharges | boolean | Prices the destination leg. Same. |
pricePreference.rateType | string | Name of a price level to average against. |
Locations
Identify a port by code, or an address by city and countryName.
Port codes are more precise and resolve faster.
| Field | Status | Notes |
|---|---|---|
type | Required | Decides how we read the rest of the object. |
code | Optional | UN/LOCODE such as USNYC. When present we use it alone and ignore the address fields. |
city, countryName | Optional | We use these when there is no code. |
countryCode | Optional | Two letter ISO code. |
street, postalCode,stateOrProvince, name | Optional | These sharpen an address lookup. |
Load rules
What you must send depends on whether the load is containerised. The two cases exclude each other.
| Packaging | Weight | Dimensions |
|---|---|---|
container20, container40,container40HC and so on | Optional | Must be absent |
pallets, boxes | Required | Required |
Sending dimensions with a container is an error. The field is not ignored. You get
Dimensions are not allowed for container40. Omit them for pallets or boxes and you get
Missing dimensions of a load package.
A loose load
"packages": [
{
"packagingType": "boxes",
"quantity": 4,
"weight": { "value": 100, "unit": "kg" },
"dimensions": {
"length": { "value": 120, "unit": "cm" },
"width": { "value": 80, "unit": "cm" },
"height": { "value": 100, "unit": "cm" }
},
"stackable": true
}
]
weight is per unit, not for the whole load. Send all three of length,
width and height, or none.
Response
HTTP 200. Both modes below returned several quotes, so neither carries a margin. A modes
array that is simply absent means we searched the lane and matched no rates, without the quoting engine giving a
reason. When it does give one, you get a 500 instead.
{
"requestId": "8f2c1d40-5b7e-4a91-9c3f-2ad6e8b104ff",
"origin": { "type": "seaport", "code": "USNYC" },
"destination": { "type": "seaport", "code": "GBFXT" },
"load": {
"packagingType": "boxes",
"quantity": 4,
"totalWeight": { "value": 400, "unit": "kg" },
"totalVolume": { "value": 3.84, "unit": "cbm" }
},
"searchUrl": "https://ship.freightos.com/results/agpzfnRyYWRlb3Mx...",
"modes": [
{
"mode": "air",
"averagePrice": { "value": 2480.00, "currency": "USD" },
"averageTransitTime": { "from": 4, "to": 7, "unit": "days" }
},
{
"mode": "LCL",
"averagePrice": { "value": 394.43, "currency": "USD" },
"averageTransitTime": { "from": 17, "to": 25, "unit": "days" }
}
]
}
Response fields
| Field | Meaning |
|---|---|
requestId | Identifier we generate for this request. Quote it when asking us about a response. |
origin, destination | The lane we priced, as you sent it. |
load.packagingType,load.quantity | The first package, naming what kind of load was priced. |
load.totalWeight | Weight of the whole load in kg, summed over every package. Absent when no package carried a weight. |
load.totalVolume | Volume of the whole load in cbm. Absent for containers, which carry no dimensions to derive it from. |
searchUrl | The same search on the Freightos site, listing every quote across all modes and never split by mode. Useful when a number looks wrong. |
modes | One entry per transport mode found, in our own mode order: air, express, LCL, FCL, barge, roRo, rail, airSea, breakBulk, LTL, FTL. The same lane reads the same way round every time. |
modes[].mode | Which mode this entry covers. |
modes[].averagePrice | Mean total price for that mode, rounded to the currency's own precision. Quotes sharing a currency are averaged in it; when they differ we convert to USD first. Carries a margin only when that mode returned a single quote. |
modes[].averageTransitTime.from / .to | The two bounds are averaged separately, so you keep the spread instead of a single midpoint. Both carry a margin only when that mode returned a single quote. |
The margin
A mode that returned only one quote carries a margin: 20 USD on its price and one day on each end of its transit range. A mode whose lone quote is 394.43 USD over 17 to 25 days is reported as 414.43 USD over 18 to 26 days.
One quote is one carrier's offer, with no spread behind it to absorb an outlier. A mode with two or more quotes is the bare mean and carries nothing, even when another mode in the same response has only one. The margin belongs to the mode, not to the search.
The price margin is defined in USD and converted, so it is the same real margin whichever currency you are quoted in. It is not a percentage, so it weighs more on a cheap lane than an expensive one.
The quotes at searchUrl are always the raw ones, and are not split by mode. A mode carrying a
margin will not match them; subtract it to get back to the quote.
Absent fields
We omit null fields rather than return them as null, so absent means not available.
Four cases to handle:
modesis absent when nothing priced at all.averageTransitTimeis absent from a mode when no quote in it reported one. On some lanes that is every quote.averagePriceis absent from a mode when no quote in it carried a price, or when a currency conversion was needed and could not be made. We report nothing rather than average across currencies, which would look valid and mean nothing.searchUrlis absent when there are no quotes, andload.totalVolumefor a container load.
What rates cover
What a returned figure already includes, and what still has to be budgeted for separately.
Returned values reflect average market rates and transit times across all available options for your search.
Rates include general freight, origin and destination local charges (default on door-to-door), and standard warehouse pickup and delivery.
Rates exclude import clearance, pickup and delivery accessorials, factory rates, and cargo insurance.
Insurance is the one exception
Send shipment.insuranceValue and a premium is priced into the figures, but only for
sellers who offer insurance. Sellers without an insurance price list quote without it, so an insured search can
average insured and uninsured quotes together inside one mode.
Omit the field and no insurance is priced at all, which is what the exclusion above describes.
Port charges on a port-to-port search
Origin and destination local charges are included by default only for door-to-door. Searching port to port,
set pricePreference.includeOriginPortCharges and includeDestinationPortCharges
explicitly to have those legs priced. Otherwise the figure covers the main leg alone.
Errors
Always JSON, with a stable code to branch on and a message for humans.
{
"code": "INVALID_REQUEST",
"message": "shipment.load.packages requires at least one entry"
}
| Status | Code | Meaning |
|---|---|---|
| 400 | MALFORMED_JSON | We could not parse the body, or it does not match the schema. |
| 400 | INVALID_REQUEST | A required field is missing, or you broke one of the rules below. The message names the field. |
| 401 | UNAUTHORIZED | Your apiKey is missing, invalid or disabled. |
| 404 | NOT_FOUND | You referenced a record that does not exist. |
| 500 | INTERNAL_ERROR | Pricing failed, or it found no rates and reported why. The message carries the reason. Retry, then send us your requestId. |
Branch on code, never on message. We may reword a message. We will not change a code.
Messages you are most likely to see
| Message | Cause |
|---|---|
shipment is required | No shipment object. |
Both shipment.origin andshipment.destination are required | One of the two is missing. |
shipment.origin.type is required... | A location with no type. |
shipment.load.packages requiresat least one entry | Empty or missing packages array. |
Every entry in shipment.load.packagesneeds a packagingType | A package with no packaging type. |
Every entry in shipment.load.packagesneeds a quantity | A package with no quantity. |
shipment.pickupDate must be anISO 8601 date... | An unparseable date. |
Rules to watch
Valid-looking requests that are rejected. These are the ones integrators hit first.
A lane with no coverage can return 500, not an empty result
When nothing prices and the quoting engine explains why (an unserviceable lane, a mode with no rates loaded), that explanation comes back as a 500 rather than as an empty result. Treat a 500 as “ask us”, not as “retry forever”: the message usually says which lane or mode has no coverage.
The pickup date cannot be in the past
We compare it against today at day resolution, so today itself always passes. Omit the field and it defaults to today. That is the simplest option unless you are quoting a future shipment.
Value fields cannot conflict
Send more than one of load.declaredValue, shipment.insuranceValue and a customs
value and we cross-check them. A mismatch is rejected. Send one, or send amounts that agree.
Insurance must be greater than zero
We reject "insuranceValue": { "value": 0 } rather than read it as “no insurance”.
Omit the field instead.
Port charges decide the scope of an address quote
With type: "address" at both ends, includeOriginPortCharges and
includeDestinationPortCharges decide whether we price the door legs. Leave them out and the scope
is implicit, which makes averages hard to compare between lanes.
Expect the call to take seconds, not milliseconds
We price every quote behind the averages against live rate sources before taking the means. Set your client timeout in tens of seconds, not a few.
Questions, or an average that looks wrong? Send us the full request body and the
requestId from the response. If the response carried a searchUrl, send that too.
It shows the individual quotes the averages were taken over.
To request a key or a higher limit, contact [email protected].