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.

FormatJSON
PricingView API plans →StatusUptime & /api/status.json →
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.

Open the SeaRates-compatible docs

All endpoints

Every v1 path, relative to the base URL, with a Bearer key — except the sandbox, which needs none.

EndpointWhat it does
GET/container/:numberFull tracking for a container number. Saves the shipment (uses a slot if new).
GET/bl/:numberFull tracking for a bill of lading. Saves the shipment (uses a slot if new).
POST/trackTrack up to 50 references in one request.
GET/shipmentsPage through everything you track, or poll deltas with updated_since.
GET/shipments/:idRead one shipment in your account, a teammate’s included — no re-track, no slot. Add ?include=full for the stored document.
POST/shipments/retry-failedRe-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/:idFix a wrong carrier or type on a shipment your own user added, or set its customer by name. No slot.
DELETE/shipments/:idStop tracking a shipment your own user added and release its slot.
POST/shipments/:id/shareCreate a public, sanitised tracking page for a customer.
DELETE/shipments/:id/shareRevoke that page, so the link stops resolving.
GET/account/usageYour plan, quota, slots remaining and billing cycle.
GET/vessel/trackLive AIS position, speed and heading for a vessel by IMO or MMSI.
GET/voyage/schedulesSailing schedules between two ports.
GET/ports/:locode/congestionCongestion score, 90-day history and the anchorage wait for one port.
GET/ports/congestionThe current reading for every scored port, in one call.
GET/portsResolve a LOCODE, port name or city to canonical port metadata.
GET/disruptionsStrikes, closures, canal delays and weather, filtered by severity, port, carrier or country.
GET/disruptions/:idOne published disruption by its id.
GET/shipments/:id/disruptionsThe disruptions touching one shipment in your account, and how each one reaches it.
GET/carriers/lookupWork out which carrier owns a reference — free, and creates no shipment.
GET/carriersEvery supported shipping line and its SCAC.
GET/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).

Any reference works, and always the same way, so a saved response stays a good test fixture. It never touches your account and never bills anyone. Replies carry "mode": "sandbox" — except ?include=full, which says "stored", like live.

What you get

EndpointIn the sandbox
/container/:number, /bl/:numberAny reference resolves — real ports, a plausible route, a full milestone timeline, equipment fields.
POST /trackPer-item results. Mix a valid reference with a magic one to see how a partial failure reports.
/shipmentsA 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-failedAny 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/congestionA fixed set of scored ports, with vessels_waiting and the two wait figures.
/disruptions, /disruptions/:id, /shipments/:id/disruptionsOne published event, 4812, with the filters applied; SHP-1000 is the shipment it touches.
/account/usage, /carriers, /carriers/lookup, /ports, /vessel/track, /voyage/schedulesAll 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.

ReferenceStatusWhat it lets you build
SBXU0000003404The carrier has no record of this reference.
NOT-A-REFERENCE400The reference is malformed — rejected before the carrier is asked.
SBXU4020008402Every shipment slot on the plan is used. The branch that fires the first time a customer outgrows their plan.
SBXU4029999402Payment failed and the grace period has ended — the account is soft-locked.
SBXU4290003429Too many requests. Carries Retry-After and the reset time.
SBXU5020002502The 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:

EventFires when
shipment.updatedA tracking milestone landed and the shipment’s status changed.
shipment.vessel_arrivedThe vessel arrived at the final port of discharge. ata carries the date; status usually still reads IN_TRANSIT.
shipment.arrivedThe status became delivered / completed — the end of the journey, after the vessel’s arrival.
eta.changedThe carrier revised the ETA. previous_eta lets you compute the delta without storing it yourself.
discovery.recoveredA shipment that failed to track was recovered under the correct carrier — a dead row became live.
webhook.testSent only by “send a test event”, never by a subscription. Same shape as the rest.
Request body
{
  "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
  }
}
Verify against the raw request body bytes, before 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.

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

Sandbox 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

RequirementWhereMissing it returns
API keyAuthorization: Bearer <key>401
Developer modeDeveloper tab, once per account403

Both apply to every endpoint below, so they are not repeated on each one.

Keep your API key secret — never in client-side JavaScript or version control. If one leaks, delete it from your dashboard and generate a new one.
Example
Authorization: Bearer YOUR_API_KEY

Base URL

All endpoints are relative to the following base URL:

Not writing the requests by hand? There is a Postman collection and an OpenAPI spec — see Postman & OpenAPI.
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.

