On this page
SeaRates-compatible API
A compatibility layer over the same core as the v1 API below. It accepts SeaRates' request shape and answers in their response shape, so a client already written against them moves across without being rewritten.
It is a migration path, not a second product: it covers tracking only, and a new integration should use v1.
All endpoints
Every v1 path, relative to the base URL, with a Bearer key — except the sandbox, which needs none.
| Endpoint | What it does |
|---|---|
GET/container/:number | Full tracking for a container number. Saves the shipment (uses a slot if new). |
GET/bl/:number | Full tracking for a bill of lading. Saves the shipment (uses a slot if new). |
POST/track | Track up to 50 references in one request. |
GET/shipments | Page through everything you track, or poll deltas with updated_since. |
GET/shipments/:id | Read one shipment in your account, a teammate’s included — no re-track, no slot. Add ?include=full for the stored document. |
POST/shipments/retry-failed | Re-attempt everything that failed to track in the last 7 days, in one call. No slot, and the re-add limit does not apply. |
PATCH/shipments/:id | Fix a wrong carrier or type on a shipment your own user added, or set its customer by name. No slot. |
DELETE/shipments/:id | Stop tracking a shipment your own user added and release its slot. |
POST/shipments/:id/share | Create a public, sanitised tracking page for a customer. |
DELETE/shipments/:id/share | Revoke that page, so the link stops resolving. |
GET/account/usage | Your plan, quota, slots remaining and billing cycle. |
GET/vessel/track | Live AIS position, speed and heading for a vessel by IMO or MMSI. |
GET/voyage/schedules | Sailing schedules between two ports. |
GET/ports/:locode/congestion | Congestion score, 90-day history and the anchorage wait for one port. |
GET/ports/congestion | The current reading for every scored port, in one call. |
GET/ports | Resolve a LOCODE, port name or city to canonical port metadata. |
GET/disruptions | Strikes, closures, canal delays and weather, filtered by severity, port, carrier or country. |
GET/disruptions/:id | One published disruption by its id. |
GET/shipments/:id/disruptions | The disruptions touching one shipment in your account, and how each one reaches it. |
GET/carriers/lookup | Work out which carrier owns a reference — free, and creates no shipment. |
GET/carriers | Every supported shipping line and its SCAC. |
/api/v1/sandbox/…Try it — live sandbox
Run a real request now — no key, no sign-up. Every v1 endpoint has a copy under /api/v1/sandbox with a schema-identical reply, so what you build here works live. Every endpoint on this page is in the box, grouped as in the sidebar.
Schema, not contents. Keys, types and error bodies match live; values are a sample, so a name, a region or a row count can differ. Having no account, it cannot show two things: a plan refusal on congestion or disruptions (always 200 here) and a re-minted share link (201, never 200 with created: false).
"mode": "sandbox" — except ?include=full, which says "stored", like live.What you get
| Endpoint | In the sandbox |
|---|---|
/container/:number, /bl/:number | Any reference resolves — real ports, a plausible route, a full milestone timeline, equipment fields. |
POST /track | Per-item results. Mix a valid reference with a magic one to see how a partial failure reports. |
/shipments | A stable set of 8 — in transit, delayed, delivered, one failed. Not delta mode: updated_since is ignored here and every row comes back. Build that branch against a live key. |
/shipments/:id and every write on it — PATCH, DELETE, /share, POST /shipments/retry-failed | Any of the eight: SHP-1000 to SHP-1007, or their SBXU… numbers. A write answers the real shape and changes nothing; the four on one shipment say so with persisted: false. |
/ports/congestion, /ports/:locode/congestion | A fixed set of scored ports, with vessels_waiting and the two wait figures. |
/disruptions, /disruptions/:id, /shipments/:id/disruptions | One published event, 4812, with the filters applied; SHP-1000 is the shipment it touches. |
/account/usage, /carriers, /carriers/lookup, /ports, /vessel/track, /voyage/schedules | All mirrored, same shapes. |
Capped at 60 requests a minute per IP, with the same X-RateLimit-* headers as live — so you can test your back-off here.
Magic references — build your error handling first
Errors are the hard part to test — a 402 only happens when a customer outgrows their plan. These reserved references return each error with the real production body.
| Reference | Status | What it lets you build |
|---|---|---|
SBXU0000003 | 404 | The carrier has no record of this reference. |
NOT-A-REFERENCE | 400 | The reference is malformed — rejected before the carrier is asked. |
SBXU4020008 | 402 | Every shipment slot on the plan is used. The branch that fires the first time a customer outgrows their plan. |
SBXU4029999 | 402 | Payment failed and the grace period has ended — the account is soft-locked. |
SBXU4290003 | 429 | Too many requests. Carries Retry-After and the reset time. |
SBXU5020002 | 502 | The carrier’s own system failed or timed out. Retryable. |
They work on /container, /bl and per item inside POST /track.
Receive a real webhook, before you have an account
A webhook receiver normally cannot be tested until a real shipment moves. Name a URL and get one delivery now:
curl -X POST "https://traqocontainer.com/api/v1/sandbox/webhooks/fire" \
-H "Content-Type: application/json" \
-d '{ "url": "https://your-server.example.com/hooks", "event": "eta.changed" }'Signed by the same code production uses, with a one-time secret in the reply so you can check it. Nothing is saved. 5 a minute; the target must be a public https URL.
Webhook event payloads
Every delivery has the same envelope — { event, created, data } — so branch on event, not on the shape:
| Event | Fires when |
|---|---|
shipment.updated | A tracking milestone landed and the shipment’s status changed. |
shipment.vessel_arrived | The vessel arrived at the final port of discharge. ata carries the date; status usually still reads IN_TRANSIT. |
shipment.arrived | The status became delivered / completed — the end of the journey, after the vessel’s arrival. |
eta.changed | The carrier revised the ETA. previous_eta lets you compute the delta without storing it yourself. |
discovery.recovered | A shipment that failed to track was recovered under the correct carrier — a dead row became live. |
webhook.test | Sent only by “send a test event”, never by a subscription. Same shape as the rest. |
{
"event": "eta.changed",
"created": 1787654321,
"data": {
"shipment_id": "SHP-1042",
"reference_number": "MRSU6859427",
"carrier": "MAEU",
"status": "IN_TRANSIT",
"previous_status": "IN_TRANSIT",
"eta": "2026-09-13T00:00:00.000Z",
"previous_eta": "2026-09-08T00:00:00.000Z",
"ata": null
}
}JSON.parse — re-serializing can reorder keys and break the signature. See Verifying signatures. curl "https://traqocontainer.com/api/v1/sandbox/container/SBXU4020008"
Clear the SCAC and send — the carrier is resolved for you.
https://traqocontainer.com/api/v1/sandbox/container/MRSU6859427?sealine=MAEUSandbox responses are demo data, generated deterministically. Create a free account and enable developer mode to track real shipments with a live key.
Authentication
Send your API key as a Bearer token in the Authorization header. Generate and manage keys in the Developer section of your dashboard.
What every request needs
| Requirement | Where | Missing it returns |
|---|---|---|
| API key | Authorization: Bearer <key> | 401 |
| Developer mode | Developer tab, once per account | 403 |
Both apply to every endpoint below, so they are not repeated on each one.
Authorization: Bearer YOUR_API_KEY
Base URL
All endpoints are relative to the following base URL:
https://traqocontainer.com/api/v1
Postman & OpenAPI
Two ready-made files, both public — no key needed.
Postman collection
Every endpoint, pre-built. Import, set the apiKey variable, run. Generated from the OpenAPI document below, so it never drifts.
Download the collection — or in Postman: Import → Link, and paste the URL above.
OpenAPI 3.1 specification
Feed it to openapi-generator, orval, Swagger UI or Redoc for a typed client. Covers the webhook payloads too.
Open the spec — or point your generator straight at the URL so a regenerated client tracks our changes.
https://traqocontainer.com/api/v1/postman.json
https://traqocontainer.com/api/v1/openapi.json
Errors
All error responses return JSON with a consistent structure:
| Status | Meaning |
|---|---|
200 | Success |
400 | Bad request — check required parameters |
401 | Invalid or missing API key |
402 | Payment required: data.error is "shipment_limit_reached" or "payment_overdue" — branch on it. Sends Retry-After, but stop retrying and fix billing; the call cannot succeed. |
403 | Developer mode not enabled — enable it from your dashboard settings |
404 | Resource not found — on /container/{number} and /bl/{number}, not always proof the reference is wrong. The body carries tried (carriers walked, (auto) for the un-hinted attempt) and discovery: "queued" means the answer will appear on GET /shipments/{id}, "not_eligible" means no job was created. Never write a reference off while queued. When no job was created, discovery_reason says why — "plan", "disabled", "invalid_reference" or "no_candidates". Only "plan" adds an upgrade object (min_plan, a message and a url) and a hint: carrier auto-discovery runs from Starter, and naming the carrier yourself with ?sealine= works on every plan for nothing. A paid account never receives upgrade. |
429 | Rate limit exceeded (per API key). Wait the Retry-After seconds; X-RateLimit-Limit / -Remaining / -Reset ride every response. The body names plan — your organisation's plan, which is what the limit is computed from — and either upgrade (next_plan, next_limit_per_min, url) or, when no plan above yours buys a bigger minute, contactUrl. Exactly one of the two. See API rate limits. |
502 | Upstream tracking API error — retry after a moment |
A 402 carries a data object. data.error of "payment_overdue" means a payment failed and the grace period is over — fix billing at data.manageUrl to start again. Shipments you already have are unaffected:
{
"statusCode": 401,
"statusMessage": "Invalid or missing API key"
}{
"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"
}
}{
"statusCode": 404,
"statusMessage": "Bill of Lading not found",
"data": {
"error": "not_found",
"tried": ["CMDU"],
"discovery": "not_eligible",
"discovery_reason": "plan",
"upgrade": {
"feature": "carrier_auto_discovery",
"min_plan": "starter",
"message": "We tried CMDU. On Starter and above we keep trying the other carriers automatically and the result appears on GET /api/v1/shipments/{id}.",
"url": "https://traqocontainer.com/dashboard/billing?internal_utm=api_discovery"
},
"hint": "Send the carrier SCAC (?sealine=) when you know it.",
"read_endpoint": "/api/v1/shipments/{id}"
}
}Shipment limits
Two separate limits: how many requests a key may make (API rate limits), and how many shipments your plan allows.
Your shipment limit is an intake allowance, not a ceiling on how many you hold at once: it counts what you add this cycle. Older shipments keep tracking free until delivered.
Calling /container or /bl with… | Costs |
|---|---|
| A reference already in your account | Nothing — re-fetching what you track is always free |
| A new reference, allowance remaining | 1 slot, counted once |
| A new reference, allowance exhausted | 402 |
DELETE stops tracking, not billing. One exception: deleted inside the grace window and never added again, a few times per cycle./vessel/track, /voyage/schedules and /carriers/lookup never consume slots — they are lookups.API rate limits
Each API key gets 120 requests per minute by default, counted in a fixed one-minute window. Your key's own limit is in the headers below.
| Plan | Requests / minute |
|---|---|
| business | 240 |
| custom | 600 |
| free | 30 |
| professional | 120 |
| starter | 60 |
Vessel & schedule lookups
GET /vessel/track and GET /voyage/schedules reach an external provider, so they share a daily allowance per key, on top of the per-minute limit. A repeat lookup served from cache does not count against it.
| Plan | Lookups / day |
|---|---|
| free | 100 |
| starter | 500 |
| professional | 1,500 |
| business | 5,000 |
| custom | 10,000 |
Over it you get a 429 carrying lookup_limit_exceeded, with X-Lookup-Limit, X-Lookup-Remaining and X-Lookup-Reset — its own header family, because it is its own budget and backing off for a minute would be the wrong wait.
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 /container and /bl, a reminder they re-fetch and are slow. Poll GET /shipments/:id instead (stored, no slot), or ?updated_since=, or webhooks. |
Go over and you get a 429 with a Retry-After. Wait that many seconds — trying sooner just costs you another 429:
/api/v1/shipments in a tight loop — you'll hit the limit fast. Use delta mode or webhooks. {
"success": false,
"statusCode": 429,
"message": "Rate limit exceeded — 120 requests per minute. Retry after 42s.",
"data": { "error": "rate_limit_exceeded", "limit": 120, "retryAfter": 42 }
}Response times
Three classes of endpoint, and they are nothing like each other. Budget for the right one:
| Class | Endpoints | Typical | Slow (p95) |
|---|---|---|---|
| Stored | /shipments, /shipments/:id, /ports, /carriers | 214 ms | 778 ms |
| Live | /container/{number}, /bl/{number} | 727 ms | 5.8 s |
| External | /vessel/track, /voyage/schedules | 248 ms | 7.6 s |
Measured from real successful requests over the last 30 days, not a target.
GET /shipments/:id. External means someone we call for you, not a carrier tracking a box; /voyage/schedules is the slowest endpoint on the API. Both save good answers — "cached": true — for 5 minutes (vessel position) and 30 minutes (sailing schedule).
Try any of them without a key — the live sandbox above runs real requests against real references.
Sync cadence & scheduling
There is no fixed sync time. Nothing runs on a schedule you could align a job to. Each shipment re-syncs on its own rolling window, and every tracking response says when this one can change.
The gap is 12 hours, and 4 hours while a shipment is arriving — from 48 hours before its ETA until 7 days after it, which is the window in which a carrier actually revises a date. You do not have to work out which applies: next_refresh_eligible_at is already computed with the right one.
| Field | Meaning | Null when |
|---|---|---|
last_synced_at | When the carrier data behind this response was last refreshed. Everything else in the payload is as of this moment. | Never synced. |
next_refresh_eligible_at | last_synced_at + the gap that applies to this shipment — the earliest it can change. | Never synced (eligible now), or no longer active. |
refresh_eligible_now | Boolean shortcut: the gap has elapsed and the shipment is still moving. | Never null. |
next_refresh_eligible_at is the earliest time, not an appointment. Before it, we do not ask the carrier. After it, you only get newer data if something asks for it — opening the shipment does; background refresh is per account and off by default. GET /container/:number and GET /bl/:number always ask, and spend a slot. The cheap way to stay in sync is not to poll each shipment: GET /shipments?updated_since=<last successful sync> returns only what moved, so 2,000 shipments cost one request on a quiet day. Or use webhooks.
GET /shipments/:id never triggers a refresh and never spends a slot, so a scheduled job can read it as often as it needs.
Cargo delivery is not sync time
Live /container, /bl and successful /track items keep the existing nullable closed_at field. Its value is the newest verified actual delivery to the consignee (CDC, or Frappe atdc). It is never port arrival, carrier closure/synchronization time, an estimate, or empty-container return. A terminal shipment without that proof returns closed_at: null rather than a guessed date.
const shipment = response.data
const deliveredAt = shipment.closed_at ?? null
if (deliveredAt) {
recordVerifiedDelivery(deliveredAt)
} else {
// The journey may be terminal, but its cargo-delivery date is not verified.
markDeliveryDateUnavailable()
} The saved GET /shipments and GET /shipments/:id summaries do not publish a delivery-date scalar; their response shape is unchanged.
/api/v1/container/:numberTrack a container
Full tracking data for a container — status, route, ETA, port events, vessels. The shipment is saved to your account in the background.
Parameters
| Parameter | In | Required | Description |
|---|---|---|---|
number | path | Yes | Container number, e.g. MSCU1234567 |
sealine | query | No | 4-character SCAC, e.g. MSCU. Recommended — see below |
Sending sealine
| Cost | Accuracy | |
|---|---|---|
| You send it | 1 upstream attempt | Exact |
| You omit it | Up to 3 attempts | Resolved from tracking history, the lessor's records and prefix ownership — can still be wrong |
The carrier actually used comes back as carrier.sealine. Don't know it? /carriers/lookup resolves it free, with no shipment created.
Send a code we don't list and we ignore it and detect the carrier ourselves — you get the shipment, and carrier.sealine tells you who answered. A missing carrier is a gap in our directory, not a mistake in your request, so it never costs you a call. A code that isn't four characters is still rejected: that one is a malformed request.
Container equipment
Every containers_table row has these three keys. They are always there and may be null — null means "the carrier did not supply it", never "no equipment".
| Field | Example | Notes |
|---|---|---|
iso_code | 45G1 | Only ever a valid ISO 6346 code. Shorthand like 40HQ is reported as null here |
size_type | 40HC | Where that shorthand appears instead |
container_description | 40ft High Cube | Readable form, for display |
container_summary (2×40HC, 1×20GP) counts only boxes whose type we know, so it can add up to fewer than the table holds.
No "return to port by" date. Carriers publish free days, not a deadline, so no field carries one. The dashboard's detention & demurrage estimate is not exposed on this API, and is not a date to invoice against.
curl "https://traqocontainer.com/api/v1/container/MRSU6859427?sealine=MAEU" \ -H "Authorization: Bearer YOUR_API_KEY"
/api/v1/bl/:numberTrack a bill of lading
Full tracking data for a BL. Same response shape as /container; saved to your account in the background.
Parameters
| Parameter | In | Required | Description |
|---|---|---|---|
number | path | Yes | BL number, 3–50 chars, as the carrier printed it. Digits, hyphens, slashes and dots all accepted (URL-encode / as %2F); whitespace trimmed |
sealine | query | No | 4-character SCAC, e.g. CMDU. Recommended: a BL embeds no carrier code, so omitting it costs up to 3 attempts and can still be wrong. Same trade-off as /container |
Container equipment fields behave exactly as on /container.
curl "https://traqocontainer.com/api/v1/bl/SHZ8037930?sealine=CMDU" \ -H "Authorization: Bearer YOUR_API_KEY"
/api/v1/trackBulk track shipments
Up to 50 containers or BLs per request. New shipments cost a slot each; ones you already track are free.
200 even when items fail — check each results[].ok. Only auth (401/403), rate limit (429), a bad body (400) or unpaid billing (402) reject the whole request.sealine is optional per item — leave it out and we work the carrier out from the reference, free. Unlike the single-reference endpoints, this one does not then try carriers one paid call at a time (a 50-item request could become 150 lookups), so a reference we cannot place is that item's error. Look codes up first with /carriers/lookup.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, separators fine). |
shipments[].sealine | string | No | 4-character SCAC. Omit it and we resolve locally; send it and we skip the lookup. |
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":"SHZ8037930"}]}'/api/v1/shipmentsList tracked shipments
A paginated list of every shipment on your account — containers and bills of lading — with current status, route and ETA.
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 — only shipments whose last_synced_at is at or after this time. Supersedes pagination, capped at 200; the response becomes { success, updated_since, count, data }. |
last_synced_at. Save the newest you see and pass it back as updated_since to fetch just what changed.curl "https://traqocontainer.com/api/v1/shipments?page=1&pageSize=20" \ -H "Authorization: Bearer YOUR_API_KEY"
/api/v1/shipments/:idGet a shipment
Where a shipment you already track stands right now, from stored data. Never calls the carrier, never uses a slot — poll this one.
:id accepts | Example | Stability |
|---|---|---|
| Container or BL number | MSCU1234567 | Preferred — never changes |
| Shipment id | SHP-1042 | Regenerated on every re-poll, so a stored one can stop resolving |
Case, spaces and hyphens are ignored.
ata is when the vessel arrived at the final port of discharge — null until it has, and never a transshipment call. status is the carrier's status for the whole journey and still reads IN_TRANSIT after arrival. Same field on /shipments, /container and /bl.
customers: names, [] if none. Also on /shipments; set via PATCH.
404.Re-reading the stored document
Add ?include=full for a document of the tracking tables we hold (events_table, containers_table, vessels_table, locations_table, voyage_plan_table, route_json, last position), plus cached_at and mode: "stored".
/container/:number or /bl/:number only when you want fresh data.Nothing cached yet? You get document: null, cached_at: null and a hint. One /container or /bl call populates it; every later read is local.
When predictive ETA is enabled for your plan, the data object also carries predictive_eta and demurrage_risk — see Predictive ETA. Always carries terminal.
curl "https://traqocontainer.com/api/v1/shipments/MSCU1234567?include=full" \ -H "Authorization: Bearer YOUR_API_KEY"
Predictive ETA
With predictive ETA on, /api/v1/container/:number adds a predictive_eta object: p50 and p80 times, a source (model / blend / carrier), a confidence (high / medium / low) and computed_at. It uses vessel progress and port congestion, so it beats an old carrier ETA.
Terminal record
data.terminal on Get a shipment.
- Same data as the Terminal card
- One item per box and terminal
"found": all facts and events"finding": still checking, no factsnull: nothing for this shipment- Terminal's own keys, snake_case
- Blank hold: absent, not released
- Over 24h: current facts dropped
- Read error:
terminal: null
curl "https://traqocontainer.com/api/v1/shipments/MSCU7751000" \ -H "Authorization: Bearer YOUR_API_KEY"
/api/v1/shipments/retry-failedRetry failed shipments
Retry every shipment that failed in the last 7 days, in one call. Nothing is fetched while you wait — read GET /api/v1/shipments/:id for the result.
Body (optional)
Send nothing to retry everything. Send { "shipment_ids": ["SHP-00000044"] } to retry only those — ids outside your organisation are ignored.
Response
examined is how many failed rows matched; queued is how many jobs started. They differ when your plan has no carrier discovery, and the gap shows as skipped.
Capped at 200 references per call, and one call per organisation every 15 minutes. The work is asynchronous, so calling it again sooner adds nothing.
curl -X POST "https://traqocontainer.com/api/v1/shipments/retry-failed" \ -H "Authorization: Bearer YOUR_API_KEY"
/api/v1/shipments/:idCorrect a shipment
Fixes the carrier or type of a shipment you already track, then tracks it again, and sets its customer. Every field is optional, at least one is needed, and anything you leave out keeps its stored value.
Only for shipments this key's own user added: a teammate's shipment reads 200 on GET and answers 404 here.
The same as editing a shipment in the dashboard: it moves a reference you already track to another carrier, which ?sealine= on a first add cannot do.
Body
| Field | Notes |
|---|---|
carrier | The SCAC you know is correct, e.g. MAEU |
type | container or bl — for a number filed as the wrong kind |
customer | A name; null clears it |
reference_number: refused, 400.
The customer
{"customer": "Acme GmbH"}sets it- Matches name, any case
- No match: creates the customer
- Replaces, never adds
nullor""clears it- Alone or with
carrier - Reads return
customers(names)
- The call sends nothing
- Contact + alerts on: mailed as usual
- API-created customer: no contact, no email
- View-only key:
403 - Plan without customers:
403 - Customer limit reached:
422
When the carrier still finds nothing
Not an error: 200 with still_failed: true means the row was saved and we are still looking for the carrier — poll GET /shipments/:id. Sending the same values back answers unchanged: true.
curl -X PATCH "https://traqocontainer.com/api/v1/shipments/MSCU1234567" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"carrier":"MAEU"}'/api/v1/shipments/:idUntrack a shipment
Removes a shipment from your account, like removing it from the dashboard. A soft delete: it leaves /api/v1/shipments and stops updating.
Only for shipments this key's own user added: a teammate's shipment reads 200 on GET and answers 404 here.
:id is the container or BL number, or the shipment id — which to prefer, and why.
Response
shipment_id is the real id of the row you untracked — not what you sent, if you used a container or BL number.
curl -X DELETE "https://traqocontainer.com/api/v1/shipments/MSCU1234567" \ -H "Authorization: Bearer YOUR_API_KEY"
/api/v1/account/usageYour quota and billing cycle
Check this instead of finding your limit by hitting it — so you can warn your team, or buy slots, before tracking stops.
402 uses, so they always agree.Notes
usedcounts distinct references tracked this cycle. A reference you later untrack still counts — deleting does not return the slot.- On a yearly plan the
cycleis the year and the shipment numbers are pool numbers for that year. Readperiod_days(30 vs 365) to tell which shape you are holding rather than inferring it from the dates. rate_limitis the per-minute window and mirrors theX-RateLimit-*headers on the same response. There is no daily API quota, so none is reported.- Both
402bodies carryusageApi: "/api/v1/account/usage", so a client that does hit the wall is pointed straight at the endpoint that would have prevented it.
curl "https://traqocontainer.com/api/v1/account/usage" \ -H "Authorization: Bearer YOUR_API_KEY"
/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.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
imo | string | Yes | 7-digit IMO number |
mmsi | string | Yes | 9-digit MMSI number |
curl "https://traqocontainer.com/api/v1/vessel/track?imo=9811000&mmsi=636022327" \ -H "Authorization: Bearer YOUR_API_KEY"
/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 "https://traqocontainer.com/api/v1/voyage/schedules?origin=INMUN&destination=AEJEA&date=2026-10-24&week_range=2&date_type=departure" \ -H "Authorization: Bearer YOUR_API_KEY"
/api/v1/ports/:locode/congestionPort congestion
The latest congestion score for a port (UN/LOCODE) plus 90 days of history: a 0–100 score, a bucket (fluid / normal / moderate / high / critical), a data-sufficiency tier (A/B/C), a 7-day trend, and per-signal components (dwell, anchorage wait, ETA-slip, schedule deviation, bunching) with raw value, baseline and z-score.
Two fields answer the question people actually ask without reading components: median_anchorage_wait_h, the hours a ship waits at anchor for a berth, and longest_wait_days, how long the longest-waiting vessel has been there. Either is null — never 0 — when we hold no reading.
403 with data.error: "feature_disabled". Once on, your organisation's plan must meet the minimum — a teammate counts as the plan the organisation pays for — and a 403 says plan_required and names it. The sandbox always answers 200.Reads Traqo's own analytics — no upstream call, so it never consumes a shipment slot.
curl "https://traqocontainer.com/api/v1/ports/INMUN/congestion" \ -H "Authorization: Bearer YOUR_API_KEY"
/api/v1/ports/congestionCongestion board
Every scored port in one call — the same score / bucket / tier, plus trend_7d, vessels_waiting (ships waiting at the port right now), computed_at and coordinates. Build a map or watchlist without polling port by port.
Readings are not all equally fresh — about half the board is recomputed inside any 24 hours — so check computed_at before treating a port as current.
calls_30d is deprecated. It carries the same value as vessels_waiting and is kept for this release only. It was never a 30-day call count: read vessels_waiting.Same gating as Port congestion, and 200 in the sandbox either way. Reads Traqo's own analytics — no upstream call, no shipment slot.
curl "https://traqocontainer.com/api/v1/ports/congestion" \ -H "Authorization: Bearer YOUR_API_KEY"
/api/v1/portsSearch ports
Resolves a UN/LOCODE, port name or city to canonical metadata (locode, name, city, country, region, coordinates) — how free-text origin/destination becomes the locodes congestion and schedules expect. Local read.
?search= needs 2+ characters, else 400. Capped at 25 results — 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) |
curl "https://traqocontainer.com/api/v1/ports?search=rotterdam" \ -H "Authorization: Bearer YOUR_API_KEY"
/api/v1/disruptionsList disruptions
Published port and trade disruptions — strikes, port closures, canal and chokepoint delays, weather, surcharges — most severe first. Active events by default; status=resolved lists the ones that are over. Each has a severity from 1 (info) to 4 (critical), the entities it names (ports, carriers, countries, chokepoints), and the publishers that reported it.
Every event is classified before it is published, and anything at severity 3 or above is verified first. The headline and summary are ours; sources are the publishers' names and links, never their text.
affected: how many of your shipments it touches, and up to five of their references. On Starter the key is absent — not zero — because "none of yours" and "not on your plan" are different answers.Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
severity | integer | No | Minimum severity, 1–4 (default 1). 3 returns 3 and 4. |
category | string | No | One of port_disruption, chokepoint, carrier_ops, tariff_trade, regulatory, weather, security_geopolitical, container_market, labor |
port | string | No | UN/LOCODE, e.g. SGSIN |
carrier | string | No | 4-letter SCAC, e.g. MAEU |
country | string | No | ISO alpha-2, e.g. SG |
status | string | No | active (default) or resolved |
since | date | No | Only events reported on at or after this time. Poll with the time of your last call. |
page / page_size | integer | No | From 1; page_size 1–100, default 25 |
Filters combine with AND. A bad value gets 400 with data.error: "invalid_parameter" and data.param naming it — checked before your plan, so a typo is never mistaken for a subscription problem.
403 with plan_required — never a partial list, which a program would read as the whole picture. While the feature is switched off for the deployment it is 403 with feature_disabled. The sandbox always answers 200.Reads Traqo's own data — no upstream call, no shipment slot.
curl "https://traqocontainer.com/api/v1/disruptions?severity=3&port=SGSIN" \ -H "Authorization: Bearer YOUR_API_KEY"
/api/v1/disruptions/:idGet a disruption
One published disruption, by the id from List disruptions, in the same shape. Useful for re-checking one event you are already showing — whether it has resolved, or picked up new sources.
404 with disruption_not_found, exactly like an id that never existed. Same plan rule as the list.curl "https://traqocontainer.com/api/v1/disruptions/4812" \ -H "Authorization: Bearer YOUR_API_KEY"
/api/v1/shipments/:id/disruptionsDisruptions on a shipment
The active disruptions touching one shipment in your account — up to ten, most severe first — and how each reaches it. match_types says why: pol / pod (its port of loading or discharge), transship, carrier, lane_interest or country.
:id is the shipment id or the container / bill-of-lading number, as on Get a shipment.
403 with plan_required. A shipment your user cannot see in the dashboard — another account's, or outside a teammate's limits — is 404, never 403.curl "https://traqocontainer.com/api/v1/shipments/MSCU1234567/disruptions" \ -H "Authorization: Bearer YOUR_API_KEY"
/api/v1/carriers/lookupResolve the carrier for a reference
Which carrier moves a container or BL, without tracking it — no shipment, no write, no slot.
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
Some sources are stronger than others, so every candidate carries a confidence — the difference between a value you can act on and one to check first.
| 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 but need opposite handling. sources_unavailable lists sources that failed or took longer than 3 seconds — empty is healthy.
| 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 200 here does not prove every source was asked — check the list.Two rules worth coding against
carrierisnulliffcandidatesis empty. No confidence floor — alowwinner is still returned. Want only strong answers? Filter onconfidenceyourself.candidatesis always an array — nevernull, never absent, possibly empty.
Limits
A daily cap per key (default 1,000), separate from the per-minute limit, because each new reference triggers several lookups.
| 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, nothing off the cap, no X-Lookup-* headers. The cap is for thousands of different references, not re-checking your own boxes.Over the cap you get a 429 saying when it lifts. Use the error code to tell it from the per-minute limit, which says rate_limit_exceeded and clears within the minute:
curl "https://traqocontainer.com/api/v1/carriers/lookup?number=MRKU8636841" \ -H "Authorization: Bearer YOUR_API_KEY"
/api/v1/carriersList supported carriers
Every ocean carrier Traqo can track, with its 4-character scac and name — the value you pass as the optional sealine. Local read, never spends a slot.
?search= (2+ chars) filters 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). |
curl "https://traqocontainer.com/api/v1/carriers?search=maersk" \ -H "Authorization: Bearer YOUR_API_KEY"
→ your endpoint URLWebhooks
Instead of polling, we POST events to your server as they happen — a status or ETA change, or carrier discovery recovering a failed shipment. Register endpoints on the Webhooks tab.
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 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
Reply 2xx within 10 seconds — say yes first, do the work after. Anything else is retried with a growing wait (≈30s, 1m, 2m, 4m … up to 6h) until we give up and mark it failed.
X-Traqo-Delivery.{
"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",
"ata": null
}
}Webhook events
Subscribe an endpoint to any of these events, or to * for all of them.
| Event | Fires when |
|---|---|
shipment.updated | A tracking milestone landed and the shipment’s status changed. |
shipment.vessel_arrived | The vessel arrived at the final port of discharge. ata carries the date; status usually still reads IN_TRANSIT. |
shipment.arrived | The status became delivered / completed — the end of the journey, after the vessel’s arrival. |
eta.changed | The carrier revised the ETA. previous_eta lets you compute the delta without storing it yourself. |
discovery.recovered | A shipment that failed to track was recovered under the correct carrier — a dead row became live. |
The four shipment events share the data shape shown in the envelope above (status/previous_status carry the transition; eta/previous_eta the ETA move; ata the vessel's arrival at the final port, null until then). discovery.recovered carries the recovered carrier:
status stays IN_TRANSIT after the vessel arrives — it is the carrier's status for the whole journey. Read ata, or subscribe to shipment.vessel_arrived; shipment.arrived fires later, on delivered.{
"reference_number": "MSCU1234567",
"type": "Container",
"carrier": "MSCU",
"carrier_name": "MSC",
"shipment_id": "SHP-000481"
}Verifying signatures
Every delivery is signed, so you can be sure it came from Traqo and nobody changed it. The X-Traqo-Signature header holds a timestamp and a signature:
v1 is the HMAC-SHA256, lowercase hex, of <t>.<raw body>, keyed with your signing secret (whsec_…, shown once). Recompute, compare in constant time, and reject any t over 300s old.
JSON.parse. Re-serializing can reorder keys or change whitespace and break the match.X-Traqo-Signature: t=1754170000,v1=5f3b1a…c9d2
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(String(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) …
})MCP server
Traqo Ocean provides a remote MCP server at https://traqocontainer.com/api/mcp; authenticate with your Traqo API key. Point Claude, ChatGPT, Cursor or any Model Context Protocol client at it and ask about your shipments in plain language — the model reads your live data instead of guessing.
It is read-only. Nothing here adds, edits, deletes or refreshes anything, so a model cannot spend a slot, change a shipment or open a ticket. Same key, same developer-mode requirement and same per-minute rate limit as the REST API above — one key, one set of rules, whichever way you reach us.
Three limits worth knowing before you write the client. A batch holds at most 20 messages; a longer one is refused whole rather than answered in part. track_vessel and get_vessel_schedules reach the same provider as GET /vessel/track and GET /voyage/schedules, so they draw on the same daily allowance per key — one pool, whichever surface you call from. And tool output is additive only: a key may appear, never vanish or change name, and every change is listed in the changelog against a server version.
Before you start
- An API key, from Dashboard → Developer. The same one the REST API uses — nothing new is issued.
- Developer mode on for your account, on that same page. Without it every call answers
403. - A client that speaks MCP. Claude Desktop, Claude Code, Cursor and Windsurf all do; anything else can bridge with
mcp-remote.
Connect
Streamable HTTP, stateless — there is no install and nothing to run. Paste the block on the right into your client's config, replace YOUR_API_KEY, and restart the client: none of them re-read that file while running, which is the single most common reason a correct config appears to do nothing.
| Client | Where the config lives |
|---|---|
| Claude Desktop | ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) · %APPDATA%\Claude\ (Windows) |
| Claude Code | claude mcp add --transport http traqo-ocean https://traqocontainer.com/api/mcp --header "Authorization: Bearer YOUR_API_KEY" |
| Cursor | .cursor/mcp.json in the project, or ~/.cursor/mcp.json for every project |
| Anything else | The mcp-remote command on the right, as the client's command |
Then ask it something
Once it is connected, these are questions it can answer from your own account:
- “Which of my containers are late, and by how long?”
- “Where is MSKU1234567 and when does it land?”
- “Any of my shipments running out of free days this week?”
- “How bad is Rotterdam right now, and does it affect anything I have moving?”
- “Summarise everything arriving in the next seven days for my customer Acme.”
- “What sails from Shanghai to Hamburg in the next three weeks?”
The tools
21 tools, all read-only, every one scoped to your own organisation — no tool takes an account, organisation or user id, so a model cannot express a request for somebody else's data. Your client calls these for you; you never name one.
Your shipments
Everything you already track. Nothing here starts tracking anything new.
list_my_shipmentsfilterYour tracked shipments, with what is late and what is arriving.
get_shipment_statusreferenceWhere one container or bill of lading has got to.
get_shipment_timelinereferenceThe whole journey of one shipment: ports, vessels, every event so far.
get_refresh_schedulereferenceWhen your data last synced with the carrier, and when it is next due.
Documents
The paperwork filed against a shipment. Metadata and a download link — the file itself is not returned.
list_documentsreferenceBills of lading, invoices and packing lists on one shipment.
get_document_metaidOne document: filename, kind, size, version, who uploaded it, and a link.
Ports, vessels and disruption
The world outside your account. These read public and licensed data, not your own rows.
get_port_conditionsportHow congested a port is, which way it is heading, and why.
get_vessel_schedulesorigindestinationdateweeksSailings between two ports over the coming weeks.
track_vesselvesselWhere a ship is now, by name or IMO number.
get_route_newsreferenceminecategoryminSeverityStrikes, weather, closures and chokepoints — optionally only the ones touching your lanes.
get_disruption_detailidOne disruption in full, and which of your shipments it touches.
Reference
Questions about the product rather than about your data. These are the same sources the help centre reads.
check_carrier_supportcarrierWhether we track a shipping line — by name, alias or SCAC.
search_api_docsqueryThe REST API: every endpoint, parameter and response field.
search_helpqueryPublished FAQs and help articles.
Your account
Your own workspace. Scoped to your organisation, and there is no argument that names another.
list_customerssearchfilterThe customers you have assigned shipments to, and how each is doing.
get_teamno argumentsWho is on your workspace and what role each has.
get_dnd_statusreferenceFree days left on one shipment, and what an overrun is costing.
get_notification_prefsno argumentsHow you are currently alerted: severity, digest hour, muted categories.
get_ticket_statusticketNumberYour open support tickets and where each one stands.
get_quote_request_statusno argumentsYour freight-quote requests and the quotes that came back.
get_mis_reportstatusdelayStatusshipmentTypesealineqlimitYour MIS report as data: open, late, arriving, and where each box is.
When it does not work
| What you see | What it means | What to do |
|---|---|---|
404, or the client reports no server | The MCP server is not switched on for this deployment | Ask us to enable it. Nothing on your side is wrong |
401 | The key is missing, wrong, or revoked | Check the header is Authorization: Bearer YOUR_KEY, and that the key still exists in your dashboard |
403 | The key is valid but developer mode is off | Turn it on under Dashboard → Developer |
429 | Over your per-minute rate limit — shared with the REST API | Wait the seconds in Retry-After. A model looping over shipments one at a time is the usual cause; ask it for a list instead |
405 on a GET | Your client is probing for the SSE transport, which we do not offer | Nothing — clients fall back to POST. If yours does not, use mcp-remote |
| Connected, but no tools listed | Almost always a client that was not restarted after the config changed | Restart it. Then check by hand with the curl on the right |
What it will not do
No writes of any kind: no adding, editing, deleting, sharing or refreshing a shipment, and no raising a ticket. No billing — invoices, plans and usage are not on this surface. No staff data, whoever holds the key. And no carrier calls, so a model cannot spend your daily lookup allowance by asking repeatedly.
{
"mcpServers": {
"traqo-ocean": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://traqocontainer.com/api/mcp",
"--header", "Authorization: Bearer YOUR_API_KEY"]
}
}
}{
"mcpServers": {
"traqo-ocean": {
"url": "https://traqocontainer.com/api/mcp",
"headers": { "Authorization": "Bearer YOUR_API_KEY" }
}
}
}npx -y mcp-remote https://traqocontainer.com/api/mcp \ --header "Authorization: Bearer YOUR_API_KEY"
curl -X POST "https://traqocontainer.com/api/mcp" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "content-type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'curl -X POST "https://traqocontainer.com/api/mcp" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "content-type: application/json" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call",
"params":{"name":"get_shipment_status",
"arguments":{"reference":"MSKU1234567"}}}'What’s new
Newest first. Every change is additive except the one row marked breaking — ignore fields you don’t know and pick up the rest at your own pace.
| When | Change | What you do |
|---|---|---|
| Sep 2026 | MCP get_dnd_status now also returns detention: once a box has left the port, the empty's due-back date, days left or over, and an estimate when a detention rate is known. null while the box is still at the port | Nothing. The port-clock keys are unchanged; read detention when it is set |
| Sep 2026 | MCP 1.1.0 — get_route_news and get_disruption_detail now return sources (publisher, headline and link, up to four). A batch is capped at 20 messages, and track_vessel / get_vessel_schedules draw on the daily lookup allowance they always cost us | Nothing, unless you batch more than 20 — split it. serverInfo.version reports 1.1.0 |
| Aug 2026 | /shipments/:id now accepts the container or BL number, not just the internal shipment id. Case, spaces and hyphens ignored | Nothing. Either identifier resolves. DELETE returns the resolved id, so it won't echo a reference you sent |
| Aug 2026 breaking | Removed shipment_public_url from every response — the carrier's own page: unbranded, on a domain we don't control, and revocable by nobody | Use POST /shipments/:id/share instead. Branded, sanitised, revocable |
| Aug 2026 | sealine is now optional on /container, /bl and POST /track. The batch endpoint resolves locally only, so an unresolvable reference is one item's error rather than a bigger bill | Keep sending it when you know it — 1 attempt instead of 3. carrier.sealine reports which was used, carrier.provided whether it was yours |
| Aug 2026 | New GET /carriers/lookup — resolve a reference's carrier without tracking it | Optional. Use it to fill sealine before tracking, free and slot-free |
| Aug 2026 | containers_table rows gained iso_code, size_type and container_description; responses gained container_summary | Nothing. Keys are always present and may be null — treat null as "carrier didn't supply it" |