{
  "openapi": "3.0.3",
  "info": {
    "title": "Ayes & Noes — UK Parliament API",
    "description": "Free, read-only JSON API for the UK House of Commons: MPs, parties, divisions (recorded votes) and Early Day Motions. Built on official UK Parliament open data (Open Parliament Licence v3.0). No API key required; a generous per-IP rate limit applies. Please credit \"Ayes & Noes\" and link back to ayesandnoes.co.uk. Human docs: https://ayesandnoes.co.uk/developers",
    "version": "1.0.0",
    "contact": { "name": "Ayes & Noes", "email": "info@ayesandnoes.co.uk", "url": "https://ayesandnoes.co.uk/developers" },
    "license": { "name": "Open Parliament Licence v3.0", "url": "https://www.parliament.uk/site-information/copyright-parliament/open-parliament-licence/" }
  },
  "servers": [{ "url": "https://ayesandnoes.co.uk/api/v1", "description": "Production" }],
  "tags": [
    { "name": "MPs" }, { "name": "Parties" }, { "name": "Divisions" }, { "name": "Early Day Motions" }
  ],
  "paths": {
    "/mps": {
      "get": {
        "tags": ["MPs"],
        "summary": "List current MPs",
        "parameters": [
          { "name": "party", "in": "query", "schema": { "type": "string" }, "description": "Filter by party slug (e.g. labour-15)." },
          { "name": "constituency", "in": "query", "schema": { "type": "string" }, "description": "Filter by constituency slug." },
          { "name": "q", "in": "query", "schema": { "type": "string" }, "description": "Case-insensitive name search." },
          { "$ref": "#/components/parameters/limit" },
          { "$ref": "#/components/parameters/offset" }
        ],
        "responses": {
          "200": {
            "description": "A page of current MPs.",
            "content": { "application/json": { "schema": {
              "allOf": [
                { "$ref": "#/components/schemas/Envelope" },
                { "$ref": "#/components/schemas/Paging" },
                { "type": "object", "properties": { "mps": { "type": "array", "items": { "$ref": "#/components/schemas/MpSummary" } } } }
              ]
            } } }
          },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/mps/{id}": {
      "get": {
        "tags": ["MPs"],
        "summary": "Get one MP (with voting summary)",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Parliament member id (integer) or slug." }
        ],
        "responses": {
          "200": {
            "description": "The MP profile and voting summary.",
            "content": { "application/json": { "schema": {
              "allOf": [
                { "$ref": "#/components/schemas/Envelope" },
                { "type": "object", "properties": { "mp": { "$ref": "#/components/schemas/MpDetail" } } }
              ]
            } } }
          },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/parties": {
      "get": {
        "tags": ["Parties"],
        "summary": "List parties with live seat counts",
        "responses": {
          "200": {
            "description": "Parties, ordered by seat count.",
            "content": { "application/json": { "schema": {
              "allOf": [
                { "$ref": "#/components/schemas/Envelope" },
                { "type": "object", "properties": { "count": { "type": "integer" }, "parties": { "type": "array", "items": { "$ref": "#/components/schemas/Party" } } } }
              ]
            } } }
          },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/parties/{slug}": {
      "get": {
        "tags": ["Parties"],
        "summary": "Get one party (with its current MPs)",
        "parameters": [
          { "name": "slug", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Party slug (e.g. labour-15)." }
        ],
        "responses": {
          "200": {
            "description": "The party, seat count, colours and member list.",
            "content": { "application/json": { "schema": {
              "allOf": [
                { "$ref": "#/components/schemas/Envelope" },
                { "type": "object", "properties": { "party": { "$ref": "#/components/schemas/PartyDetail" } } }
              ]
            } } }
          },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/divisions": {
      "get": {
        "tags": ["Divisions"],
        "summary": "List Commons divisions (recorded votes)",
        "parameters": [
          { "name": "from", "in": "query", "schema": { "type": "string", "format": "date" }, "description": "Only divisions on/after this date (YYYY-MM-DD)." },
          { "name": "to", "in": "query", "schema": { "type": "string", "format": "date" }, "description": "Only divisions on/before this date (YYYY-MM-DD)." },
          { "name": "topic", "in": "query", "schema": { "type": "string" }, "description": "Filter by topic slug (locally assigned tags)." },
          { "name": "category", "in": "query", "schema": { "type": "string" }, "description": "Filter by division category." },
          { "$ref": "#/components/parameters/limit" },
          { "$ref": "#/components/parameters/offset" }
        ],
        "responses": {
          "200": {
            "description": "A page of divisions, newest first.",
            "content": { "application/json": { "schema": {
              "allOf": [
                { "$ref": "#/components/schemas/Envelope" },
                { "$ref": "#/components/schemas/Paging" },
                { "type": "object", "properties": { "divisions": { "type": "array", "items": { "$ref": "#/components/schemas/DivisionSummary" } } } }
              ]
            } } }
          },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/divisions/{id}": {
      "get": {
        "tags": ["Divisions"],
        "summary": "Get one division (with per-party breakdown)",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Parliament division id." }
        ],
        "responses": {
          "200": {
            "description": "The division, result, motion and per-party aye/no split.",
            "content": { "application/json": { "schema": {
              "allOf": [
                { "$ref": "#/components/schemas/Envelope" },
                { "type": "object", "properties": { "division": { "$ref": "#/components/schemas/DivisionDetail" } } }
              ]
            } } }
          },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/edms": {
      "get": {
        "tags": ["Early Day Motions"],
        "summary": "List Early Day Motions",
        "parameters": [
          { "name": "from", "in": "query", "schema": { "type": "string", "format": "date" }, "description": "Only EDMs tabled on/after this date (YYYY-MM-DD)." },
          { "name": "to", "in": "query", "schema": { "type": "string", "format": "date" }, "description": "Only EDMs tabled on/before this date (YYYY-MM-DD)." },
          { "$ref": "#/components/parameters/limit" },
          { "$ref": "#/components/parameters/offset" }
        ],
        "responses": {
          "200": {
            "description": "A page of Early Day Motions, newest first.",
            "content": { "application/json": { "schema": {
              "allOf": [
                { "$ref": "#/components/schemas/Envelope" },
                { "$ref": "#/components/schemas/Paging" },
                { "type": "object", "properties": { "edms": { "type": "array", "items": { "$ref": "#/components/schemas/EdmSummary" } } } }
              ]
            } } }
          },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/edms/{id}": {
      "get": {
        "tags": ["Early Day Motions"],
        "summary": "Get one Early Day Motion (with signatories)",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Parliament EDM id." }
        ],
        "responses": {
          "200": {
            "description": "The EDM, full text, sponsor and signatories.",
            "content": { "application/json": { "schema": {
              "allOf": [
                { "$ref": "#/components/schemas/Envelope" },
                { "type": "object", "properties": { "edm": { "$ref": "#/components/schemas/EdmDetail" } } }
              ]
            } } }
          },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "limit": { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 50, "maximum": 200, "minimum": 1 }, "description": "Page size (max 200)." },
      "offset": { "name": "offset", "in": "query", "schema": { "type": "integer", "default": 0, "minimum": 0 }, "description": "Rows to skip." }
    },
    "responses": {
      "NotFound": { "description": "No matching record.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "RateLimited": {
        "description": "Rate limit exceeded (per-IP). Retry after the window.",
        "headers": { "Retry-After": { "schema": { "type": "integer" }, "description": "Seconds to wait." } },
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    },
    "schemas": {
      "Envelope": {
        "type": "object",
        "description": "Standard header on every response.",
        "properties": {
          "dataset": { "type": "string" },
          "api_version": { "type": "string", "example": "v1" },
          "licence": { "type": "string", "example": "Open Parliament Licence v3.0" },
          "source": { "type": "string", "format": "uri" },
          "attribution": { "type": "string" }
        }
      },
      "Paging": {
        "type": "object",
        "properties": {
          "total": { "type": "integer", "description": "Total matching rows." },
          "count": { "type": "integer", "description": "Rows in this response." },
          "limit": { "type": "integer" },
          "offset": { "type": "integer" }
        }
      },
      "PartyRef": {
        "type": "object",
        "properties": {
          "name": { "type": "string" },
          "abbreviation": { "type": "string", "nullable": true },
          "slug": { "type": "string" }
        }
      },
      "ConstituencyRef": {
        "type": "object",
        "properties": { "name": { "type": "string" }, "slug": { "type": "string" } }
      },
      "MpSummary": {
        "type": "object",
        "properties": {
          "parliament_member_id": { "type": "integer" },
          "name": { "type": "string" },
          "slug": { "type": "string" },
          "party": { "$ref": "#/components/schemas/PartyRef" },
          "constituency": { "$ref": "#/components/schemas/ConstituencyRef" },
          "thumbnail_url": { "type": "string", "format": "uri", "nullable": true },
          "x_handle": { "type": "string", "nullable": true },
          "bluesky_handle": { "type": "string", "nullable": true },
          "url": { "type": "string", "format": "uri" }
        }
      },
      "MpDetail": {
        "type": "object",
        "properties": {
          "parliament_member_id": { "type": "integer" },
          "name": { "type": "string" },
          "slug": { "type": "string" },
          "party": { "$ref": "#/components/schemas/PartyRef" },
          "constituency": { "$ref": "#/components/schemas/ConstituencyRef" },
          "is_current_mp": { "type": "boolean" },
          "date_of_birth": { "type": "string", "format": "date", "nullable": true },
          "commons_membership_start": { "type": "string", "format": "date", "nullable": true },
          "tenure_days": { "type": "integer", "nullable": true },
          "official_website": { "type": "string", "format": "uri", "nullable": true },
          "thumbnail_url": { "type": "string", "format": "uri", "nullable": true },
          "x_handle": { "type": "string", "nullable": true },
          "bluesky_handle": { "type": "string", "nullable": true },
          "voting": {
            "type": "object",
            "nullable": true,
            "properties": {
              "recorded_votes": { "type": "integer" },
              "ayes": { "type": "integer" },
              "noes": { "type": "integer" },
              "rebellions": { "type": "integer", "description": "Votes against the MP's own party (excludes tellers)." },
              "tellerships": { "type": "integer" }
            }
          },
          "url": { "type": "string", "format": "uri" }
        }
      },
      "Party": {
        "type": "object",
        "properties": {
          "name": { "type": "string" },
          "slug": { "type": "string" },
          "abbreviation": { "type": "string", "nullable": true },
          "seats": { "type": "integer", "description": "Live count of current MPs." },
          "url": { "type": "string", "format": "uri" }
        }
      },
      "PartyDetail": {
        "type": "object",
        "properties": {
          "name": { "type": "string" },
          "slug": { "type": "string" },
          "abbreviation": { "type": "string", "nullable": true },
          "colour": {
            "type": "object",
            "properties": { "background": { "type": "string", "nullable": true }, "foreground": { "type": "string", "nullable": true } }
          },
          "seats": { "type": "integer" },
          "url": { "type": "string", "format": "uri" },
          "mps": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "parliament_member_id": { "type": "integer" },
                "name": { "type": "string" },
                "slug": { "type": "string" },
                "constituency": { "$ref": "#/components/schemas/ConstituencyRef" },
                "url": { "type": "string", "format": "uri" }
              }
            }
          }
        }
      },
      "TopicRef": {
        "type": "object",
        "properties": { "name": { "type": "string" }, "slug": { "type": "string" } }
      },
      "DivisionSummary": {
        "type": "object",
        "properties": {
          "parliament_division_id": { "type": "integer" },
          "date": { "type": "string", "format": "date" },
          "title": { "type": "string" },
          "result": { "type": "string", "enum": ["Passed", "Rejected", "Tied"] },
          "ayes": { "type": "integer" },
          "noes": { "type": "integer" },
          "category": { "type": "string", "nullable": true },
          "url": { "type": "string", "format": "uri" }
        }
      },
      "DivisionDetail": {
        "type": "object",
        "properties": {
          "parliament_division_id": { "type": "integer" },
          "date": { "type": "string", "format": "date" },
          "title": { "type": "string" },
          "result": { "type": "string", "enum": ["Passed", "Rejected", "Tied"] },
          "ayes": { "type": "integer" },
          "noes": { "type": "integer" },
          "category": { "type": "string", "nullable": true },
          "summary": { "type": "string", "nullable": true },
          "plain_english_summary": { "type": "string", "nullable": true },
          "motion_text": { "type": "string", "nullable": true },
          "proposer": { "type": "string", "nullable": true },
          "topics": { "type": "array", "items": { "$ref": "#/components/schemas/TopicRef" } },
          "party_breakdown": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": { "party": { "type": "string" }, "aye": { "type": "integer" }, "no": { "type": "integer" } }
            }
          },
          "url": { "type": "string", "format": "uri" },
          "source_url": { "type": "string", "format": "uri" }
        }
      },
      "SponsorRef": {
        "type": "object",
        "properties": { "member_id": { "type": "integer" }, "name": { "type": "string" } }
      },
      "EdmSummary": {
        "type": "object",
        "properties": {
          "parliament_edm_id": { "type": "integer" },
          "uin": { "type": "string" },
          "title": { "type": "string" },
          "date_tabled": { "type": "string", "format": "date" },
          "primary_sponsor": { "$ref": "#/components/schemas/SponsorRef" },
          "sponsors_count": { "type": "integer" },
          "url": { "type": "string", "format": "uri" }
        }
      },
      "EdmDetail": {
        "type": "object",
        "properties": {
          "parliament_edm_id": { "type": "integer" },
          "uin": { "type": "string" },
          "title": { "type": "string" },
          "motion_text": { "type": "string" },
          "date_tabled": { "type": "string", "format": "date" },
          "primary_sponsor": { "$ref": "#/components/schemas/SponsorRef" },
          "sponsors_count": { "type": "integer" },
          "topics": { "type": "array", "items": { "$ref": "#/components/schemas/TopicRef" } },
          "signatories": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "member_id": { "type": "integer" },
                "name": { "type": "string" },
                "signing_order": { "type": "integer" }
              }
            }
          },
          "url": { "type": "string", "format": "uri" }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": { "type": "string", "example": "not_found" },
          "message": { "type": "string" }
        }
      }
    }
  }
}
