{
  "openapi": "3.1.0",
  "info": {
    "title": "Navo Schedules API",
    "version": "1.0.0",
    "description": "REST endpoints for live ocean sailing schedules and observed carrier on-time reliability, the same engine that powers schedulesmcp.com. The data endpoints (`GET /public/schedules`, `/public/reliability/lanes`, `/public/reliability/leaderboard` and `/public/port-congestion`) require an API key: send it as `Authorization: Bearer <key>`. Each answer that carries data counts as 1 call; an empty answer, a refusal and an error are free. There are no plans or tiers on this product: what holds a caller is the call quota agreed for their account, whether access is still valid, and a pace of 120 requests per 20 seconds for a production key or 60 for a test key. The paths keep their `/public` prefix so that existing integrations change one header and not their URL. The reference lists (covered lanes, carriers and port search) need no key, are never counted, and carry two limits per address, 120 requests per 20 seconds and 2,000 per day resetting at midnight UTC. Either refusal is a 429 carrying Retry-After, RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset and RateLimit-Policy; read RateLimit-Policy to tell the two apart, since both answer with error code RATE_LIMITED and the window is the whole difference between waiting twenty seconds and waiting until midnight. Answers from the keyless endpoints are cached for five minutes at the edge. Keyed answers are not: they are marked `private`, because a shared cache would serve one customer's answer to another, and they vary on `Authorization`. Webhooks (`/v1/webhooks` and its sub-paths): every successful request counts as 1 call, refusals and errors are free, and the event deliveries we send are free. Sailing cards (`/public/schedules/cards`) are not available: 403 FORBIDDEN. Where a sailing reached us from is not part of the contract and is not in any response. TIMES: every departure, arrival, leg time and cut-off is the local time of the port it happens at, in the digits the line itself publishes, for every line alike. We convert nothing, and these values carry no offset: `2026-09-29T05:00:00` on a Shanghai departure is 05:00 Shanghai time. Read the digits, do not convert them to your own zone. `departure_precision` and `arrival_precision` say whether the line gave a time (`minute`) or only a date (`day`, the 00:00 is ours). Because each time is its own port's clock, the difference between an origin and a destination time is not the elapsed time; use `transit_days`. Our own bookkeeping instants are different and ARE real UTC: `as_of`, `last_changed_at`, `retrieved_at` and the `changed_since` you pass back. Authenticated /v1 endpoints (full coverage, history, alerts) require an API key; see https://schedulesmcp.com/pricing. Agent reference: https://schedulesmcp.com/llms-full.txt",
    "contact": {
      "name": "SchedulesMCP",
      "url": "https://schedulesmcp.com"
    }
  },
  "servers": [
    {
      "url": "https://api.navo24.com/v1/schedules",
      "description": "Navo API"
    }
  ],
  "paths": {
    "/sailings": {
      "get": {
        "operationId": "getLaneSchedules",
        "summary": "Upcoming published sailings on a lane (API key required)",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "description": "Upcoming sailings on a lane from the carriers' own schedules, one per carrier offer, soonest departure first. Response time depends on the line and on load, up to a few minutes. Paged: follow `next` until it is null; an empty lane is a 200 with no sailings, and free.",
        "parameters": [
          {
            "$ref": "#/components/parameters/origin"
          },
          {
            "$ref": "#/components/parameters/destination"
          },
          {
            "$ref": "#/components/parameters/from"
          },
          {
            "$ref": "#/components/parameters/weeks"
          },
          {
            "$ref": "#/components/parameters/carrier"
          },
          {
            "$ref": "#/components/parameters/transshipment"
          },
          {
            "$ref": "#/components/parameters/changed_since"
          },
          {
            "$ref": "#/components/parameters/routings"
          },
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "Sailings for the lane",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "description": "True on every answer that carries `data`; an error answers false with `error` instead."
                    },
                    "data": {
                      "type": "object",
                      "description": "The lane, the window used, the per-line summary and this page of sailings.",
                      "properties": {
                        "origin": {
                          "type": "string",
                          "description": "The origin UN/LOCODE this answer was built for, which can be the canonical code rather than the one you sent."
                        },
                        "origin_name": {
                          "type": "string",
                          "nullable": true,
                          "description": "The origin port's name, or its UN/LOCODE where we hold no name."
                        },
                        "destination": {
                          "type": "string",
                          "description": "The destination UN/LOCODE this answer was built for, canonical in the same way as `origin`."
                        },
                        "destination_name": {
                          "type": "string",
                          "nullable": true,
                          "description": "The destination port's name, or its UN/LOCODE where we hold no name."
                        },
                        "from": {
                          "type": "string",
                          "example": "2026-10-01T00:00:00",
                          "description": "Start of the window, 00:00 of the first departure date considered, port-local with no offset like `published_departure`."
                        },
                        "to": {
                          "type": "string",
                          "example": "2026-10-29T00:00:00",
                          "description": "End of the window, exclusive, in the same form as `from`: `from` plus `weeks` times seven days."
                        },
                        "weeks": {
                          "type": "integer",
                          "description": "The window length in weeks this answer used."
                        },
                        "count": {
                          "type": "integer",
                          "description": "How many sailings are on this page, not in the whole window."
                        },
                        "carriers": {
                          "type": "array",
                          "nullable": true,
                          "items": {
                            "type": "object",
                            "required": [
                              "carrier_code",
                              "carrier_name",
                              "count",
                              "latest_departure",
                              "status",
                              "checked_at",
                              "covered_until"
                            ],
                            "properties": {
                              "carrier_code": {
                                "type": "string",
                                "description": "The line's SCAC, the same code its sailings carry in `carrier_code`."
                              },
                              "carrier_name": {
                                "type": "string",
                                "nullable": true,
                                "description": "The line's name, as on its sailings."
                              },
                              "count": {
                                "type": "integer",
                                "description": "How many of this line's sailings the whole answer holds across every page, counted exactly as without `routings=all`."
                              },
                              "latest_departure": {
                                "type": "string",
                                "example": "2026-11-28T00:00:00",
                                "nullable": true,
                                "description": "This line's last departure in this answer (your window and filters), port-local, or null when it has none there. It is not how far the line publishes, even when `covered_until` equals `to`."
                              },
                              "status": {
                                "type": "string",
                                "enum": [
                                  "ok",
                                  "no_service",
                                  "no_sailings",
                                  "timeout",
                                  "failed",
                                  "not_attempted",
                                  "not_supported"
                                ],
                                "description": "How this line's answer stands for the lane: `ok`, `no_service`, `no_sailings`, `timeout`, `failed`, `not_attempted` or `not_supported`. `no_service` means the line told us it does not serve the lane, `no_sailings` that the line has no sailings for the lane in your window and did not say whether it serves the lane; both as of `checked_at`, for departures before `covered_until`. `timeout` and `failed` mean asked now without an answer, `not_attempted` means unconfirmed (not asked, or the line could not answer the question), `not_supported` means no source."
                              },
                              "checked_at": {
                                "type": "string",
                                "format": "date-time",
                                "nullable": true,
                                "description": "When the line last confirmed this lane (its newest sailing here, or when we asked it), in UTC, or null when we hold no such date."
                              },
                              "covered_until": {
                                "type": "string",
                                "format": "date",
                                "example": "2026-11-28",
                                "nullable": true,
                                "description": "How far we checked this line for your search: departures before this date. It is not how far the line publishes. Equal to the end of your window, the line was checked for all of it; earlier, its sailings from this date on are not confirmed and `complete` is false. For a line we ask at search time, a new search with `from` set to that date or later checks the later dates. A line we read on a regular schedule rather than at each search moves forward only with that read; a new search does not check it further. `null`: we cannot say how far this line was checked. `complete` is false if our last check is too old to cover your `to` date."
                              },
                              "error": {
                                "type": "string",
                                "enum": [
                                  "timeout",
                                  "failed",
                                  "paused"
                                ],
                                "description": "Kept for existing clients, read `status` instead; `paused` means two timeouts in a row, so we wait six hours before asking again."
                              }
                            }
                          },
                          "description": "One entry per line across the whole answer, most sailings first, on the first page only (null on later pages). Lines with `count: 0` can be listed; with a `carrier` filter, exactly the lines you asked for."
                        },
                        "complete": {
                          "type": "boolean",
                          "nullable": true,
                          "description": "True when the live search ran and every listed line is `ok`, `no_service`, `no_sailings` or `not_supported`, false when a missing sailing may be a gap, null when `carriers` is null. Never true with `transshipment`, where the live search does not run. A line that has a sailing on or after your `from` confirmed within the last 12 hours (24 for HMM and for lines we do not search live) counts without being checked again; a line that failed in this search, or was skipped for our own request limits or paused after the line's repeated failures, counts only with sailings confirmed within that window. A line whose latest check of the lane found no sailings keeps its `no_service` or `no_sailings` without being checked again when that check was within the last 8 days (its `checked_at`) and your `from` is within the dates it covered or, when the end of those dates is not known, no more than 8 days after it. Also false when we could not ask a line over the whole requested window (our asks of it reach less far than the window's end); the line's own `status` does not change. A line that is `no_sailings` without a known date it was checked to (`covered_until` null) also makes `complete` false."
                        },
                        "transshipment": {
                          "type": "boolean",
                          "nullable": true,
                          "description": "The `transshipment` filter as we read it, or null for both kinds."
                        },
                        "changed_since": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "The `changed_since` you sent, as we read it, or null when the whole window was served."
                        },
                        "latest_departure_held": {
                          "type": "string",
                          "example": "2027-04-12T00:00:00",
                          "nullable": true,
                          "description": "The last departure anyone has filed on this lane, whatever your window and filters, port-local, or null when none. Before `to`, nobody has published that far yet, which is not the same as no service."
                        },
                        "as_of": {
                          "type": "string",
                          "format": "date-time",
                          "description": "Keep the `as_of` of the first page of a walk and pass it back as `changed_since` on your next poll. Each page has its own, taken before its read. A delta can repeat a sailing; a sailing that changed while you were paging comes with that next poll."
                        },
                        "via_port_names": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "string"
                          },
                          "description": "Port names for every hub code in `transship_via` on this page."
                        },
                        "limit": {
                          "type": "integer",
                          "description": "The page size this answer used: the `limit` you sent, or 100."
                        },
                        "next": {
                          "type": "string",
                          "nullable": true,
                          "description": "The next page's request, cursor included: follow it while present and stop only when it is null, never on a short or empty page."
                        },
                        "sailings": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Sailing"
                          },
                          "description": "This page's sailings, soonest departure first, in a stable order. Each page is read when you request it, not from a snapshot: a sailing that changes while you page can be missed or repeated in this walk, and comes with the next poll from the first page's `as_of`."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A query we refuse before reading anything, free. `error.parameter` names the input at fault where there is one. Codes: `MISSING_PARAMETER` (origin or destination), `INVALID_DATE_FORMAT` (from), `DATE_IN_PAST` (from), `INVALID_WEEKS`, `INVALID_SCAC` (carrier), `INVALID_TRANSSHIPMENT`, `INVALID_CHANGED_SINCE`, `CHANGED_SINCE_IN_FUTURE`, `INVALID_LIMIT`, `INVALID_CURSOR`, `BAD_REQUEST` (routings), `UNKNOWN_ORIGIN_PORT` / `UNKNOWN_DESTINATION_PORT` (a code not in our port list and on none of our sailings: no line is checked and the request is not charged; both unknown is `UNKNOWN_ORIGIN_PORT` naming both, without `parameter`). `from`, `weeks`, `transshipment`, `changed_since` or `routings` given twice is refused with that parameter's own code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`UNAUTHORIZED`: no `Authorization: Bearer` header, or a key that is unknown, revoked or expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "`INSUFFICIENT_CREDITS`: Navo Tokens only (an MCP call from an account holding Navo Tokens). The balance could not pay for the answer once it was ready (it changed meanwhile, or the answer came from the one-minute cache). Nothing is charged and no data is returned. `error` carries `credits_required`, `credits_balance`, `top_up_url` and `packages`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`FORBIDDEN`: the account has no Schedules API access, or the key is not enabled for Schedules. `ACCESS_EXPIRED`: the access period of the account, or of the key, has ended. An MCP call (an API-key call carrying `X-Schedules-Surface: mcp`, or a call signed in through an MCP client's OAuth sign-in; a website sign-in never is, whatever header it sends) is held by the account's MCP access, not its API access, and without MCP access it is refused `FORBIDDEN` with the message that Ship Schedules is not enabled for MCP on this account. Navo Tokens open Ship Schedules through MCP without MCP access: an account holding Navo Tokens is refused only when MCP access was closed for it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "`QUOTA_EXCEEDED`: the monthly call limit of the account, or of the key where the key has its own, or the key's daily limit, is spent. `RATE_LIMITED`: over the key's pace (120 requests per 20 seconds, 60 for a test key), or too many failed key checks from one address. `SEARCH_LIMIT_REACHED`: a signed-in website search by an account without a Schedules subscription, past its searches for the month; resets with the month (UTC). On Navo Tokens (an MCP call from an account holding Navo Tokens), `QUOTA_EXCEEDED` means the balance cannot pay for this lookup; it is refused before any data is read, nothing is charged, and `error` carries `credits_required`, `credits_balance`, `top_up_url` and `packages`. A refused call is not counted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`DB_ERROR`: our database did not answer. Free; retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "`SERVICE_UNAVAILABLE`: the key or the account's access could not be checked just now. Free; retry after `Retry-After` seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "Schedules"
        ]
      }
    },
    "/sailings/covered": {
      "get": {
        "operationId": "getScheduleCoveredLanes",
        "summary": "Lanes with upcoming cached sailings",
        "responses": {
          "200": {
            "description": "The busiest lanes with upcoming sailings, deepest first, each with a count. A lane carries origin and destination UN/LOCODEs plus origin_name and destination_name, which fall back to the locode where a port has no name on file. This is a picker, not an inventory: we hold sailings on tens of thousands of lanes and this answers with the deepest few hundred of them, so a lane's absence here says it is not among the busiest and not that we have nothing on it. Ask /public/schedules for any lane directly."
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Schedules"
        ]
      }
    },
    "/reliability/lanes": {
      "get": {
        "operationId": "getLaneReliability",
        "summary": "Carriers on a lane ranked by observed on-time reliability (API key required; empty answer free)",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "description": "90-day on-time %, average/median/p90/max delay, berth precision in minutes, sample size, and a provisional flag for thin samples (observations below min_obs). Scores are measured against the schedule the carrier first declared.",
        "parameters": [
          {
            "$ref": "#/components/parameters/origin"
          },
          {
            "$ref": "#/components/parameters/destination"
          }
        ],
        "responses": {
          "200": {
            "description": "Ranked carriers for the lane",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "origin": {
                          "type": "string"
                        },
                        "destination": {
                          "type": "string"
                        },
                        "min_obs": {
                          "type": "integer"
                        },
                        "carriers": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/LaneCarrierReliability"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Schedules"
        ]
      }
    },
    "/reliability/covered": {
      "get": {
        "operationId": "getReliabilityCoveredLanes",
        "summary": "Lanes with reliability observations",
        "responses": {
          "200": {
            "description": "Lanes with carrier and observation counts, most-observed first. Each lane carries origin/destination UN/LOCODEs plus origin_name/destination_name (the locode itself when a port has no name on file)"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Schedules"
        ]
      }
    },
    "/reliability/leaderboard": {
      "get": {
        "operationId": "getCarrierLeaderboard",
        "summary": "Carrier-level on-time league table (API key required; empty answer free)",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "description": "Carriers ranked by overall observed on-time reliability across all lanes, with schedule-churn volatility per carrier.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of ranked carriers to return. Omit for the complete table.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Ranked carriers. total is the complete table size before limit is applied.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "object",
                      "required": [
                        "min_obs",
                        "total",
                        "carriers"
                      ],
                      "properties": {
                        "min_obs": {
                          "type": "integer"
                        },
                        "total": {
                          "type": "integer"
                        },
                        "carriers": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "required": [
                              "carrier_code",
                              "carrier_name",
                              "on_time_pct",
                              "avg_delay_hours",
                              "p90_delay_hours",
                              "max_delay_hours",
                              "observations",
                              "provisional",
                              "volatility"
                            ],
                            "properties": {
                              "carrier_code": {
                                "type": "string"
                              },
                              "carrier_name": {
                                "type": "string"
                              },
                              "on_time_pct": {
                                "type": [
                                  "number",
                                  "null"
                                ]
                              },
                              "avg_delay_hours": {
                                "type": [
                                  "number",
                                  "null"
                                ]
                              },
                              "p90_delay_hours": {
                                "type": [
                                  "number",
                                  "null"
                                ]
                              },
                              "max_delay_hours": {
                                "type": [
                                  "number",
                                  "null"
                                ]
                              },
                              "observations": {
                                "type": [
                                  "integer",
                                  "null"
                                ]
                              },
                              "provisional": {
                                "type": "boolean"
                              },
                              "volatility": {
                                "oneOf": [
                                  {
                                    "type": "null"
                                  },
                                  {
                                    "type": "object",
                                    "required": [
                                      "revisions_per_sailing",
                                      "mean_abs_etd_hours",
                                      "p90_abs_etd_hours",
                                      "late_etd_share",
                                      "churn_hours_per_sailing",
                                      "etd_revisions",
                                      "eta_revisions",
                                      "revised_sailings",
                                      "active_sailings",
                                      "level"
                                    ],
                                    "properties": {
                                      "revisions_per_sailing": {
                                        "type": [
                                          "number",
                                          "null"
                                        ]
                                      },
                                      "mean_abs_etd_hours": {
                                        "type": [
                                          "number",
                                          "null"
                                        ]
                                      },
                                      "p90_abs_etd_hours": {
                                        "type": [
                                          "number",
                                          "null"
                                        ]
                                      },
                                      "late_etd_share": {
                                        "type": [
                                          "number",
                                          "null"
                                        ]
                                      },
                                      "churn_hours_per_sailing": {
                                        "type": [
                                          "number",
                                          "null"
                                        ]
                                      },
                                      "etd_revisions": {
                                        "type": "integer"
                                      },
                                      "eta_revisions": {
                                        "type": "integer"
                                      },
                                      "revised_sailings": {
                                        "type": "integer"
                                      },
                                      "active_sailings": {
                                        "type": "integer"
                                      },
                                      "level": {
                                        "type": [
                                          "string",
                                          "null"
                                        ],
                                        "enum": [
                                          "low",
                                          "medium",
                                          "high",
                                          null
                                        ]
                                      }
                                    }
                                  }
                                ]
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "limit is not an integer from 1 through 100"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Schedules"
        ]
      }
    },
    "/ports/search": {
      "get": {
        "operationId": "searchPorts",
        "summary": "Resolve a port name to its UN/LOCODE",
        "parameters": [
          {
            "name": "name",
            "in": "query",
            "required": true,
            "description": "Port name, partial name, country, or UN/LOCODE (min 2 chars). Also accepted as q, the older spelling.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 8,
              "maximum": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Matching ports with UN/LOCODE, country, and coordinates"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "tags": [
          "Schedules"
        ]
      }
    },
    "/port-congestion": {
      "get": {
        "operationId": "getPortCongestion",
        "summary": "Congestion signal for a port (or the congested-ports leaderboard) (API key required; empty answer free)",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "port",
            "in": "query",
            "required": false,
            "description": "UN/LOCODE, e.g. NLRTM. Omit for the leaderboard of congested ports.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "level",
            "in": "query",
            "required": false,
            "description": "Leaderboard filter",
            "schema": {
              "type": "string",
              "enum": [
                "mild",
                "moderate",
                "severe"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Congestion level, expected added wait days, confidence, sources"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "`DB_ERROR`: our database did not answer. Free; retry. A failed read is never answered as `congested: false`."
          }
        },
        "tags": [
          "Schedules"
        ]
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Your API key, sent as `Authorization: Bearer <key>`. Keys are created in the dashboard. A production key is paced at 120 requests per 20 seconds and a test key at 60; beyond the pace, the only limit is the call quota agreed for your account. Each answer with data counts as 1 call; an empty answer is free."
      }
    },
    "parameters": {
      "origin": {
        "name": "origin",
        "in": "query",
        "required": true,
        "description": "Origin port UN/LOCODE, e.g. CNSHA",
        "schema": {
          "type": "string",
          "example": "CNSHA"
        }
      },
      "destination": {
        "name": "destination",
        "in": "query",
        "required": true,
        "description": "Destination port UN/LOCODE, e.g. NLRTM",
        "schema": {
          "type": "string",
          "example": "NLRTM"
        }
      },
      "from": {
        "name": "from",
        "in": "query",
        "required": false,
        "description": "First departure date to consider, YYYY-MM-DD; default today's date in UTC. Matched against each sailing's departure date in its port's local time, so with the default a sailing stays in the answer until the UTC date passes its departure date. A date before today (UTC) is refused `DATE_IN_PAST`.",
        "schema": {
          "type": "string",
          "format": "date"
        },
        "example": "2026-10-01"
      },
      "weeks": {
        "name": "weeks",
        "in": "query",
        "required": false,
        "description": "Window length in weeks from `from`: 4, 8 or 12, default 4. The end is exclusive, and a window longer than one page continues through `next`.",
        "schema": {
          "type": "integer",
          "enum": [
            4,
            8,
            12
          ],
          "default": 4
        }
      },
      "carrier": {
        "name": "carrier",
        "in": "query",
        "required": false,
        "description": "One or more four-character SCAC codes, comma-separated or repeated; omitted means every carrier. A code also finds the line's sailings filed under its other codes, and the answer echoes the code you sent.",
        "schema": {
          "type": "string"
        },
        "example": "MAEU,MSCU"
      },
      "transshipment": {
        "name": "transshipment",
        "in": "query",
        "required": false,
        "description": "`true` for sailings with transshipment, `false` for direct sailings only, omitted for both; any other value is refused.",
        "schema": {
          "type": "string",
          "enum": [
            "true",
            "false"
          ]
        }
      },
      "changed_since": {
        "name": "changed_since",
        "in": "query",
        "required": false,
        "description": "Only sailings new to us, or whose published departure or arrival moved, since this moment; pass back the `as_of` of the first page of your last walk. A new sailing's `last_changed_at` can be null or earlier than this moment. Takes YYYY-MM-DD (midnight UTC) or a full date-time; cut-off and name changes and removals are not reported.",
        "schema": {
          "type": "string",
          "example": "2026-09-11T06:00:00Z"
        }
      },
      "routings": {
        "name": "routings",
        "in": "query",
        "required": false,
        "description": "`all` returns every routing the line published for each departure, not just the main one. Extra routings come as separate sailings marked `is_alternative: true`. Not every line reports alternatives; for such a line each routing comes as a sailing of its own. Priced as a normal search. With `routings=all`, `limit` and `carriers[].count` count sailings as they do without the flag (with `transshipment`, a sailing counts when any of its routings fits), not routings, and alternatives come on top, so a page can hold more sailings than `limit`. `next` keeps `routings=all`; a cursor sent with the flag added or removed does not continue the walk and is billed as a new search. With `routings=all`, `changed_since` also returns a departure whose other routings changed; without the flag it does not, and `last_changed_at` moves only with the departure or arrival. With `routings=all`, `transshipment` judges each routing on its own legs: a departure is returned when any of its routings fits, and only the routings that fit are returned, so a departure can come with `is_alternative: true` alone. An alternative that repeats a routing already served for its departure except for the voyages (same ports, ships and times) is left out. A departure can come with two main routings (`is_alternative: false`) when the line offers two, or, until our next complete read of the lane, one the line has since dropped.",
        "schema": {
          "type": "string",
          "enum": [
            "all"
          ]
        }
      },
      "limit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "description": "Sailings per page, 1 to 500, default 100; a value outside the range is refused, not clamped. With `routings=all`, alternatives come on top of it.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 500,
          "default": 100
        }
      },
      "cursor": {
        "name": "cursor",
        "in": "query",
        "required": false,
        "description": "An opaque continuation token that `next` already carries; omit it on the first page and never build or edit it.",
        "schema": {
          "type": "string"
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "origin and destination are required"
      },
      "RateLimited": {
        "description": "Rate limit exceeded (120 requests per 20 seconds per IP)"
      }
    },
    "schemas": {
      "Sailing": {
        "type": "object",
        "description": "One carrier offer on the lane: what that line published for this departure.",
        "properties": {
          "sailing_id": {
            "type": "string",
            "format": "uuid",
            "description": "The key to store this sailing under. It stays the same when the line moves the dates of a sailing with a voyage number. It changes when the line changes the ship, the voyage or the routing, including the onward voyage at a transshipment hub, which some lines do often: to you that is one sailing gone and a new one added. With `routings=all`, an alternative carries the `sailing_id` of its departure and `is_alternative: true`; tell the routings of one departure apart by `route_legs`. Two main routings of one departure (`is_alternative: false`) each have their own `sailing_id`. An id can end when we merge two rows of one sailing, and removals are not reported."
          },
          "carrier_code": {
            "type": "string",
            "description": "The line's SCAC; with a `carrier` filter, the code you asked for."
          },
          "carrier_name": {
            "type": "string",
            "nullable": true,
            "description": "The line's trading name, or null where we hold none; program against `carrier_code`."
          },
          "service_name": {
            "type": "string",
            "nullable": true,
            "description": "The line's own name for the service loop, as published, or null; not comparable across carriers."
          },
          "vessel_name": {
            "type": "string",
            "nullable": true,
            "description": "The ship's name, `TBN` when the line has not named one yet, or null when it gave no ship."
          },
          "voyage_number": {
            "type": "string",
            "nullable": true,
            "description": "The line's voyage number, or null when it has not assigned one."
          },
          "vessel_imo": {
            "type": "string",
            "nullable": true,
            "description": "The ship's IMO number, or null when we cannot say for certain which ship this is."
          },
          "vessel": {
            "type": "object",
            "nullable": true,
            "description": "Particulars of the ship in `vessel_imo`, or null when we hold none for it.",
            "properties": {
              "teu": {
                "type": "integer",
                "nullable": true,
                "description": "Nominal capacity in TEU; read it with `teu_estimated`."
              },
              "teu_estimated": {
                "type": "boolean",
                "description": "True when `teu` is derived from deadweight rather than published for the ship."
              },
              "flag": {
                "type": "string",
                "nullable": true,
                "description": "Flag state as a country name, as the vessel register spells it."
              },
              "year_built": {
                "type": "integer",
                "nullable": true,
                "description": "Year the ship was built, from the vessel register."
              },
              "length_m": {
                "type": "number",
                "nullable": true,
                "description": "Ship's length in metres, from the vessel register."
              }
            }
          },
          "published_departure": {
            "type": "string",
            "example": "2026-10-01T13:00:00",
            "description": "Departure as the line publishes it, in the origin port's local time, with no offset; do not convert it."
          },
          "published_arrival": {
            "type": "string",
            "example": "2026-11-03T08:00:00",
            "nullable": true,
            "description": "Arrival as the line publishes it, in the destination port's local time, with no offset, or null when not published. Use `transit_days` for voyage length, not arrival minus departure."
          },
          "departure_precision": {
            "type": "string",
            "enum": [
              "minute",
              "day"
            ],
            "nullable": true,
            "description": "`minute` when the line published a departure time, `day` when only a date (the 00:00 is ours), null when unknown.",
            "example": "day"
          },
          "arrival_precision": {
            "type": "string",
            "enum": [
              "minute",
              "day"
            ],
            "nullable": true,
            "description": "The same as `departure_precision`, for `published_arrival`.",
            "example": "day"
          },
          "cy_cutoff": {
            "type": "string",
            "example": "2026-09-29T12:00:00",
            "nullable": true,
            "description": "Container yard cut-off, port-local with no offset; `cutoffs` holds every deadline and `cutoff_status` explains a null."
          },
          "doc_cutoff": {
            "type": "string",
            "example": "2026-09-28T16:00:00",
            "nullable": true,
            "description": "Documentation cut-off, port-local with no offset; `cutoffs` holds every deadline and `cutoff_status` explains a null."
          },
          "vgm_cutoff": {
            "type": "string",
            "example": "2026-09-29T09:00:00",
            "nullable": true,
            "description": "VGM (verified gross mass) cut-off, port-local with no offset; `cutoffs` holds every deadline and `cutoff_status` explains a null."
          },
          "cutoffs": {
            "type": "array",
            "nullable": true,
            "description": "Every deadline the line published for this sailing, or null when we hold none (see `cutoff_status`).",
            "items": {
              "type": "object",
              "properties": {
                "type": {
                  "type": "string",
                  "description": "Normalised deadline code (DCSA where one exists, `X-` prefixed otherwise); filter and compare on this."
                },
                "at": {
                  "type": "string",
                  "example": "2026-09-29T12:00:00",
                  "description": "The deadline in the local time of its port, with no offset."
                },
                "label": {
                  "type": "string",
                  "nullable": true,
                  "description": "The line's own wording for the deadline, to show a person, or null when we hold none."
                }
              }
            }
          },
          "cutoff_status": {
            "type": "string",
            "enum": [
              "published",
              "not_published",
              "not_held"
            ],
            "description": "Why cut-offs are present or null: `published`, `not_published` (the line publishes none), or `not_held` (we hold none from this line; ask it directly)."
          },
          "last_changed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When we last saw the line move the published departure or arrival, in UTC, or null if never. `changed_since` filters on this and also returns sailings new to us, whose `last_changed_at` can be null or earlier."
          },
          "is_alternative": {
            "type": "boolean",
            "description": "With `routings=all` only: true for an extra routing of the departure, false for its main one. Not every line reports alternatives; for such a line each routing is a sailing of its own."
          },
          "retrieved_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the line last confirmed this sailing to us, in UTC; never older than 14 days."
          },
          "transit_days": {
            "type": "integer",
            "nullable": true,
            "description": "The line's own transit time in days, or null when not published."
          },
          "is_transshipment": {
            "type": "boolean",
            "nullable": true,
            "description": "True when the box changes ship, false when it does not, null when we cannot tell."
          },
          "transship_via": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "nullable": true,
            "description": "Hub UN/LOCODEs on a transshipment, null otherwise."
          },
          "route_legs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RouteLeg"
            },
            "nullable": true,
            "description": "The routing as the line filed it, one entry per leg in order, or null when we have no legs for it (null alone does not mean direct). A direct sailing has its one leg when the line counts one, and null when the line lists only port calls."
          },
          "routing_status": {
            "type": "string",
            "enum": [
              "known",
              "unknown"
            ],
            "description": "`known` when `is_transshipment` is true or false, `unknown` when it is null."
          }
        }
      },
      "LaneCarrierReliability": {
        "type": "object",
        "properties": {
          "carrier_code": {
            "type": "string",
            "description": "SCAC"
          },
          "carrier_name": {
            "type": "string"
          },
          "on_time_pct": {
            "type": [
              "number",
              "string"
            ],
            "description": "Share of sailings that left within 24h of the carrier's declared date"
          },
          "avg_delay_hours": {
            "type": [
              "number",
              "string",
              "null"
            ]
          },
          "median_delay_hours": {
            "type": [
              "number",
              "string",
              "null"
            ]
          },
          "p90_delay_hours": {
            "type": [
              "number",
              "string",
              "null"
            ]
          },
          "max_delay_hours": {
            "type": [
              "number",
              "string",
              "null"
            ]
          },
          "observations": {
            "type": "integer",
            "description": "Completed sailings measured"
          },
          "provisional": {
            "type": "boolean",
            "description": "true when observations < min_obs (early data)"
          },
          "dep_precision_p50_min": {
            "type": [
              "number",
              "string",
              "null"
            ]
          },
          "berth_precision_p50_min": {
            "type": [
              "number",
              "string",
              "null"
            ],
            "description": "Median planned-vs-actual berth window, minutes"
          },
          "delay_basis": {
            "type": "string",
            "enum": [
              "published_etd",
              "first_declared"
            ],
            "nullable": true,
            "description": "What the delay is measured against: the carrier's published ETD, or the first time declared for the sailing. Null when we have no documented basis."
          }
        }
      },
      "RouteLeg": {
        "type": "object",
        "description": "One leg of the routing as the line filed it, with the same keys on every leg.",
        "properties": {
          "leg": {
            "type": "integer",
            "nullable": true,
            "description": "Position of the leg in the routing, from 1."
          },
          "from": {
            "type": "string",
            "nullable": true,
            "description": "The leg's departure port: a UN/LOCODE, or the line's own code where we hold no translation."
          },
          "from_name": {
            "type": "string",
            "nullable": true,
            "description": "The departure port's name as the line wrote it; null when the line gives none."
          },
          "to": {
            "type": "string",
            "nullable": true,
            "description": "The leg's arrival port: a UN/LOCODE, or the line's own code where we hold no translation."
          },
          "to_name": {
            "type": "string",
            "nullable": true,
            "description": "The arrival port's name as the line wrote it; null when the line gives none."
          },
          "vessel": {
            "type": "string",
            "nullable": true,
            "description": "The ship on this leg, `TBN` when not yet named, or null when the leg has no ship (see `mode`)."
          },
          "mode": {
            "type": "string",
            "enum": [
              "water",
              "feeder",
              "rail",
              "truck",
              "barge",
              "inland",
              "unknown"
            ],
            "description": "What the leg travels on: `water` and `feeder` are ships, `rail`, `truck`, `barge` and `inland` are not transshipment, `unknown` is a ship not yet named."
          },
          "voyage": {
            "type": "string",
            "nullable": true,
            "description": "The line's voyage number for this leg, or null when not assigned."
          },
          "service": {
            "type": "string",
            "nullable": true,
            "description": "The line's own service name for this leg, or null when not given."
          },
          "departure": {
            "type": "string",
            "example": "2026-10-01T13:00:00",
            "nullable": true,
            "description": "Departure in the local time of this leg's port, with no offset."
          },
          "arrival": {
            "type": "string",
            "example": "2026-10-05T06:00:00",
            "nullable": true,
            "description": "Arrival in the local time of this leg's port, with no offset."
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "ok",
          "error"
        ],
        "description": "Every refusal and error: branch on `error.code`, show `error.message` to a person.",
        "properties": {
          "ok": {
            "type": "boolean",
            "enum": [
              false
            ]
          },
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string"
              },
              "message": {
                "type": "string"
              },
              "parameter": {
                "type": "string",
                "description": "On a 400: the query parameter at fault, where there is one."
              },
              "credits_required": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "Navo Tokens refusals only: the tokens this call needs."
              },
              "credits_balance": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "Navo Tokens refusals only: the account's balance."
              },
              "top_up_url": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Navo Tokens refusals only: where to buy more tokens; null when no link is configured."
              },
              "packages": {
                "type": "array",
                "description": "Navo Tokens refusals only: the packages on sale.",
                "items": {
                  "type": "object",
                  "properties": {
                    "plan": {
                      "type": "string",
                      "description": "starter, growth or scale."
                    },
                    "credits": {
                      "type": "integer",
                      "description": "Tokens in the package."
                    },
                    "usd": {
                      "type": "number"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}