No key yet? The collection’s Sandbox folder runs the same requests with no auth, same response shape. See Try it.
https://traqocontainer.com/api/v1/postman.json
https://traqocontainer.com/api/v1/openapi.json

Errors

All error responses return JSON with a consistent structure:

StatusMeaning
200Success
400Bad request — check required parameters
401Invalid or missing API key
402Payment 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.
403Developer mode not enabled — enable it from your dashboard settings
404Resource 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.
429Rate 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.
502Upstream 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 accountNothing — re-fetching what you track is always free
A new reference, allowance remaining1 slot, counted once
A new reference, allowance exhausted402
Deleting a shipment does not return the slot. 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.

PlanRequests / minute
business240
custom600
free30
professional120
starter60

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.

PlanLookups / day
free100
starter500
professional1,500
business5,000
custom10,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:

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 /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:

Don't poll /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:

ClassEndpointsTypicalSlow (p95)
Stored/shipments, /shipments/:id, /ports, /carriers214 ms778 ms
Live/container/{number}, /bl/{number}727 ms5.8 s
External/vessel/track, /voyage/schedules248 ms7.6 s

Measured from real successful requests over the last 30 days, not a target.

Live endpoints re-fetch from the carrier every call, so set your timeout well above the p95 and keep them off a user's critical path. For repeat checks use 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.

FieldMeaningNull when
last_synced_atWhen the carrier data behind this response was last refreshed. Everything else in the payload is as of this moment.Never synced.
next_refresh_eligible_atlast_synced_at + the gap that applies to this shipment — the earliest it can change.Never synced (eligible now), or no longer active.
refresh_eligible_nowBoolean shortcut: the gap has elapsed and the shipment is still moving.Never null.
Eligible is not scheduled. 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.

GET/api/v1/container/:number

Track 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

ParameterInRequiredDescription
numberpathYesContainer number, e.g. MSCU1234567
sealinequeryNo4-character SCAC, e.g. MSCU. Recommended — see below

Sending sealine

CostAccuracy
You send it1 upstream attemptExact
You omit itUp to 3 attemptsResolved 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".

FieldExampleNotes
iso_code45G1Only ever a valid ISO 6346 code. Shorthand like 40HQ is reported as null here
size_type40HCWhere that shorthand appears instead
container_description40ft High CubeReadable 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.

Request
curl "https://traqocontainer.com/api/v1/container/MRSU6859427?sealine=MAEU" \
  -H "Authorization: Bearer YOUR_API_KEY"
