{
  "openapi": "3.0.3",
  "info": {
    "title": "RidingWatch API",
    "version": "1.0.0",
    "description": "Live output of the RidingWatch seat-projection model for the 46th Canadian federal election. The model aggregates all published federal polls (sample-size weighting, 28-day exponential decay, pollster house effects), projects all 343 ridings, and runs 10,000 correlated Monte Carlo simulations. Data refreshes with each weekly update cycle. Rate limit: 100 requests/minute per IP. Attribution required (CC BY-NC 4.0): cite RidingWatch with the reference date.",
    "contact": {
      "name": "RidingWatch",
      "email": "hello@ridingwatch.ca",
      "url": "https://ridingwatch.ca/"
    },
    "license": {
      "name": "CC BY-NC 4.0",
      "url": "https://creativecommons.org/licenses/by-nc/4.0/"
    }
  },
  "servers": [
    {
      "url": "https://api.ridingwatch.ca"
    }
  ],
  "paths": {
    "/api/health": {
      "get": {
        "operationId": "getHealth",
        "summary": "Server health and model metadata",
        "responses": {
          "200": {
            "description": "Health status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "ok"
                    },
                    "version": {
                      "type": "string"
                    },
                    "ridingsCount": {
                      "type": "integer",
                      "example": 343
                    },
                    "nSimulations": {
                      "type": "integer",
                      "example": 10000
                    },
                    "lastModelUpdate": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "uptime": {
                      "type": "number"
                    },
                    "timestamp": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/projections": {
      "get": {
        "operationId": "getProjections",
        "summary": "Full current model output",
        "description": "National polling average, snapshots (last week / three months / last election), deterministic seat counts, Monte Carlo seat distributions per party, per-simulation outcome probabilities, and all 343 riding projections. Supports ETag caching (If-None-Match returns 304).",
        "responses": {
          "200": {
            "description": "Complete model state",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "pollingAvg": {
                      "$ref": "#/components/schemas/PartyShares"
                    },
                    "snapshots": {
                      "type": "object",
                      "properties": {
                        "lastWeek": {
                          "$ref": "#/components/schemas/PartyShares"
                        },
                        "threeMonths": {
                          "$ref": "#/components/schemas/PartyShares"
                        },
                        "election": {
                          "$ref": "#/components/schemas/PartyShares"
                        }
                      }
                    },
                    "seats": {
                      "$ref": "#/components/schemas/SeatsShort"
                    },
                    "mcSeats": {
                      "type": "object",
                      "description": "Monte Carlo seat distribution per party (keys LPC, CPC, NDP, BQ, GRN)",
                      "additionalProperties": {
                        "$ref": "#/components/schemas/McPartySeats"
                      }
                    },
                    "mcScenarios": {
                      "type": "object",
                      "description": "Per-simulation outcome probabilities (0–1)",
                      "properties": {
                        "lpcMajority": {
                          "type": "number"
                        },
                        "lpcMinority": {
                          "type": "number"
                        },
                        "cpcMajority": {
                          "type": "number"
                        },
                        "cpcMinority": {
                          "type": "number"
                        },
                        "otherMajority": {
                          "type": "number"
                        },
                        "otherMinority": {
                          "type": "number"
                        },
                        "hung": {
                          "type": "number"
                        }
                      }
                    },
                    "ridings": {
                      "type": "array",
                      "description": "All 343 riding projections",
                      "items": {
                        "type": "object"
                      }
                    },
                    "mcDuration": {
                      "type": "number",
                      "description": "Simulation runtime in ms"
                    },
                    "timestamp": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not modified (ETag match)"
          }
        }
      }
    },
    "/api/scenario": {
      "get": {
        "operationId": "getScenario",
        "summary": "What-if seat calculator",
        "description": "Deterministic seat counts for hypothetical national vote shares. Unallocated share is treated as Other (PPC, independents). Useful for questions like 'what happens if the Liberals drop to 38%?'.",
        "parameters": [
          {
            "name": "lpc",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100
            },
            "description": "Liberal national vote share (%)"
          },
          {
            "name": "cpc",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100
            },
            "description": "Conservative national vote share (%)"
          },
          {
            "name": "ndp",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100
            },
            "description": "NDP national vote share (%)"
          },
          {
            "name": "bq",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100
            },
            "description": "Bloc Québécois national vote share (%); the model applies it within Quebec only"
          },
          {
            "name": "grn",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100
            },
            "description": "Green national vote share (%)"
          }
        ],
        "responses": {
          "200": {
            "description": "Seat counts for the scenario",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "pollAvg": {
                      "$ref": "#/components/schemas/PartyShares"
                    },
                    "seats": {
                      "type": "object",
                      "properties": {
                        "LPC": {
                          "type": "integer"
                        },
                        "CPC": {
                          "type": "integer"
                        },
                        "NDP": {
                          "type": "integer"
                        },
                        "BQ": {
                          "type": "integer"
                        },
                        "GRN": {
                          "type": "integer"
                        },
                        "total": {
                          "type": "integer",
                          "example": 343
                        }
                      }
                    },
                    "majorityThreshold": {
                      "type": "integer",
                      "example": 172
                    },
                    "hasMajority": {
                      "type": "object",
                      "properties": {
                        "LPC": {
                          "type": "boolean"
                        },
                        "CPC": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid vote shares (each must be 0–100; combined ≤ 105)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/riding/{name}": {
      "get": {
        "operationId": "getRiding",
        "summary": "Single riding detail",
        "description": "2025 and 2021 results, current projection, and Monte Carlo win probabilities for one riding. Name must match the official riding name (URL-encoded), e.g. /api/riding/Avalon or /api/riding/Notre-Dame-de-Gr%C3%A2ce%E2%80%94Westmount.",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Official riding name (2023 Representation Order), URL-encoded"
          }
        ],
        "responses": {
          "200": {
            "description": "Riding detail",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "province": {
                      "type": "string",
                      "example": "NL"
                    },
                    "name": {
                      "type": "string"
                    },
                    "winner": {
                      "type": "string",
                      "description": "Projected winning party"
                    },
                    "ratingClass": {
                      "type": "string",
                      "description": "Competitiveness rating class"
                    },
                    "vote2025": {
                      "$ref": "#/components/schemas/PartyShares"
                    },
                    "vote2021": {
                      "$ref": "#/components/schemas/PartyShares"
                    },
                    "projected": {
                      "$ref": "#/components/schemas/PartyShares"
                    },
                    "mcData": {
                      "type": "object",
                      "description": "Monte Carlo output for this riding (win probabilities per party)"
                    },
                    "timestamp": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Riding not found"
          }
        }
      }
    },
    "/api/ridings": {
      "get": {
        "operationId": "listRidings",
        "summary": "All 343 ridings summary",
        "responses": {
          "200": {
            "description": "Summary list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "count": {
                      "type": "integer",
                      "example": 343
                    },
                    "ridings": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "province": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "winner": {
                            "type": "string"
                          },
                          "ratingClass": {
                            "type": "string"
                          },
                          "projected": {
                            "$ref": "#/components/schemas/PartyShares"
                          },
                          "mcWinner": {
                            "type": "string"
                          },
                          "mcTopProb": {
                            "type": "number"
                          }
                        }
                      }
                    },
                    "timestamp": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/headline": {
      "get": {
        "operationId": "getHeadline",
        "summary": "Daily editorial headline",
        "description": "One-sentence summary of the current projection, regenerated daily (Eastern time). May reference the day's national political news. Numbers in the text match the live model output.",
        "responses": {
          "200": {
            "description": "Current headline",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "headline": {
                      "type": "string"
                    },
                    "generatedAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "source": {
                      "type": "string",
                      "enum": [
                        "llm",
                        "fallback"
                      ]
                    },
                    "modelTimestamp": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "PartyShares": {
        "type": "object",
        "description": "Vote shares (%) per party",
        "properties": {
          "lpc": {
            "type": "number"
          },
          "cpc": {
            "type": "number"
          },
          "ndp": {
            "type": "number"
          },
          "bq": {
            "type": "number"
          },
          "grn": {
            "type": "number"
          }
        }
      },
      "SeatsShort": {
        "type": "object",
        "description": "Deterministic seat counts (l=LPC, c=CPC, n=NDP, b=BQ, g=GRN)",
        "properties": {
          "l": {
            "type": "integer"
          },
          "c": {
            "type": "integer"
          },
          "n": {
            "type": "integer"
          },
          "b": {
            "type": "integer"
          },
          "g": {
            "type": "integer"
          }
        }
      },
      "McPartySeats": {
        "type": "object",
        "description": "Monte Carlo seat distribution for one party",
        "properties": {
          "mean": {
            "type": "integer"
          },
          "lo": {
            "type": "integer",
            "description": "95% CI lower bound"
          },
          "hi": {
            "type": "integer",
            "description": "95% CI upper bound"
          },
          "probMajority": {
            "type": "number",
            "description": "P(≥172 seats), 0–1"
          }
        }
      }
    }
  }
}
