Rates API

Partner-facing reference for reading freight rates from FreightAtScale over HTTP.

Download Postman collection Set apiKey and baseUrl after importing.

This document is the whole contract: everything it does not describe is not part of the API and may change without notice. It is written to be handed to an integrating partner as-is.


Authentication

Every request needs the API key issued to you. Send it as a bearer token:

http
Authorization: Bearer fas_live_a1b2c3d4e5f6_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

An X-API-Key: <key> header is accepted as an equivalent alternative if your HTTP client makes that easier. Send one or the other, not both.

Key format. fas_<environment>_<key id>_<secret>.

Segment Meaning
fas Namespace. Lets a key be recognised in a log or a config file at a glance.
live / test Which environment the key belongs to. Keys are issued per environment and exist only there, so a test key does not authenticate against production.
key id Public. Safe to quote in a support ticket — it is how we identify your key without you sending us the secret.
secret The credential itself.

The key is shown once, at issuance, and is not recoverable. We store only a hash of it. If it is lost, ask us to issue a replacement; if it is exposed, tell us and we will revoke it — revocation takes effect on the very next request, with no deploy or delay.

Handling. Treat the key like a password: keep it server-side, in a secret store or environment variable. Do not embed it in a browser, a mobile app, or anything else an end user can read — a key is not scoped to a single caller, so anyone holding it has everything it grants. Do not put it in a URL query string, where it lands in access logs and browser history.

Scopes. A key carries an explicit list of what it may reach. Today the only scope is rates:read. A request to an endpoint outside your key's scopes returns 403, not 404 — you will always be able to tell "not allowed" from "not there".

Keys may also carry an expiry date. An expired key returns 401, the same as an invalid one.


Rate limits

Your key has its own request-per-minute allowance, independent of any other key and of the IP you call from. GET /public/v1/whoami reports it as rateLimitPerMinute. The default for a new key is 60 requests/minute and the current ceiling for a single key is 600/minute; if your integration needs more than the default, ask — it is a per-key setting, not a code change.

A separate per-IP limit also protects the endpoint against unauthenticated traffic. It sits above the per-key ceiling, so it will not affect a single partner calling within their own allowance.

The allowance is shared across every endpoint in this document, so it is a budget for the whole API rather than per route. Every response carries your current standing:

Header Meaning
X-RateLimit-Limit Your allowance for the window.
X-RateLimit-Remaining Requests left in the current window.
X-RateLimit-Reset Seconds until the window resets.

Exceeding it returns 429 with a Retry-After header, in seconds. Wait that long and retry; retrying sooner just burns the next window too. Requests rejected with 401 do not count against your allowance, so a misconfigured key cannot exhaust your budget.


Errors

Every error has the same shape:

json
{
  "statusCode": 401,
  "message": "Invalid API key.",
  "error": "Unauthorized",
  "timestamp": "2026-09-01T09:24:12.000Z",
  "path": "/public/v1/rates"
}

message is a string, or an array of strings when several query parameters fail validation at once.

Status When What to do
400 A parameter is malformed, out of range, or not recognised Fix the request; the message names the parameter
401 Key missing, malformed, unknown, revoked, or expired Check the key; do not retry unchanged
403 Key is valid but lacks the scope for this endpoint Ask us to widen the key's scopes
404 No rate with that id
429 Rate limit exceeded Wait Retry-After seconds
500 Our fault Retry with backoff; if it persists, contact us with the timestamp and path

Two behaviours worth knowing before you start:

Unknown query parameters are rejected, they are not ignored. ?orderBy=amount returns 400 ["property orderBy should not exist"]. This is deliberate: a silently-ignored typo in a filter returns a plausible-looking but wrong result set, which is far worse than a loud failure.

Every authentication failure returns the same 401 and the same message, whether the key is malformed, unknown, revoked, or expired. This is intentional and not a diagnostic gap — the alternative lets anyone probe which keys exist. Use /whoami to check a key you legitimately hold.


