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/search

Your 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

HeaderStatusNotes
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

FieldTypeNotes
shipment.originobjectSee Locations.
  origin.typestringseaport, airport, address or terminal.
shipment.destinationobjectSame shape as origin.
  destination.typestringAs above.
shipment.load.packagesarrayAt least one entry.
  packagingTypestringcontainer20, container40, container40HC, pallets, boxes and others.
  quantityintegerHow many units of this packaging.

Optional

Each one changes the averages you get back. None of them is decoration.

FieldTypeEffect
shipment.pickupDatestringISO 8601. Defaults to today. Cannot be in the past. Filters rates by validity.
shipment.directionstringExport or Import.
shipment.transportModestringLimits which modes we quote. Omit it and you get an entry for every mode that applies, which is usually what you want.
shipment.insuranceValuemoneyRequests 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.idstringBusiness key of the account. Only read when name is present too.
customer.namestringAccount name. Required alongside id.
load.declaredValuemoneyCommercial 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.currencystringCurrency to price in, for example USD.
pricePreference.includeOriginPortChargesbooleanPrices the origin leg. Set explicitly for address-to-address.
pricePreference.includeDestinationPortChargesbooleanPrices the destination leg. Same.
pricePreference.rateTypestringName 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.

FieldStatusNotes
typeRequiredDecides how we read the rest of the object.
codeOptionalUN/LOCODE such as USNYC. When present we use it alone and ignore the address fields.
city, countryNameOptionalWe use these when there is no code.
countryCodeOptionalTwo letter ISO code.
street, postalCode,
stateOrProvince, name
OptionalThese sharpen an address lookup.

Load rules

What you must send depends on whether the load is containerised. The two cases exclude each other.

PackagingWeightDimensions
container20, container40,
container40HC and so on
OptionalMust be absent
pallets, boxesRequiredRequired

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

FieldMeaning
requestIdIdentifier we generate for this request. Quote it when asking us about a response.
origin, destinationThe lane we priced, as you sent it.
load.packagingType,
load.quantity
The first package, naming what kind of load was priced.
load.totalWeightWeight of the whole load in kg, summed over every package. Absent when no package carried a weight.
load.totalVolumeVolume of the whole load in cbm. Absent for containers, which carry no dimensions to derive it from.
searchUrlThe same search on the Freightos site, listing every quote across all modes and never split by mode. Useful when a number looks wrong.
modesOne 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[].modeWhich mode this entry covers.
modes[].averagePriceMean 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:

  • modes is absent when nothing priced at all.
  • averageTransitTime is absent from a mode when no quote in it reported one. On some lanes that is every quote.
  • averagePrice is 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.
  • searchUrl is absent when there are no quotes, and load.totalVolume for 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"
}
StatusCodeMeaning
400MALFORMED_JSONWe could not parse the body, or it does not match the schema.
400INVALID_REQUESTA required field is missing, or you broke one of the rules below. The message names the field.
401UNAUTHORIZEDYour apiKey is missing, invalid or disabled.
404NOT_FOUNDYou referenced a record that does not exist.
500INTERNAL_ERRORPricing 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

MessageCause
shipment is requiredNo shipment object.
Both shipment.origin and
shipment.destination are required
One of the two is missing.
shipment.origin.type is required...A location with no type.
shipment.load.packages requires
at least one entry
Empty or missing packages array.
Every entry in shipment.load.packages
needs a packagingType
A package with no packaging type.
Every entry in shipment.load.packages
needs a quantity
A package with no quantity.
shipment.pickupDate must be an
ISO 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].