Response200 OK
{
"success": true,
"data": {
"reference_number": "MRSU6859427",
"origin": "Ahmedgarh, ",
"sealine": "MAEU",
"number_of_containers": 1,
"eta": "2026-05-17 00:00:00",
"ata": null,
"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",
"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

Full tracking data for a BL. Same response shape as /container; saved to your account in the background.

Parameters

ParameterInRequiredDescription
numberpathYesBL number, 3–50 chars, as the carrier printed it. Digits, hyphens, slashes and dots all accepted (URL-encode / as %2F); whitespace trimmed
sealinequeryNo4-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.

Request
curl "https://traqocontainer.com/api/v1/bl/SHZ8037930?sealine=CMDU" \
  -H "Authorization: Bearer YOUR_API_KEY"
Response200 OK
{
"success": true,
"data": {
"reference_number": "SHZ8037930",
"origin": "Nansha, China",
"sealine": "CMDU",
"number_of_containers": 1,
"eta": "2026-07-28 00:00:00",
"ata": null,
"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",
"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

Up to 50 containers or BLs per request. New shipments cost a slot each; ones you already track are free.

Returns 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

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, separators fine).
shipments[].sealinestringNo4-character SCAC. Omit it and we resolve locally; send it and we skip the lookup.
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":"SHZ8037930"}]}'
Response200 OK
{
"success": true,
"total": 2,
"tracked": 1,
"failed": 1,
"slots": {
"used": 41,
"limit": 50,
"remaining": 9,
"consumed": 1
}
,
"results": [
{
"index": 0,
"number": "MRSU6859427",
"type": "Container",
"ok": true,
"slot_consumed": true,
"data": {8 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

A paginated list of every shipment on your account — containers and bills of lading — with current status, route and ETA.

Query parameters

ParameterTypeRequiredDescription
pageintegerNoPage number, default 1
pageSizeintegerNoResults per page, default 20, max 100
updated_sincestringNoISO 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 }.
Each shipment carries last_synced_at. Save the newest you see and pass it back as updated_since to fetch just what changed.
Request
curl "https://traqocontainer.com/api/v1/shipments?page=1&pageSize=20" \
  -H "Authorization: Bearer YOUR_API_KEY"
Response200 OK
{
"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",
"ata": null,
"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,
"customers": [1 item]
}
,
{
"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": 0,
"eta": "2026-05-11 00:00:00",
"ata": "2026-05-10 22:40: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,
"customers": [0 items]
}
]
}
GET/api/v1/shipments/:id

Get 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 acceptsExampleStability
Container or BL numberMSCU1234567Preferred — never changes
Shipment idSHP-1042Regenerated 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.

Any shipment in your account that your user can see answers here, including one a teammate added in the dashboard. A teammate limited to certain customers or lanes sees only those. Anything else — another account's, or one that was untracked — returns 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".

Read from storage — no carrier call, no slot, no re-add limit. Use /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.

Request
curl "https://traqocontainer.com/api/v1/shipments/MSCU1234567?include=full" \
  -H "Authorization: Bearer YOUR_API_KEY"
Response200 OK
{
"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": 0,
"eta": "2026-05-11 00:00:00",
"ata": "2026-05-10 22:40: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,
"terminal": null,
"customers": [
"Acme GmbH"
]
}
}

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 facts
  • null: nothing for this shipment
  • Terminal's own keys, snake_case
  • Blank hold: absent, not released
  • Over 24h: current facts dropped
  • Read error: terminal: null
Request
curl "https://traqocontainer.com/api/v1/shipments/MSCU7751000" \
  -H "Authorization: Bearer YOUR_API_KEY"
Response200 OK
{
"success": true,
"data": {
"name": "SHP-88000001",
"reference_number": "MSCU7751000",
"status": "IN_TRANSIT",
"destination": "Los Angeles, United States",
"terminal": [
{13 fields},
{6 fields}
]
}
}
POST/api/v1/shipments/retry-failed

Retry 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.

A retry is not a re-add. A failed shipment never consumed a slot, and a slot is charged only if data lands. The per-reference re-add limit does not apply here.

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.

Request
curl -X POST "https://traqocontainer.com/api/v1/shipments/retry-failed" \
  -H "Authorization: Bearer YOUR_API_KEY"
Response200 OK
{
"success": true,
"examined": 12,
"queued": 9,
"skipped": 3,
"shipment_ids": [
"SHP-00000044",
"SHP-00000051",
"SHP-00000063"
]
,
"window_days": 7,
"slot_consumed": 0,
"message": "Queued 9 reference(s). Read GET /api/v1/shipments/{id} for each — no slot is consumed until data lands."
}
PATCH/api/v1/shipments/:id

Correct 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.

Correcting never costs a slot. It edits a row you already have, so no intake allowance and no same-reference daily re-add limit. Read the slot policy.

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

FieldNotes
carrierThe SCAC you know is correct, e.g. MAEU
typecontainer or bl — for a number filed as the wrong kind
customerA 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
  • null or "" clears it
  • Alone or with carrier
  • Reads return customers (names)
Email
  • 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.

Request
curl -X PATCH "https://traqocontainer.com/api/v1/shipments/MSCU1234567" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"carrier":"MAEU"}'
Response200 OK
{
"success": true,
"shipment_id": "SHP-1042",
"carrier": "MAEU",
"reference_number": "MSCU1234567",
"type": "container"
}
DELETE/api/v1/shipments/:id

Untrack 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.

Deleting does not return the billing slot. Your limit counts references added in the cycle, so delete-and-re-add spends quota. One exception: deleted within the grace window and never re-added, a few times per cycle. Read the slot policy.

: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.

Request
curl -X DELETE "https://traqocontainer.com/api/v1/shipments/MSCU1234567" \
  -H "Authorization: Bearer YOUR_API_KEY"
Response200 OK
{
"success": true,
"deleted": true,
"shipment_id": "SHP-1042"
}
GET/api/v1/account/usage

Your 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.

Free: no slot, no carrier call, only your per-minute budget. The numbers come from the same code the 402 uses, so they always agree.

Notes

  • used counts distinct references tracked this cycle. A reference you later untrack still counts — deleting does not return the slot.
  • On a yearly plan the cycle is the year and the shipment numbers are pool numbers for that year. Read period_days (30 vs 365) to tell which shape you are holding rather than inferring it from the dates.
  • rate_limit is the per-minute window and mirrors the X-RateLimit-* headers on the same response. There is no daily API quota, so none is reported.
  • Both 402 bodies carry usageApi: "/api/v1/account/usage", so a client that does hit the wall is pointed straight at the endpoint that would have prevented it.
Request
curl "https://traqocontainer.com/api/v1/account/usage" \
  -H "Authorization: Bearer YOUR_API_KEY"
Response200 OK
{
"success": true,
"data": {
"plan": "professional",
"period_days": 30,
"cycle": {
"start": "2026-08-01T00:00:00.000Z",
"end": "2026-09-01T00:00:00.000Z"
}
,
"shipments": {
"limit": 20,
"addon_slots": 5,
"effective_limit": 25,
"used": 12,
"active": 9,
"remaining": 13
}
,
"rate_limit": {
"per_minute": 120,
"remaining": 118,
"reset": 1788194750
}
}
}
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.

Query parameters

ParameterTypeRequiredDescription
imostringYes7-digit IMO number
mmsistringYes9-digit MMSI number
Request
curl "https://traqocontainer.com/api/v1/vessel/track?imo=9811000&mmsi=636022327" \
  -H "Authorization: Bearer YOUR_API_KEY"
Response200 OK
{
"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"
Request
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"
Response200 OK
{
"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-10-24 10:00:00",
"arrival_date": "2026-10-29 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-10-25 06:00:00",
"arrival_date": "2026-11-01 08:00:00",
"duration": 8,
"transit_type": "transshipment"
}
]
}
GET/api/v1/ports/:locode/congestion

Port 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.

Gated twice, and the first gate is not your plan. A deployment-wide switch is off by default; while off, every account gets 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.

Request
curl "https://traqocontainer.com/api/v1/ports/INMUN/congestion" \
  -H "Authorization: Bearer YOUR_API_KEY"
Response200 OK
{
"success": true,
"data": {
"locode": "INMUN",
"latest": {
"score": 34,
"bucket": "moderate",
"tier": "watch",
"trend7d": 3,
"components": {3 fields},
"computedAt": "2026-08-31T16:00:00.000Z"
}
,
"history": [
{3 fields},
{3 fields},
{3 fields}
]
,
"median_anchorage_wait_h": 68,
"longest_wait_days": 3
}
}
GET/api/v1/ports/congestion

Congestion 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.

Request
curl "https://traqocontainer.com/api/v1/ports/congestion" \
  -H "Authorization: Bearer YOUR_API_KEY"
Response200 OK
{
"success": true,
"total": 128,
"data": [
{
"locode": "USLAX",
"name": "Los Angeles",
"country": "United States",
"score": 72,
"bucket": "high",
"tier": "A",
"trend_7d": 6,
"vessels_waiting": 21,
"calls_30d": 21,
"computed_at": "2026-09-28T06:00:00.000Z",
"lat": 33.7406,
"lng": -118.2706
}
]
}
GET/api/v1/ports

Search 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

ParameterTypeRequiredDescription
searchstringYesA UN/LOCODE, port name, or city — minimum 2 characters (e.g. rotterdam or NLRTM)
Request
curl "https://traqocontainer.com/api/v1/ports?search=rotterdam" \
  -H "Authorization: Bearer YOUR_API_KEY"
Response200 OK
{
"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/disruptions

List 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.

On Professional and above, each event also carries 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

ParameterTypeRequiredDescription
severityintegerNoMinimum severity, 1–4 (default 1). 3 returns 3 and 4.
categorystringNoOne of port_disruption, chokepoint, carrier_ops, tariff_trade, regulatory, weather, security_geopolitical, container_market, labor
portstringNoUN/LOCODE, e.g. SGSIN
carrierstringNo4-letter SCAC, e.g. MAEU
countrystringNoISO alpha-2, e.g. SG
statusstringNoactive (default) or resolved
sincedateNoOnly events reported on at or after this time. Poll with the time of your last call.
page / page_sizeintegerNoFrom 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.

Starter and above. Below that the answer is 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.

Request
curl "https://traqocontainer.com/api/v1/disruptions?severity=3&port=SGSIN" \
  -H "Authorization: Bearer YOUR_API_KEY"
Response200 OK
{
"success": true,
"data": [
{
"id": 4812,
"headline": "Dockworkers at Singapore’s Pasir Panjang terminal stop work for 48 hours",
"summary": "Berthing at Pasir Panjang is suspended until the stoppage ends. Carriers are diverting calls to Tuas; expect 2–3 days of added dwell on Singapore transshipments.",
"category": "labor",
"severity": 3,
"severity_label": "warning",
"status": "active",
"first_seen": "2026-09-26T04:10:00.000Z",
"last_seen": "2026-09-27T21:45:00.000Z",
"resolved_at": null,
"entities": {4 fields},
"sources": [2 items],
"affected": {2 fields}
}
]
,
"total": 1,
"page": 1,
"page_size": 25
}
GET/api/v1/disruptions/:id

Get 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.

An event still under review, withdrawn or expired answers 404 with disruption_not_found, exactly like an id that never existed. Same plan rule as the list.
Request
curl "https://traqocontainer.com/api/v1/disruptions/4812" \
  -H "Authorization: Bearer YOUR_API_KEY"
Response200 OK
{
"success": true,
"data": {
"id": 4812,
"headline": "Dockworkers at Singapore’s Pasir Panjang terminal stop work for 48 hours",
"summary": "Berthing at Pasir Panjang is suspended until the stoppage ends. Carriers are diverting calls to Tuas; expect 2–3 days of added dwell on Singapore transshipments.",
"category": "labor",
"severity": 3,
"severity_label": "warning",
"status": "active",
"first_seen": "2026-09-26T04:10:00.000Z",
"last_seen": "2026-09-27T21:45:00.000Z",
"resolved_at": null,
"entities": {
"ports": [1 item],
"carriers": [2 items],
"countries": [1 item],
"chokepoints": [0 items]
}
,
"sources": [
{2 fields},
{2 fields}
]
}
}
GET/api/v1/shipments/:id/disruptions

Disruptions 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.

Professional and above. Below that every id gets 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.
Request
curl "https://traqocontainer.com/api/v1/shipments/MSCU1234567/disruptions" \
  -H "Authorization: Bearer YOUR_API_KEY"
Response200 OK
{
"success": true,
"shipment_id": "SHP-1042",
"total": 1,
"data": [
{
"id": 4812,
"headline": "Dockworkers at Singapore’s Pasir Panjang terminal stop work for 48 hours",
"summary": "Berthing at Pasir Panjang is suspended until the stoppage ends. Carriers are diverting calls to Tuas; expect 2–3 days of added dwell on Singapore transshipments.",
"category": "labor",
"severity": 3,
"severity_label": "warning",
"status": "active",
"first_seen": "2026-09-26T04:10:00.000Z",
"last_seen": "2026-09-27T21:45:00.000Z",
"resolved_at": null,
"entities": {4 fields},
"sources": [2 items],
"match_types": [1 item]
}
]
}
GET/api/v1/carriers/lookup

Resolve the carrier for a reference

Which carrier moves a container or BL, without tracking it — no shipment, no write, no slot.

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

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.

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 but need opposite handling. sources_unavailable lists sources that failed or took longer than 3 seconds — empty is healthy.

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 partial answer still returns 200. A 200 here does not prove every source was asked — check the list.

Two rules worth coding against

  • carrier is null iff candidates is empty. No confidence floor — a low winner is still returned. Want only strong answers? Filter on confidence yourself.
  • candidates is always an array — never null, 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.

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. Looked up in the last 10 minutes → 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:

Request
curl "https://traqocontainer.com/api/v1/carriers/lookup?number=MRKU8636841" \
  -H "Authorization: Bearer YOUR_API_KEY"
Response200 OK
{
"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

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.

Optional ?search= (2+ chars) filters by SCAC, name or slug.

Query parameters

ParameterTypeRequiredDescription
searchstringNoCase-insensitive filter by SCAC, name or slug — minimum 2 characters (e.g. maersk or MAEU).
Request
curl "https://traqocontainer.com/api/v1/carriers?search=maersk" \
  -H "Authorization: Bearer YOUR_API_KEY"
Response200 OK
{
"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, 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

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

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.

Delivery is at-least-once — a retry can re-send an event you already handled. De-duplicate on X-Traqo-Delivery.
An endpoint that keeps failing is turned off automatically. Switch it back on from the dashboard once it is healthy — that resets its failure count.
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",
    "ata": null
  }
}

Webhook events

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

EventFires when
shipment.updatedA tracking milestone landed and the shipment’s status changed.
shipment.vessel_arrivedThe vessel arrived at the final port of discharge. ata carries the date; status usually still reads IN_TRANSIT.
shipment.arrivedThe status became delivered / completed — the end of the journey, after the vessel’s arrival.
eta.changedThe carrier revised the ETA. previous_eta lets you compute the delta without storing it yourself.
discovery.recoveredA 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.
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 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.

Sign the raw request body bytes as received — verify before 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

  1. An API key, from Dashboard → Developer. The same one the REST API uses — nothing new is issued.
  2. Developer mode on for your account, on that same page. Without it every call answers 403.
  3. 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.

ClientWhere the config lives
Claude Desktop~/Library/Application Support/Claude/claude_desktop_config.json (macOS) · %APPDATA%\Claude\ (Windows)
Claude Codeclaude 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 elseThe 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_shipmentsfilter

    Your tracked shipments, with what is late and what is arriving.

  • get_shipment_statusreference

    Where one container or bill of lading has got to.

  • get_shipment_timelinereference

    The whole journey of one shipment: ports, vessels, every event so far.

  • get_refresh_schedulereference

    When 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_documentsreference

    Bills of lading, invoices and packing lists on one shipment.

  • get_document_metaid

    One 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_conditionsport

    How congested a port is, which way it is heading, and why.

  • get_vessel_schedulesorigindestinationdateweeks

    Sailings between two ports over the coming weeks.

  • track_vesselvessel

    Where a ship is now, by name or IMO number.

  • get_route_newsreferenceminecategoryminSeverity

    Strikes, weather, closures and chokepoints — optionally only the ones touching your lanes.

  • get_disruption_detailid

    One 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_supportcarrier

    Whether we track a shipping line — by name, alias or SCAC.

  • search_api_docsquery

    The REST API: every endpoint, parameter and response field.

  • search_helpquery

    Published FAQs and help articles.

Your account

Your own workspace. Scoped to your organisation, and there is no argument that names another.

  • list_customerssearchfilter

    The customers you have assigned shipments to, and how each is doing.

  • get_teamno arguments

    Who is on your workspace and what role each has.

  • get_dnd_statusreference

    Free days left on one shipment, and what an overrun is costing.

  • get_notification_prefsno arguments

    How you are currently alerted: severity, digest hour, muted categories.

  • get_ticket_statusticketNumber

    Your open support tickets and where each one stands.

  • get_quote_request_statusno arguments

    Your freight-quote requests and the quotes that came back.

  • get_mis_reportstatusdelayStatusshipmentTypesealineqlimit

    Your MIS report as data: open, late, arriving, and where each box is.

When it does not work

What you seeWhat it meansWhat to do
404, or the client reports no serverThe MCP server is not switched on for this deploymentAsk us to enable it. Nothing on your side is wrong
401The key is missing, wrong, or revokedCheck the header is Authorization: Bearer YOUR_KEY, and that the key still exists in your dashboard
403The key is valid but developer mode is offTurn it on under Dashboard → Developer
429Over your per-minute rate limit — shared with the REST APIWait 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 GETYour client is probing for the SSE transport, which we do not offerNothing — clients fall back to POST. If yours does not, use mcp-remote
Connected, but no tools listedAlmost always a client that was not restarted after the config changedRestart 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.

Need one of those? The REST API above does writes, billing reads and live carrier refreshes, with the same key.
Claude Desktop — claude_desktop_config.json
{
  "mcpServers": {
    "traqo-ocean": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://traqocontainer.com/api/mcp",
               "--header", "Authorization: Bearer YOUR_API_KEY"]
    }
  }
}
Cursor — .cursor/mcp.json
{
  "mcpServers": {
    "traqo-ocean": {
      "url": "https://traqocontainer.com/api/mcp",
      "headers": { "Authorization": "Bearer YOUR_API_KEY" }
    }
  }
}
Any client, via mcp-remote
npx -y mcp-remote https://traqocontainer.com/api/mcp \
  --header "Authorization: Bearer YOUR_API_KEY"
Check it by hand — lists every tool
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"}'
A tool call, by hand
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.

WhenChangeWhat you do
Sep 2026MCP 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 portNothing. The port-clock keys are unchanged; read detention when it is set
Sep 2026MCP 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 usNothing, 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 ignoredNothing. 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 nobodyUse POST /shipments/:id/share instead. Branded, sanitised, revocable
Aug 2026sealine 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 billKeep sending it when you know it — 1 attempt instead of 3. carrier.sealine reports which was used, carrier.provided whether it was yours
Aug 2026New GET /carriers/lookup — resolve a reference's carrier without tracking itOptional. Use it to fill sealine before tracking, free and slot-free
Aug 2026containers_table rows gained iso_code, size_type and container_description; responses gained container_summaryNothing. Keys are always present and may be null — treat null as "carrier didn't supply it"