GET/public/v1/whoamirates:read

Confirms your key works and reports what it can do. No parameters. Start here when integrating.

curl -H "Authorization: Bearer $FAS_API_KEY" \
  https://api.freightatscale.com/public/v1/whoami
json
{
  "partner": "Acme Logistics",
  "keyPrefix": "fas_live_a1b2c3d4e5f6",
  "scopes": ["rates:read"],
  "rateLimitPerMinute": 60
}

GET/public/v1/ratesrates:read

Searches the rate library. Requires the rates:read scope.

Query parameters

All are optional. Every filter is combined with AND; a filter that accepts a list matches any of its values (OR).

Parameter Type Default Notes
origin string list Port name, UN/LOCODE, or a common alias
destination string list Same
carrier string Case-insensitive exact match. See /rates/carriers
mode air | sea
equipment string list Container codes, e.g. 40HC, 20GP
rateType contract | spot See rate types
search string Free-text across origin, destination and region
includeExpired boolean false See validity
page integer ≥ 1 1
limit integer 1–200 50

List parameters accept either repeated keys or one comma-separated value — both forms are equivalent:

text
?origin=INNSA&origin=INMAA
?origin=INNSA,INMAA

If a port name itself contains a comma, send it as its own repeated parameter.

Port matching is deliberately forgiving. Rates originate in carrier spreadsheets that name ports however the carrier chose, so a filter term is expanded against known port names, codes and aliases before matching. INNSA, Nhava Sheva and Jawaharlal Nehru all find a rate the sheet recorded as Nhava Sheva / Mumbai. Prefer the UN/LOCODE where you have it — it is the least ambiguous.

Unspecified is not incompatible. Some source sheets do not state a mode or a container type. Those rows match any mode or equipment filter rather than being excluded, because a blank cell means "not stated", not "does not apply". Check the returned mode and equipment fields (they may be null) rather than assuming the filter guaranteed them.

Example

curl -H "Authorization: Bearer $FAS_API_KEY" \
  "https://api.freightatscale.com/public/v1/rates?origin=INNSA&destination=Rotterdam&equipment=40HC&mode=sea"
json
{
  "data": [
    {
      "id": "aaaaaaaa-0000-4000-8000-000000000001",
      "carrier": "MAERSK",
      "rateType": "contract",
      "mode": "sea",
      "origin": "Nhava Sheva / Mumbai",
      "destination": "Rotterdam",
      "region": "North Europe",
      "equipment": "40' HC",
      "amount": 1850,
      "currency": "USD",
      "freeDays": 14,
      "surcharges": [
        { "code": "BAF", "amount": 120, "currency": "USD", "unit": "CONTAINER", "paymentTerm": "PREPAID" }
      ],
      "validFrom": "01 Sep",
      "validTill": "30 Sep",
      "validFromDate": "2026-09-01",
      "validTillDate": "2026-09-30",
      "updatedAt": "2026-09-01T09:22:47.002Z"
    }
  ],
  "meta": { "page": 1, "limit": 50, "total": 1, "totalPages": 1 }
}

Pagination

meta.total is the count across all pages, meta.totalPages the number of pages at the current limit. Results are ordered oldest-first and that order is stable, so paging through a result set will not show you the same rate twice.


GET/public/v1/rates/carriersrates:read

Lists the carriers present in the library and which rate types exist for each. Use it to discover valid carrier filter values rather than guessing at our spelling.

json
{
  "data": [
    { "carrier": "HAPAG-LLOYD", "rateTypes": ["spot"] },
    { "carrier": "MAERSK", "rateTypes": ["contract", "spot"] }
  ]
}

GET/public/v1/rates/{id}rates:read

Returns one rate by its id. 404 if there is no such rate. Ignores expiry — a rate you already hold an id for is returned whether or not it has since expired, so a stored reference does not silently become a 404.


