Docs/API Reference

API Reference

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

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

Try it — live sandbox

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

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

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

Authentication

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

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

Base URL

All endpoints are relative to the following base URL:

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

Errors

All error responses return JSON with a consistent structure:

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

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

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

Shipment limits

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

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

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

API rate limits

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

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

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

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

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

Track a container

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

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

Path parameters

ParameterTypeRequiredDescription
numberstringYesContainer number (e.g. MSCU1234567)

Query parameters

ParameterTypeRequiredDescription
sealinestringYes4-character SCAC code of the shipping line (letters and/or digits, e.g. MSCU). Required — omitting returns a 400 error.
curl -H "Authorization: Bearer YOUR_API_KEY" \
  "https://traqocontainer.com/api/v1/container/MRSU6859427?sealine=MAEU"

Response

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

Track a bill of lading

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

Unlike a container number, a Bill of Lading number carries no embedded carrier code, so the sealine (SCAC) query parameter is mandatory — the API cannot auto-detect it. Omitting it returns 400 "sealine is required".
Requires developer mode to be enabled on your account. Toggle it from the Developer tab in your dashboard. Requests without it enabled return 403.

Path parameters

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

Query parameters

ParameterTypeRequiredDescription
sealinestringYes4-character SCAC code of the shipping line (letters and/or digits, e.g. CMDU). Required — omitting returns a 400 error.
curl -H "Authorization: Bearer YOUR_API_KEY" \
  "https://traqocontainer.com/api/v1/bl/SHZ8037930?sealine=CMDU"

Response

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

Bulk track shipments

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

Per-item results: the request returns 200 even when some items fail — branch on each results[].ok. The whole request is only rejected up front for auth (401/403), rate limit (429), a malformed body (400), or a payment past the grace period (402).
Each item's sealine (SCAC) is mandatory — see Carriers for the list of valid codes.

Request body

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

Example request

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

Response

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

List tracked shipments

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

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

Query parameters

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

Response

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

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

Get a shipment

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

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

Example request

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

Response

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

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

DELETE/api/v1/shipments/{id}

Untrack a shipment

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

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

Example request

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

Response

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

Track a vessel

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

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

Query parameters

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

Response

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

Voyage schedules

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

Query parameters

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

Response

Showing 2 of many results for brevity.

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

Port congestion

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

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

Predictive ETA on container tracking

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

GET/api/v1/ports/congestion

Congestion board

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

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

Example request

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

Response

Showing 1 of many ports for brevity.

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

Search ports

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

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

Query parameters

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

Example request

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

Response

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

List supported carriers

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

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

Query parameters

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

Example request

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

Response

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

Webhooks

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

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

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

Request headers

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

Delivery & retries

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

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

Webhook events

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

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

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

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

Verifying signatures

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

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

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

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

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

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

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