Docs/API Reference

API Reference

RESTful JSON API for container tracking, BL lookup, vessel positions, and sailing schedules. Authenticate with a Bearer token — no SDK required.

Base URLhttps://traqocontainer.com/api/v1
AuthAuthorization: Bearer <key>
FormatJSON
PricingView API plans →StatusUptime & /api/status.json →
GET/api/v1/sandbox/…

Try it — live sandbox

Run a real request against the API right now — no API key or sign-up needed. The sandbox returns fixed sample data (it doesn't touch your account or live carriers), so you can see the exact request and response shape before you integrate.

Clear the SCAC and send — the carrier is resolved for you.

GEThttps://traqocontainer.com/api/v1/sandbox/container/MRSU6859427?sealine=MAEU

Sandbox data is illustrative and fixed. Create a free account and enable developer mode to track real shipments with a live key.

Authentication

All API requests must include your API key as a Bearer token in the Authorization header. You can generate and manage API keys from the Developer section of your dashboard.

Example
Authorization: Bearer YOUR_API_KEY
Keep your API key secret. Do not expose it in client-side JavaScript or commit it to version control. If a key is compromised, delete it from your dashboard and generate a new one.

Base URL

All endpoints are relative to the following base URL:

https://traqocontainer.com/api/v1
Prefer to generate a typed client? The full OpenAPI 3.1 specification is served at /api/v1/openapi.json — feed it to openapi-generator, orval, Swagger UI or Redoc. It also describes the webhook events. Prefer Postman? Import the ready-made Postman collection, set the apiKey variable, and every endpoint is one click from running.

Errors

All error responses return JSON with a consistent structure:

{
  "statusCode": 401,
  "statusMessage": "Invalid or missing API key"
}
StatusMeaning
200Success
400Bad request — check required parameters
401Invalid or missing API key
402Payment required — either your shipment limit is reached (data.error: "shipment_limit_reached") or your payment is overdue past the grace period (data.error: "payment_overdue"). Branch on data.error. Sends a Retry-After header — stop retrying and fix billing / upgrade; re-hammering the same call won't succeed.
403Developer mode not enabled — enable it from your dashboard settings
404Resource not found
429Rate limit exceeded (per API key). Includes X-RateLimit-Limit / -Remaining / -Reset on every response and a Retry-After (seconds) on the 429 — wait that long before retrying. See API rate limits.
502Upstream tracking API error — retry after a moment

A 402 includes a structured data object so you can react programmatically. When data.error is "payment_overdue", tracking is paused because a payment failed and the grace period ended — update billing at data.manageUrl to resume (your existing shipments are unaffected):

{
  "statusCode": 402,
  "statusMessage": "Payment overdue — API access is paused. Update your billing to resume tracking.",
  "data": {
    "error": "payment_overdue",
    "overdueDays": 9,
    "graceDays": 7,
    "plan": "business",
    "manageUrl": "https://traqocontainer.com/dashboard/billing"
  }
}

Shipment limits

Two independent limits apply: a request rate limit per API key (see API rate limits) and a shipment slot limit — how many shipments your account can track simultaneously, set by your plan.

Each call to /api/v1/container/:number, /api/v1/bl/:number checks whether the shipment is already in your account. If it is, the call succeeds without consuming a slot. If it's new and you have remaining slots, it's added. If you've reached your limit, the API returns 402.

The /api/v1/vessel/track and /api/v1/voyage/schedules endpoints do not consume shipment slots — they are purely lookup calls.

API rate limits

Every authenticated request is rate-limited per API key, in a fixed one-minute window. The default is 120 requests per minute (some plans allow more — check the headers below for your actual limit).

Every response carries your current budget, so you never have to guess:

HeaderMeaning
X-RateLimit-LimitRequests allowed in the current window (your key's limit).
X-RateLimit-RemainingRequests left in the current window.
X-RateLimit-ResetUnix time (seconds) when the window resets and the count returns to the full limit.
Retry-AfterOn a 429 only — how many seconds to wait before retrying.
X-Traqo-Refresh-HintOn the live-fetch endpoints (/container, /bl) — a reminder that they re-fetch from the carrier and are slow. For repeat status checks use GET /shipments/{id} (stored data, no re-fetch, no slot); for changes poll ?updated_since= or subscribe to webhooks.

Exceed the limit and you get a 429 with a Retry-After. Back off for that many seconds — retrying sooner just burns another 429:

{
  "success": false,
  "statusCode": 429,
  "message": "Rate limit exceeded — 120 requests per minute. Retry after 42s.",
  "data": { "error": "rate_limit_exceeded", "limit": 120, "retryAfter": 42 }
}
Don't poll /api/v1/shipments in a tight loop to spot changes — you'll hit the rate limit fast. Fetch on a sensible interval (or use the upcoming delta / webhook features).
GET/api/v1/container/:number

Track a container

Returns full tracking data for a container number — status, route, ETA, port events, and vessel info. Pass the container number directly in the URL. The shipment is automatically saved to your account in the background.

Requires developer mode to be enabled on your account. Toggle it from the Developer tab in your dashboard. Requests without it enabled return 403.

Path parameters

ParameterTypeRequiredDescription
numberstringYesContainer number (e.g. MSCU1234567)

Query parameters

ParameterTypeRequiredDescription
sealinestringYes4-character SCAC code of the shipping line (letters and/or digits, e.g. MSCU). Required — omitting returns a 400 error.
curl -H "Authorization: Bearer YOUR_API_KEY" \
  "https://traqocontainer.com/api/v1/container/MRSU6859427?sealine=MAEU"
sealine is optional but recommended. Omit it and we resolve the carrier from the container operator in the lessor's records, this reference's own tracking history, and prefix ownership. Send it when you know it: a carrier you name is tried once, while one we resolve may cost up to three upstream attempts and can still be wrong. The carrier actually used comes back in carrier.sealine. Don't know it? Resolve it up front — for free, with no shipment created — via /carriers/lookup.
Container equipment. Every row in containers_table carries iso_code, size_type and container_description, and each may be null — the keys are always present, so the shape is safe to code against, but population depends on what the carrier publishes and varies by carrier. iso_code is only ever set to a value that validates as a real ISO 6346 code; some carriers send shorthand such as 40HQ in that position, which is reported as null here with the readable form in size_type instead. Treat null as "the carrier did not supply it", never as "no equipment". container_summary (e.g. 2×40HC, 1×20GP) counts only the rows whose type is known.

Response

{
"success": true,
"data": {
"reference_number": "MRSU6859427",
"origin": "Ahmedgarh, ",
"sealine": "MAEU",
"number_of_containers": 1,
"eta": "2026-05-17 00:00:00",
"last_updated_at": "2026-03-26 13:08:44.789895",
"is_active": 1,
"closed_at": null,
"shipment_type": "Container",
"destination": "Caucedo, Dominican Republic",
"sealine_name": "Maersk",
"total_days": 86,
"remaining_days": 51,
"created_at": "2026-03-26 13:08:44.411404",
"last_synced_at": "2026-03-26 13:08:44.411404",
"is_delayed": 0,
"latitude": "-35",
"longitude": "18",
"status": "IN_TRANSIT",
"route_json": "[[22.84,69.72],[22.80,69.75],[22.54,68.72],[20.81,69.59],...150 coordinate pairs]",
"is_failed_shipment": 0,
"shipment_uid": "ef67bb8b4c1f8564282c88efb2fbffbe",
"shipment_public_url": "https://traqo.ai/ocean/share-shipment?shipment_uid=ef67bb8b4c1f8564282c88efb2fbffbe",
"voyage_plan_table": [
{9 fields},
{9 fields},
{9 fields},
{9 fields}
]
,
"containers_table": [
{10 fields}
]
,
"container_summary": "1×40HC",
"locations_table": [
{11 fields},
{11 fields},
{11 fields},
{11 fields},
{11 fields}
]
,
"facilities_table": [
{7 fields},
{7 fields},
{7 fields},
{7 fields},
{7 fields}
]
,
"eta_history_table": [
{6 fields}
]
,
"events_table": [
{20 fields},
{20 fields},
{20 fields},
{23 fields},
{24 fields},
{24 fields},
{24 fields},
{24 fields},
{24 fields},
{24 fields},
{24 fields}
]
,
"vessels_table": [
{11 fields},
{11 fields},
{11 fields}
]
}
}
GET/api/v1/bl/:number

Track a bill of lading

Returns full tracking data for a Bill of Lading number. Identical response structure to the container endpoint. The shipment is automatically saved to your account in the background.

sealine is optional but recommended. A Bill of Lading number carries no embedded carrier code, so when you omit it we resolve the carrier from this reference's own tracking history, the line that issued the bill, and our bill-prefix map. Send it when you know it: a carrier you name is tried once, while one we resolve may cost up to three upstream attempts and can still be wrong. The carrier actually used comes back in carrier.sealine. Don't know it? Resolve it up front — for free, with no shipment created — via /carriers/lookup.
Requires developer mode to be enabled on your account. Toggle it from the Developer tab in your dashboard. Requests without it enabled return 403.

Path parameters

ParameterTypeRequiredDescription
numberstringYesBill of Lading number, 3–50 characters, exactly as the carrier printed it. No format is imposed: digits-only BLs, and BLs containing hyphens, slashes or dots, are all accepted (URL-encode a / as %2F). Surrounding whitespace is trimmed.

Query parameters

ParameterTypeRequiredDescription
sealinestringYes4-character SCAC code of the shipping line (letters and/or digits, e.g. CMDU). Required — omitting returns a 400 error.
curl -H "Authorization: Bearer YOUR_API_KEY" \
  "https://traqocontainer.com/api/v1/bl/SHZ8037930?sealine=CMDU"
Container equipment. Every row in containers_table carries iso_code, size_type and container_description, and each may be null — the keys are always present, so the shape is safe to code against, but population depends on what the carrier publishes and varies by carrier. iso_code is only ever set to a value that validates as a real ISO 6346 code; some carriers send shorthand such as 40HQ in that position, which is reported as null here with the readable form in size_type instead. Treat null as "the carrier did not supply it", never as "no equipment". container_summary (e.g. 2×40HC, 1×20GP) counts only the rows whose type is known.

Response

{
"success": true,
"data": {
"reference_number": "SHZ8037930",
"origin": "Nansha, China",
"sealine": "CMDU",
"number_of_containers": 1,
"eta": "2026-07-28 00:00:00",
"last_updated_at": "2026-05-25 15:42:18.850704",
"is_active": 1,
"closed_at": null,
"shipment_type": "Bill of Lading",
"destination": "Skikda, Algeria",
"sealine_name": "CMA CGM",
"total_days": 83,
"remaining_days": 63,
"created_at": "2026-05-25 15:42:13.528031",
"last_synced_at": "2026-05-25 15:42:13.528031",
"is_delayed": 0,
"latitude": 22.661,
"longitude": 113.6668,
"status": "IN_TRANSIT",
"route_json": "[[22.661,113.6668],[22.743,113.576],[22.704,113.685],[22.531,113.752],...coordinate pairs]",
"is_failed_shipment": 0,
"shipment_uid": "c7ac8ef4cae8955224714fb33770927a",
"shipment_public_url": "https://traqo.ai/ocean/share-shipment?shipment_uid=c7ac8ef4cae8955224714fb33770927a",
"voyage_plan_table": [
{9 fields},
{9 fields},
{9 fields},
{9 fields}
]
,
"containers_table": [
{10 fields}
]
,
"container_summary": null,
"locations_table": [
{11 fields},
{11 fields},
{11 fields}
]
,
"facilities_table": [
]
,
"eta_history_table": [
{6 fields}
]
,
"events_table": [
{22 fields},
{22 fields},
{23 fields},
{23 fields},
{23 fields},
{23 fields}
]
,
"vessels_table": [
{11 fields},
{11 fields}
]
}
}
POST/api/v1/track

Bulk track shipments

Track up to 50 containers or bills of lading in a single request — the batch form of the container and BL endpoints. Each item needs its 4-character SCAC (sealine). New shipments are saved to your account and consume a slot each; ones you already track are re-fetched for free.

Per-item results: the request returns 200 even when some items fail — branch on each results[].ok. The whole request is only rejected up front for auth (401/403), rate limit (429), a malformed body (400), or a payment past the grace period (402).
Each item's sealine (SCAC) is mandatory on the batch endpoint — unlike the single-shipment /container and /bl routes, which resolve it for you. Resolve codes up front with /carriers/lookup, or see Carriers for the full list.

Request body

FieldTypeRequiredDescription
shipmentsarrayYes1–50 items.
shipments[].typestringYescontainer or bl.
shipments[].numberstringYesContainer number (4 letters + 7 digits, ISO 6346 check digit verified) or BL number (3–50 characters, as printed by the carrier — separators are fine).
shipments[].sealinestringYes4-character SCAC of the carrier.

Example request

curl -X POST https://traqocontainer.com/api/v1/track \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"shipments":[{"type":"container","number":"MRSU6859427","sealine":"MAEU"},{"type":"bl","number":"MEDUFR123456","sealine":"MSCU"}]}'

Response

{
"success": true,
"total": 2,
"tracked": 1,
"failed": 1,
"slots": {
"used": 41,
"limit": 50,
"remaining": 9
}
,
"results": [
{
"index": 0,
"number": "MRSU6859427",
"type": "Container",
"ok": true,
"slot_consumed": true,
"data": {7 fields}
}
,
{
"index": 1,
"number": "MEDUFR123456",
"type": "Bill of Lading",
"ok": false,
"error": "not_found",
"message": "Bill of Lading not found"
}
]
}
GET/api/v1/shipments

List tracked shipments

Returns a paginated list of all shipments saved to your account — containers and bills of lading — with their current status, route, and ETA. Useful for building dashboards and monitoring multiple shipments at once.

Requires developer mode to be enabled on your account. Toggle it from the Developer tab in your dashboard. Requests without it enabled return 403.

Query parameters

ParameterTypeRequiredDescription
pageintegerNoPage number, default 1
pageSizeintegerNoResults per page, default 20, max 100
updated_sincestringNoISO 8601 timestamp. Delta mode — returns only shipments whose tracking data changed at or after this time (by last_synced_at), so you can sync changes instead of polling the whole list. Supersedes pagination; capped at 200 results. Response shape becomes { success, updated_since, count, data }.
Each shipment includes a last_synced_at timestamp — the last time we refreshed its tracking. Save the newest one you see and pass it back as updated_since on your next call to fetch just what changed.
curl -H "Authorization: Bearer YOUR_API_KEY" \
  "https://traqocontainer.com/api/v1/shipments?page=1&pageSize=20"

Response

Showing 2 of 4 items for brevity. Flat objects — no nested arrays.

{
"success": true,
"total": 2,
"page": 1,
"pageSize": 20,
"data": [
{
"name": "SHP-00294",
"number_of_containers": 1,
"shipment_type": "Bill of Lading",
"reference_number": "SHZ8037930",
"sealine_name": "CMA CGM",
"sealine": "CMDU",
"status": "IN_TRANSIT",
"origin": "Nansha, China",
"destination": "Skikda, Algeria",
"total_days": 83,
"remaining_days": 63,
"eta": "2026-07-28 00:00:00",
"created_at": "2026-05-25 10:12:19.166049",
"last_synced_at": "2026-08-01 04:15:22.310",
"is_active": 1,
"is_delayed": 0,
"shipment_public_url": "https://traqo.ai/ocean/share-shipment?shipment_uid=c7ac8ef4cae8955224714fb33770927a"
}
,
{
"name": "SHP-00265",
"number_of_containers": 1,
"shipment_type": "Container",
"reference_number": "TRHU8051322",
"sealine_name": "Maersk",
"sealine": "MAEU",
"status": "IN_TRANSIT",
"origin": "Pithampur, India",
"destination": "Mombasa, Kenya",
"total_days": 27,
"remaining_days": 25,
"eta": "2026-05-11 00:00:00",
"created_at": "2026-04-15 11:45:01.712436",
"last_synced_at": "2026-08-01 04:12:47.882",
"is_active": 1,
"is_delayed": 0,
"shipment_public_url": "https://traqo.ai/ocean/share-shipment?shipment_uid=925b8cc44d9692fb29cbe64d86fe6fd8"
}
]
}
GET/api/v1/shipments/{id}

Get a shipment

Returns the current summary of a single shipment you already track — status, route, ETA and last_synced_at — straight from Traqo's stored data. Unlike /container and /bl, this never re-fetches from the carrier and never consumes a shipment slot, so it's the right call for cheap status checks and reconciliation. The {id} is the shipment id returned by /api/v1/shipments.

Requires developer mode. A shipment you don't track (or one you've untracked) returns 404.

Example request

curl https://traqo.io/api/v1/shipments/MSCU1234567 \
  -H "Authorization: Bearer YOUR_API_KEY"

Response

{
"success": true,
"data": {
"name": "SHP-00265",
"number_of_containers": 1,
"shipment_type": "Container",
"reference_number": "MSCU1234567",
"sealine_name": "Maersk",
"sealine": "MAEU",
"status": "IN_TRANSIT",
"origin": "Pithampur, India",
"destination": "Mombasa, Kenya",
"total_days": 27,
"remaining_days": 25,
"eta": "2026-05-11 00:00:00",
"created_at": "2026-04-15 11:45:01.712436",
"last_synced_at": "2026-08-01 04:12:47.882",
"is_active": 1,
"is_delayed": 0,
"shipment_public_url": "https://traqo.ai/ocean/share-shipment?shipment_uid=925b8cc44d9692fb29cbe64d86fe6fd8"
}
}

When predictive ETA is enabled for your plan, the data object also carries predictive_eta and demurrage_risk — see Predictive ETA.

DELETE/api/v1/shipments/{id}

Untrack a shipment

Removes a shipment from your account and frees the slot it occupied, exactly like removing it from your dashboard. This is a soft delete — the shipment stops counting toward your monthly limit and disappears from /api/v1/shipments.

Requires developer mode. Deleting a shipment you don't track (or a repeat delete) returns 404 — nothing changed.

Example request

curl -X DELETE https://traqo.io/api/v1/shipments/MSCU1234567 \
  -H "Authorization: Bearer YOUR_API_KEY"

Response

{
  "success": true,
  "deleted": true,
  "shipment_id": "MSCU1234567"
}
GET/api/v1/vessel/track

Track a vessel

Returns real-time AIS position, speed, heading, and voyage information for a vessel. Both imo (7 digits) and mmsi (9 digits) are required.

Requires developer mode to be enabled on your account. Toggle it from the Developer tab in your dashboard. Requests without it enabled return 403.

Query parameters

ParameterTypeRequiredDescription
imostringYes7-digit IMO number
mmsistringYes9-digit MMSI number
curl -H "Authorization: Bearer YOUR_API_KEY" \
  "https://traqocontainer.com/api/v1/vessel/track?imo=9811000&mmsi=636022327"

Response

{
"success": true,
"data": {
"lat": 18.416247,
"lon": -17.618743,
"speed": 16.5,
"draught": 9.4,
"navigation_status": "Underway using engine",
"course_over_ground": 3.4,
"true_heading": 5,
"timestamp": "2026-05-25T10:45:20",
"origin_port": "Dakar, Senegal",
"origin_locode": "SNDKR",
"atd": "2026-05-20T08:00:00",
"destination_port": "TANGER-MEDITERRANEAN, MA",
"destination_locode": "MATAN",
"eta": "2026-05-30T00:01:00"
}
}
GET/api/v1/voyage/schedules

Voyage schedules

Returns sailing schedules between two ports for a given date, across available carriers.

Query parameters

ParameterTypeRequiredDescription
originstringYesOrigin port UN/LOCODE (e.g. CNSHA)
destinationstringYesDestination port UN/LOCODE (e.g. NLRTM)
datestringYesDate in YYYY-MM-DD format
week_rangeintegerNoNumber of weeks to search, default 1
date_typestringNo"departure" (default) or "arrival"
curl -H "Authorization: Bearer YOUR_API_KEY" \
  "https://traqocontainer.com/api/v1/voyage/schedules?origin=INMUN&destination=AEJEA&date=2026-06-01&week_range=2&date_type=departure"

Response

Showing 2 of many results for brevity.

{
"success": true,
"data": [
{
"sealine": "MSCU",
"sealine_name": "MSC",
"service_name": "EAST AFRICA EXPRESS",
"voyage": "OM622A",
"vessel": {2 fields},
"origin": {5 fields},
"destination": {5 fields},
"departure_date": "2026-06-01 10:00:00",
"arrival_date": "2026-06-06 19:00:00",
"duration": 6,
"transit_type": "unspecified"
}
,
{
"sealine": "MSCU",
"sealine_name": "MSC",
"service_name": "ARABIAN SEA SHUTTLE",
"voyage": "JR622A",
"vessel": {2 fields},
"origin": {5 fields},
"destination": {5 fields},
"departure_date": "2026-06-02 06:00:00",
"arrival_date": "2026-06-09 08:00:00",
"duration": 8,
"transit_type": "transshipment"
}
]
}
GET/api/v1/ports/:locode/congestion

Port congestion

Returns the latest congestion score for a port (UN/LOCODE) plus its 90-day score history: an explainable 0–100 score, a bucket (fluid / normal / moderate / high / critical), a data-sufficiency tier (A/B/C), a 7-day trend, and the per-signal components (dwell, anchorage wait, ETA-slip, schedule deviation, bunching) with each signal's raw value, baseline and z-score.

Requires port-congestion to be enabled on your plan. Reads Traqo's own analytics — no upstream call, so it never consumes a shipment slot.

Predictive ETA on container tracking

When predictive ETA is enabled for your account, /api/v1/container/:number responses include a predictive_eta object: p50 and p80 timestamps, a source (model / blend / carrier), a confidence (high / medium / low), and computed_at. It's derived from live vessel progress plus port congestion — a more accurate arrival estimate than the raw carrier ETA, which goes stale.

GET/api/v1/ports/congestion

Congestion board

Returns the current congestion reading for every scored port in one call — the same score / bucket / tier as the per-port endpoint, plus a 7-day trend_7d, a 30-day calls_30d volume, and coordinates. Use it to build a map or a watchlist without polling ports one at a time.

Same gating as Port congestion: requires port-congestion on your plan. Reads Traqo's own analytics — no upstream call, no shipment slot.

Example request

curl https://traqo.io/api/v1/ports/congestion \
  -H "Authorization: Bearer YOUR_API_KEY"

Response

Showing 1 of many ports for brevity.

{
"success": true,
"total": 128,
"data": [
{
"locode": "USLAX",
"name": "Los Angeles",
"country": "United States",
"score": 72,
"bucket": "high",
"tier": "A",
"trend_7d": 6,
"calls_30d": 214,
"lat": 33.7406,
"lng": -118.2706
}
]
}
GET/api/v1/ports

Search ports

A directory lookup against Traqo's port database — resolve a UN/LOCODE, port name, or city to canonical metadata (locode, name, city, country, region, coordinates). Handy for turning free-text origin/destination into the locodes the congestion and schedule endpoints expect. Local read, no upstream call.

Requires developer mode. ?search= must be at least 2 characters, or the endpoint returns 400. Results are capped at 25, exact LOCODE matches first, then busiest ports.

Query parameters

ParameterTypeRequiredDescription
searchstringYesA UN/LOCODE, port name, or city — minimum 2 characters (e.g. rotterdam or NLRTM)

Example request

curl "https://traqo.io/api/v1/ports?search=rotterdam" \
  -H "Authorization: Bearer YOUR_API_KEY"

Response

{
"success": true,
"total": 1,
"data": [
{
"locode": "NLRTM",
"name": "Rotterdam",
"city": "Rotterdam",
"country": "Netherlands",
"country_code": "NL",
"region": "North Europe",
"lat": 51.9496,
"lng": 4.1453,
"slug": "rotterdam"
}
]
}
GET/api/v1/carriers/lookup

Resolve the carrier for a reference

Which carrier moves a given container or bill of lading — without tracking it. Creates no shipment, writes nothing, and consumes no shipment slot, so it is safe to call before you commit to tracking. Use it to fill the sealine on /track, or to reconcile a carrier against your own master data.

Answers from this reference's own tracking history, the container operator in the lessor's records, the line that issued a bill, container prefix ownership, and prefix popularity — strongest evidence first. A null carrier is a real answer, not an error: track without a sealine and upstream auto-detect takes over.

Query parameters

ParameterTypeRequiredDescription
numberstringYesContainer or bill of lading number.
typestringNocontainer or bl. Inferred from the number when omitted — 4 letters + 7 digits is a container.

Confidence

Every candidate carries a confidence, because the sources are not equally strong. Treat it as the difference between a value you can file against and one you should verify.

ConfidenceSourcesWhat it means
highhistory, spec, bl-sealinesThis reference was identified — it has tracked under this carrier before, or the lessor/issuing line names it directly.
mediumbic, bl-prefixInferred from who owns the prefix. Right far more often than not, but it is about the box, not this journey.
lowprefix-popularityExtrapolated from other boxes that merely share the prefix. A starting guess, not a fact.

“We don’t know” vs “we couldn’t ask”

Both look like carrier: null, and they call for opposite responses — so sources_unavailable tells you which one you got. It lists the sources that errored or ran past the 3-second resolution budget. Empty is the healthy case.

You getIt meansDo this
carrier: null, sources_unavailable: []Everything answered; nobody recognised this reference.A real answer. Track without a sealine and let upstream auto-detect take over.
carrier: null, sources_unavailable: ["spec"]Container identification was unreachable. The answer is degraded, not final.Retry later before concluding the carrier is unknown.
A carrier, sources_unavailable: ["prefix"]A stronger source answered; only a weaker one was skipped.Use it. The list is informational here.
A degraded answer still returns 200. A dependency blinking is not a reason to fail a request we can partly answer from your own tracking history — so never treat this endpoint’s 200 as proof that every source was consulted. Check the list.

Two rules worth coding against

  • carrier is null if and only if candidates is empty. There is no confidence floor — a low-confidence winner is still returned as carrier. If you only want strong answers, filter on confidence yourself; don’t assume we did.
  • candidates is always an array, never null and never absent — possibly empty.

Limits

This endpoint has its own daily cap per API key, separate from the per-minute rate limit, because each new reference fans out to identification lookups on our side. The default is 1,000 lookups per key per day.

HeaderMeaning
X-Lookup-LimitLookups allowed per day on this key.
X-Lookup-RemainingLookups left today.
X-Lookup-ResetUnix time (seconds) when the daily window resets.
Repeat lookups are free. A reference resolved in the last 10 minutes comes back from cache with cached: true, doesn’t count against the cap, and carries no X-Lookup-* headers. The cap is aimed at walking thousands of distinct references, not at re-checking your own boxes.

Over the cap you get a 429 that says when it lifts. Note the error code — it’s how you tell this cap apart from the per-minute limit, which returns rate_limit_exceeded and lifts within the minute:

{
  "success": false,
  "statusCode": 429,
  "message": "Carrier lookup limit reached — 1000 lookups per day. Resets at 2026-08-26T09:14:00.000Z (retry after 41230s). Repeat lookups of a reference you already resolved do not count against this.",
  "data": {
    "error": "lookup_limit_exceeded",
    "limit": 1000,
    "retryAfter": 41230,
    "resetAt": "2026-08-26T09:14:00.000Z"
  }
}

Example request

curl "https://traqocontainer.com/api/v1/carriers/lookup?number=MRKU8636841" \
  -H "Authorization: Bearer YOUR_API_KEY"

Response

{
"success": true,
"data": {
"number": "MRKU8636841",
"type": "container",
"carrier": {
"scac": "MAEU",
"name": "Maersk",
"source": "spec",
"confidence": "high",
"reason": "Container operator from the lessor’s records"
}
,
"candidates": [
{5 fields},
{5 fields}
]
,
"sources_unavailable": [
]
,
"cached": false,
"slot_consumed": false
}
}
GET/api/v1/carriers

List supported carriers

The directory of every ocean carrier Traqo can track, each with its 4-character scac and name. This is the lookup you need before calling /container, /bl or /track: the scac returned here is exactly what you pass as the optional sealine parameter. Local read — never consumes a shipment slot.

Requires developer mode. Pass an optional ?search= (≥2 chars) to filter by SCAC, name or slug.

Query parameters

ParameterTypeRequiredDescription
searchstringNoCase-insensitive filter by SCAC, name or slug — minimum 2 characters (e.g. maersk or MAEU).

Example request

curl "https://traqocontainer.com/api/v1/carriers?search=maersk" \
  -H "Authorization: Bearer YOUR_API_KEY"

Response

{
"success": true,
"total": 2,
"data": [
{
"scac": "CMDU",
"name": "CMA CGM",
"slug": "cma-cgm"
}
,
{
"scac": "MAEU",
"name": "Maersk",
"slug": "maersk"
}
]
}
POST→ your endpoint URL

Webhooks

Instead of polling, subscribe to events and we'll POST them to your server as they happen — a shipment's status or ETA changes, or carrier auto-discovery recovers a shipment that first failed to track. Register and manage endpoints from the Webhooks tab of your dashboard; you can subscribe each endpoint to specific events or to all of them.

Every delivery is a JSON POST with the same envelope — an event name, a created unix timestamp (seconds), and an event-specific data object:

Request body
{
  "event": "shipment.updated",
  "created": 1754170000,
  "data": {
    "shipment_id": "SHP-000481",
    "reference_number": "MSCU1234567",
    "carrier": "MSCU",
    "status": "IN_TRANSIT",
    "previous_status": "BOOKED",
    "eta": "2026-08-14T09:00:00.000Z",
    "previous_eta": "2026-08-12T09:00:00.000Z"
  }
}

Request headers

HeaderDescription
X-Traqo-EventThe event name (also in the body), so you can route without parsing.
X-Traqo-DeliveryUnique id for this delivery attempt's delivery record — use it to dedupe (deliveries are at-least-once).
X-Traqo-SignatureHMAC signature of the body — see Verifying signatures.
User-AgentTraqo-Webhooks/1

Delivery & retries

Acknowledge a delivery by responding with any 2xx status within 10 seconds — respond first, then do your processing asynchronously. Any non-2xx response, or a timeout, is retried with exponential backoff (≈30s, 1m, 2m, 4m … capped at 6h) up to the configured attempt limit, after which the delivery is marked failed.

Delivery is at-least-once: a retry can re-send an event your server already received (e.g. it processed the event but its 2xx never reached us). Make your handler idempotent by de-duplicating on X-Traqo-Delivery.
An endpoint that keeps failing is automatically disabled after a run of consecutive failures, and we stop sending to it. Re-enable it from the dashboard once it's healthy — that also clears its failure count.

Webhook events

Subscribe an endpoint to any of these events, or to * for all of them.

EventFires when
shipment.updatedA tracked shipment's status changes (any transition other than arrival).
shipment.arrivedA tracked shipment's status becomes delivered / completed.
eta.changedThe carrier ETA for a tracked shipment changes.
discovery.recoveredCarrier auto-discovery found the carrier for a shipment that first failed to track — it's now live.

The three shipment events share the data shape shown in the envelope above (status/previous_status carry the transition; eta/previous_eta the ETA move). discovery.recovered carries the recovered carrier:

discovery.recovered — data
{
  "reference_number": "MSCU1234567",
  "type": "Container",
  "carrier": "MSCU",
  "carrier_name": "MSC",
  "shipment_id": "SHP-000481"
}

Verifying signatures

Every delivery is signed so you can confirm it came from Traqo and wasn't tampered with. The X-Traqo-Signature header has a timestamp and a signature:

X-Traqo-Signature: t=1754170000,v1=5f3b1a…c9d2

v1 is the HMAC-SHA256, as lowercase hex, of the string <t>.<raw request body>, keyed with your endpoint's signing secret (whsec_…, shown once when you create or rotate the endpoint). To verify: recompute the HMAC over t + "." + the raw body, compare it to v1 in constant time, and reject anything whose t is more than a few minutes (300s) from now to stop replay.

Sign the raw request body bytes exactly as received — verify before you JSON.parse. Re-serializing the parsed object can reorder keys or change whitespace and the signature won't match.
Node.js (Express)
import { createHmac, timingSafeEqual } from 'node:crypto'

// Capture the RAW body for this route (do NOT let a JSON parser consume it first):
//   app.post('/webhooks/traqo', express.raw({ type: 'application/json' }), handler)

function verify(rawBody, header, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(header.split(',').map(s => s.split('=')))
  const t = Number(parts.t)
  if (!t || !parts.v1) return false
  if (Math.abs(Math.floor(Date.now() / 1000) - t) > toleranceSec) return false
  const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
  const a = Buffer.from(parts.v1, 'hex'), b = Buffer.from(expected, 'hex')
  return a.length === b.length && timingSafeEqual(a, b)
}

app.post('/webhooks/traqo', (req, res) => {
  const raw = req.body.toString('utf8')
  if (!verify(raw, req.get('X-Traqo-Signature'), process.env.TRAQO_WEBHOOK_SECRET)) {
    return res.sendStatus(400)
  }
  const { event, data } = JSON.parse(raw)
  res.sendStatus(200)          // ack fast, then process asynchronously
  // … handle `event` (dedupe on the X-Traqo-Delivery header) …
})

What’s new

Changes to the public API, newest first. Everything here is additive — new optional parameters and new response fields. No request that worked before has stopped working, and no field has been removed or changed type. Code defensively against fields you don’t recognise and you can adopt these at your own pace.

August 2026

Carrier code is now optional on /container and /bl

sealine was mandatory; it is now optional on both single-shipment endpoints. Omit it and we resolve the carrier from the reference’s own tracking history, the container operator in the lessor’s records, the issuing line of the bill, and prefix ownership.

Still send it when you know it. A carrier you name is tried once; one we resolve may cost up to three upstream attempts and can still be wrong. Which carrier actually answered now comes back on every response as carrier.sealine, alongside carrier.provided telling you whether that was your value or ours.

Unchanged: sealine is still mandatory per item on POST /track, where 50 resolutions in one request would be a very different cost.

August 2026

New: GET /carriers/lookup

Ask which carrier moves a reference without tracking it — no shipment created, no slot consumed. Returns the strongest candidate plus everything else that had an opinion, each with a confidence and a plain-English reason. See Resolve a carrier.

Has its own daily cap per key (default 1,000), separate from the per-minute rate limit. Repeat lookups of the same reference are served from cache and don’t count.

August 2026

Container equipment on tracking responses

Every row in containers_table now carries iso_code, size_type and container_description, and the response carries a container_summary such as 2×40HC, 1×20GP.

The fields are always present and nullable, so the shape is stable to code against from day one. Whether they are populated depends on what each carrier publishes and varies by carrier — treat null as “not supplied”, never as “no equipment”. Coverage improves over time without a breaking change.