{
  "openapi": "3.1.0",
  "info": {
    "title": "SchedulesMCP Public API",
    "version": "1.0.0",
    "description": "Free, unauthenticated REST endpoints for live ocean sailing schedules and observed carrier on-time reliability — the same engine that powers schedulesmcp.com. Rate limit: 120 requests/minute per IP; responses cached for 60 seconds. 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://schedulesmcp-api.fly.dev" }],
  "paths": {
    "/public/schedules": {
      "get": {
        "operationId": "getLaneSchedules",
        "summary": "Upcoming published sailings on a lane",
        "description": "Cached forward sailings from the carriers' own point-to-point schedules, soonest departure first (max 200).",
        "parameters": [
          { "$ref": "#/components/parameters/origin" },
          { "$ref": "#/components/parameters/destination" }
        ],
        "responses": {
          "200": {
            "description": "Sailings for the lane",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "data": {
                      "type": "object",
                      "properties": {
                        "origin": { "type": "string" },
                        "destination": { "type": "string" },
                        "sailings": {
                          "type": "array",
                          "items": { "$ref": "#/components/schemas/Sailing" }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/public/schedules/cards": {
      "get": {
        "operationId": "getLaneSailingCards",
        "summary": "Vessel-first sailing cards for a lane",
        "description": "One card per physical vessel and departure: slot partners grouped inside, routing (transit/legs/transshipment/CO2), live AIS vessel particulars, cut-offs, per-carrier reliability, lane capacity, and suspected blank sailings.",
        "parameters": [
          { "$ref": "#/components/parameters/origin" },
          { "$ref": "#/components/parameters/destination" }
        ],
        "responses": {
          "200": { "description": "Vessel cards, lane capacity, and suspected blank sailings" },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/public/schedules/covered": {
      "get": {
        "operationId": "getScheduleCoveredLanes",
        "summary": "Lanes with upcoming cached sailings",
        "responses": {
          "200": { "description": "Lanes with a count of upcoming sailings, busiest first" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/public/reliability/lanes": {
      "get": {
        "operationId": "getLaneReliability",
        "summary": "Carriers on a lane ranked by observed on-time reliability",
        "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" }
        }
      }
    },
    "/public/reliability/covered": {
      "get": {
        "operationId": "getReliabilityCoveredLanes",
        "summary": "Lanes with reliability observations",
        "responses": {
          "200": {
            "description": "Lanes with carrier and observation counts, most-observed first"
          },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/public/reliability/leaderboard": {
      "get": {
        "operationId": "getCarrierLeaderboard",
        "summary": "Carrier-level on-time league table",
        "description": "Carriers ranked by overall observed on-time reliability across all lanes, with schedule-churn volatility per carrier.",
        "responses": {
          "200": { "description": "Ranked carriers" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/public/ports/search": {
      "get": {
        "operationId": "searchPorts",
        "summary": "Resolve a port name to its UN/LOCODE",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "Port name, partial name, or UN/LOCODE (min 2 chars)",
            "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" }
        }
      }
    },
    "/public/port-congestion": {
      "get": {
        "operationId": "getPortCongestion",
        "summary": "Congestion signal for a port (or the congested-ports leaderboard)",
        "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" }
        }
      }
    }
  },
  "components": {
    "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" }
      }
    },
    "responses": {
      "BadRequest": { "description": "origin and destination are required" },
      "RateLimited": { "description": "Rate limit exceeded (120 requests/minute per IP)" }
    },
    "schemas": {
      "Sailing": {
        "type": "object",
        "properties": {
          "carrier_code": { "type": "string", "description": "SCAC" },
          "carrier_name": { "type": "string" },
          "service_name": { "type": ["string", "null"] },
          "vessel_name": { "type": ["string", "null"] },
          "voyage_number": { "type": ["string", "null"] },
          "published_departure": { "type": ["string", "null"], "format": "date-time" },
          "published_arrival": { "type": ["string", "null"], "format": "date-time" },
          "cy_cutoff": { "type": ["string", "null"], "format": "date-time" },
          "source": { "type": ["string", "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"
          },
          "source": {
            "type": ["string", "null"],
            "description": "weiyun_declared = measured vs first-declared schedule"
          }
        }
      }
    }
  }
}