The rate object

Field Type Notes
id string (UUID) Stable. Safe to store as a reference
carrier string
rateType string contract or spot — see below
mode "air" | "sea" | null null = not stated in the source
origin string Port as the source names it, not a code
destination string Same
region string | null Carrier's own trade-region grouping, when stated
equipment string | null Normalised where recognised (40' HC, 20' GP)
amount number The rate. Always paired with currency
currency string ISO 4217
freeDays integer | null Free detention/demurrage days, when stated
surcharges array | null See below
validFrom / validTill string | null Validity as written on the source sheet, e.g. "01 Sep"
validFromDate / validTillDate string (YYYY-MM-DD) | null Resolved calendar dates. See validity
updatedAt string (ISO 8601) Last change to this rate

Rate types

Value Meaning
contract From a negotiated carrier contract or a published rate sheet
spot A live or recently-fetched spot quote

These are the only two values this API returns or accepts. Passing anything else to the rateType filter returns 400 listing the valid values.

Amounts

amount is a JSON number with at most 2 decimal places. What one unit is depends on the trade: for FCL it is per container of the stated equipment; for air it is typically per kilogram. The API does not currently state the unit explicitly — if that matters to your integration, tell us and we will add it as a field rather than have you infer it.

Surcharges are not included in amount. A landed cost is amount plus the applicable entries in surcharges, each of which looks like:

json
{ "code": "BAF", "amount": 120, "currency": "USD", "unit": "CONTAINER", "paymentTerm": "PREPAID" }

unit is one of TEU, CONTAINER, SHIPMENT, PERCENTAGE, UNKNOWN; paymentTerm one of PREPAID, COLLECT, UNKNOWN. A surcharge's currency may differ from the rate's.

Validity

Two pairs of fields, because they answer different questions.

validFrom / validTill are the raw text from the source sheet, e.g. "01 Sep". Carrier sheets routinely omit the year, so this text is preserved exactly as received and never "corrected".

validFromDate / validTillDate are our resolved calendar dates, as YYYY-MM-DD. They are a best-effort interpretation of the text above, and either may be null when a date could not be determined confidently.

A null validTillDate means "no expiry could be determined" — it does not mean "never expires". Do not treat such a rate as valid indefinitely. Check validTill and confirm with us if the distinction matters commercially.

By default the search excludes rates whose validTillDate is in the past. Rates with a null validTillDate are never excluded, since we cannot prove they are stale. Pass includeExpired=true to see expired rates too.

Freshness

Rates are a library, not a live quoting engine. They change when a new sheet is imported or a quote is received — hours to days, not seconds. Polling more than a few times an hour will not surface anything new; updatedAt is the reliable way to detect change. A rate returned here is reference pricing, not a firm offer or a booking guarantee. Confirm commercially before relying on it.


What this API does not cover

This endpoint publishes carrier contract and spot pricing. It is not a complete view of every rate we hold, and it is not intended to be — some pricing is commercially confidential and is not published through any partner channel.

Practically, this means a lane you know we service may return no rows. That is not an error and not a gap in your integration: it means we hold no publishable contract or spot rate for it. Treat an empty data array as a normal outcome, and ask us directly about a lane that matters to you. GET /public/v1/rates/carriers likewise lists only carriers with publishable rates, so it is the authoritative list of what you can filter by.


Versioning and change policy

The version lives in the path (/public/v1). Within v1 we may add new endpoints, add new fields to existing responses, and add new optional query parameters.

Your client must tolerate new fields appearing in a response. Parse defensively — read the fields you need rather than requiring an exact match on the object's shape.

We will not, within v1, remove or rename a field, change a field's type, or change the meaning of an existing value. Anything of that kind ships as /public/v2, and v1 keeps running alongside it for an agreed migration window.


Support

Quote your keyPrefix (never the full key) plus the timestamp and path from the error body.