/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.
https://traqocontainer.com/api/v1/sandbox/container/MRSU6859427?sealine=MAEUSandbox 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.
Authorization: Bearer YOUR_API_KEY
Base URL
All endpoints are relative to the following base URL:
https://traqocontainer.com/api/v1
/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"
}| Status | Meaning |
|---|---|
200 | Success |
400 | Bad request — check required parameters |
401 | Invalid or missing API key |
402 | Payment 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. |
403 | Developer mode not enabled — enable it from your dashboard settings |
404 | Resource not found |
429 | Rate 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. |
502 | Upstream 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.
/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:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed in the current window (your key's limit). |
X-RateLimit-Remaining | Requests left in the current window. |
X-RateLimit-Reset | Unix time (seconds) when the window resets and the count returns to the full limit. |
Retry-After | On a 429 only — how many seconds to wait before retrying. |
X-Traqo-Refresh-Hint | On 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 }
}/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). /api/v1/container/:numberTrack 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.
403.Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
number | string | Yes | Container number (e.g. MSCU1234567) |
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
sealine | string | Yes | 4-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. 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
/api/v1/bl/:numberTrack 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.403.Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
number | string | Yes | Bill 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
| Parameter | Type | Required | Description |
|---|---|---|---|
sealine | string | Yes | 4-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"
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
/api/v1/trackBulk 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.
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).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
| Field | Type | Required | Description |
|---|---|---|---|
shipments | array | Yes | 1–50 items. |
shipments[].type | string | Yes | container or bl. |
shipments[].number | string | Yes | Container 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[].sealine | string | Yes | 4-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
/api/v1/shipmentsList 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.
403.Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
page | integer | No | Page number, default 1 |
pageSize | integer | No | Results per page, default 20, max 100 |
updated_since | string | No | ISO 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 }. |
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.
/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.
404.Example request
curl https://traqo.io/api/v1/shipments/MSCU1234567 \ -H "Authorization: Bearer YOUR_API_KEY"
Response
When predictive ETA is enabled for your plan, the data object also carries predictive_eta and demurrage_risk — see Predictive ETA.
/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.
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"
}/api/v1/vessel/trackTrack 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.
403.Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
imo | string | Yes | 7-digit IMO number |
mmsi | string | Yes | 9-digit MMSI number |
curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://traqocontainer.com/api/v1/vessel/track?imo=9811000&mmsi=636022327"
Response
/api/v1/voyage/schedulesVoyage schedules
Returns sailing schedules between two ports for a given date, across available carriers.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
origin | string | Yes | Origin port UN/LOCODE (e.g. CNSHA) |
destination | string | Yes | Destination port UN/LOCODE (e.g. NLRTM) |
date | string | Yes | Date in YYYY-MM-DD format |
week_range | integer | No | Number of weeks to search, default 1 |
date_type | string | No | "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.
/api/v1/ports/:locode/congestionPort 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.
/api/v1/ports/congestionCongestion 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.
/api/v1/portsSearch 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.
?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
| Parameter | Type | Required | Description |
|---|---|---|---|
search | string | Yes | A 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
/api/v1/carriers/lookupResolve 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.
null carrier is a real answer, not an error: track without a sealine and upstream auto-detect takes over.Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
number | string | Yes | Container or bill of lading number. |
type | string | No | container 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.
| Confidence | Sources | What it means |
|---|---|---|
high | history, spec, bl-sealines | This reference was identified — it has tracked under this carrier before, or the lessor/issuing line names it directly. |
medium | bic, bl-prefix | Inferred from who owns the prefix. Right far more often than not, but it is about the box, not this journey. |
low | prefix-popularity | Extrapolated 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 get | It means | Do 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. |
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
carrierisnullif and only ifcandidatesis empty. There is no confidence floor — alow-confidence winner is still returned ascarrier. If you only want strong answers, filter onconfidenceyourself; don’t assume we did.candidatesis always an array, nevernulland 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.
| Header | Meaning |
|---|---|
X-Lookup-Limit | Lookups allowed per day on this key. |
X-Lookup-Remaining | Lookups left today. |
X-Lookup-Reset | Unix time (seconds) when the daily window resets. |
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
/api/v1/carriersList 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.
?search= (≥2 chars) to filter by SCAC, name or slug.Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
search | string | No | Case-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
→ your endpoint URLWebhooks
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:
{
"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
| Header | Description |
|---|---|
X-Traqo-Event | The event name (also in the body), so you can route without parsing. |
X-Traqo-Delivery | Unique id for this delivery attempt's delivery record — use it to dedupe (deliveries are at-least-once). |
X-Traqo-Signature | HMAC signature of the body — see Verifying signatures. |
User-Agent | Traqo-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.
2xx never reached us). Make your handler idempotent by de-duplicating on X-Traqo-Delivery.Webhook events
Subscribe an endpoint to any of these events, or to * for all of them.
| Event | Fires when |
|---|---|
shipment.updated | A tracked shipment's status changes (any transition other than arrival). |
shipment.arrived | A tracked shipment's status becomes delivered / completed. |
eta.changed | The carrier ETA for a tracked shipment changes. |
discovery.recovered | Carrier 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:
{
"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.
JSON.parse. Re-serializing the parsed object can reorder keys or change whitespace and the signature won't match.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.
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.
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.
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.