{
  "openapi": "3.1.0",
  "info": {
    "title": "DeuceLeague API (Cloudflare migration preview)",
    "version": "0.1.0"
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer"
      },
      "session": {
        "type": "http",
        "scheme": "bearer"
      },
      "loginLink": {
        "type": "http",
        "scheme": "bearer"
      }
    },
    "schemas": {
      "Problem": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "example": "urn:deuceleague:problem:invalid_credential"
          },
          "code": {
            "type": "string",
            "example": "invalid_credential"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "integer"
          },
          "detail": {
            "type": "string"
          },
          "request_id": {
            "type": "string",
            "description": "Matches the X-Request-Id header and the server's log line."
          }
        },
        "required": [
          "type",
          "code",
          "title",
          "status",
          "request_id"
        ],
        "additionalProperties": {},
        "description": "An error, as RFC 9457 problem details."
      },
      "Me": {
        "type": "object",
        "properties": {
          "club": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "slug": {
                "type": "string",
                "example": "deuce-ltc"
              },
              "name": {
                "type": "string",
                "example": "Deuce Lawn Tennis Club"
              },
              "timezone": {
                "type": "string",
                "example": "Europe/London",
                "description": "IANA zone every deadline is counted in."
              }
            },
            "required": [
              "id",
              "slug",
              "name",
              "timezone"
            ]
          },
          "credential": {
            "oneOf": [
              {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "enum": [
                      "api_key"
                    ]
                  },
                  "id": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "name": {
                    "type": "string",
                    "example": "Telegram bot"
                  },
                  "prefix": {
                    "type": "string",
                    "example": "dl_Xk3v9Q",
                    "description": "The key's first characters, to tell keys apart."
                  },
                  "scopes": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "league:read",
                        "league:write",
                        "results:write",
                        "members:read",
                        "members:write",
                        "members:pii",
                        "admin"
                      ]
                    }
                  }
                },
                "required": [
                  "type",
                  "id",
                  "name",
                  "prefix",
                  "scopes"
                ]
              },
              {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "enum": [
                      "session"
                    ]
                  },
                  "id": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "scopes": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "league:read",
                        "league:write",
                        "results:write",
                        "members:read",
                        "members:write",
                        "members:pii",
                        "admin"
                      ]
                    },
                    "description": "A player's: read the league, and report results — for their own side only."
                  },
                  "member": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string",
                        "format": "uuid"
                      },
                      "display_name": {
                        "type": "string",
                        "example": "Sam Kerr"
                      },
                      "status": {
                        "type": "string",
                        "enum": [
                          "active",
                          "paused",
                          "left"
                        ]
                      },
                      "leaving_at": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "date-time",
                        "description": "When they said they are not playing next season at all, if they did. See `POST /v1/members/{id}/leave`."
                      },
                      "wants_to_play": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "enum": [
                          "singles",
                          "doubles",
                          "mixed_doubles",
                          "both",
                          "singles_mixed",
                          "doubles_mixed",
                          "all",
                          "not_now",
                          null
                        ],
                        "description": "What they want to play next season, if they have said. See `PUT /v1/members/{id}/wants-to-play`."
                      }
                    },
                    "required": [
                      "id",
                      "display_name",
                      "status",
                      "leaving_at",
                      "wants_to_play"
                    ],
                    "description": "Who is signed in."
                  }
                },
                "required": [
                  "type",
                  "id",
                  "scopes",
                  "member"
                ]
              }
            ]
          }
        },
        "required": [
          "club",
          "credential"
        ]
      },
      "PlayerPlacements": {
        "type": "object",
        "properties": {
          "has_entries": {
            "type": "boolean",
            "description": "Whether this member has ever held an entry in a started competition, or one with a result entered; returning or excluded players are not newcomers, and a place in an unstarted draft does not count."
          },
          "next_season": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "name": {
                "type": "string"
              },
              "starts_on": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date"
              },
              "ends_on": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date"
              }
            },
            "required": [
              "id",
              "name",
              "starts_on",
              "ends_on"
            ]
          },
          "placements": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "season": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "name": {
                      "type": "string"
                    },
                    "starts_on": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date"
                    },
                    "ends_on": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date"
                    }
                  },
                  "required": [
                    "id",
                    "name",
                    "starts_on",
                    "ends_on"
                  ]
                },
                "competition_id": {
                  "type": "string",
                  "format": "uuid"
                },
                "competition_name": {
                  "type": "string"
                },
                "division_name": {
                  "type": "string"
                },
                "partner": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "display_name": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "id",
                    "display_name"
                  ]
                },
                "provisional": {
                  "type": "boolean"
                },
                "fixtures_ready": {
                  "type": "boolean"
                }
              },
              "required": [
                "season",
                "competition_id",
                "competition_name",
                "division_name",
                "partner",
                "provisional",
                "fixtures_ready"
              ]
            }
          }
        },
        "required": [
          "has_entries",
          "next_season",
          "placements"
        ]
      },
      "PlayerContacts": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "member_id": {
                  "type": "string",
                  "format": "uuid"
                },
                "display_name": {
                  "type": "string"
                },
                "full_name": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "email": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "phone": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "entry_ids": {
                  "type": "array",
                  "items": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "description": "Their entries you partner or play against."
                }
              },
              "required": [
                "member_id",
                "display_name",
                "full_name",
                "email",
                "phone",
                "entry_ids"
              ]
            }
          }
        },
        "required": [
          "data"
        ]
      },
      "LoginLink": {
        "type": "object",
        "properties": {
          "member_id": {
            "type": "string",
            "format": "uuid"
          },
          "token": {
            "type": "string",
            "example": "dll_q8Vn…",
            "description": "Put it in a link to your website, which exchanges it with `POST /v1/session`. Shown this once: only its SHA-256 is stored."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "When it stops working: 10080 minutes from now unless you asked for longer."
          }
        },
        "required": [
          "member_id",
          "token",
          "expires_at"
        ]
      },
      "LoginLinkOptions": {
        "type": "object",
        "properties": {
          "expected_email": {
            "type": "string",
            "maxLength": 254,
            "format": "email",
            "description": "PII. Requires members:pii. Refuses minting if the member has left or the current email differs from this delivery address; checked in the grant creation snapshot."
          },
          "expires_in_minutes": {
            "type": "integer",
            "minimum": 1,
            "maximum": 10080,
            "example": 10080,
            "description": "How long the link works, up to and by default 10080 minutes (7 days). Ask for less only if the link must expire sooner."
          }
        }
      },
      "Session": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "token": {
            "type": "string",
            "example": "dls_Zr41…",
            "description": "The player's session, sent as `Authorization: Bearer dls_…`. It does not expire. Shown this once: only its SHA-256 is stored."
          },
          "member": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "display_name": {
                "type": "string",
                "example": "Sam Kerr"
              }
            },
            "required": [
              "id",
              "display_name"
            ]
          }
        },
        "required": [
          "id",
          "token",
          "member"
        ]
      },
      "SignedOut": {
        "type": "object",
        "properties": {
          "member_id": {
            "type": "string",
            "format": "uuid"
          },
          "sessions_ended": {
            "type": "integer",
            "description": "How many sessions they held, now ended."
          }
        },
        "required": [
          "member_id",
          "sessions_ended"
        ]
      },
      "MatchPage": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Match"
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Pass as `after` for the next page. Null on the last one."
          }
        },
        "required": [
          "data",
          "next_cursor"
        ]
      },
      "Match": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "competition_id": {
            "type": "string",
            "format": "uuid"
          },
          "division_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "competition_name": {
            "type": "string",
            "example": "Men's Singles",
            "description": "What the competition is called now."
          },
          "division_name": {
            "type": [
              "string",
              "null"
            ],
            "example": "Division 1",
            "description": "What the division is called now."
          },
          "status": {
            "type": "string",
            "enum": [
              "open",
              "reported",
              "played",
              "disputed"
            ]
          },
          "sides": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "side": {
                  "anyOf": [
                    {
                      "type": "number",
                      "enum": [
                        0
                      ]
                    },
                    {
                      "type": "number",
                      "enum": [
                        1
                      ]
                    }
                  ]
                },
                "entry_id": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "uuid"
                },
                "label": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              },
              "required": [
                "side",
                "entry_id",
                "label"
              ]
            },
            "description": "Side 0, then side 1. A match has no date, time or court."
          },
          "result": {
            "$ref": "#/components/schemas/Result"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "competition_id",
          "division_id",
          "competition_name",
          "division_name",
          "status",
          "sides",
          "result",
          "created_at",
          "updated_at"
        ]
      },
      "Result": {
        "type": [
          "object",
          "null"
        ],
        "properties": {
          "outcome": {
            "type": "string",
            "enum": [
              "completed",
              "retired",
              "walkover",
              "conceded",
              "unplayed"
            ]
          },
          "score": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "sets": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "games": {
                      "type": "array",
                      "prefixItems": [
                        {
                          "type": "integer",
                          "minimum": 0,
                          "maximum": 99
                        },
                        {
                          "type": "integer",
                          "minimum": 0,
                          "maximum": 99
                        }
                      ]
                    },
                    "tiebreak": {
                      "type": "array",
                      "prefixItems": [
                        {
                          "type": "integer",
                          "minimum": 0,
                          "maximum": 99
                        },
                        {
                          "type": "integer",
                          "minimum": 0,
                          "maximum": 99
                        }
                      ]
                    }
                  },
                  "required": [
                    "games"
                  ]
                },
                "minItems": 1,
                "maxItems": 7
              }
            },
            "required": [
              "sets"
            ],
            "description": "`[0]` is always side 0. Null unless completed or retired."
          },
          "winning_side": {
            "anyOf": [
              {
                "type": "number",
                "enum": [
                  0
                ]
              },
              {
                "type": "number",
                "enum": [
                  1
                ]
              },
              {
                "type": "null"
              }
            ],
            "description": "Null only when unplayed."
          },
          "retired_side": {
            "anyOf": [
              {
                "type": "number",
                "enum": [
                  0
                ]
              },
              {
                "type": "number",
                "enum": [
                  1
                ]
              },
              {
                "type": "null"
              }
            ],
            "description": "Who retired, conceded or failed to appear."
          },
          "played_on": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "claim_id": {
            "type": "string",
            "format": "uuid",
            "description": "The claim that put this result in the ledger."
          }
        },
        "required": [
          "outcome",
          "score",
          "winning_side",
          "retired_side",
          "played_on",
          "claim_id"
        ],
        "description": "What the ledger holds. Null until the match is played."
      },
      "MatchDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Match"
          },
          {
            "type": "object",
            "properties": {
              "claims": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Claim"
                },
                "description": "For API keys: every submission and its history, oldest first. Player sessions see only their own side's submissions, including replacements. Opposing submissions and coach entries remain private; the final result is in result."
              },
              "waiting_on": {
                "anyOf": [
                  {
                    "type": "number",
                    "enum": [
                      0
                    ]
                  },
                  {
                    "type": "number",
                    "enum": [
                      1
                    ]
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "When reported: the side that still needs to enter its result."
              },
              "differences": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "For API keys when disputed: what the two claims disagree on, e.g. \"set 2: side 0 says 6-4, side 1 says 6-3\". Always empty for player sessions.",
                "example": [
                  "set 2: side 0 says 6-4, side 1 says 6-3"
                ]
              }
            },
            "required": [
              "claims",
              "waiting_on",
              "differences"
            ]
          }
        ]
      },
      "Claim": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "side": {
            "anyOf": [
              {
                "type": "number",
                "enum": [
                  0
                ]
              },
              {
                "type": "number",
                "enum": [
                  1
                ]
              },
              {
                "type": "null"
              }
            ],
            "description": "The side making the claim. Null for a coach's entry."
          },
          "outcome": {
            "type": "string",
            "enum": [
              "completed",
              "retired",
              "walkover",
              "conceded",
              "unplayed"
            ]
          },
          "score": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "sets": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "games": {
                      "type": "array",
                      "prefixItems": [
                        {
                          "type": "integer",
                          "minimum": 0,
                          "maximum": 99
                        },
                        {
                          "type": "integer",
                          "minimum": 0,
                          "maximum": 99
                        }
                      ]
                    },
                    "tiebreak": {
                      "type": "array",
                      "prefixItems": [
                        {
                          "type": "integer",
                          "minimum": 0,
                          "maximum": 99
                        },
                        {
                          "type": "integer",
                          "minimum": 0,
                          "maximum": 99
                        }
                      ]
                    }
                  },
                  "required": [
                    "games"
                  ]
                },
                "minItems": 1,
                "maxItems": 7
              }
            },
            "required": [
              "sets"
            ]
          },
          "retired_side": {
            "anyOf": [
              {
                "type": "number",
                "enum": [
                  0
                ]
              },
              {
                "type": "number",
                "enum": [
                  1
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "played_on": {
            "type": [
              "string",
              "null"
            ],
            "format": "date",
            "description": "When this side says it was played. Never compared."
          },
          "state": {
            "type": "string",
            "enum": [
              "pending",
              "confirmed",
              "superseded"
            ],
            "description": "`pending` until agreed; `confirmed` once behind the ledger's result; `superseded` once replaced."
          },
          "source": {
            "type": "string",
            "enum": [
              "web",
              "telegram",
              "api",
              "coach_entry",
              "nl_parse"
            ]
          },
          "accepts_claim_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Historical acceptance reference. New submissions are entered independently and leave this null."
          },
          "submitted_at": {
            "type": "string",
            "format": "date-time"
          },
          "confirmed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "raw_input": {
            "type": [
              "string",
              "null"
            ],
            "description": "What was typed, when the claim came from free text. Never shown to a player's session."
          }
        },
        "required": [
          "id",
          "side",
          "outcome",
          "score",
          "retired_side",
          "played_on",
          "state",
          "source",
          "accepts_claim_id",
          "submitted_at",
          "confirmed_at",
          "raw_input"
        ]
      },
      "NewClaim": {
        "type": "object",
        "properties": {
          "side": {
            "anyOf": [
              {
                "type": "number",
                "enum": [
                  0
                ]
              },
              {
                "type": "number",
                "enum": [
                  1
                ]
              }
            ],
            "description": "The side this claim speaks for. Required with an API key. A player's session speaks for its own side only, and may leave it out."
          },
          "outcome": {
            "type": "string",
            "enum": [
              "completed",
              "retired",
              "walkover",
              "conceded",
              "unplayed"
            ]
          },
          "score": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "sets": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "games": {
                      "type": "array",
                      "prefixItems": [
                        {
                          "type": "integer",
                          "minimum": 0,
                          "maximum": 99
                        },
                        {
                          "type": "integer",
                          "minimum": 0,
                          "maximum": 99
                        }
                      ]
                    },
                    "tiebreak": {
                      "type": "array",
                      "prefixItems": [
                        {
                          "type": "integer",
                          "minimum": 0,
                          "maximum": 99
                        },
                        {
                          "type": "integer",
                          "minimum": 0,
                          "maximum": 99
                        }
                      ]
                    }
                  },
                  "required": [
                    "games"
                  ]
                },
                "minItems": 1,
                "maxItems": 7
              }
            },
            "required": [
              "sets"
            ],
            "description": "Required for completed and retired; `[0]` is side 0."
          },
          "retired_side": {
            "anyOf": [
              {
                "type": "number",
                "enum": [
                  0
                ]
              },
              {
                "type": "number",
                "enum": [
                  1
                ]
              },
              {
                "type": "null"
              }
            ],
            "description": "Required for retired, walkover and conceded: the side that stopped."
          },
          "played_on": {
            "type": "string",
            "format": "date",
            "description": "No later than today."
          },
          "raw_input": {
            "type": "string",
            "maxLength": 2000,
            "description": "What the player typed, if they typed it."
          },
          "source": {
            "type": "string",
            "enum": [
              "api",
              "telegram",
              "web",
              "nl_parse"
            ],
            "description": "Defaults to `web` for a player's session, `api` for an API key."
          }
        },
        "required": [
          "outcome"
        ]
      },
      "SettlementPreview": {
        "type": "object",
        "properties": {
          "version": {
            "type": "string"
          },
          "match": {
            "$ref": "#/components/schemas/MatchDetail"
          },
          "result": {
            "type": "object",
            "properties": {
              "outcome": {
                "type": "string",
                "enum": [
                  "completed",
                  "retired",
                  "walkover",
                  "conceded",
                  "unplayed"
                ]
              },
              "score": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "sets": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "games": {
                          "type": "array",
                          "prefixItems": [
                            {
                              "type": "integer",
                              "minimum": 0,
                              "maximum": 99
                            },
                            {
                              "type": "integer",
                              "minimum": 0,
                              "maximum": 99
                            }
                          ]
                        },
                        "tiebreak": {
                          "type": "array",
                          "prefixItems": [
                            {
                              "type": "integer",
                              "minimum": 0,
                              "maximum": 99
                            },
                            {
                              "type": "integer",
                              "minimum": 0,
                              "maximum": 99
                            }
                          ]
                        }
                      },
                      "required": [
                        "games"
                      ]
                    },
                    "minItems": 1,
                    "maxItems": 7
                  }
                },
                "required": [
                  "sets"
                ],
                "description": "`[0]` is always side 0. Null unless completed or retired."
              },
              "winning_side": {
                "anyOf": [
                  {
                    "type": "number",
                    "enum": [
                      0
                    ]
                  },
                  {
                    "type": "number",
                    "enum": [
                      1
                    ]
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Null only when unplayed."
              },
              "retired_side": {
                "anyOf": [
                  {
                    "type": "number",
                    "enum": [
                      0
                    ]
                  },
                  {
                    "type": "number",
                    "enum": [
                      1
                    ]
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Who retired, conceded or failed to appear."
              },
              "played_on": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date"
              }
            },
            "required": [
              "outcome",
              "score",
              "winning_side",
              "retired_side",
              "played_on"
            ]
          },
          "requires_override": {
            "type": "boolean"
          },
          "effects": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "side": {
                  "anyOf": [
                    {
                      "type": "number",
                      "enum": [
                        0
                      ]
                    },
                    {
                      "type": "number",
                      "enum": [
                        1
                      ]
                    }
                  ]
                },
                "entry_id": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "uuid"
                },
                "label": {
                  "type": "string"
                },
                "points_before": {
                  "type": "number"
                },
                "points_after": {
                  "type": "number"
                },
                "played_before": {
                  "type": "integer"
                },
                "played_after": {
                  "type": "integer"
                },
                "minimum": {
                  "type": "integer"
                },
                "withdrawn": {
                  "type": "boolean"
                }
              },
              "required": [
                "side",
                "entry_id",
                "label",
                "points_before",
                "points_after",
                "played_before",
                "played_after",
                "minimum",
                "withdrawn"
              ]
            }
          }
        },
        "required": [
          "version",
          "match",
          "result",
          "requires_override",
          "effects"
        ]
      },
      "Settlement": {
        "type": "object",
        "properties": {
          "outcome": {
            "type": "string",
            "enum": [
              "completed",
              "retired",
              "walkover",
              "conceded",
              "unplayed"
            ]
          },
          "score": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "sets": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "games": {
                      "type": "array",
                      "prefixItems": [
                        {
                          "type": "integer",
                          "minimum": 0,
                          "maximum": 99
                        },
                        {
                          "type": "integer",
                          "minimum": 0,
                          "maximum": 99
                        }
                      ]
                    },
                    "tiebreak": {
                      "type": "array",
                      "prefixItems": [
                        {
                          "type": "integer",
                          "minimum": 0,
                          "maximum": 99
                        },
                        {
                          "type": "integer",
                          "minimum": 0,
                          "maximum": 99
                        }
                      ]
                    }
                  },
                  "required": [
                    "games"
                  ]
                },
                "minItems": 1,
                "maxItems": 7
              }
            },
            "required": [
              "sets"
            ],
            "description": "Required for completed and retired; `[0]` is side 0."
          },
          "retired_side": {
            "anyOf": [
              {
                "type": "number",
                "enum": [
                  0
                ]
              },
              {
                "type": "number",
                "enum": [
                  1
                ]
              },
              {
                "type": "null"
              }
            ],
            "description": "Required for retired, walkover and conceded: the side that stopped."
          },
          "played_on": {
            "type": "string",
            "format": "date",
            "description": "No later than today."
          },
          "raw_input": {
            "type": "string",
            "maxLength": 2000,
            "description": "What the player typed, if they typed it."
          },
          "reason": {
            "type": "string",
            "enum": [
              "no_response",
              "conflicting_entries",
              "incorrect_result",
              "unreported_result",
              "injury_or_withdrawal"
            ],
            "description": "Why the coach decided the result. Recorded with the acting API key in the audit history; use a category rather than personal or medical details."
          },
          "expected_version": {
            "type": "string",
            "pattern": "^[a-f0-9]{64}$",
            "description": "The version returned by settlement-preview. Refuses a changed match, division ledger, entries or rules so the coach can review the effect again."
          },
          "override": {
            "type": "boolean",
            "description": "Required to replace any confirmed result, including an earlier coach settlement. An identical retry or a match still open, reported or disputed does not need it."
          }
        },
        "required": [
          "outcome"
        ]
      },
      "SettledMatch": {
        "allOf": [
          {
            "$ref": "#/components/schemas/MatchDetail"
          },
          {
            "type": "object",
            "properties": {
              "short_of_minimum": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "entry_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "label": {
                      "type": "string"
                    },
                    "played": {
                      "type": "integer",
                      "description": "As the tables count it: a match against a no-show counts for the side that turned up."
                    },
                    "target": {
                      "type": "integer",
                      "description": "The competition's minimum, or all its fixtures if fewer."
                    },
                    "still_possible": {
                      "type": "boolean",
                      "description": "Whether its matches still to be played could bring it to the minimum before the season ends."
                    }
                  },
                  "required": [
                    "entry_id",
                    "label",
                    "played",
                    "target",
                    "still_possible"
                  ]
                },
                "description": "Only for `outcome: unplayed`, which credits neither side: the entries in this match now short of the competition's minimum, who are left out of next season's draft if they stay short once the tables are final. Empty for any other outcome, and when neither side is short."
              }
            },
            "required": [
              "short_of_minimum"
            ]
          }
        ]
      },
      "MatchPlan": {
        "type": "object",
        "properties": {
          "match_id": {
            "type": "string",
            "format": "uuid"
          },
          "state": {
            "type": "string",
            "enum": [
              "to_arrange",
              "planned",
              "arranged"
            ]
          },
          "arranged_on": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          }
        },
        "required": [
          "match_id",
          "state",
          "arranged_on"
        ]
      },
      "Club": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "slug": {
            "type": "string",
            "example": "deuce-ltc",
            "description": "The club's public address. Fixed once created."
          },
          "name": {
            "type": "string",
            "example": "Deuce Lawn Tennis Club"
          },
          "timezone": {
            "type": "string",
            "example": "Europe/London",
            "description": "IANA zone every deadline is counted in."
          },
          "branding": {
            "type": "object",
            "additionalProperties": {},
            "description": "Whatever a club's websites and apps need to look like the club: logo, colours, sponsors. Stored as given; the core never reads it.",
            "example": {
              "logo_url": "https://example.org/logo.svg",
              "primary_colour": "#0b6e4f"
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "slug",
          "name",
          "timezone",
          "branding",
          "created_at",
          "updated_at"
        ]
      },
      "ClubChanges": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "timezone": {
            "type": "string",
            "example": "Europe/London"
          },
          "branding": {
            "type": "object",
            "additionalProperties": {},
            "description": "Whatever a club's websites and apps need to look like the club: logo, colours, sponsors. Stored as given; the core never reads it.",
            "example": {
              "logo_url": "https://example.org/logo.svg",
              "primary_colour": "#0b6e4f"
            }
          }
        }
      },
      "Weather": {
        "type": "object",
        "properties": {
          "units": {
            "type": "string",
            "enum": [
              "uk",
              "metric",
              "us"
            ]
          },
          "court_locations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CourtLocation"
            },
            "maxItems": 8
          }
        },
        "required": [
          "units",
          "court_locations"
        ]
      },
      "CourtLocation": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "example": "Main Courts"
          },
          "latitude": {
            "type": "number",
            "example": 51.4343
          },
          "longitude": {
            "type": "number",
            "example": -0.2141
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "name",
          "latitude",
          "longitude",
          "created_at",
          "updated_at"
        ]
      },
      "WeatherChanges": {
        "type": "object",
        "properties": {
          "units": {
            "type": "string",
            "enum": [
              "uk",
              "metric",
              "us"
            ]
          }
        }
      },
      "CourtLocationList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CourtLocation"
            },
            "maxItems": 8
          }
        },
        "required": [
          "data"
        ]
      },
      "NewCourtLocation": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "latitude": {
            "type": "number",
            "minimum": -90,
            "maximum": 90
          },
          "longitude": {
            "type": "number",
            "minimum": -180,
            "maximum": 180
          }
        },
        "required": [
          "name",
          "latitude",
          "longitude"
        ]
      },
      "CourtLocationChanges": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "latitude": {
            "type": "number",
            "minimum": -90,
            "maximum": 90
          },
          "longitude": {
            "type": "number",
            "minimum": -180,
            "maximum": 180
          }
        }
      },
      "ApiKeyPage": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ApiKey"
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Pass as `after` for the next page. Null on the last one."
          }
        },
        "required": [
          "data",
          "next_cursor"
        ]
      },
      "ApiKey": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "example": "Telegram bot"
          },
          "prefix": {
            "type": "string",
            "example": "dl_Xk3v9Q",
            "description": "The key's first characters, to tell keys apart."
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "league:read",
                "league:write",
                "results:write",
                "members:read",
                "members:write",
                "members:pii",
                "admin"
              ]
            }
          },
          "last_used_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "To the minute, not the second."
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "revoked_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "name",
          "prefix",
          "scopes",
          "last_used_at",
          "expires_at",
          "revoked_at",
          "created_at"
        ]
      },
      "NewApiKey": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ApiKey"
          },
          {
            "type": "object",
            "properties": {
              "key": {
                "type": "string",
                "example": "dl_Xk3v9Q…",
                "description": "The key itself. Shown this once and never again: only its SHA-256 is stored."
              }
            },
            "required": [
              "key"
            ]
          }
        ]
      },
      "CreateApiKey": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100,
            "description": "What the key is for.",
            "example": "Telegram bot"
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "league:read",
                "league:write",
                "results:write",
                "members:read",
                "members:write",
                "members:pii",
                "admin"
              ]
            },
            "maxItems": 7,
            "description": "Defaults to league:read + results:write."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "When it stops working. Omit for never."
          }
        },
        "required": [
          "name"
        ]
      },
      "MemberPage": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Member"
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Pass as `after` for the next page. Null on the last one."
          }
        },
        "required": [
          "data",
          "next_cursor"
        ]
      },
      "Member": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "display_name": {
            "type": "string",
            "example": "Sam Kerr",
            "description": "The name they play under: the only one shown to players and the public."
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "paused",
              "left"
            ]
          },
          "rating": {
            "type": [
              "number",
              "null"
            ],
            "description": "Stored as given; the core computes no ratings."
          },
          "rating_system": {
            "type": [
              "string",
              "null"
            ],
            "example": "UTR"
          },
          "level": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "maximum": 10,
            "example": 5,
            "description": "How well they play, as the coach judges it: 10 a beginner, 5 intermediate, 4 a strong club player, 1 a national player. A joining player selects an initial self-rating; the coach can adjust this separate level. The first-season setup uses this level to suggest divisions, while the core computes no rating from it."
          },
          "self_level": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "maximum": 10,
            "example": 5,
            "description": "The level this member selected when applying, if any. Kept separately from the coach's editable level."
          },
          "joined_on": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "wants_to_play": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "singles",
              "doubles",
              "mixed_doubles",
              "both",
              "singles_mixed",
              "doubles_mixed",
              "all",
              "not_now",
              null
            ],
            "description": "What they want to play next season: singles, doubles, mixed doubles, any combination, or `not_now` for a social member. Null: they have not said. Asked on the join form; the coach or the player can change it."
          },
          "leaving_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When they said they are not playing next season at all, if they did. It takes every entry they held then out of next season's draft, singles and doubles, as if each had opted out; their results and outstanding matches stand. See `POST /v1/members/{id}/leave`."
          },
          "deleted_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When they were removed from the club's list. Their results remain."
          },
          "signed_in_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When they signed in on the newest device where they are still signed in. Null when they are signed in nowhere, so they need a login link."
          },
          "last_signed_in_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When they last signed in anywhere, from the audit history; signing out does not clear it. Null when they have never signed in."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "full_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "PII. Their full name. Present only for a credential holding `members:pii`."
          },
          "email": {
            "type": [
              "string",
              "null"
            ],
            "description": "PII. Unique within the club. Present only for a credential holding `members:pii`."
          },
          "invitation_state": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "accepted",
              "failed",
              null
            ],
            "description": "PII. Latest coach invitation outcome. Accepted means provider acceptance, not inbox delivery. Present only for a credential holding `members:pii`."
          },
          "invitation_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "PII. When the latest invitation outcome was recorded. Present only for a credential holding `members:pii`."
          },
          "phone": {
            "type": [
              "string",
              "null"
            ],
            "description": "PII. As they gave it. Present only for a credential holding `members:pii`."
          },
          "date_of_birth": {
            "type": [
              "string",
              "null"
            ],
            "format": "date",
            "description": "PII. For junior eligibility. Present only for a credential holding `members:pii`."
          },
          "gender": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "female",
              "male",
              "other",
              "undisclosed",
              null
            ],
            "description": "PII. Used only to warn about an unusual mixed pair; never enforced. Present only for a credential holding `members:pii`."
          },
          "age_group": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "under_18",
              "18_34",
              "35_49",
              "50_64",
              "65_plus",
              null
            ],
            "description": "PII. An age band the club reads at a glance, never a birth date. Used to plan draws. Present only for a credential holding `members:pii`."
          },
          "notes": {
            "type": [
              "string",
              "null"
            ],
            "description": "PII. Anything the coach wrote down. Present only for a credential holding `members:pii`."
          }
        },
        "required": [
          "id",
          "display_name",
          "status",
          "rating",
          "rating_system",
          "level",
          "self_level",
          "joined_on",
          "wants_to_play",
          "leaving_at",
          "deleted_at",
          "signed_in_at",
          "last_signed_in_at",
          "created_at",
          "updated_at"
        ]
      },
      "NewMember": {
        "type": "object",
        "properties": {
          "display_name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 60
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "paused",
              "left"
            ],
            "description": "Defaults to `active` for a new member."
          },
          "rating": {
            "type": [
              "number",
              "null"
            ],
            "minimum": -999.999,
            "maximum": 999.999
          },
          "rating_system": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 1,
            "maxLength": 40
          },
          "level": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "maximum": 10,
            "example": 5,
            "description": "How well they play, as the coach judges it: 10 a beginner, 5 intermediate, 4 a strong club player, 1 a national player. A joining player selects an initial self-rating; the coach can adjust this separate level. The first-season setup uses this level to suggest divisions, while the core computes no rating from it."
          },
          "joined_on": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "wants_to_play": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "singles",
              "doubles",
              "mixed_doubles",
              "both",
              "singles_mixed",
              "doubles_mixed",
              "all",
              "not_now",
              null
            ]
          },
          "full_name": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 1,
            "maxLength": 200,
            "description": "PII. Setting it needs `members:pii`."
          },
          "email": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 254,
            "format": "email",
            "description": "PII. Setting it needs `members:pii`."
          },
          "phone": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 1,
            "maxLength": 40,
            "description": "PII. Setting it needs `members:pii`."
          },
          "date_of_birth": {
            "type": [
              "string",
              "null"
            ],
            "format": "date",
            "description": "PII. Setting it needs `members:pii`."
          },
          "gender": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "female",
              "male",
              "other",
              "undisclosed",
              null
            ],
            "description": "PII. Setting it needs `members:pii`."
          },
          "age_group": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "under_18",
              "18_34",
              "35_49",
              "50_64",
              "65_plus",
              null
            ],
            "description": "PII. Setting it needs `members:pii`."
          },
          "notes": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 5000,
            "description": "PII. Setting it needs `members:pii`."
          }
        },
        "required": [
          "display_name"
        ]
      },
      "MemberChanges": {
        "type": "object",
        "properties": {
          "display_name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 60
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "paused",
              "left"
            ],
            "description": "Defaults to `active` for a new member."
          },
          "rating": {
            "type": [
              "number",
              "null"
            ],
            "minimum": -999.999,
            "maximum": 999.999
          },
          "rating_system": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 1,
            "maxLength": 40
          },
          "level": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "maximum": 10,
            "example": 5,
            "description": "How well they play, as the coach judges it: 10 a beginner, 5 intermediate, 4 a strong club player, 1 a national player. A joining player selects an initial self-rating; the coach can adjust this separate level. The first-season setup uses this level to suggest divisions, while the core computes no rating from it."
          },
          "joined_on": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "wants_to_play": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "singles",
              "doubles",
              "mixed_doubles",
              "both",
              "singles_mixed",
              "doubles_mixed",
              "all",
              "not_now",
              null
            ]
          },
          "full_name": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 1,
            "maxLength": 200,
            "description": "PII. Setting it needs `members:pii`."
          },
          "email": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 254,
            "format": "email",
            "description": "PII. Setting it needs `members:pii`."
          },
          "phone": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 1,
            "maxLength": 40,
            "description": "PII. Setting it needs `members:pii`."
          },
          "date_of_birth": {
            "type": [
              "string",
              "null"
            ],
            "format": "date",
            "description": "PII. Setting it needs `members:pii`."
          },
          "gender": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "female",
              "male",
              "other",
              "undisclosed",
              null
            ],
            "description": "PII. Setting it needs `members:pii`."
          },
          "age_group": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "under_18",
              "18_34",
              "35_49",
              "50_64",
              "65_plus",
              null
            ],
            "description": "PII. Setting it needs `members:pii`."
          },
          "notes": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 5000,
            "description": "PII. Setting it needs `members:pii`."
          }
        }
      },
      "WantsToPlayChoice": {
        "type": "object",
        "properties": {
          "wants_to_play": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "singles",
              "doubles",
              "mixed_doubles",
              "both",
              "singles_mixed",
              "doubles_mixed",
              "all",
              "not_now",
              null
            ]
          }
        },
        "required": [
          "wants_to_play"
        ]
      },
      "JoinRequestPage": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/JoinRequest"
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Pass as `after` for the next page. Null on the last one."
          }
        },
        "required": [
          "data",
          "next_cursor"
        ]
      },
      "JoinRequest": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "first_name": {
            "type": "string",
            "description": "PII. As they typed it. Present only for a credential holding `members:pii`."
          },
          "surname": {
            "type": "string",
            "description": "PII. As they typed it. Present only for a credential holding `members:pii`."
          },
          "email": {
            "type": [
              "string",
              "null"
            ],
            "description": "PII. Required for new requests; older requests may have no email. Present only for a credential holding `members:pii`."
          },
          "phone": {
            "type": [
              "string",
              "null"
            ],
            "description": "PII. As they typed it. Present only for a credential holding `members:pii`."
          },
          "gender": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "female",
              "male",
              "other",
              "undisclosed",
              null
            ],
            "description": "PII. As they chose it on the form; null if the request did not carry one. Present only for a credential holding `members:pii`."
          },
          "age_group": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "under_18",
              "18_34",
              "35_49",
              "50_64",
              "65_plus",
              null
            ],
            "description": "PII. The band they chose on the form, or null if they left it blank. Present only for a credential holding `members:pii`."
          },
          "wants_to_play": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "singles",
              "doubles",
              "mixed_doubles",
              "both",
              "singles_mixed",
              "doubles_mixed",
              "all",
              "not_now",
              null
            ],
            "description": "PII. What they want to play, as they chose it on the form; null if the request did not carry it. Present only for a credential holding `members:pii`."
          },
          "self_level": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "maximum": 10,
            "example": 5,
            "description": "PII. Their own estimate: 10 is an absolute beginner and 1 is a national competitor. Null for social or older requests. Present only for a credential holding `members:pii`."
          },
          "privacy_notice": {
            "type": "string",
            "example": "uk-2026-09-30",
            "description": "Which privacy notice they read and agreed to, when they asked."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When they asked, and agreed to the privacy notice."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the request is deleted if nobody has decided it."
          },
          "member": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "display_name": {
                "type": "string"
              }
            },
            "required": [
              "id",
              "display_name"
            ],
            "description": "A member already on the club's list with the same email address."
          }
        },
        "required": [
          "id",
          "first_name",
          "surname",
          "email",
          "phone",
          "gender",
          "age_group",
          "wants_to_play",
          "self_level",
          "privacy_notice",
          "created_at",
          "expires_at",
          "member"
        ]
      },
      "NewJoinRequest": {
        "type": "object",
        "properties": {
          "first_name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 60
          },
          "surname": {
            "type": "string",
            "minLength": 1,
            "maxLength": 60
          },
          "email": {
            "type": "string",
            "maxLength": 254,
            "format": "email",
            "description": "Required email address for sign-in links."
          },
          "phone": {
            "type": "string",
            "pattern": "^\\+?[0-9][0-9 ()-]{5,23}$",
            "description": "Required telephone number for WhatsApp league communications; 7 to 15 digits with optional punctuation."
          },
          "gender": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "female",
              "male",
              "other",
              "undisclosed",
              null
            ],
            "description": "The club's own form always sends one. Copied to the member on approval."
          },
          "age_group": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "under_18",
              "18_34",
              "35_49",
              "50_64",
              "65_plus",
              null
            ],
            "description": "Optional. Copied to the member on approval."
          },
          "wants_to_play": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "singles",
              "doubles",
              "mixed_doubles",
              "both",
              "singles_mixed",
              "doubles_mixed",
              "all",
              "not_now",
              null
            ],
            "description": "Singles, doubles, mixed doubles, any combination, or `not_now` for a social member. The club's own form always asks. Copied to the member on approval."
          },
          "self_level": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "maximum": 10,
            "example": 5,
            "description": "The applicant's own estimate, separate from the coach's editable level."
          },
          "privacy_notice": {
            "type": "string",
            "minLength": 1,
            "maxLength": 40,
            "example": "uk-2026-09-30",
            "description": "Which privacy notice the person read and agreed to. The form that showed it names it."
          }
        },
        "required": [
          "first_name",
          "surname",
          "email",
          "phone",
          "privacy_notice"
        ]
      },
      "JoinRequestApproval": {
        "type": "object",
        "properties": {
          "display_name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 60,
            "description": "The name they play under. Defaults to their full name, such as \"Sam Kerr\", or their first name and initial if that is longer than 60 characters."
          },
          "level": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "maximum": 10,
            "example": 5,
            "description": "How well they play, as the coach judges it: 10 a beginner, 5 intermediate, 4 a strong club player, 1 a national player. A joining player selects an initial self-rating; the coach can adjust this separate level. The first-season setup uses this level to suggest divisions, while the core computes no rating from it."
          },
          "gender": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "female",
              "male",
              "other",
              "undisclosed",
              null
            ],
            "description": "Replaces the one on the request, if the coach knows better."
          },
          "age_group": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "under_18",
              "18_34",
              "35_49",
              "50_64",
              "65_plus",
              null
            ],
            "description": "Replaces the one on the request."
          },
          "wants_to_play": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "singles",
              "doubles",
              "mixed_doubles",
              "both",
              "singles_mixed",
              "doubles_mixed",
              "all",
              "not_now",
              null
            ],
            "description": "Replaces the one on the request."
          }
        }
      },
      "SeasonPage": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Season"
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Pass as `after` for the next page. Null on the last one."
          }
        },
        "required": [
          "data",
          "next_cursor"
        ]
      },
      "Season": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "example": "Summer 2026"
          },
          "kind": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "spring",
              "summer",
              "autumn",
              "winter",
              null
            ],
            "description": "An optional label for clubs that run quarterly."
          },
          "year": {
            "type": [
              "integer",
              "null"
            ]
          },
          "starts_on": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "ends_on": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "results_deadline_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "The one deadline for every competition in the season, counted in the club's time zone."
          },
          "state": {
            "type": "string",
            "enum": [
              "planning",
              "active",
              "complete",
              "archived"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "name",
          "kind",
          "year",
          "starts_on",
          "ends_on",
          "results_deadline_at",
          "state",
          "created_at",
          "updated_at"
        ]
      },
      "NewSeason": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "kind": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "spring",
              "summer",
              "autumn",
              "winter",
              null
            ]
          },
          "year": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 2000,
            "maximum": 2100
          },
          "starts_on": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "ends_on": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "results_deadline_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        },
        "required": [
          "name"
        ]
      },
      "SeasonChanges": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "kind": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "spring",
              "summer",
              "autumn",
              "winter",
              null
            ]
          },
          "year": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 2000,
            "maximum": 2100
          },
          "starts_on": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "ends_on": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "results_deadline_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "state": {
            "type": "string",
            "enum": [
              "planning",
              "active",
              "complete",
              "archived"
            ],
            "description": "One step at a time, forward or back: planning, active, complete, archived. Activating needs `starts_on` and `ends_on`; a season with an active competition stays active."
          }
        }
      },
      "CompetitionPage": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Competition"
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Pass as `after` for the next page. Null on the last one."
          }
        },
        "required": [
          "data",
          "next_cursor"
        ]
      },
      "Competition": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "season_id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "example": "Men's Singles"
          },
          "discipline": {
            "type": "string",
            "enum": [
              "singles",
              "doubles"
            ]
          },
          "category": {
            "type": "string",
            "enum": [
              "open",
              "mens",
              "womens",
              "mixed"
            ],
            "description": "Advisory: an unusual pairing is warned about, never refused."
          },
          "match_format": {
            "type": "object",
            "properties": {
              "setsToWin": {
                "type": "integer",
                "minimum": 1,
                "maximum": 3
              },
              "set": {
                "type": "object",
                "properties": {
                  "gamesToWin": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 20
                  },
                  "clearBy": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 2
                  },
                  "tiebreakAt": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "minimum": 1,
                    "maximum": 20
                  },
                  "tiebreakTo": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "minimum": 1,
                    "maximum": 30
                  }
                },
                "required": [
                  "gamesToWin",
                  "clearBy",
                  "tiebreakAt",
                  "tiebreakTo"
                ]
              },
              "finalSet": {
                "anyOf": [
                  {
                    "type": "object",
                    "properties": {
                      "type": {
                        "type": "string",
                        "enum": [
                          "champions_tiebreak"
                        ]
                      },
                      "to": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 30
                      },
                      "clearBy": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 2
                      }
                    },
                    "required": [
                      "type",
                      "to",
                      "clearBy"
                    ]
                  },
                  {
                    "type": "object",
                    "properties": {
                      "type": {
                        "type": "string",
                        "enum": [
                          "standard"
                        ]
                      }
                    },
                    "required": [
                      "type"
                    ]
                  }
                ],
                "default": {
                  "type": "standard"
                }
              }
            },
            "required": [
              "setsToWin",
              "set"
            ]
          },
          "rules": {
            "type": "object",
            "properties": {
              "version": {
                "type": "number",
                "enum": [
                  1
                ]
              },
              "scoringMode": {
                "type": "string",
                "enum": [
                  "points",
                  "games_won",
                  "sets_won"
                ]
              },
              "points": {
                "type": "object",
                "properties": {
                  "win": {
                    "type": "number"
                  },
                  "lossPlayed": {
                    "type": "number"
                  },
                  "retiredWin": {
                    "type": "number"
                  },
                  "retiredLoss": {
                    "type": "number"
                  },
                  "walkoverWin": {
                    "type": "number"
                  },
                  "walkoverLoss": {
                    "type": "number"
                  },
                  "concededWin": {
                    "type": "number"
                  },
                  "concededLoss": {
                    "type": "number"
                  },
                  "unplayedBoth": {
                    "type": "number"
                  },
                  "perSetWon": {
                    "type": "number",
                    "default": 0
                  },
                  "closeLoss": {
                    "type": [
                      "object",
                      "null"
                    ],
                    "properties": {
                      "withinGames": {
                        "type": "integer",
                        "minimum": 0,
                        "maximum": 99
                      },
                      "points": {
                        "type": "number"
                      }
                    },
                    "default": null,
                    "required": [
                      "withinGames",
                      "points"
                    ]
                  },
                  "convincingWin": {
                    "type": [
                      "object",
                      "null"
                    ],
                    "properties": {
                      "byGames": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 99
                      },
                      "points": {
                        "type": "number"
                      }
                    },
                    "default": null,
                    "required": [
                      "byGames",
                      "points"
                    ]
                  },
                  "allPlayed": {
                    "type": "number",
                    "default": 0
                  }
                },
                "required": [
                  "win",
                  "lossPlayed",
                  "retiredWin",
                  "retiredLoss",
                  "walkoverWin",
                  "walkoverLoss",
                  "concededWin",
                  "concededLoss",
                  "unplayedBoth"
                ]
              },
              "tiebreaks": {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "points",
                    "head_to_head",
                    "matches_won",
                    "matches_played",
                    "sets_won",
                    "set_difference",
                    "set_ratio",
                    "games_won",
                    "game_difference",
                    "game_ratio"
                  ]
                },
                "minItems": 1
              },
              "movement": {
                "type": "object",
                "properties": {
                  "promote": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 10
                  },
                  "relegate": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 10
                  },
                  "minMatchesForPromotion": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 50
                  }
                },
                "required": [
                  "promote",
                  "relegate",
                  "minMatchesForPromotion"
                ]
              },
              "withdrawal": {
                "type": "object",
                "properties": {
                  "playedMatches": {
                    "type": "string",
                    "enum": [
                      "keep",
                      "void"
                    ]
                  },
                  "remainingMatches": {
                    "type": "string",
                    "enum": [
                      "unplayed",
                      "walkover_to_opponent"
                    ]
                  }
                },
                "required": [
                  "playedMatches",
                  "remainingMatches"
                ]
              },
              "minMatchesForRanking": {
                "type": "integer",
                "minimum": 0,
                "maximum": 50,
                "default": 0
              },
              "minMatchesToPlay": {
                "type": "integer",
                "minimum": 0,
                "maximum": 50,
                "default": 4
              },
              "walkoverScore": {
                "type": "string",
                "enum": [
                  "nominal",
                  "none"
                ],
                "default": "nominal"
              }
            },
            "required": [
              "version",
              "scoringMode",
              "points",
              "tiebreaks",
              "movement",
              "withdrawal"
            ]
          },
          "sequence_in_season": {
            "type": "integer",
            "description": "For clubs running several rounds in one season; most leave it at 1."
          },
          "previous_competition_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Where promotion and relegation are suggested from."
          },
          "state": {
            "type": "string",
            "enum": [
              "draft",
              "active",
              "complete",
              "archived"
            ]
          },
          "visibility": {
            "type": "string",
            "enum": [
              "members",
              "private"
            ],
            "description": "`members`: every member of the club who signs in. `private`: only the coach's credentials, never a player's login. Nothing is readable without a credential."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "season_id",
          "name",
          "discipline",
          "category",
          "match_format",
          "rules",
          "sequence_in_season",
          "previous_competition_id",
          "state",
          "visibility",
          "created_at",
          "updated_at"
        ]
      },
      "NewCompetition": {
        "type": "object",
        "properties": {
          "season_id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "discipline": {
            "type": "string",
            "enum": [
              "singles",
              "doubles"
            ]
          },
          "category": {
            "type": "string",
            "enum": [
              "open",
              "mens",
              "womens",
              "mixed"
            ],
            "description": "Defaults to `open`."
          },
          "match_format": {
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "best_of_3_champions_tiebreak",
                  "best_of_3_sets",
                  "pro_set_8",
                  "short_set_4"
                ]
              },
              {
                "type": "object",
                "properties": {
                  "setsToWin": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 3
                  },
                  "set": {
                    "type": "object",
                    "properties": {
                      "gamesToWin": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 20
                      },
                      "clearBy": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 2
                      },
                      "tiebreakAt": {
                        "type": [
                          "integer",
                          "null"
                        ],
                        "minimum": 1,
                        "maximum": 20
                      },
                      "tiebreakTo": {
                        "type": [
                          "integer",
                          "null"
                        ],
                        "minimum": 1,
                        "maximum": 30
                      }
                    },
                    "required": [
                      "gamesToWin",
                      "clearBy",
                      "tiebreakAt",
                      "tiebreakTo"
                    ]
                  },
                  "finalSet": {
                    "anyOf": [
                      {
                        "type": "object",
                        "properties": {
                          "type": {
                            "type": "string",
                            "enum": [
                              "champions_tiebreak"
                            ]
                          },
                          "to": {
                            "type": "integer",
                            "minimum": 1,
                            "maximum": 30
                          },
                          "clearBy": {
                            "type": "integer",
                            "minimum": 1,
                            "maximum": 2
                          }
                        },
                        "required": [
                          "type",
                          "to",
                          "clearBy"
                        ]
                      },
                      {
                        "type": "object",
                        "properties": {
                          "type": {
                            "type": "string",
                            "enum": [
                              "standard"
                            ]
                          }
                        },
                        "required": [
                          "type"
                        ]
                      }
                    ],
                    "default": {
                      "type": "standard"
                    }
                  }
                },
                "required": [
                  "setsToWin",
                  "set"
                ]
              }
            ],
            "description": "What a legal score is: a format, or the name of a preset (best_of_3_champions_tiebreak, best_of_3_sets, pro_set_8, short_set_4)."
          },
          "rules": {
            "type": "object",
            "properties": {
              "version": {
                "type": "number",
                "enum": [
                  1
                ]
              },
              "scoringMode": {
                "type": "string",
                "enum": [
                  "points",
                  "games_won",
                  "sets_won"
                ]
              },
              "points": {
                "type": "object",
                "properties": {
                  "win": {
                    "type": "number"
                  },
                  "lossPlayed": {
                    "type": "number"
                  },
                  "retiredWin": {
                    "type": "number"
                  },
                  "retiredLoss": {
                    "type": "number"
                  },
                  "walkoverWin": {
                    "type": "number"
                  },
                  "walkoverLoss": {
                    "type": "number"
                  },
                  "concededWin": {
                    "type": "number"
                  },
                  "concededLoss": {
                    "type": "number"
                  },
                  "unplayedBoth": {
                    "type": "number"
                  },
                  "perSetWon": {
                    "type": "number",
                    "default": 0
                  },
                  "closeLoss": {
                    "type": [
                      "object",
                      "null"
                    ],
                    "properties": {
                      "withinGames": {
                        "type": "integer",
                        "minimum": 0,
                        "maximum": 99
                      },
                      "points": {
                        "type": "number"
                      }
                    },
                    "default": null,
                    "required": [
                      "withinGames",
                      "points"
                    ]
                  },
                  "convincingWin": {
                    "type": [
                      "object",
                      "null"
                    ],
                    "properties": {
                      "byGames": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 99
                      },
                      "points": {
                        "type": "number"
                      }
                    },
                    "default": null,
                    "required": [
                      "byGames",
                      "points"
                    ]
                  },
                  "allPlayed": {
                    "type": "number",
                    "default": 0
                  }
                },
                "required": [
                  "win",
                  "lossPlayed",
                  "retiredWin",
                  "retiredLoss",
                  "walkoverWin",
                  "walkoverLoss",
                  "concededWin",
                  "concededLoss",
                  "unplayedBoth"
                ]
              },
              "tiebreaks": {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "points",
                    "head_to_head",
                    "matches_won",
                    "matches_played",
                    "sets_won",
                    "set_difference",
                    "set_ratio",
                    "games_won",
                    "game_difference",
                    "game_ratio"
                  ]
                },
                "minItems": 1
              },
              "movement": {
                "type": "object",
                "properties": {
                  "promote": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 10
                  },
                  "relegate": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 10
                  },
                  "minMatchesForPromotion": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 50
                  }
                },
                "required": [
                  "promote",
                  "relegate",
                  "minMatchesForPromotion"
                ]
              },
              "withdrawal": {
                "type": "object",
                "properties": {
                  "playedMatches": {
                    "type": "string",
                    "enum": [
                      "keep",
                      "void"
                    ]
                  },
                  "remainingMatches": {
                    "type": "string",
                    "enum": [
                      "unplayed",
                      "walkover_to_opponent"
                    ]
                  }
                },
                "required": [
                  "playedMatches",
                  "remainingMatches"
                ]
              },
              "minMatchesForRanking": {
                "type": "integer",
                "minimum": 0,
                "maximum": 50,
                "default": 0
              },
              "minMatchesToPlay": {
                "type": "integer",
                "minimum": 0,
                "maximum": 50,
                "default": 4
              },
              "walkoverScore": {
                "type": "string",
                "enum": [
                  "nominal",
                  "none"
                ],
                "default": "nominal"
              }
            },
            "required": [
              "version",
              "scoringMode",
              "points",
              "tiebreaks",
              "movement",
              "withdrawal"
            ],
            "description": "Defaults to the standard rules: 1 for playing, 3 more for winning and 1 per set won; 1 for losing by 4 games or fewer or winning by 8 or more; 1 for turning up to every match; 3 in total for a win by retirement, walkover or concession, and 0 for that loss. Everyone is expected to play at least 4 matches (`minMatchesToPlay`), or all their fixtures if fewer."
          },
          "sequence_in_season": {
            "type": "integer",
            "minimum": 1,
            "maximum": 50
          },
          "previous_competition_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "visibility": {
            "type": "string",
            "enum": [
              "members",
              "private"
            ],
            "description": "Defaults to `members`."
          }
        },
        "required": [
          "season_id",
          "name",
          "discipline",
          "match_format"
        ]
      },
      "CompetitionChanges": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "discipline": {
            "type": "string",
            "enum": [
              "singles",
              "doubles"
            ],
            "description": "Only while the competition has no entries."
          },
          "category": {
            "type": "string",
            "enum": [
              "open",
              "mens",
              "womens",
              "mixed"
            ]
          },
          "match_format": {
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "best_of_3_champions_tiebreak",
                  "best_of_3_sets",
                  "pro_set_8",
                  "short_set_4"
                ]
              },
              {
                "type": "object",
                "properties": {
                  "setsToWin": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 3
                  },
                  "set": {
                    "type": "object",
                    "properties": {
                      "gamesToWin": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 20
                      },
                      "clearBy": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 2
                      },
                      "tiebreakAt": {
                        "type": [
                          "integer",
                          "null"
                        ],
                        "minimum": 1,
                        "maximum": 20
                      },
                      "tiebreakTo": {
                        "type": [
                          "integer",
                          "null"
                        ],
                        "minimum": 1,
                        "maximum": 30
                      }
                    },
                    "required": [
                      "gamesToWin",
                      "clearBy",
                      "tiebreakAt",
                      "tiebreakTo"
                    ]
                  },
                  "finalSet": {
                    "anyOf": [
                      {
                        "type": "object",
                        "properties": {
                          "type": {
                            "type": "string",
                            "enum": [
                              "champions_tiebreak"
                            ]
                          },
                          "to": {
                            "type": "integer",
                            "minimum": 1,
                            "maximum": 30
                          },
                          "clearBy": {
                            "type": "integer",
                            "minimum": 1,
                            "maximum": 2
                          }
                        },
                        "required": [
                          "type",
                          "to",
                          "clearBy"
                        ]
                      },
                      {
                        "type": "object",
                        "properties": {
                          "type": {
                            "type": "string",
                            "enum": [
                              "standard"
                            ]
                          }
                        },
                        "required": [
                          "type"
                        ]
                      }
                    ],
                    "default": {
                      "type": "standard"
                    }
                  }
                },
                "required": [
                  "setsToWin",
                  "set"
                ]
              }
            ],
            "description": "What a legal score is: a format, or the name of a preset (best_of_3_champions_tiebreak, best_of_3_sets, pro_set_8, short_set_4)."
          },
          "rules": {
            "type": "object",
            "properties": {
              "version": {
                "type": "number",
                "enum": [
                  1
                ]
              },
              "scoringMode": {
                "type": "string",
                "enum": [
                  "points",
                  "games_won",
                  "sets_won"
                ]
              },
              "points": {
                "type": "object",
                "properties": {
                  "win": {
                    "type": "number"
                  },
                  "lossPlayed": {
                    "type": "number"
                  },
                  "retiredWin": {
                    "type": "number"
                  },
                  "retiredLoss": {
                    "type": "number"
                  },
                  "walkoverWin": {
                    "type": "number"
                  },
                  "walkoverLoss": {
                    "type": "number"
                  },
                  "concededWin": {
                    "type": "number"
                  },
                  "concededLoss": {
                    "type": "number"
                  },
                  "unplayedBoth": {
                    "type": "number"
                  },
                  "perSetWon": {
                    "type": "number",
                    "default": 0
                  },
                  "closeLoss": {
                    "type": [
                      "object",
                      "null"
                    ],
                    "properties": {
                      "withinGames": {
                        "type": "integer",
                        "minimum": 0,
                        "maximum": 99
                      },
                      "points": {
                        "type": "number"
                      }
                    },
                    "default": null,
                    "required": [
                      "withinGames",
                      "points"
                    ]
                  },
                  "convincingWin": {
                    "type": [
                      "object",
                      "null"
                    ],
                    "properties": {
                      "byGames": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 99
                      },
                      "points": {
                        "type": "number"
                      }
                    },
                    "default": null,
                    "required": [
                      "byGames",
                      "points"
                    ]
                  },
                  "allPlayed": {
                    "type": "number",
                    "default": 0
                  }
                },
                "required": [
                  "win",
                  "lossPlayed",
                  "retiredWin",
                  "retiredLoss",
                  "walkoverWin",
                  "walkoverLoss",
                  "concededWin",
                  "concededLoss",
                  "unplayedBoth"
                ]
              },
              "tiebreaks": {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "points",
                    "head_to_head",
                    "matches_won",
                    "matches_played",
                    "sets_won",
                    "set_difference",
                    "set_ratio",
                    "games_won",
                    "game_difference",
                    "game_ratio"
                  ]
                },
                "minItems": 1
              },
              "movement": {
                "type": "object",
                "properties": {
                  "promote": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 10
                  },
                  "relegate": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 10
                  },
                  "minMatchesForPromotion": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 50
                  }
                },
                "required": [
                  "promote",
                  "relegate",
                  "minMatchesForPromotion"
                ]
              },
              "withdrawal": {
                "type": "object",
                "properties": {
                  "playedMatches": {
                    "type": "string",
                    "enum": [
                      "keep",
                      "void"
                    ]
                  },
                  "remainingMatches": {
                    "type": "string",
                    "enum": [
                      "unplayed",
                      "walkover_to_opponent"
                    ]
                  }
                },
                "required": [
                  "playedMatches",
                  "remainingMatches"
                ]
              },
              "minMatchesForRanking": {
                "type": "integer",
                "minimum": 0,
                "maximum": 50,
                "default": 0
              },
              "minMatchesToPlay": {
                "type": "integer",
                "minimum": 0,
                "maximum": 50,
                "default": 4
              },
              "walkoverScore": {
                "type": "string",
                "enum": [
                  "nominal",
                  "none"
                ],
                "default": "nominal"
              }
            },
            "required": [
              "version",
              "scoringMode",
              "points",
              "tiebreaks",
              "movement",
              "withdrawal"
            ],
            "description": "Replaces the rules whole. Standings follow at once: they are computed on every read."
          },
          "sequence_in_season": {
            "type": "integer",
            "minimum": 1,
            "maximum": 50
          },
          "previous_competition_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "state": {
            "type": "string",
            "enum": [
              "draft",
              "active",
              "complete",
              "archived"
            ],
            "description": "One step at a time, forward or back: draft, active, complete, archived. Only a competition in an active season can be activated. A complete or archived competition changes only its state and visibility."
          },
          "visibility": {
            "type": "string",
            "enum": [
              "members",
              "private"
            ]
          }
        }
      },
      "DivisionList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Division"
            }
          }
        },
        "required": [
          "data"
        ]
      },
      "Division": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "competition_id": {
            "type": "string",
            "format": "uuid"
          },
          "ordinal": {
            "type": "integer",
            "description": "1 is the top division."
          },
          "name": {
            "type": "string",
            "example": "Division 1"
          },
          "target_size": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Advisory: informs placement suggestions, never enforced."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "competition_id",
          "ordinal",
          "name",
          "target_size",
          "created_at",
          "updated_at"
        ]
      },
      "NewDivision": {
        "type": "object",
        "properties": {
          "ordinal": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "target_size": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 2,
            "maximum": 100
          }
        },
        "description": "Everything is optional: a new division goes below the lowest, named \"Division N\"."
      },
      "DivisionChanges": {
        "type": "object",
        "properties": {
          "ordinal": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "target_size": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 2,
            "maximum": 100
          }
        }
      },
      "Fixtures": {
        "type": "object",
        "properties": {
          "division_id": {
            "type": "string",
            "format": "uuid"
          },
          "created": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "match_id": {
                  "type": "string",
                  "format": "uuid"
                },
                "side0_entry_id": {
                  "type": "string",
                  "format": "uuid"
                },
                "side1_entry_id": {
                  "type": "string",
                  "format": "uuid"
                }
              },
              "required": [
                "match_id",
                "side0_entry_id",
                "side1_entry_id"
              ]
            },
            "description": "The matches this call added. Empty if the division had every pairing already."
          },
          "pairings": {
            "type": "integer",
            "description": "How many pairings the division's active entries make: n entries make n(n-1)/2."
          }
        },
        "required": [
          "division_id",
          "created",
          "pairings"
        ]
      },
      "EntryList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Entry"
            }
          }
        },
        "required": [
          "data"
        ]
      },
      "Entry": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "competition_id": {
            "type": "string",
            "format": "uuid"
          },
          "division_id": {
            "type": "string",
            "format": "uuid"
          },
          "label": {
            "type": "string",
            "example": "Sam Kerr / Alex Price",
            "description": "How the entry is written in a table: its own name, or its members' display names."
          },
          "display_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "The entry's own name, if it has one."
          },
          "members": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "format": "uuid"
                },
                "display_name": {
                  "type": "string"
                },
                "role": {
                  "type": "string",
                  "enum": [
                    "player",
                    "partner"
                  ]
                }
              },
              "required": [
                "id",
                "display_name",
                "role"
              ]
            },
            "description": "One for singles, two for doubles: the player, then their partner."
          },
          "seed": {
            "type": [
              "integer",
              "null"
            ]
          },
          "state": {
            "type": "string",
            "enum": [
              "active",
              "withdrawn"
            ]
          },
          "placement_reason": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "promoted",
              "relegated",
              "held",
              "new",
              "returning",
              "manual",
              null
            ],
            "description": "Why the entry is in this division."
          },
          "previous_entry_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The same unit's entry in the previous competition."
          },
          "withdrawn_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "opted_out_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the player said they are not playing in the next competition, or the coach recorded it for them. Filling next season's draft leaves them out. It changes nothing about this competition."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "competition_id",
          "division_id",
          "label",
          "display_name",
          "members",
          "seed",
          "state",
          "placement_reason",
          "previous_entry_id",
          "withdrawn_at",
          "opted_out_at",
          "created_at",
          "updated_at"
        ]
      },
      "NewEntryResult": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Entry"
          },
          {
            "type": "object",
            "properties": {
              "warnings": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Warning"
                }
              }
            },
            "required": [
              "warnings"
            ]
          }
        ]
      },
      "Warning": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "example": "mixed_pair"
          },
          "detail": {
            "type": "string"
          }
        },
        "required": [
          "code",
          "detail"
        ],
        "description": "Something that looks wrong, but was done anyway: the coach's judgement wins."
      },
      "NewEntry": {
        "type": "object",
        "properties": {
          "division_id": {
            "type": "string",
            "format": "uuid"
          },
          "member_ids": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            },
            "minItems": 1,
            "maxItems": 2,
            "description": "One member for singles, two for doubles: the player first, then the partner."
          },
          "display_name": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 1,
            "maxLength": 100
          },
          "seed": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "maximum": 1000
          },
          "placement_reason": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "promoted",
              "relegated",
              "held",
              "new",
              "returning",
              "manual",
              null
            ]
          },
          "previous_entry_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          }
        },
        "required": [
          "division_id",
          "member_ids"
        ]
      },
      "EntryChanges": {
        "type": "object",
        "properties": {
          "division_id": {
            "type": "string",
            "format": "uuid",
            "description": "Moves the entry to another division of its competition. Only an entry with no match under way can move; its untouched fixtures are removed, and generating fixtures in the new division adds its new ones. An entry placed as promoted, relegated or held becomes `manual` unless a new `placement_reason` is sent."
          },
          "display_name": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 1,
            "maxLength": 100
          },
          "seed": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "maximum": 1000
          },
          "state": {
            "type": "string",
            "enum": [
              "active",
              "withdrawn"
            ],
            "description": "`withdrawn` withdraws it: its matches stay, and the competition's rules say what they count for. `active` reinstates it."
          },
          "placement_reason": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "promoted",
              "relegated",
              "held",
              "new",
              "returning",
              "manual",
              null
            ]
          },
          "previous_entry_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          }
        }
      },
      "PartnerChoiceList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PartnerChoice"
            }
          }
        },
        "required": [
          "data"
        ]
      },
      "PartnerChoice": {
        "type": "object",
        "properties": {
          "member_id": {
            "type": "string",
            "format": "uuid"
          },
          "member_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Their display name."
          },
          "choice": {
            "type": "string",
            "enum": [
              "keep",
              "leaving",
              "new_partner"
            ],
            "description": "`keep`: the same partner next season, which is what nobody saying anything means. `leaving`: not playing next season. `new_partner`: a new partner, the one named, or one the coach finds."
          },
          "partner_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Who they asked, if anyone."
          },
          "partner_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "agreed": {
            "type": "boolean",
            "description": "The partner named them back: a new pair, waiting for the coach to place it next season."
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        },
        "required": [
          "member_id",
          "member_name",
          "choice",
          "partner_id",
          "partner_name",
          "agreed",
          "updated_at"
        ]
      },
      "PartnerChoiceChanges": {
        "type": "object",
        "properties": {
          "choice": {
            "type": "string",
            "enum": [
              "keep",
              "leaving",
              "new_partner"
            ]
          },
          "partner_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "With `new_partner`: a player in this competition to ask. Leave it out for the coach to find one. Naming someone who has already named you agrees to it."
          }
        },
        "required": [
          "choice"
        ]
      },
      "Standings": {
        "type": "object",
        "properties": {
          "competition_id": {
            "type": "string",
            "format": "uuid"
          },
          "final": {
            "type": "boolean",
            "description": "True once the results deadline has passed or the competition is complete: matches still outstanding then count as unplayed."
          },
          "divisions": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "division_id": {
                  "type": "string",
                  "format": "uuid"
                },
                "ordinal": {
                  "type": "integer"
                },
                "name": {
                  "type": "string"
                },
                "rows": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/StandingsRow"
                  }
                }
              },
              "required": [
                "division_id",
                "ordinal",
                "name",
                "rows"
              ]
            }
          }
        },
        "required": [
          "competition_id",
          "final",
          "divisions"
        ],
        "description": "Computed from the matches on every request, from the competition's rules."
      },
      "StandingsRow": {
        "type": "object",
        "properties": {
          "position": {
            "type": [
              "integer",
              "null"
            ],
            "description": "1 is top. Null when unranked or withdrawn."
          },
          "standing": {
            "type": "string",
            "enum": [
              "ranked",
              "unranked",
              "withdrawn"
            ],
            "description": "Unranked: played fewer than the rules' minimum, so listed below the ranked. Withdrawn: listed last."
          },
          "entry_id": {
            "type": "string",
            "format": "uuid"
          },
          "label": {
            "type": "string",
            "example": "Sam Kerr"
          },
          "points": {
            "type": "number"
          },
          "played": {
            "type": "integer",
            "description": "Matches it took the court for, or turned up ready to."
          },
          "won": {
            "type": "integer"
          },
          "lost": {
            "type": "integer"
          },
          "unplayed": {
            "type": "integer",
            "description": "Matches that never happened."
          },
          "outstanding": {
            "type": "integer",
            "description": "Matches not yet in the ledger; they count for nothing yet."
          },
          "sets_won": {
            "type": "integer"
          },
          "sets_lost": {
            "type": "integer"
          },
          "games_won": {
            "type": "integer"
          },
          "games_lost": {
            "type": "integer"
          },
          "separated_by": {
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "points",
                  "games_won",
                  "sets_won",
                  "name"
                ]
              },
              {
                "type": "string",
                "enum": [
                  "points",
                  "head_to_head",
                  "matches_won",
                  "matches_played",
                  "sets_won",
                  "set_difference",
                  "set_ratio",
                  "games_won",
                  "game_difference",
                  "game_ratio"
                ]
              },
              {
                "type": "null"
              }
            ],
            "description": "What put this entry below the one above it. Null at the top of each group."
          },
          "matches": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StandingsMatch"
            },
            "description": "Where the points came from, match by match. With `all_played_bonus` they add up to `points`."
          },
          "all_played_bonus": {
            "type": "number",
            "description": "For turning up to every match once all are in, if the rules give it; otherwise 0."
          },
          "movement": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "promoted",
              "relegated",
              null
            ],
            "description": "Where the entry would go if the competition ended now, by its own movement rules — the same suggestion placements would make. Null: held, or not yet played a match. The top division promotes nobody and the bottom relegates nobody. Only ever a suggestion: the coach decides."
          }
        },
        "required": [
          "position",
          "standing",
          "entry_id",
          "label",
          "points",
          "played",
          "won",
          "lost",
          "unplayed",
          "outstanding",
          "sets_won",
          "sets_lost",
          "games_won",
          "games_lost",
          "separated_by",
          "matches",
          "all_played_bonus",
          "movement"
        ]
      },
      "StandingsMatch": {
        "type": "object",
        "properties": {
          "match_id": {
            "type": "string",
            "format": "uuid"
          },
          "opponent_entry_id": {
            "type": "string",
            "format": "uuid"
          },
          "result": {
            "type": "string",
            "enum": [
              "won",
              "lost",
              "unplayed"
            ]
          },
          "outcome": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "completed",
              "retired",
              "walkover",
              "conceded",
              null
            ],
            "description": "Null for a match that never happened."
          },
          "points": {
            "type": "number"
          },
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "for": {
                  "type": "string",
                  "enum": [
                    "result",
                    "sets",
                    "close_loss",
                    "convincing_win",
                    "unplayed"
                  ],
                  "description": "`result`: what the win or loss is worth, playing included. `sets`: for sets won. `close_loss` and `convincing_win`: the margin bonuses. `unplayed`: a match that never happened."
                },
                "points": {
                  "type": "number"
                }
              },
              "required": [
                "for",
                "points"
              ]
            },
            "description": "What earned the points, adding up to `points`."
          }
        },
        "required": [
          "match_id",
          "opponent_entry_id",
          "result",
          "outcome",
          "points",
          "items"
        ],
        "description": "One match as it counts in an entry's row. Matches not yet in the ledger are not listed."
      },
      "Progress": {
        "type": "object",
        "properties": {
          "competition_id": {
            "type": "string",
            "format": "uuid"
          },
          "results_deadline_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "days_remaining": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Whole days to the results deadline, on the club's own calendar."
          },
          "active_entries": {
            "type": "integer"
          },
          "matches": {
            "type": "integer"
          },
          "played": {
            "type": "integer",
            "description": "In the ledger."
          },
          "outstanding": {
            "type": "integer",
            "description": "Not yet in the ledger: open, reported or disputed."
          },
          "reported": {
            "type": "integer",
            "description": "One side has claimed; waiting on the other."
          },
          "disputed": {
            "type": "integer",
            "description": "The two claims differ."
          },
          "percent_played": {
            "type": [
              "number",
              "null"
            ]
          },
          "minimum_matches": {
            "type": "integer",
            "description": "How many matches each entry is expected to play: the competition's `minMatchesToPlay` rule."
          },
          "below_minimum": {
            "type": "integer",
            "description": "Active entries that have played fewer than `minimum_matches`, or than all their fixtures if they have fewer. Played is counted as the tables count it."
          },
          "divisions": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "division_id": {
                  "type": "string",
                  "format": "uuid"
                },
                "ordinal": {
                  "type": "integer"
                },
                "name": {
                  "type": "string"
                },
                "active_entries": {
                  "type": "integer"
                },
                "matches": {
                  "type": "integer"
                },
                "played": {
                  "type": "integer",
                  "description": "In the ledger."
                },
                "outstanding": {
                  "type": "integer",
                  "description": "Not yet in the ledger: open, reported or disputed."
                },
                "reported": {
                  "type": "integer",
                  "description": "One side has claimed; waiting on the other."
                },
                "disputed": {
                  "type": "integer",
                  "description": "The two claims differ."
                },
                "percent_played": {
                  "type": [
                    "number",
                    "null"
                  ]
                },
                "below_minimum": {
                  "type": "integer",
                  "description": "Active entries that have played fewer than `minimum_matches`, or than all their fixtures if they have fewer. Played is counted as the tables count it."
                }
              },
              "required": [
                "division_id",
                "ordinal",
                "name",
                "active_entries",
                "matches",
                "played",
                "outstanding",
                "reported",
                "disputed",
                "percent_played",
                "below_minimum"
              ]
            }
          }
        },
        "required": [
          "competition_id",
          "results_deadline_at",
          "days_remaining",
          "active_entries",
          "matches",
          "played",
          "outstanding",
          "reported",
          "disputed",
          "percent_played",
          "minimum_matches",
          "below_minimum",
          "divisions"
        ]
      },
      "SeasonProgress": {
        "type": "object",
        "properties": {
          "season_id": {
            "type": "string",
            "format": "uuid"
          },
          "results_deadline_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "days_remaining": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Whole days to the results deadline, on the club's own calendar."
          },
          "competitions": {
            "type": "array",
            "items": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/Progress"
                },
                {
                  "type": "object",
                  "properties": {
                    "name": {
                      "type": "string"
                    },
                    "discipline": {
                      "type": "string",
                      "enum": [
                        "singles",
                        "doubles"
                      ]
                    },
                    "state": {
                      "type": "string",
                      "enum": [
                        "draft",
                        "active",
                        "complete",
                        "archived"
                      ]
                    },
                    "opted_out": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "entry_id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "label": {
                            "type": "string"
                          },
                          "said_by": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "Who said so, by display name: the players leaving, on a break or not playing it, else the player who opted the entry out. Null when the coach did."
                          }
                        },
                        "required": [
                          "entry_id",
                          "label",
                          "said_by"
                        ]
                      },
                      "description": "Entries whose players said they are not playing in the next competition."
                    }
                  },
                  "required": [
                    "name",
                    "discipline",
                    "state",
                    "opted_out"
                  ]
                }
              ]
            }
          }
        },
        "required": [
          "season_id",
          "results_deadline_at",
          "days_remaining",
          "competitions"
        ]
      },
      "EntryProgress": {
        "type": "object",
        "properties": {
          "entry_id": {
            "type": "string",
            "format": "uuid"
          },
          "matches": {
            "type": "integer"
          },
          "played": {
            "type": "integer"
          },
          "outstanding": {
            "type": "integer"
          }
        },
        "required": [
          "entry_id",
          "matches",
          "played",
          "outstanding"
        ]
      },
      "DisputeHistory": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DisputeHistoryRow"
            }
          }
        },
        "required": [
          "data"
        ]
      },
      "DisputeHistoryRow": {
        "type": "object",
        "properties": {
          "member_id": {
            "type": "string",
            "format": "uuid"
          },
          "display_name": {
            "type": "string",
            "example": "Sam Kerr"
          },
          "this_season": {
            "type": "object",
            "properties": {
              "disputes": {
                "type": "integer",
                "description": "Matches where the two reports differed at some point."
              },
              "gave_way": {
                "type": "integer",
                "description": "Disputes that ended when this member accepted the other's score, or reported their own again to match it."
              },
              "held": {
                "type": "integer",
                "description": "Disputes that ended when the other side gave way to this member's score."
              },
              "settled_by_coach": {
                "type": "integer",
                "description": "Disputes the coach settled."
              },
              "unresolved": {
                "type": "integer",
                "description": "Disputes still not agreed or settled."
              }
            },
            "required": [
              "disputes",
              "gave_way",
              "held",
              "settled_by_coach",
              "unresolved"
            ],
            "description": "Matches in a season that is running now."
          },
          "earlier": {
            "type": "object",
            "properties": {
              "disputes": {
                "type": "integer",
                "description": "Matches where the two reports differed at some point."
              },
              "gave_way": {
                "type": "integer",
                "description": "Disputes that ended when this member accepted the other's score, or reported their own again to match it."
              },
              "held": {
                "type": "integer",
                "description": "Disputes that ended when the other side gave way to this member's score."
              },
              "settled_by_coach": {
                "type": "integer",
                "description": "Disputes the coach settled."
              },
              "unresolved": {
                "type": "integer",
                "description": "Disputes still not agreed or settled."
              }
            },
            "required": [
              "disputes",
              "gave_way",
              "held",
              "settled_by_coach",
              "unresolved"
            ],
            "description": "Matches in a season that has ended or is not yet running."
          }
        },
        "required": [
          "member_id",
          "display_name",
          "this_season",
          "earlier"
        ]
      },
      "ChaseList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ChaseEntry"
            }
          }
        },
        "required": [
          "data"
        ]
      },
      "ChaseEntry": {
        "type": "object",
        "properties": {
          "competition_id": {
            "type": "string",
            "format": "uuid"
          },
          "competition_name": {
            "type": "string"
          },
          "division_id": {
            "type": "string",
            "format": "uuid"
          },
          "division_name": {
            "type": "string"
          },
          "member_id": {
            "type": "string",
            "format": "uuid"
          },
          "display_name": {
            "type": "string"
          },
          "email": {
            "type": [
              "string",
              "null"
            ],
            "description": "PII. Present only for a credential holding `members:pii`."
          },
          "phone": {
            "type": [
              "string",
              "null"
            ],
            "description": "PII. Present only for a credential holding `members:pii`."
          },
          "outstanding_matches": {
            "type": "integer",
            "description": "Always needs_playing + awaiting_you + awaiting_them."
          },
          "needs_playing": {
            "type": "integer",
            "description": "Nobody has reported a result: go and arrange it."
          },
          "awaiting_you": {
            "type": "integer",
            "description": "The opponent has a score in that this member has not agreed to: one click clears it."
          },
          "awaiting_them": {
            "type": "integer",
            "description": "This member has claimed; the opponent has not answered."
          },
          "days_remaining": {
            "type": [
              "integer",
              "null"
            ]
          },
          "waiting_on": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The opponents, as written on a results sheet."
          },
          "matches_played": {
            "type": "integer",
            "description": "As the tables count it."
          },
          "minimum_matches": {
            "type": "integer",
            "description": "What this member is expected to play: the competition's `minMatchesToPlay`, or every fixture if fewer."
          },
          "matches_short": {
            "type": "integer",
            "description": "How many more to reach `minimum_matches`; 0 once reached."
          }
        },
        "required": [
          "competition_id",
          "competition_name",
          "division_id",
          "division_name",
          "member_id",
          "display_name",
          "outstanding_matches",
          "needs_playing",
          "awaiting_you",
          "awaiting_them",
          "days_remaining",
          "waiting_on",
          "matches_played",
          "minimum_matches",
          "matches_short"
        ]
      },
      "PlacementPlan": {
        "type": "object",
        "properties": {
          "competition_id": {
            "type": "string",
            "format": "uuid"
          },
          "previous_competition_id": {
            "type": "string",
            "format": "uuid"
          },
          "final": {
            "type": "boolean"
          },
          "suggestions": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "previous_entry_id": {
                  "type": "string",
                  "format": "uuid"
                },
                "label": {
                  "type": "string"
                },
                "from": {
                  "type": "object",
                  "properties": {
                    "division": {
                      "type": "integer"
                    },
                    "position": {
                      "type": [
                        "integer",
                        "null"
                      ]
                    }
                  },
                  "required": [
                    "division",
                    "position"
                  ]
                },
                "to_division": {
                  "type": [
                    "integer",
                    "null"
                  ],
                  "description": "The ordinal it would play in, or null: not carried over."
                },
                "reason": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "enum": [
                    "promoted",
                    "relegated",
                    "held",
                    null
                  ]
                },
                "explanation": {
                  "type": "string"
                }
              },
              "required": [
                "previous_entry_id",
                "label",
                "from",
                "to_division",
                "reason",
                "explanation"
              ]
            }
          },
          "vacancies": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "kind": {
                  "type": "string",
                  "enum": [
                    "promotion",
                    "relegation"
                  ]
                },
                "from_division": {
                  "type": "integer",
                  "description": "The ordinal of the division the place is in."
                },
                "to_division": {
                  "type": "integer",
                  "description": "The ordinal of the division it leads to."
                },
                "previous_entry_id": {
                  "type": "string",
                  "format": "uuid",
                  "description": "The entry that held the place in last time's table."
                },
                "label": {
                  "type": "string"
                },
                "because": {
                  "type": "string",
                  "example": "opted out of the next competition",
                  "description": "Why the entry cannot move into the place."
                },
                "fill": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "properties": {
                    "previous_entry_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "label": {
                      "type": "string"
                    },
                    "place": {
                      "type": "string",
                      "example": "3rd in Division 2"
                    }
                  },
                  "required": [
                    "previous_entry_id",
                    "label",
                    "place"
                  ],
                  "description": "Who the engine would suggest instead: the best-placed entry that is carried over and not moving, for a promotion; the worst, for a relegation, but never from the top half of the division for a relegation or the bottom half for a promotion. Null when nobody qualifies. The coach decides."
                },
                "explanation": {
                  "type": "string"
                }
              },
              "required": [
                "kind",
                "from_division",
                "to_division",
                "previous_entry_id",
                "label",
                "because",
                "fill",
                "explanation"
              ]
            }
          }
        },
        "required": [
          "competition_id",
          "previous_competition_id",
          "final",
          "suggestions",
          "vacancies"
        ]
      },
      "Placements": {
        "type": "object",
        "properties": {
          "competition_id": {
            "type": "string",
            "format": "uuid"
          },
          "previous_competition_id": {
            "type": "string",
            "format": "uuid"
          },
          "final": {
            "type": "boolean",
            "description": "Whether the previous tables were final. If not, outstanding matches counted for nothing yet."
          },
          "divisions_copied": {
            "type": "boolean",
            "description": "True when the draft had no divisions, so the previous competition's were copied."
          },
          "placed": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "entry_id": {
                  "type": "string",
                  "format": "uuid",
                  "description": "The new entry, in the draft."
                },
                "previous_entry_id": {
                  "type": "string",
                  "format": "uuid"
                },
                "label": {
                  "type": "string"
                },
                "from": {
                  "type": "object",
                  "properties": {
                    "division": {
                      "type": "integer",
                      "description": "The ordinal it played in last time."
                    },
                    "position": {
                      "type": [
                        "integer",
                        "null"
                      ]
                    }
                  },
                  "required": [
                    "division",
                    "position"
                  ]
                },
                "division_id": {
                  "type": "string",
                  "format": "uuid",
                  "description": "Where it has been placed."
                },
                "reason": {
                  "type": "string",
                  "enum": [
                    "promoted",
                    "relegated",
                    "held"
                  ]
                },
                "explanation": {
                  "type": "string",
                  "example": "1st of 6 in Division 2: promoted to Division 1."
                }
              },
              "required": [
                "entry_id",
                "previous_entry_id",
                "label",
                "from",
                "division_id",
                "reason",
                "explanation"
              ]
            }
          },
          "vacancies": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "kind": {
                  "type": "string",
                  "enum": [
                    "promotion",
                    "relegation"
                  ]
                },
                "from_division": {
                  "type": "integer",
                  "description": "The ordinal of the division the place is in."
                },
                "to_division": {
                  "type": "integer",
                  "description": "The ordinal of the division it leads to."
                },
                "previous_entry_id": {
                  "type": "string",
                  "format": "uuid",
                  "description": "The entry that held the place in last time's table."
                },
                "label": {
                  "type": "string"
                },
                "because": {
                  "type": "string",
                  "example": "opted out of the next competition",
                  "description": "Why the entry cannot move into the place."
                },
                "fill": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "properties": {
                    "previous_entry_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "label": {
                      "type": "string"
                    },
                    "place": {
                      "type": "string",
                      "example": "3rd in Division 2"
                    }
                  },
                  "required": [
                    "previous_entry_id",
                    "label",
                    "place"
                  ],
                  "description": "Who the engine would suggest instead: the best-placed entry that is carried over and not moving, for a promotion; the worst, for a relegation, but never from the top half of the division for a relegation or the bottom half for a promotion. Null when nobody qualifies. The coach decides."
                },
                "explanation": {
                  "type": "string"
                }
              },
              "required": [
                "kind",
                "from_division",
                "to_division",
                "previous_entry_id",
                "label",
                "because",
                "fill",
                "explanation"
              ]
            },
            "description": "Promotion and relegation places nobody takes. Movement is decided by table position: the top `promote` of a division are its promotion places and the bottom `relegate` its relegation places. An entry in one that is not carried over, or that played too few matches to go up, leaves it empty; the entry below does not move up to take it."
          },
          "not_carried": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "previous_entry_id": {
                  "type": "string",
                  "format": "uuid"
                },
                "label": {
                  "type": "string"
                },
                "explanation": {
                  "type": "string"
                }
              },
              "required": [
                "previous_entry_id",
                "label",
                "explanation"
              ]
            },
            "description": "Entries left out: opted out of this competition, a doubles pair breaking up (a player not playing, or wanting a new partner: see partner choices), withdrawn last time, short of the previous competition's minimum number of matches, or with a member who has since left or been removed from the club."
          }
        },
        "required": [
          "competition_id",
          "previous_competition_id",
          "final",
          "divisions_copied",
          "placed",
          "vacancies",
          "not_carried"
        ]
      },
      "EventPage": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Event"
            }
          },
          "next_cursor": {
            "type": "string",
            "pattern": "^\\d{1,20}\\.\\d{1,20}$",
            "example": "7461.1203",
            "description": "Where to carry on: pass it as `after` next time. Always present, because the feed never ends — an empty page means nothing new has happened yet."
          }
        },
        "required": [
          "data",
          "next_cursor"
        ]
      },
      "Event": {
        "type": "object",
        "properties": {
          "cursor": {
            "type": "string",
            "pattern": "^\\d{1,20}\\.\\d{1,20}$",
            "example": "7461.1203",
            "description": "This event's place in the feed."
          },
          "id": {
            "type": "string",
            "description": "Increasing, but not in the order events became visible: page by cursor."
          },
          "type": {
            "type": "string",
            "example": "match.result.confirmed"
          },
          "subject_type": {
            "type": "string",
            "example": "match"
          },
          "subject_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "actor_type": {
            "type": "string",
            "enum": [
              "member",
              "api_key",
              "system"
            ]
          },
          "actor_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The API key or member that caused it; null for the system."
          },
          "actor_name": {
            "type": [
              "string",
              "null"
            ],
            "example": "Coach website, 2026-09-29",
            "description": "What the actor is called now: the key's name, or the member's display name. Looked up on reading, never stored, so an erased member reads \"Erased member\". A member's name needs `members:read`; null without it, for the system, or for a record since deleted."
          },
          "subject_name": {
            "type": [
              "string",
              "null"
            ],
            "example": "Sam Kerr v Alex Price",
            "description": "What the subject is called now: a match as its two sides, an entry's label, a member's display name (with `members:read`), or the name of a season, competition, division, key, court or the club. Looked up on reading, like `actor_name`."
          },
          "competition_name": {
            "type": [
              "string",
              "null"
            ],
            "example": "Mixed doubles",
            "description": "The competition the payload's `competition_id` names, such as next season's choice is about. Looked up on reading; null when the payload names none, or it has been deleted."
          },
          "partner_name": {
            "type": [
              "string",
              "null"
            ],
            "example": "Priya S.",
            "description": "The display name of the member the payload's `partner_id` names, with `members:read`. Looked up on reading, like `actor_name`; null without the scope or when the payload names none."
          },
          "occurred_at": {
            "type": "string",
            "format": "date-time"
          },
          "payload": {
            "type": "object",
            "additionalProperties": {},
            "description": "Ids, names and what changed — never personal data, since the log cannot be erased."
          }
        },
        "required": [
          "cursor",
          "id",
          "type",
          "subject_type",
          "subject_id",
          "actor_type",
          "actor_id",
          "actor_name",
          "subject_name",
          "competition_name",
          "partner_name",
          "occurred_at",
          "payload"
        ]
      }
    },
    "parameters": {}
  },
  "paths": {
    "/healthz": {
      "get": {
        "tags": [
          "Meta"
        ],
        "summary": "Is the server up, and can it reach its database?",
        "description": "For a host's health checks. Needs no credential.",
        "responses": {
          "200": {
            "description": "Up, with the database reachable.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "ok"
                      ]
                    }
                  },
                  "required": [
                    "status"
                  ]
                }
              }
            }
          },
          "503": {
            "description": "Up, but the database is not reachable.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/me": {
      "get": {
        "tags": [
          "Me"
        ],
        "summary": "Who am I?",
        "description": "The club this credential belongs to, and what it may do there — and, for a player's session, who is signed in. The first call anything makes, and the quickest way to check a key works. Needs no particular scope.",
        "security": [
          {
            "apiKey": []
          },
          {
            "session": []
          }
        ],
        "responses": {
          "200": {
            "description": "The credential's club and scopes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Me"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/me/placements": {
      "get": {
        "tags": [
          "Me"
        ],
        "summary": "Your placements in started competitions",
        "description": "Only the signed-in player's active entries in member-visible competitions the coach has started, with their division and partner. Next season's drafts stay private until the coach starts them. A placement is provisional while its season is not yet active. The earliest planning season supplies the next season's announced name and dates even before placement. No other draft entries or private competitions are disclosed. Fixtures ready means this entry has fixtures.",
        "security": [
          {
            "session": []
          }
        ],
        "responses": {
          "200": {
            "description": "Your placements and the next announced season, if any.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlayerPlacements"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/me/contacts": {
      "get": {
        "tags": [
          "Me"
        ],
        "summary": "Who you play with and against, and how to reach them",
        "description": "Only for a player's session: their doubles partners and their opponents in competitions under way, with full name, email and telephone, so they can arrange their matches. Nobody else's details, nothing once the competition has ended, and nothing to or about a member who has left the club.",
        "security": [
          {
            "session": []
          }
        ],
        "responses": {
          "200": {
            "description": "Your partners and opponents.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlayerContacts"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/members/{id}/login-link": {
      "post": {
        "tags": [
          "Player logins"
        ],
        "summary": "Make a login link for a member",
        "description": "A one-time token for a login link, which works for 10080 minutes (7 days), or less if you ask. It is returned to you, and your own tooling delivers it — the core sends nothing. The player's website exchanges it for a session with `POST /v1/session`: do that from a page the player submits, not on opening the link, since mail scanners open links before people do.",
        "security": [
          {
            "apiKey": [
              "members:write"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LoginLinkOptions"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The link's token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LoginLink"
                }
              }
            }
          },
          "400": {
            "description": "The request is not valid; `errors` says where.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`member_removed`: a removed member cannot log in. `contact_changed`: the expected email no longer matches. `member_left`: a departed member cannot receive an emailed invitation.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/session": {
      "post": {
        "tags": [
          "Player logins"
        ],
        "summary": "Exchange a login link for a session",
        "description": "Send the link's token as the credential: `Authorization: Bearer dll_…`. The link works once — a second exchange is refused as if it never existed — and the session it starts never expires. It lasts until the player signs out, the coach signs them out everywhere, or they are removed from the club.",
        "security": [
          {
            "loginLink": []
          }
        ],
        "responses": {
          "201": {
            "description": "The player is signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Session"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Player logins"
        ],
        "summary": "Sign out",
        "description": "Ends the session presented. The player's sessions on other phones carry on.",
        "security": [
          {
            "session": []
          }
        ],
        "responses": {
          "204": {
            "description": "Signed out."
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/members/{id}/sign-out": {
      "post": {
        "tags": [
          "Player logins"
        ],
        "summary": "Sign a member out everywhere",
        "description": "Ends every session the member holds, and any login link not yet used — for a lost phone. They sign in again with a new link.",
        "security": [
          {
            "apiKey": [
              "members:write"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Signed out everywhere.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SignedOut"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/matches": {
      "get": {
        "tags": [
          "Matches"
        ],
        "summary": "List matches",
        "description": "Oldest first, or most recently changed first with `order=recent`. A fixture is simply a match that is `open`. A player's session sees the matches of competitions open to members, once they are no longer drafts.",
        "security": [
          {
            "apiKey": [
              "league:read"
            ]
          },
          {
            "session": [
              "league:read"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "description": "At most this many."
            },
            "required": false,
            "description": "At most this many.",
            "name": "limit",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The `next_cursor` of the page before; omit for the first page."
            },
            "required": false,
            "description": "The `next_cursor` of the page before; omit for the first page.",
            "name": "after",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "required": false,
            "name": "competition_id",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "Matches of this season's competitions only."
            },
            "required": false,
            "description": "Matches of this season's competitions only.",
            "name": "season_id",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "required": false,
            "name": "division_id",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "Matches this entry was drawn in."
            },
            "required": false,
            "description": "Matches this entry was drawn in.",
            "name": "entry_id",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "Matches this member plays in, singles or doubles. With a player's own id, their matches."
            },
            "required": false,
            "description": "Matches this member plays in, singles or doubles. With a player's own id, their matches.",
            "name": "member_id",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "open",
                "reported",
                "played",
                "disputed"
              ]
            },
            "required": false,
            "name": "status",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "created",
                "recent"
              ],
              "default": "created",
              "description": "`created`: oldest first. `recent`: most recently changed first, so `status=played` gives the latest results; `after` carries on from that match, and a match changing while you page moves to the top."
            },
            "required": false,
            "description": "`created`: oldest first. `recent`: most recently changed first, so `status=played` gives the latest results; `after` carries on from that match, and a match changing while you page moves to the top.",
            "name": "order",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of matches.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MatchPage"
                }
              }
            }
          },
          "400": {
            "description": "The request is not valid; `errors` says where.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/matches/{id}": {
      "get": {
        "tags": [
          "Matches"
        ],
        "summary": "A match, with submissions visible to the caller",
        "security": [
          {
            "apiKey": [
              "league:read"
            ]
          },
          {
            "session": [
              "league:read"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The match.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MatchDetail"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/matches/{id}/claims": {
      "post": {
        "tags": [
          "Matches"
        ],
        "summary": "Report a result for one side, or correct that side's report",
        "description": "The score is checked against the competition's match format, then compared with the other side's independently entered claim. Matching outcomes, set scores (including the deciding match tiebreak) and affected sides put it in the ledger; a difference makes the match `disputed`, and no opposing entry leaves it `reported` until the other side answers — however long that takes. A side's new claim replaces its previous one, which is kept. Sending the same claim again changes nothing, so a retry is safe. Once a match is played, only the coach can change it, and once the season's results deadline has passed no new claim is taken. A player's session reports for its own side of its own matches, and nothing else. Scores use the named-side order, with side 0 first regardless of the submitting side. Players see only their own side's submissions and the waiting or mismatch status. On a mismatch, speak outside the app and enter the agreed result; opposing submissions and differences remain private.",
        "security": [
          {
            "apiKey": [
              "results:write"
            ]
          },
          {
            "session": [
              "results:write"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewClaim"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The same claim was already standing; nothing changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MatchDetail"
                }
              }
            }
          },
          "201": {
            "description": "The claim was recorded; the match as it now stands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MatchDetail"
                }
              }
            }
          },
          "400": {
            "description": "The request is not valid; `errors` says where.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take. For a player's session: `not_your_match` when they are not playing in it, `not_your_side` when the claim is the other side's to make.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`already_played`, `competition_not_active`, or `deadline_passed`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/matches/{id}/settlement-preview": {
      "post": {
        "tags": [
          "Matches"
        ],
        "summary": "Review a coach decision before saving it",
        "description": "Validates the proposed result and computes both sides' division points and played credit before and after it, including withdrawal rules and all-played bonuses. The minimum is capped at the entry's fixture count. Does not submit a claim or change the ledger. Pass version as expected_version when settling; a change to the decision inputs requires another preview.",
        "security": [
          {
            "apiKey": [
              "league:write"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Settlement"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The proposed result and its effect.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SettlementPreview"
                }
              }
            }
          },
          "400": {
            "description": "The request is not valid; `errors` says where.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`competition_not_active`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/matches/{id}/settle": {
      "post": {
        "tags": [
          "Matches"
        ],
        "summary": "Settle a match as the coach",
        "description": "Enters the result directly: for a dispute the players cannot resolve, a match nobody reported, or a correction to one already played. Every earlier claim is kept, marked superseded. Settling with the result already in the ledger changes nothing. Replacing any confirmed result needs `override: true`, so the correction is deliberate. An optional reason records why the coach decided, and expected_version checks the state reviewed in settlement-preview. The deadline does not stop a settlement. A match that one side did not turn up to is a `walkover` with `retired_side` the absent side: the side that was there is credited the points and a match played, and the other's row counts it as unplayed. `unplayed` credits neither, and the response says who it leaves short of the competition's minimum.",
        "security": [
          {
            "apiKey": [
              "league:write"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Settlement"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The ledger already held this result; nothing changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SettledMatch"
                }
              }
            }
          },
          "201": {
            "description": "Settled; the match is played.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SettledMatch"
                }
              }
            }
          },
          "400": {
            "description": "The request is not valid; `errors` says where.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`competition_not_active`, `already_agreed` without `override`, or `settlement_changed` when the preview is out of date.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/me/match-plans": {
      "get": {
        "tags": [
          "Me"
        ],
        "summary": "Your match planning board",
        "description": "Saved markers for your open matches in active competitions. A match without a marker is to arrange. Planned is private; arranged is shared by every participant in that fixture.",
        "security": [
          {
            "session": []
          }
        ],
        "responses": {
          "200": {
            "description": "Your saved match plans.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/MatchPlan"
                      }
                    }
                  },
                  "required": [
                    "data"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/matches/{id}/plan": {
      "put": {
        "tags": [
          "Matches"
        ],
        "summary": "Plan one of your matches",
        "description": "Only a participant's session can change a marker. Planned is a private priority. Arranged means the fixture was agreed outside the app and updates every participant's board; any participant can move it back to To Arrange. It does not send a message or change results.",
        "security": [
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "state": {
                    "type": "string",
                    "enum": [
                      "to_arrange",
                      "planned",
                      "arranged"
                    ]
                  },
                  "arranged_on": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "date",
                    "default": null
                  }
                },
                "required": [
                  "state"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Your saved marker.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MatchPlan"
                }
              }
            }
          },
          "400": {
            "description": "The request is not valid; `errors` says where.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`match_closed`: only open fixtures in active competitions before the results deadline can be planned. `match_arranged`: move the shared arranged marker back before setting a private plan.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/club": {
      "get": {
        "tags": [
          "Club"
        ],
        "summary": "The club",
        "description": "The credential's club: its name, time zone and branding. The club's settings are the coach's, so this needs `admin`; any credential can learn which club it belongs to from `GET /v1/me`.",
        "security": [
          {
            "apiKey": [
              "admin"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "The club.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Club"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Club"
        ],
        "summary": "Change the club's name, time zone or branding",
        "description": "Only the fields sent change. Changing the time zone moves the day every deadline falls on. The slug is the club's public address and cannot change.",
        "security": [
          {
            "apiKey": [
              "admin"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ClubChanges"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The club, changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Club"
                }
              }
            }
          },
          "400": {
            "description": "The request is not valid; `errors` says where.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/weather": {
      "get": {
        "tags": [
          "Weather"
        ],
        "summary": "Forecast configuration",
        "description": "The reference website uses this with its service key to choose public Open-Meteo forecasts. It contains named coordinates only, never forecasts or player data.",
        "security": [
          {
            "apiKey": [
              "admin"
            ]
          },
          {
            "apiKey": [
              "members:read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "Units and court locations.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Weather"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Weather"
        ],
        "summary": "Change forecast units",
        "description": "Only the fields sent change. Court locations have their own endpoints.",
        "security": [
          {
            "apiKey": [
              "admin"
            ]
          },
          {
            "apiKey": [
              "league:write"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WeatherChanges"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The changed configuration.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Weather"
                }
              }
            }
          },
          "400": {
            "description": "The request is not valid; `errors` says where.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/court-locations": {
      "get": {
        "tags": [
          "Weather"
        ],
        "summary": "Court locations",
        "description": "Creation order. A club can have at most eight forecast locations.",
        "security": [
          {
            "apiKey": [
              "admin"
            ]
          },
          {
            "apiKey": [
              "members:read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "The court locations.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CourtLocationList"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Weather"
        ],
        "summary": "Add a court location",
        "security": [
          {
            "apiKey": [
              "admin"
            ]
          },
          {
            "apiKey": [
              "league:write"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewCourtLocation"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new court location.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CourtLocation"
                }
              }
            }
          },
          "400": {
            "description": "The request is not valid; `errors` says where.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`court_location_limit`: the club already has eight locations.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/court-locations/{id}": {
      "patch": {
        "tags": [
          "Weather"
        ],
        "summary": "Change a court location",
        "description": "Only the fields sent change.",
        "security": [
          {
            "apiKey": [
              "admin"
            ]
          },
          {
            "apiKey": [
              "league:write"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CourtLocationChanges"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The changed court location.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CourtLocation"
                }
              }
            }
          },
          "400": {
            "description": "The request is not valid; `errors` says where.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Weather"
        ],
        "summary": "Remove a court location",
        "security": [
          {
            "apiKey": [
              "admin"
            ]
          },
          {
            "apiKey": [
              "league:write"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "204": {
            "description": "Removed."
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/api-keys": {
      "get": {
        "tags": [
          "API keys"
        ],
        "summary": "List the club's API keys",
        "description": "Every key ever made, revoked ones included, oldest first. Never the keys themselves.",
        "security": [
          {
            "apiKey": [
              "admin"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "description": "At most this many."
            },
            "required": false,
            "description": "At most this many.",
            "name": "limit",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The `next_cursor` of the page before; omit for the first page."
            },
            "required": false,
            "description": "The `next_cursor` of the page before; omit for the first page.",
            "name": "after",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of keys.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKeyPage"
                }
              }
            }
          },
          "400": {
            "description": "The request is not valid; `errors` says where.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "API keys"
        ],
        "summary": "Make an API key",
        "description": "Returns the key once; store it then. A key can grant only scopes it holds itself. Granting `members:pii` is recorded in the event log with every other new key's scopes.",
        "security": [
          {
            "apiKey": [
              "admin"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateApiKey"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new key, with the key itself.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NewApiKey"
                }
              }
            }
          },
          "400": {
            "description": "The request is not valid; `errors` says where.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/api-keys/{id}/revoke": {
      "post": {
        "tags": [
          "API keys"
        ],
        "summary": "Revoke an API key",
        "description": "The key stops working at once, and for good. Revoking one already revoked changes nothing. The club's last working admin key cannot be revoked: make another first.",
        "security": [
          {
            "apiKey": [
              "admin"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The key, revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKey"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`last_admin_key`: nothing could manage the club without it.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/members": {
      "get": {
        "tags": [
          "Members"
        ],
        "summary": "List members",
        "description": "Oldest first. Display names and status for `members:read`; the personal fields as well for a credential that also holds `members:pii`. Removed members are left out unless asked for. Finding a member by email needs `members:pii` too: it is how a website sends a player their login link.",
        "security": [
          {
            "apiKey": [
              "members:read"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "description": "At most this many."
            },
            "required": false,
            "description": "At most this many.",
            "name": "limit",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The `next_cursor` of the page before; omit for the first page."
            },
            "required": false,
            "description": "The `next_cursor` of the page before; omit for the first page.",
            "name": "after",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "paused",
                "left"
              ]
            },
            "required": false,
            "name": "status",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "maxLength": 254,
              "format": "email",
              "description": "PII. The member with this email address, whatever its case. Needs `members:pii`."
            },
            "required": false,
            "description": "PII. The member with this email address, whatever its case. Needs `members:pii`.",
            "name": "email",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ],
              "description": "Only members who have never held a league entry, including withdrawn or historical entries. Needs `league:read`."
            },
            "required": false,
            "description": "Only members who have never held a league entry, including withdrawn or historical entries. Needs `league:read`.",
            "name": "never_entered",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ],
              "description": "Include members removed from the list."
            },
            "required": false,
            "description": "Include members removed from the list.",
            "name": "include_removed",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of members.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MemberPage"
                }
              }
            }
          },
          "400": {
            "description": "The request is not valid; `errors` says where.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Members"
        ],
        "summary": "Add a member",
        "description": "Only `display_name` is required. Setting any personal field also needs `members:pii`.",
        "security": [
          {
            "apiKey": [
              "members:write"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewMember"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new member.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Member"
                }
              }
            }
          },
          "400": {
            "description": "The request is not valid; `errors` says where.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`email_taken`: another member has that email address.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/members/{id}": {
      "get": {
        "tags": [
          "Members"
        ],
        "summary": "A member",
        "description": "Removed members too: their id still appears on old results.",
        "security": [
          {
            "apiKey": [
              "members:read"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The member.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Member"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Members"
        ],
        "summary": "Change a member",
        "description": "Only the fields sent change; null clears one. Changing any personal field also needs `members:pii`. `status: paused` is a break (see `POST /v1/members/{id}/pause`). `status: left` records that they have left the club: their results stay, they are not placed in the next season's draft, and they cannot be entered in a competition. Any place they hold in a draft is taken out at once. Setting it back to `active` undoes the status, not the places taken out.",
        "security": [
          {
            "apiKey": [
              "members:write"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MemberChanges"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The member, changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Member"
                }
              }
            }
          },
          "400": {
            "description": "The request is not valid; `errors` says where.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`email_taken`, or `member_removed`: a removed member cannot be changed.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Members"
        ],
        "summary": "Remove a member",
        "description": "Takes them off the club's list and ends any login they hold. Nothing is deleted: their results stay, under their display name. To remove their personal data as well, erase them.",
        "security": [
          {
            "apiKey": [
              "members:write"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "204": {
            "description": "Removed."
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/members/{id}/invitation": {
      "post": {
        "tags": [
          "Members"
        ],
        "summary": "Record a sign-in invitation outcome",
        "description": "The sending adapter records provider acceptance or failure. This does not send email or confirm inbox delivery.",
        "security": [
          {
            "apiKey": [
              "members:write",
              "members:pii"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "state": {
                    "type": "string",
                    "enum": [
                      "accepted",
                      "failed"
                    ]
                  },
                  "email": {
                    "type": "string",
                    "maxLength": 254,
                    "format": "email"
                  }
                },
                "required": [
                  "state",
                  "email"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Outcome recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Member"
                }
              }
            }
          },
          "400": {
            "description": "The request is not valid; `errors` says where.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/members/{id}/wants-to-play": {
      "put": {
        "tags": [
          "Members"
        ],
        "summary": "Say what a member wants to play next season",
        "description": "Singles, doubles, mixed doubles, any combination, or `not_now` for a social member; null clears it. The coach uses it to see who wants a place in next season's draft, and a newcomer who wants to play is listed for one. A player's session may set it for themselves; a key needs `members:write`. Saying the same again does nothing more.",
        "security": [
          {
            "apiKey": [
              "members:write"
            ]
          },
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WantsToPlayChoice"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The member, as they now stand.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Member"
                }
              }
            }
          },
          "400": {
            "description": "The request is not valid; `errors` says where.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`member_removed`: a removed member cannot be changed.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/members/{id}/leave": {
      "post": {
        "tags": [
          "Members"
        ],
        "summary": "Say this member is not playing next season at all",
        "description": "For a player who is leaving the league altogether, not only one competition: every entry they hold now, in singles and doubles, is left out of next season's draft with a sentence saying why, and a doubles partner is left needing a partner. Recorded once, as one event. It changes nothing about this season: their results and outstanding matches stand, and they can still report them. An entry made after they said it is not covered, so a player who changes their mind and is entered again is not caught by it. A player's session may do this for themselves; a key needs `league:write`, for a player who said so in person. Saying it twice keeps the first time. It is not leaving the club: that is `status: left` on the member.",
        "security": [
          {
            "apiKey": [
              "league:write"
            ]
          },
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The member, leaving.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Member"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`member_removed`: a removed member cannot be changed.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Members"
        ],
        "summary": "Take back not playing next season at all",
        "description": "Clears it, so the entries it covered are back in the reckoning for next season, except those that had opted out on their own: the opt-outs a player made one entry at a time are left as they were. Harmless if they never said it.",
        "security": [
          {
            "apiKey": [
              "league:write"
            ]
          },
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The member, playing on.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Member"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`member_removed`: a removed member cannot be changed.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/members/{id}/pause": {
      "post": {
        "tags": [
          "Members"
        ],
        "summary": "Take a break from the league until they say they are back",
        "description": "Sets the member's status to `paused`: a player taking a season off, or longer, who will return. Unlike `/leave`, which covers the next draft only, a break lasts until it is ended with `DELETE`. While it lasts they are left out of every draft (the reason says a member is taking a break), a doubles partner is left needing a partner, they cannot be entered in a competition, and a match against an entry whose members are all away is not chased. It changes nothing about this season: their results and matches stand, and anyone whose opponent has stepped away for good is credited by withdrawing the entry. A player's session may do this for themselves; a key needs `league:write`. Not for someone who has left the club (`status: left`). Saying it twice does nothing more.",
        "security": [
          {
            "apiKey": [
              "league:write"
            ]
          },
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The member, on a break.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Member"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`member_removed`, or `member_left`: someone who has left the club is not on a break.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Members"
        ],
        "summary": "Say they are back from a break",
        "description": "Sets the member's status back to `active`. They are in the reckoning for drafts again, but not placed in one already filled: the coach adds them from the draft's newcomers, or from those not carried over, who are listed with where they finished. Harmless if they were not on a break.",
        "security": [
          {
            "apiKey": [
              "league:write"
            ]
          },
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The member, active.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Member"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`member_removed`, or `member_left`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/members/{id}/erase": {
      "post": {
        "tags": [
          "Members"
        ],
        "summary": "Erase a member's personal data",
        "description": "For an erasure request under GDPR. Clears every personal field, replaces their display name with \"Erased member\", clears entry names and typed-in score reports that could name them, ends their logins and removes them from the list. Their results stay, so every table still adds up. Cannot be undone.",
        "security": [
          {
            "apiKey": [
              "admin"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "What is left of the member.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Member"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/join-requests": {
      "get": {
        "tags": [
          "Join requests"
        ],
        "summary": "List join requests",
        "description": "People who asked to join on the club's form and are waiting for the coach, oldest first. Each is deleted after 30 days if nobody decides it. Everything in one is personal, so this needs `members:pii`.",
        "security": [
          {
            "apiKey": [
              "members:read",
              "members:pii"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "description": "At most this many."
            },
            "required": false,
            "description": "At most this many.",
            "name": "limit",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The `next_cursor` of the page before; omit for the first page."
            },
            "required": false,
            "description": "The `next_cursor` of the page before; omit for the first page.",
            "name": "after",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of join requests.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JoinRequestPage"
                }
              }
            }
          },
          "400": {
            "description": "The request is not valid; `errors` says where.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Join requests"
        ],
        "summary": "Ask to join",
        "description": "What a club's public sign-up form sends, with its own key. The person is not a member until the coach approves them. Nothing here limits how many arrive: the form's website does that, since only it sees who is asking.",
        "security": [
          {
            "apiKey": [
              "members:write",
              "members:pii"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewJoinRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The request, waiting for the coach.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JoinRequest"
                }
              }
            }
          },
          "400": {
            "description": "The request is not valid; `errors` says where.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`already_requested`: someone with that email address is already waiting.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/join-requests/{id}": {
      "get": {
        "tags": [
          "Join requests"
        ],
        "summary": "A join request",
        "security": [
          {
            "apiKey": [
              "members:read",
              "members:pii"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The join request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JoinRequest"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Join requests"
        ],
        "summary": "Decline a join request",
        "description": "Deletes the request and everything they sent. They are not told.",
        "security": [
          {
            "apiKey": [
              "members:write"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "204": {
            "description": "Declined."
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/join-requests/{id}/approve": {
      "post": {
        "tags": [
          "Join requests"
        ],
        "summary": "Approve a join request",
        "description": "Adds them to the club's list as an active member, with their full name, contact details, gender and age group, and the coach's level (defaulting to the applicant's self-rating), and deletes the request. They are not placed in a running season: the coach places them in the draft for the next one. The member's `member.created` event records the request and the privacy notice they agreed to.",
        "security": [
          {
            "apiKey": [
              "members:write",
              "members:pii"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/JoinRequestApproval"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new member.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Member"
                }
              }
            }
          },
          "400": {
            "description": "The request is not valid; `errors` says where.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`email_taken`: a member already has that email address.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/seasons": {
      "get": {
        "tags": [
          "Seasons"
        ],
        "summary": "List seasons",
        "description": "Oldest first.",
        "security": [
          {
            "apiKey": [
              "league:read"
            ]
          },
          {
            "session": [
              "league:read"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "description": "At most this many."
            },
            "required": false,
            "description": "At most this many.",
            "name": "limit",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The `next_cursor` of the page before; omit for the first page."
            },
            "required": false,
            "description": "The `next_cursor` of the page before; omit for the first page.",
            "name": "after",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "planning",
                "active",
                "complete",
                "archived"
              ]
            },
            "required": false,
            "name": "state",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of seasons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SeasonPage"
                }
              }
            }
          },
          "400": {
            "description": "The request is not valid; `errors` says where.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Seasons"
        ],
        "summary": "Start planning a season",
        "description": "A new season is `planning`. Its dates can wait until it is activated.",
        "security": [
          {
            "apiKey": [
              "league:write"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewSeason"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new season.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Season"
                }
              }
            }
          },
          "400": {
            "description": "The request is not valid; `errors` says where.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`name_taken`: the club already has a season with that name.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/seasons/{id}": {
      "get": {
        "tags": [
          "Seasons"
        ],
        "summary": "A season",
        "security": [
          {
            "apiKey": [
              "league:read"
            ]
          },
          {
            "session": [
              "league:read"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The season.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Season"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Seasons"
        ],
        "summary": "Change a season, or move it to another state",
        "description": "Only the fields sent change; null clears one.",
        "security": [
          {
            "apiKey": [
              "league:write"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SeasonChanges"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The season, changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Season"
                }
              }
            }
          },
          "400": {
            "description": "The request is not valid; `errors` says where.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`name_taken`; `invalid_transition`, a step too far; `dates_needed`, an active season without dates; or `competition_active`, leaving active while a competition is.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/competitions": {
      "get": {
        "tags": [
          "Competitions"
        ],
        "summary": "List competitions",
        "description": "Oldest first. A player's session sees those open to members, once they are no longer drafts.",
        "security": [
          {
            "apiKey": [
              "league:read"
            ]
          },
          {
            "session": [
              "league:read"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "description": "At most this many."
            },
            "required": false,
            "description": "At most this many.",
            "name": "limit",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The `next_cursor` of the page before; omit for the first page."
            },
            "required": false,
            "description": "The `next_cursor` of the page before; omit for the first page.",
            "name": "after",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "required": false,
            "name": "season_id",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "active",
                "complete",
                "archived"
              ]
            },
            "required": false,
            "name": "state",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of competitions.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompetitionPage"
                }
              }
            }
          },
          "400": {
            "description": "The request is not valid; `errors` says where.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Competitions"
        ],
        "summary": "Create a competition",
        "description": "A new competition is a `draft`. Its match format and rules are checked now, because rules are data: a mistake here would otherwise surface at the end of the season.",
        "security": [
          {
            "apiKey": [
              "league:write"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewCompetition"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new competition.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Competition"
                }
              }
            }
          },
          "400": {
            "description": "The request is not valid; `errors` says where.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`name_taken`, or `season_closed`: the season is complete or archived.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/competitions/{id}": {
      "get": {
        "tags": [
          "Competitions"
        ],
        "summary": "A competition",
        "security": [
          {
            "apiKey": [
              "league:read"
            ]
          },
          {
            "session": [
              "league:read"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The competition.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Competition"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Competitions"
        ],
        "summary": "Change a competition, or move it to another state",
        "description": "Only the fields sent change.",
        "security": [
          {
            "apiKey": [
              "league:write"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CompetitionChanges"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The competition, changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Competition"
                }
              }
            }
          },
          "400": {
            "description": "The request is not valid; `errors` says where.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`name_taken`; `invalid_transition`; `season_not_active`; `competition_closed`; or `entries_exist`, changing the discipline of a competition with entries.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/competitions/{id}/divisions": {
      "get": {
        "tags": [
          "Divisions"
        ],
        "summary": "A competition's divisions",
        "description": "Top first. Not paged: a competition has a handful.",
        "security": [
          {
            "apiKey": [
              "league:read"
            ]
          },
          {
            "session": [
              "league:read"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The divisions.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DivisionList"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Divisions"
        ],
        "summary": "Add a division to a competition",
        "security": [
          {
            "apiKey": [
              "league:write"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewDivision"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new division.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Division"
                }
              }
            }
          },
          "400": {
            "description": "The request is not valid; `errors` says where.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`ordinal_taken`, or `competition_closed`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/divisions/{id}": {
      "get": {
        "tags": [
          "Divisions"
        ],
        "summary": "A division",
        "security": [
          {
            "apiKey": [
              "league:read"
            ]
          },
          {
            "session": [
              "league:read"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The division.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Division"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Divisions"
        ],
        "summary": "Change a division",
        "description": "Only the fields sent change. To swap two divisions' ordinals, move one out of the way first.",
        "security": [
          {
            "apiKey": [
              "league:write"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DivisionChanges"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The division, changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Division"
                }
              }
            }
          },
          "400": {
            "description": "The request is not valid; `errors` says where.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`ordinal_taken`, or `competition_closed`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Divisions"
        ],
        "summary": "Delete an empty division",
        "description": "Only a division with no entries and no matches, so nothing is ever lost with it.",
        "security": [
          {
            "apiKey": [
              "league:write"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted."
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`division_in_use`, or `competition_closed`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/divisions/{id}/fixtures": {
      "post": {
        "tags": [
          "Divisions"
        ],
        "summary": "Generate a division's round robin",
        "description": "Adds an open match for every pairing of the division's active entries that it does not have yet. Safe to run again, after a late entry for instance: only the missing pairings are added, and a pairing already there, played or not, is left alone. Matches have no dates; arranging them is up to the players.",
        "security": [
          {
            "apiKey": [
              "league:write"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "What was added.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fixtures"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`competition_closed`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/competitions/{id}/entries": {
      "get": {
        "tags": [
          "Entries"
        ],
        "summary": "A competition's entries",
        "description": "In the order they were made. Not paged: a competition has a few dozen at most.",
        "security": [
          {
            "apiKey": [
              "league:read"
            ]
          },
          {
            "session": [
              "league:read"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "required": false,
            "name": "division_id",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "withdrawn"
              ]
            },
            "required": false,
            "name": "state",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "The entries.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EntryList"
                }
              }
            }
          },
          "400": {
            "description": "The request is not valid; `errors` says where.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Entries"
        ],
        "summary": "Enter a player or pair",
        "description": "Checks the entry has one member for singles or two for doubles, and that none of them is already in the competition. A pair that looks wrong for a mixed competition is entered with a warning, never refused — and the warning, which reveals recorded gender, goes only to a credential holding `members:pii`. Confirming placements is making entries with a `placement_reason`.",
        "security": [
          {
            "apiKey": [
              "league:write"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewEntry"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new entry, and anything that looked wrong about it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NewEntryResult"
                }
              }
            }
          },
          "400": {
            "description": "The request is not valid; `errors` says where.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`already_entered`, or `competition_closed`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/entries/{id}": {
      "get": {
        "tags": [
          "Entries"
        ],
        "summary": "An entry",
        "security": [
          {
            "apiKey": [
              "league:read"
            ]
          },
          {
            "session": [
              "league:read"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The entry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Entry"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Entries"
        ],
        "summary": "Change, move, withdraw or reinstate an entry",
        "description": "Only the fields sent change; null clears one.",
        "security": [
          {
            "apiKey": [
              "league:write"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EntryChanges"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The entry, changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Entry"
                }
              }
            }
          },
          "400": {
            "description": "The request is not valid; `errors` says where.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`entry_has_matches`, moving an entry with a match under way; or `competition_closed`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Entries"
        ],
        "summary": "Delete an entry made by mistake",
        "description": "Only an entry with no match under way, together with its untouched fixtures. An entry that has played is withdrawn instead, so its results stay on record.",
        "security": [
          {
            "apiKey": [
              "league:write"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted."
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`entry_has_matches`, or `competition_closed`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/entries/{id}/opt-out": {
      "post": {
        "tags": [
          "Entries"
        ],
        "summary": "Say this entry is not playing in the next competition",
        "description": "The one thing a player says about next season, and they say it about the entry they hold now: filling next season's draft leaves them out, with a sentence saying why, and the coach can add them back if they change their mind. It changes nothing about this competition — their outstanding matches stand, and they can still report them. A player's session may do this for an entry they play in; a key needs `league:write`, for a player who said so in person. Saying it twice keeps the first time.",
        "security": [
          {
            "apiKey": [
              "league:write"
            ]
          },
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The entry, opted out.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Entry"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Entries"
        ],
        "summary": "Take back opting out of the next competition",
        "description": "Puts the entry back in the reckoning for next season's placements. Harmless if it never opted out.",
        "security": [
          {
            "apiKey": [
              "league:write"
            ]
          },
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The entry, back in the reckoning.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Entry"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/competitions/{id}/partner-choices": {
      "get": {
        "tags": [
          "Entries"
        ],
        "summary": "What a doubles competition's players want next season",
        "description": "Every player who is not keeping their partner: not playing, or wanting a new partner, named or not, and whether the one named has agreed. Filling next season's draft leaves out every pair with a player here, saying why; an agreed pair waits for the coach to place it. A player's session sees their own choice, anyone asking them, and their partner's.",
        "security": [
          {
            "apiKey": [
              "league:read"
            ]
          },
          {
            "session": [
              "league:read"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The choices, by player.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerChoiceList"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/competitions/{id}/partner-choices/{member_id}": {
      "put": {
        "tags": [
          "Entries"
        ],
        "summary": "Say what a doubles player wants next season",
        "description": "While the competition is under way. A player's session speaks only for its own player; a key needs `league:write`, for a player who told the coach in person. The partner named must be playing in the same competition. Naming someone who has named you back agrees: both are marked agreed. Changing an agreed choice leaves the other player looking for a partner, since they have still left theirs. Not playing says no to anyone waiting for an answer. Returns every choice the change touched.",
        "security": [
          {
            "apiKey": [
              "league:write"
            ]
          },
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The player whose choice it is."
            },
            "required": true,
            "description": "The player whose choice it is.",
            "name": "member_id",
            "in": "path"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PartnerChoiceChanges"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The choices as they are now, for the players it changed, the player first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerChoiceList"
                }
              }
            }
          },
          "400": {
            "description": "The request is not valid; `errors` says where.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`not_doubles`, or `choices_closed` once the competition is no longer under way.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/competitions/{id}/partner-choices/{member_id}/decline": {
      "post": {
        "tags": [
          "Entries"
        ],
        "summary": "Say no to partnering a player next season",
        "description": "By the player they asked, or a key with `league:write`. The asker is left looking for a partner, for the coach to find or for them to ask someone else. Saying no to an agreed pair leaves both looking.",
        "security": [
          {
            "apiKey": [
              "league:write"
            ]
          },
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The player whose choice it is."
            },
            "required": true,
            "description": "The player whose choice it is.",
            "name": "member_id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The choices as they are now, for the players it changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerChoiceList"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`not_doubles`, or `choices_closed` once the competition is no longer under way.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/competitions/{id}/standings": {
      "get": {
        "tags": [
          "Standings and progress"
        ],
        "summary": "A competition's tables",
        "description": "Every division's table, top division first, computed now from the competition's rules: points, then its tiebreaks in order, then the name, so a table never reorders itself. Each row says what separated it from the one above.",
        "security": [
          {
            "apiKey": [
              "league:read"
            ]
          },
          {
            "session": [
              "league:read"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "required": false,
            "name": "division_id",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "The tables.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Standings"
                }
              }
            }
          },
          "400": {
            "description": "The request is not valid; `errors` says where.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/competitions/{id}/progress": {
      "get": {
        "tags": [
          "Standings and progress"
        ],
        "summary": "How far through a competition is",
        "description": "Overall and by division: how much is played, waiting on a reply or disputed, and how long is left.",
        "security": [
          {
            "apiKey": [
              "league:read"
            ]
          },
          {
            "session": [
              "league:read"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The competition's progress.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Progress"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/seasons/{id}/progress": {
      "get": {
        "tags": [
          "Standings and progress"
        ],
        "summary": "How far through a season each competition is",
        "description": "Every competition in the season, oldest first, with the same counts as its own progress and who has opted out of the next one: a whole season's dashboard in one read. A player's session sees the competitions open to members, once they are no longer drafts.",
        "security": [
          {
            "apiKey": [
              "league:read"
            ]
          },
          {
            "session": [
              "league:read"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The season's progress.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SeasonProgress"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/entries/{id}/progress": {
      "get": {
        "tags": [
          "Standings and progress"
        ],
        "summary": "How far through its matches an entry is",
        "security": [
          {
            "apiKey": [
              "league:read"
            ]
          },
          {
            "session": [
              "league:read"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The entry's progress.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EntryProgress"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dispute-history": {
      "get": {
        "tags": [
          "Matches"
        ],
        "summary": "Who has been in disputes, and how each ended",
        "description": "For the coach: each member who has been on a side of a match whose two reports differed, this season and earlier, most disputes first. Both players are in every dispute, so the count alone does not say who is at fault: `gave_way` and `held` say how each ended for that member. Needs an API key, never a player's session: players are not shown each other's history. Counted from the event log, so it covers every season.",
        "security": [
          {
            "apiKey": [
              "league:read",
              "members:read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "Members with at least one dispute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DisputeHistory"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/chase-list": {
      "get": {
        "tags": [
          "Standings and progress"
        ],
        "summary": "Who has matches outstanding",
        "description": "One row per member per division, most outstanding first, split by what is needed from them. Only the running season is listed: a finished season's matches cannot be settled, so nothing would clear them. A match against a withdrawn entry, or one whose members have all left the club, is outstanding for no one until the entry returns. `within_days` is the whole reminder workflow: 30 a month out, 14 a fortnight later. Emails and phone numbers appear only for a credential holding `members:pii`. What gets sent, and to whom, is the coach's decision — the core sends nothing.",
        "security": [
          {
            "apiKey": [
              "members:read"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "required": false,
            "name": "competition_id",
            "in": "query"
          },
          {
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "minimum": 0,
              "maximum": 366,
              "description": "Only competitions whose deadline is at most this many days away."
            },
            "required": false,
            "description": "Only competitions whose deadline is at most this many days away.",
            "name": "within_days",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "The list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChaseList"
                }
              }
            }
          },
          "400": {
            "description": "The request is not valid; `errors` says where.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/competitions/{id}/placements": {
      "get": {
        "tags": [
          "Entries"
        ],
        "summary": "What filling a draft would do, and which places it would leave empty",
        "description": "The same decision as filling the draft, worked out now from the previous competition's tables and writing nothing, so it can be read before the draft is filled and again after the coach has changed it. Each entry of last time with where it would go and why, and the promotion and relegation places left empty with who to suggest instead. For a draft that names its previous competition.",
        "security": [
          {
            "apiKey": [
              "league:read"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlacementPlan"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`not_draft`; `no_previous_competition`; or `discipline_mismatch`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Entries"
        ],
        "summary": "Fill a draft competition from the previous one's tables",
        "description": "For a draft competition that names its previous competition and has no entries yet. Every entry that finished last time is entered again, each with its reason and a sentence saying why: by default the top three of each division promoted, the bottom three relegated and the rest held — this draft's own rules set the counts, so changing them changes the suggestion. Anyone who opted out of the next competition is left out, and leaves their place empty (the entry below does not move up or down to take it: the response lists each such place under `vacancies`, with who to suggest instead), as is a doubles pair with a player not playing next season or wanting a new partner; so, once the previous competition's tables are final, is anyone who played fewer matches than its `minMatchesToPlay`, or all their fixtures if fewer. A draft with no divisions gets a copy of the previous ones. The coach then adjusts the draft with the entry routes and submits it by activating the competition. Nothing is in effect until then: the engine suggests, and the coach decides.",
        "security": [
          {
            "apiKey": [
              "league:write"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The record's id."
            },
            "required": true,
            "description": "The record's id.",
            "name": "id",
            "in": "path"
          }
        ],
        "responses": {
          "201": {
            "description": "The draft, filled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Placements"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing with that id in this club.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "`not_draft`; `no_previous_competition`; `entries_exist`, already filled; or `discipline_mismatch`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/events": {
      "get": {
        "tags": [
          "Events"
        ],
        "summary": "Read the event feed",
        "description": "Everything that has happened in the club, oldest first, in the order it is safe to read: an event appears only once nothing earlier can still appear, so reading on from `next_cursor` never skips one. This is how adapters react to change — announcing results, refreshing a website — since the core sends nothing itself. `order=newest` reads backwards from the latest event instead, for showing people what happened; it makes no promise about events that commit while you page.",
        "security": [
          {
            "apiKey": [
              "league:read"
            ]
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d{1,20}\\.\\d{1,20}$",
              "example": "7461.1203",
              "description": "Carry on after this cursor, in the order asked for. Omit to start from the beginning, or from the latest event with `order=newest`."
            },
            "required": false,
            "description": "Carry on after this cursor, in the order asked for. Omit to start from the beginning, or from the latest event with `order=newest`.",
            "name": "after",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "oldest",
                "newest"
              ],
              "default": "oldest"
            },
            "required": false,
            "name": "order",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "Only this match's audit history, including who settled it and the recorded reason."
            },
            "required": false,
            "description": "Only this match's audit history, including who settled it and the recorded reason.",
            "name": "match_id",
            "in": "query"
          },
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100
            },
            "required": false,
            "name": "limit",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "The next events.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventPage"
                }
              }
            }
          },
          "400": {
            "description": "The request is not valid; `errors` says where.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "No credential, or one that is unknown, revoked or expired.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The credential lacks a scope this needs, or is of a kind this does not take.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    }
  },
  "webhooks": {}
}
