{
  "openapi": "3.1.0",
  "info": {
    "title": "Navo24 SeaRates-compatible Tracking API",
    "version": "1.0.0",
    "description": "A drop-in migration surface for teams already integrated with the SeaRates container-tracking API. The envelope, the field names and the milestone `status` vocabulary are reproduced so an existing client keeps parsing exactly as it did; only the base URL and the key change. Authentication is a `?api_key=` query parameter carrying a Navo24 `tmcp_` key. The data is ours: direct carrier connectors, fused AIS and sailing schedules. Field meanings in this document are derived from our own renderer, not from any third-party documentation.",
    "contact": {
      "name": "Navo24",
      "url": "https://navo24.com/compat",
      "email": "hello@navo24.com"
    }
  },
  "servers": [{ "url": "https://api.trackingmcp.com" }],
  "security": [{ "apiKeyQuery": [] }],
  "paths": {
    "/compat/searates/tracking": {
      "get": {
        "operationId": "compatSearatesTracking",
        "summary": "Track a container, bill of lading or booking (SeaRates-compatible envelope)",
        "description": "Resolves one reference and returns the SeaRates-shaped envelope. A reference we have never seen is registered and resolved live: the request is held until real data lands (reply-first, typically 2 to 4 seconds, ceiling 25 seconds) and answers `NO_TRACKING_INFO` if nothing resolves in time, so a retry a moment later usually succeeds. `/compat/searates/container` and `/compat/searates/reference` are aliases of this route and behave identically. Responses are served from a short per-organisation cache (roughly 90 seconds) when the same reference is polled repeatedly.",
        "parameters": [
          {
            "name": "api_key",
            "in": "query",
            "required": true,
            "description": "A Navo24 API key (`tmcp_...`). Issued from the developer portal. This surface sits outside the bearer-token API, so the key travels as a query parameter exactly as it did with the incumbent.",
            "schema": { "type": "string" }
          },
          {
            "name": "number",
            "in": "query",
            "required": true,
            "description": "The reference to track: a container number (4 letters and 7 digits), a bill of lading, or a booking number. Upper-cased and trimmed server-side. The kind of reference is detected, not declared.",
            "schema": { "type": "string" },
            "example": "MSCU1234567"
          }
        ],
        "responses": {
          "200": {
            "description": "Either a data-bearing success envelope or, following the incumbent's convention, an error envelope carrying a machine code in `message`. A reference that is not found or is still resolving is an HTTP 200 with `status: \"error\"`, never a 404.",
            "headers": {
              "API-Version": {
                "description": "The version this response was rendered against.",
                "schema": { "type": "string" }
              },
              "X-Schema-Version": {
                "description": "Envelope identifier for this surface (`searates-compat/1`).",
                "schema": { "type": "string" }
              },
              "X-Usage-Month": {
                "description": "Usage answerback for the calling key. Companion `X-Usage-*` headers report the day, week and lifetime counters.",
                "schema": { "type": "string" }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    { "$ref": "#/components/schemas/TrackingResponse" },
                    { "$ref": "#/components/schemas/NotFoundEnvelope" }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "No `number` (or `container`) was supplied.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorEnvelope" }
              }
            }
          },
          "401": {
            "description": "The `api_key` is missing, unknown, revoked or expired.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorEnvelope" }
              }
            }
          },
          "403": {
            "description": "The key's entitlement has expired.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorEnvelope" }
              }
            }
          },
          "429": {
            "description": "Per-organisation burst throttle tripped, or the plan allowance is spent.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorEnvelope" }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKeyQuery": {
        "type": "apiKey",
        "in": "query",
        "name": "api_key",
        "description": "Navo24 API key (`tmcp_...`) passed as a query parameter, matching the incumbent's authentication style."
      }
    },
    "schemas": {
      "TrackingResponse": {
        "type": "object",
        "description": "The success envelope. Three top-level keys, and `data` carries exactly seven keys.",
        "required": ["status", "message", "data"],
        "properties": {
          "status": { "type": "string", "const": "success" },
          "message": {
            "type": "string",
            "description": "`OK` on a normal reply. Carries a short human sentence when the reference was resolved but is known to be untrackable."
          },
          "data": { "$ref": "#/components/schemas/TrackingData" }
        }
      },
      "NotFoundEnvelope": {
        "type": "object",
        "description": "Returned with HTTP 200. `message` is a machine code; `detail` is an additive human hint the incumbent does not carry, and a strict parser can ignore it.",
        "required": ["status", "message", "data"],
        "properties": {
          "status": { "type": "string", "const": "error" },
          "message": {
            "type": "string",
            "enum": ["NO_TRACKING_INFO", "WRONG_NUMBER"],
            "description": "`WRONG_NUMBER` for a reference we have determined is invalid. `NO_TRACKING_INFO` for a well-formed reference with no data yet: not found on the identified line, or still resolving."
          },
          "data": { "type": "object", "description": "Empty object on an error." },
          "detail": {
            "type": "string",
            "description": "Human explanation, for example a check-digit typo suggestion naming a container number that does exist."
          }
        }
      },
      "ErrorEnvelope": {
        "type": "object",
        "description": "Transport-level error envelope (HTTP 400, 401, 403, 429). Same three keys, `data` is null.",
        "required": ["status", "message", "data"],
        "properties": {
          "status": { "type": "string", "const": "error" },
          "message": { "type": "string" },
          "data": { "type": "null" }
        }
      },
      "TrackingData": {
        "type": "object",
        "description": "Exactly seven keys. Nothing else is added here: the version is disclosed in headers precisely so this object keeps its shape.",
        "required": [
          "metadata",
          "locations",
          "facilities",
          "route",
          "vessels",
          "containers",
          "route_data"
        ],
        "properties": {
          "metadata": { "$ref": "#/components/schemas/Metadata" },
          "locations": {
            "type": "array",
            "description": "Every place the payload references, deduplicated. Events and route points point here by `id`.",
            "items": { "$ref": "#/components/schemas/Location" }
          },
          "facilities": {
            "type": "array",
            "description": "Terminals, depots and yards, deduplicated by name. A place with no UN/LOCODE whose name reads as a terminal or operator is filed here rather than polluting `locations`.",
            "items": { "$ref": "#/components/schemas/Facility" }
          },
          "route": { "$ref": "#/components/schemas/Route" },
          "vessels": {
            "type": "array",
            "description": "Every hull named by the timeline, deduplicated by IMO and by canonical name.",
            "items": { "$ref": "#/components/schemas/Vessel" }
          },
          "containers": {
            "type": "array",
            "description": "One entry per box. A container lookup yields one. A bill of lading or booking yields every box on the manifest, each with its own timeline when the carrier publishes per-box events.",
            "items": { "$ref": "#/components/schemas/Container" }
          },
          "route_data": {
            "description": "Map payload: plotted legs, the current pin and live AIS. Null when we hold no plottable route.",
            "oneOf": [
              { "$ref": "#/components/schemas/RouteData" },
              { "type": "null" }
            ]
          }
        }
      },
      "Metadata": {
        "type": "object",
        "required": [
          "type",
          "number",
          "sealine",
          "sealine_name",
          "status",
          "is_status_from_sealine",
          "from_cache",
          "updated_at",
          "cache_expires",
          "api_calls",
          "unique_shipments"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": ["CT", "BL", "BK"],
            "description": "Which identifier resolved: container, bill of lading, booking."
          },
          "number": { "type": "string", "description": "The reference as queried." },
          "sealine": {
            "type": "string",
            "description": "SCAC of the operating line. Falls back to the container prefix when the stored carrier is unresolved and the prefix names a known operating line, otherwise `auto`."
          },
          "sealine_name": {
            "type": "string",
            "description": "Human carrier name, or `Unknown` when no line has been resolved."
          },
          "status": { "$ref": "#/components/schemas/ShipmentStatus" },
          "is_status_from_sealine": {
            "type": "boolean",
            "description": "Always false on this surface, and deliberately so: our status is derived from the carrier's events rather than lifted from a carrier status string, and claiming otherwise would misattribute it."
          },
          "from_cache": {
            "type": "boolean",
            "description": "Always false: the store is continuously polled and served live rather than as a stale cache hit."
          },
          "updated_at": {
            "type": ["string", "null"],
            "description": "When the row was last written, in UTC, formatted `YYYY-MM-DD HH:MM:SS`."
          },
          "cache_expires": {
            "type": ["string", "null"],
            "description": "`updated_at` plus 12 hours, in UTC."
          },
          "api_calls": { "$ref": "#/components/schemas/Quota" },
          "unique_shipments": { "$ref": "#/components/schemas/Quota" }
        }
      },
      "Quota": {
        "type": "object",
        "description": "A plan counter. `used` is always a real number. `total` and `remaining` are null when no cap is configured, which is the common case. Null means no limit is set; zero would mean the limit is zero, and those are different facts.",
        "required": ["total", "used", "remaining"],
        "properties": {
          "total": { "type": ["integer", "null"] },
          "used": { "type": "integer" },
          "remaining": { "type": ["integer", "null"] }
        }
      },
      "ShipmentStatus": {
        "type": "string",
        "enum": ["PLANNED", "IN_TRANSIT", "DELIVERED", "UNKNOWN"],
        "description": "Shipment-level status. PLANNED before departure. IN_TRANSIT from the origin port through discharge, availability and customs hold. DELIVERED once delivered or returned empty. UNKNOWN when nothing has resolved or the booking was cancelled."
      },
      "Location": {
        "type": "object",
        "required": ["id", "name", "state", "country", "country_code", "locode", "lat", "lng", "timezone"],
        "properties": {
          "id": { "type": "integer", "description": "1-based, referenced by events and route points." },
          "name": { "type": "string" },
          "state": { "type": ["string", "null"] },
          "country": { "type": ["string", "null"], "description": "English country name." },
          "country_code": { "type": ["string", "null"], "description": "ISO 3166-1 alpha-2." },
          "locode": { "type": ["string", "null"], "description": "Full 5-character UN/LOCODE. Null when the carrier gave only a place name." },
          "lat": { "type": ["number", "null"] },
          "lng": { "type": ["number", "null"] },
          "timezone": { "type": ["string", "null"], "description": "IANA zone for the port, which is the clock every event date on this surface is printed on." }
        }
      },
      "Facility": {
        "type": "object",
        "required": ["id", "name", "country_code", "locode", "bic_code", "smdg_code", "lat", "lng"],
        "properties": {
          "id": { "type": "integer" },
          "name": { "type": "string" },
          "country_code": { "type": ["string", "null"] },
          "locode": { "type": ["string", "null"], "description": "Currently always null: carriers give us the facility name only." },
          "bic_code": { "type": ["string", "null"], "description": "Currently always null." },
          "smdg_code": { "type": ["string", "null"], "description": "Currently always null." },
          "lat": { "type": ["number", "null"], "description": "Currently always null." },
          "lng": { "type": ["number", "null"], "description": "Currently always null." }
        }
      },
      "Route": {
        "type": "object",
        "description": "The shipment's four anchor points. Shipment-level, so on a multi-box bill of lading it is derived from the union of the child timelines and can only cite an event that appears in this same payload.",
        "required": ["prepol", "pol", "pod", "postpod"],
        "properties": {
          "prepol": {
            "allOf": [{ "$ref": "#/components/schemas/RoutePoint" }],
            "description": "The inland origin move before the sea leg: the first gate or pickup event ahead of the first load or departure. Falls back to the `pol` location with a null date when there is no inland origin move."
          },
          "pol": {
            "allOf": [{ "$ref": "#/components/schemas/RoutePoint" }],
            "description": "Port of loading, anchored on the vessel departure and falling back to the first load. The departure is preferred because a door or rail origin files an inland load first, which would otherwise put the loading port inland."
          },
          "pod": {
            "allOf": [{ "$ref": "#/components/schemas/RoutePoint" }],
            "description": "Port of discharge, which is where the box leaves the ship and is NOT the inland destination on a door move. Anchored on the last vessel discharge the box never sails away from, and never on a discharge the carrier itself calls a transshipment."
          },
          "postpod": {
            "allOf": [{ "$ref": "#/components/schemas/RoutePoint" }],
            "description": "The inland destination move after the final discharge: the last availability or gate event past the port of discharge. On a multi-box bill of lading this is the last box back to the depot, which is what closes the demurrage clock. Falls back to `pod` when there is no on-carriage move."
          }
        }
      },
      "RoutePoint": {
        "type": "object",
        "required": ["location", "date", "actual"],
        "properties": {
          "location": { "type": ["integer", "null"], "description": "Index into `locations[]`." },
          "date": { "type": ["string", "null"], "description": "`YYYY-MM-DD HH:MM:SS`, port-local." },
          "actual": { "type": ["boolean", "null"], "description": "True once the milestone has occurred. Null on a `prepol` that fell back to `pol`." },
          "predictive_eta": {
            "type": ["string", "null"],
            "description": "Present on `pod` only. Once the arrival is actual there is nothing left to predict, so it equals the recorded arrival rather than a frozen pre-arrival estimate."
          }
        }
      },
      "Vessel": {
        "type": "object",
        "required": ["id", "name", "imo", "call_sign", "mmsi", "flag"],
        "properties": {
          "id": { "type": "integer" },
          "name": { "type": "string" },
          "imo": { "type": ["integer", "null"] },
          "call_sign": { "type": ["string", "null"] },
          "mmsi": { "type": ["integer", "null"] },
          "flag": { "type": ["string", "null"], "description": "ISO 3166-1 alpha-2, derived from the MMSI when the stored flag is absent." }
        }
      },
      "Container": {
        "type": "object",
        "required": ["number", "iso_code", "size_type", "status", "is_status_from_sealine", "events_mirrored", "events"],
        "properties": {
          "number": { "type": "string" },
          "iso_code": { "type": ["string", "null"], "description": "ISO 6346 size and type code, for example `45G1`." },
          "size_type": { "type": ["string", "null"], "description": "Human size and type, for example `40' High Cube Dry`. Normalised from the ISO code where possible and from the carrier short code otherwise, against one shared vocabulary, so a value such as `40RH` renders `40' Reefer` whichever of the two fields the carrier populated. An unrecognised carrier value is passed through untouched." },
          "status": { "$ref": "#/components/schemas/ShipmentStatus" },
          "is_status_from_sealine": { "type": "boolean", "description": "Always false. See the note on `metadata.is_status_from_sealine`." },
          "events_mirrored": {
            "type": "boolean",
            "description": "A provenance disclosure, not a formatting flag: true when this box's timeline was copied from a sibling on the same bill of lading rather than sourced for this box. A box whose events were fetched in their own request is always false, even when its dates match a sibling's."
          },
          "events": { "type": "array", "items": { "$ref": "#/components/schemas/Event" } }
        }
      },
      "Event": {
        "type": "object",
        "required": [
          "order_id",
          "location",
          "facility",
          "description",
          "event_type",
          "event_code",
          "status",
          "date",
          "actual",
          "is_date_from_sealine",
          "is_additional_event",
          "type",
          "transport_type",
          "vessel",
          "voyage"
        ],
        "properties": {
          "order_id": { "type": "integer", "description": "1-based position in this container's timeline. Chronological, with a journey-stage tie-break so a same-timestamp estimated tail cannot read as a gate-out before the ship arrives." },
          "location": { "type": ["integer", "null"], "description": "Index into `locations[]`." },
          "facility": { "type": ["integer", "null"], "description": "Index into `facilities[]`." },
          "description": { "type": "string", "description": "The carrier's own wording for the move where we hold it, otherwise a generic label for the code. The carrier's sentence is preferred deliberately: a code-derived label discards a fact the line already stated." },
          "event_type": {
            "type": ["string", "null"],
            "enum": ["EQUIPMENT", "TRANSPORT", null],
            "description": "Derived from the code: arrivals and departures are TRANSPORT, every equipment move is EQUIPMENT. Null on a status-only row (the transshipment delay notice, which carries no family at all)."
          },
          "event_code": {
            "type": ["string", "null"],
            "enum": [
              "ARRI",
              "DEPA",
              "LOAD",
              "DISC",
              "GTIN",
              "GTOT",
              "PICK",
              "RELS",
              "STUF",
              "STRP",
              "RECE",
              "CONF",
              "CUSR",
              "CUSI",
              "ISSU",
              "INSP",
              null
            ],
            "description": "The movement code, from a CLOSED vocabulary of sixteen values. `PICK`, `GTOT`, `GTIN`, `LOAD`, `DEPA`, `ARRI` and `DISC` cover the ocean itinerary; `RELS`, `STUF`, `STRP`, `RECE`, `CONF`, `CUSR`, `CUSI`, `ISSU` and `INSP` are the ancillary codes and carry the unclassified milestone. A movement we hold under any other code is emitted as null rather than under a name outside this list. NULL IS A NORMAL VALUE on an ordinary movement, most often an availability-for-delivery notice, and not only on the transshipment delay row. Fall back to `description` when it is null: the row keeps its wording, place, date, status and actual flag."
          },
          "status": { "$ref": "#/components/schemas/MilestoneStatus" },
          "date": { "type": "string", "description": "`YYYY-MM-DD HH:MM:SS`, no offset, on the PORT-LOCAL clock. A departure from Yantian at 14:15 means 14:15 on the quay." },
          "actual": { "type": "boolean", "description": "True once the milestone has occurred. Reconciled to be monotonic: everything at or before the latest actual event is actual, so a sequence never reads as out of order." },
          "is_date_from_sealine": { "type": "boolean", "description": "Always true: dates come from the carrier's own filing." },
          "is_additional_event": { "type": "boolean", "description": "Always false." },
          "type": {
            "type": "string",
            "enum": ["sea", "land"],
            "description": "Which side of the journey the move belongs to. VESSEL and BARGE ride water, RAIL and TRUCK ride land. Where the carrier reports no conveyance, a vessel operation is sea and everything else is land. An arrival before the first load or after the final discharge is land by construction, because the box is on a train or a truck, not a ship. The transshipment delay notice is sea."
          },
          "transport_type": { "$ref": "#/components/schemas/TransportType" },
          "vessel": { "type": ["integer", "null"], "description": "Index into `vessels[]`. Always null on a land move, so a truck or rail leg can never claim to ride a ship. When the whole timeline names exactly one hull, a bare vessel move inherits it; a shipment that transships onto a second hull keeps only what the carrier stated." },
          "voyage": { "type": ["string", "null"], "description": "Voyage number, nulled alongside `vessel` on a land move." }
        }
      },
      "TransportType": {
        "type": ["string", "null"],
        "enum": ["VESSEL", "RAIL", "TRUCK", "BARGE", null],
        "description": "The leg's conveyance. Set from the carrier's structured mode where it is reported, otherwise from the carrier's own event wording (`Rail departure`, `Barge departure`) on an unambiguous whole-word match. With no signal at all a vessel operation is VESSEL and everything else falls back to TRUCK, which is a fallback rather than a claim that a truck was used. Null only on the transshipment delay notice."
      },
      "MilestoneStatus": {
        "type": "string",
        "enum": [
          "CPS",
          "CEP",
          "CGI",
          "CLL",
          "VDL",
          "VAT",
          "CDT",
          "TSD",
          "CLT",
          "VDT",
          "BTS",
          "VAD",
          "CDD",
          "CGO",
          "CDC",
          "CER",
          "LTS",
          "UNKN",
          "UNK"
        ],
        "description": "The milestone this event represents. The vocabulary is position-aware: the same movement code means a different milestone before the loading port, at the loading port, at a transshipment hub, at the port of discharge and on an inland move past it. Cargo state, not journey position, separates the gate pairs (an empty release from a laden gate-out, an empty return from a laden gate-in). Sea milestones are ordinal rather than matched on UN/LOCODE, so a shipment with a missing port code still splits correctly. `UNKN` is the unclassified value on version 2026-08-04; version 2027-02-01 spells the same value `UNK`. Full descriptions: https://navo24.com/compat"
      },
      "RouteData": {
        "type": "object",
        "description": "Map payload. Additive to the tracking data and safe to ignore.",
        "required": ["route", "pin", "ais", "source"],
        "properties": {
          "route": { "type": "array", "items": { "$ref": "#/components/schemas/RouteLeg" } },
          "pin": {
            "type": ["array", "null"],
            "description": "Current position as [lat, lng].",
            "items": { "type": "number" },
            "minItems": 2,
            "maxItems": 2
          },
          "ais": {
            "oneOf": [{ "$ref": "#/components/schemas/Ais" }, { "type": "null" }]
          },
          "source": {
            "type": "object",
            "description": "Provenance for the geometry and the AIS, so a consumer can tell computed geometry from provider geometry.",
            "properties": {
              "provider": { "type": "string" },
              "provider_name": { "type": "string" },
              "route_geometry": { "type": "string" },
              "ais": { "type": "string" },
              "note": { "type": "string" }
            }
          }
        }
      },
      "RouteLeg": {
        "type": "object",
        "required": ["from", "to", "path", "type", "vessel", "transport_type"],
        "properties": {
          "from": { "$ref": "#/components/schemas/RouteLocation" },
          "to": { "$ref": "#/components/schemas/RouteLocation" },
          "path": {
            "type": "array",
            "description": "Plotted points as [lat, lng] pairs.",
            "items": {
              "type": "array",
              "items": { "type": "number" },
              "minItems": 2,
              "maxItems": 2
            }
          },
          "type": { "type": "string", "enum": ["SEA", "LAND"] },
          "vessel": {
            "oneOf": [{ "$ref": "#/components/schemas/AisVessel" }, { "type": "null" }]
          },
          "transport_type": { "$ref": "#/components/schemas/TransportType" }
        }
      },
      "RouteLocation": {
        "type": "object",
        "description": "One end of a plotted leg, repeated inline (it mirrors an entry in `locations[]`).",
        "properties": {
          "lat": { "type": ["number", "null"] },
          "lng": { "type": ["number", "null"] },
          "name": { "type": ["string", "null"] },
          "state": { "type": ["string", "null"] },
          "locode": { "type": ["string", "null"] },
          "country": { "type": ["string", "null"] },
          "timezone": { "type": ["string", "null"] },
          "country_code": { "type": ["string", "null"] }
        }
      },
      "Ais": {
        "type": "object",
        "required": ["data", "status"],
        "properties": {
          "status": { "type": "string", "description": "`OK` when we have a vessel to report." },
          "data": {
            "type": "object",
            "properties": {
              "vessel": {
                "oneOf": [{ "$ref": "#/components/schemas/AisVessel" }, { "type": "null" }]
              },
              "last_event": {
                "type": ["object", "null"],
                "properties": {
                  "date": { "type": ["string", "null"] },
                  "voyage": { "type": ["string", "null"] },
                  "description": { "type": ["string", "null"] }
                }
              },
              "updated_at": { "type": ["string", "null"] },
              "arrival_port": { "$ref": "#/components/schemas/AisPort" },
              "departure_port": { "$ref": "#/components/schemas/AisPort" },
              "discharge_port": { "$ref": "#/components/schemas/AisPort" },
              "last_vessel_position": {
                "type": ["object", "null"],
                "properties": {
                  "lat": { "type": "number" },
                  "lng": { "type": "number" },
                  "updated_at": { "type": ["string", "null"] }
                }
              }
            }
          }
        }
      },
      "AisVessel": {
        "type": "object",
        "properties": {
          "imo": { "type": ["integer", "null"] },
          "flag": { "type": ["string", "null"] },
          "mmsi": { "type": ["integer", "null"] },
          "name": { "type": ["string", "null"] },
          "call_sign": { "type": ["string", "null"] }
        }
      },
      "AisPort": {
        "type": ["object", "null"],
        "properties": {
          "code": { "type": ["string", "null"], "description": "The incumbent's 3-letter port code, which is the UN/LOCODE minus its country prefix and is not itself a UN/LOCODE." },
          "locode": { "type": ["string", "null"], "description": "Additive: the full 5-character UN/LOCODE, so a consumer keying on UN/LOCODE can match this block." },
          "name": { "type": ["string", "null"] },
          "date": { "type": ["string", "null"] },
          "date_label": { "type": ["string", "null"], "enum": ["ETA", "ETD", "ATA", "ATD", null] },
          "country_code": { "type": ["string", "null"] }
        }
      }
    }
  }
}
