Core concepts
Errors and rate limits
Response envelopes, error codes, and the usage headers that ride on every call.
Response envelopes
Products share a small set of shapes. Read the envelope before the payload.
- TrackingMCP and the SchedulesMCP public surface wrap success in
{ "ok": true, "data": ... }. - AirCargoMCP and FreightRatesMCP return the payload under
{ "data": ... }. - LoadingMCP returns
{ "ok": true, "plan": { ... } }. - The TrackingMCP DCSA feed at
/v2/eventsis the one exception. It follows the standard rather than our envelope, so it answers a bare JSON array and puts pagination in the response headers.
The keyed TrackingMCP surface returns a coded error object, so you can branch on the code and show the message:
{
"ok": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "identifier_type is required",
"severity": "warning"
}
}
The open TrackingMCP endpoints answer a plain message instead, and they are explicit rather than silent:
{
"ok": false,
"message": "That number fails the ISO check digit, which almost always means one digit is off. Did you mean MEDU1234562?"
}
Codes you will meet on TrackingMCP
| Code | Status | What it means |
|---|---|---|
VALIDATION_ERROR | 400 | A required field is missing or out of range. Posting the wrong field names lands here. |
UNAUTHORIZED | 401 | The key is missing, malformed or revoked. |
UNIQUE_QUOTA_EXCEEDED | 402 | The add would pass the shipment quota on your plan. |
CONTAINER_QUOTA_EXCEEDED | 402 | No container slot is free on your plan. |
PAYG_SPEND_CAP_REACHED | 402 | Pay-as-you-go only: the add would cross the spend cap you set. |
INSUFFICIENT_CREDITS | 402 | MCP calls only: your Navo Tokens balance cannot pay for the call. Nothing is charged. The error also carries credits_required, credits_balance, top_up_url and packages (see Navo Tokens). |
KEY_PRODUCT_NOT_ALLOWED | 403 | The key is scoped away from tracking. |
NOT_FOUND | 404 | No record with that UUID on your account. |
REFERENCE_NOT_RESOLVABLE | 404 | No line recognises the reference. |
AUTO_CANT_FIND_INFO | 404 | The line could not be identified for that reference. |
ALREADY_TRACKED | 409 | That identifier is already tracked under a different line. Adding it again with the same line, or without a line, answers 200 with the record. |
CONTAINER_ADD_IN_PROGRESS | 409 | The same identifier is mid-add on another request. |
MCP_QUOTA_EXCEEDED | 429 | MCP calls without a Navo Tokens wallet: the monthly MCP call limit is spent. |
quota_exceeded | 429 | REST: the call limit of your plan or of the key is spent. A refused call is not counted. |
RATE_LIMITED | 429 | Too soon after the last recheck of that box. |
DB_ERROR, INTERNAL_ERROR | 500 | Our side. Retry. |
The one that is not a failure
Two 202 answers mean “accepted, still resolving”:
GET /v1/containers/lookup/{identifier}answers202with the codeSTILL_RESOLVINGwhen the line is known but has not answered in time. Resolution continues in the background. The body carriesretry_after_secondsandprogress, and the response has aRetry-Afterheader. Poll again on that delay rather than adding the box a second time.POST /v1/containersanswers202for a new reference that did not resolve within the request: with"resolution_status": "pending"and apoll_url, or withSTILL_RESOLVINGwhen the line is known. Poll the record.
Note that ok is false on STILL_RESOLVING and true on the pending add. Branch on the status code, not on ok alone, or a healthy in-progress lookup will read as an error.
FreightRatesMCP uses a structured error object:
{ "error": { "code": "not_found", "message": "No rate for CNSHA->NLRTM." } }
Status codes
| Code | Meaning |
|---|---|
200 | Success. TrackingMCP answers this to an add of a box you already track. |
201 | Created. A TrackingMCP add of a new box that resolved within the request. |
202 | Accepted and still resolving. See the note above. |
400 | The request is malformed, or a required parameter is missing. |
401 | The key is missing or not recognised. |
402 | A container or shipment quota, or a pay-as-you-go spend cap, stands in the way of an add; your account has no access to the product (not_entitled on FreightRatesMCP); or an MCP call’s Navo Tokens balance cannot pay. |
403 | The key is not allowed this product, or the access has ended (access_expired). |
404 | The identifier resolved to nothing, or there is no rate for the lane. |
405 | Wrong method. FreightRatesMCP is GET only, except POST /v1/rates/quote and the webhook routes. |
409 | The same record is already tracked, or an identical request is in flight. |
413, 422 | FreightRatesMCP quote: the body is too large, or the quote request cannot be priced. |
429 | You are over the rate limit or a call quota. Back off and retry. |
5xx | Our side, or an upstream source. Retry. |
Error codes you will see in the FreightRatesMCP envelope include missing_key, invalid_key, revoked_key, expired_key, invalid_request, not_entitled, not_found, method_not_allowed, quota_exceeded, auth_unavailable and server_error. An MCP call whose Navo Tokens balance cannot pay gets 402 with insufficient_credits, carrying the same four optional fields as on TrackingMCP. AirCargoMCP, SchedulesMCP and LoadingMCP carry the same four fields on every Navo Tokens refusal too (see Navo Tokens).
What counts as a successful request
A success is a 2xx. Everything else is a refusal (4xx) or our failure (5xx). What is charged depends on how you call: REST with an API key is metered in calls against your plan’s call quota; an MCP call from an account with a Navo Tokens wallet is charged in Navo Tokens and never touches the call quota.
| Product and access | Successful answers | What is counted | What is free |
|---|---|---|---|
| TrackingMCP, REST (API key) | 200; 201 new box resolved; 202 pending add; 202 STILL_RESOLVING | 1 call for every keyed request to a tracking path, counted when the key is checked, before the route runs. The route’s own answer does not change it: 404 REFERENCE_NOT_RESOLVABLE, 409 ALREADY_TRACKED, other 4xx and 5xx from the route, and a 429 rate-limit refusal are each 1 call. An add can also be refused with 402 when a container or shipment quota is used up. | 401; the quota refusal itself (429 quota_exceeded, 403 access_expired) and a key-limit refusal, which go to refused calls; account paths (/v1/usage, /v1/account, /v1/api-keys, /v1/billing, /v1/invoices, /v1/finance, /v1/team, /v1/webhooks, /v1/mcp). POST /v1/track/public takes no key and spends no quota; it is capped per account and per IP. |
| TrackingMCP, MCP (Navo Tokens) | Same as REST | Adding a box: 250 Navo Tokens, taken before it is registered. Adding a box you already track again: free, unless it triggers a refresh (10). Reads (lookup, container detail, summary, demurrage): 10 Navo Tokens for each reference whose answer carries data; a B/L counts as one reference. | 402 INSUFFICIENT_CREDITS (nothing charged); an answer without data; 202 STILL_RESOLVING; 404 REFERENCE_NOT_RESOLVABLE; 404 AUTO_CANT_FIND_INFO; 409 ALREADY_TRACKED and 500 on an add (the 250 is refunded); a box we never find (refunded); get_account_access. No 429 MCP_QUOTA_EXCEEDED on this path. |
| FreightRatesMCP, REST (API key) | 200 | 1 call for every keyed request, counted when the key is checked. A quote (POST /v1/rates/quote) that returns no rate is uncounted again. Other routes stay counted whatever they answer, including 404 not_found and 400. | 401; 402 not_entitled; the quota refusal (429 quota_exceeded, 403), which goes to refused calls; a quote with no result; webhook and account routes. |
| FreightRatesMCP, MCP (Navo Tokens) | 200 | 100 Navo Tokens for a port pair with data. The same port pair again within 24 hours of the last charge: 10. After 24 hours it is priced as new (100). Lane cards, spot and quote are charged only when they return at least one rate. | The balance is checked before the read: 402 insufficient_credits charges nothing. Also free: 404 not_found, an empty answer, 400, 413, 422, 5xx, the index list and lane history. |
MCP calls from an account without a Navo Tokens wallet are counted in calls. On TrackingMCP they are counted as on REST and each also takes one MCP call from the plan’s monthly MCP limit; a refusal there is 429 MCP_QUOTA_EXCEEDED, and a purchased call pack with calls left lifts that limit. On FreightRatesMCP they are counted on the plan’s MCP line, with the same rules as REST, and the key’s own limits do not apply. get_account_access is always free.
Rate limits and usage
Limits are enforced over a rolling 20 second window, so bursts are smoothed rather than a single per-minute bucket. The defaults:
| Tier | Per 20s window | Sustained |
|---|---|---|
| Free / anonymous / no plan | 40 requests | 120 / min |
| Paid plan | 120 requests | 360 / min |
Need a higher ceiling for batch workloads? Contact us and we raise it per account.
Every keyed response carries usage headers so you can watch your budget without a second call:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed in the current window (40 or 120). |
X-RateLimit-Remaining | Requests left in this window. |
X-RateLimit-Reset | Unix time (seconds) when the window resets. |
When you exceed the window you get 429 with the standard envelope:
{ "ok": false, "error": { "code": "VALIDATION_ERROR", "message": "Rate limit exceeded", "severity": "warning" } }
Read the headers, and on a 429 back off with a short exponential delay before retrying. Public (unkeyed) endpoints are limited per IP; keyed endpoints are limited per account and plan.
Handling it well
- Treat any non-
2xxas a typed outcome, not a crash. Branch on the code. - Log the
message. It is written to be read by a human on your side. - Do not hammer a
404. An identifier that fails the check digit will keep failing until it is corrected. - Do not retry a
409 ALREADY_TRACKED. The box is tracked under another line: read the record you already hold.