{
  "openapi": "3.1.0",
  "info": {
    "title": "LawFive API",
    "version": "1",
    "description": "The API of a LawFive server, which the web app, the watch apps and the phone apps use. Generated from schema/src/endpoints.ts; generated/API.md is the same reference to read.",
    "license": {
      "name": "See lawfive.dev",
      "url": "https://lawfive.dev"
    }
  },
  "servers": [
    {
      "url": "https://api.lawfive.app",
      "description": "LawFive's own service"
    },
    {
      "url": "{server}",
      "description": "A server of your own, or the demo",
      "variables": {
        "server": {
          "default": "https://lawfive.example.org",
          "description": "Its address, for example http://localhost:3000 for the demo."
        }
      }
    }
  ],
  "security": [
    {
      "bearer": []
    },
    {
      "session": []
    }
  ],
  "tags": [
    {
      "name": "Server",
      "description": "What the server is and offers."
    },
    {
      "name": "Accounts",
      "description": "Signing up, in and out (Better Auth's endpoints). Browsers keep the session in a cookie and must send an Origin header matching the server's address; apps send no Origin, take the token from the set-auth-token response header and send it as Authorization: Bearer <token>."
    },
    {
      "name": "Linking a watch",
      "description": "A watch links to an account with a code (RFC 8628): it asks for a code and shows it, someone signed in enters it on the web app's Link page and approves it, and the watch, polling meanwhile, gets its own token."
    },
    {
      "name": "Records",
      "description": "The user's records, one collection per kind. Ids are UUIDs made by the client, so a record created offline keeps its id. Every change raises version; an update sends the version it last saw and gets 409 if the record changed in the meantime. Deleted records are kept as markers so other devices learn of the deletion."
    },
    {
      "name": "Tracks",
      "description": "GPS and heart rate for a match report (TRK-1 to TRK-3), in LawFive's own format whatever recorded them: convert from the source (RefSix, an activity file, a phone's health store) before uploading. Not part of sync, as tracks are big; fetch one when it's shown. The figures (distance, speeds, sprints, heart rate and zones) are worked out by the server each time."
    },
    {
      "name": "Sync",
      "description": "Everything that changed since a device last looked."
    },
    {
      "name": "People",
      "description": "Admins (USR-1) see everyone on the server, approve or decline new accounts (USR-4), make other admins, and make password reset links, suspend and delete accounts, and change who can sign up (USR-7). Everyone else gets 403."
    },
    {
      "name": "App sign-in",
      "description": "Signing in on a phone with a code the web app shows, as a QR code or in letters (APP-13), instead of typing a password."
    },
    {
      "name": "Invitations",
      "description": "Links that let someone make an account even when sign-up is closed (USR-3). Admins make and cancel them; the link opens the web app's invitation page, which looks the invitation up and signs up with it."
    },
    {
      "name": "Import",
      "description": "Adding and updating matches from a spreadsheet, CSV or JSON file (IMP-1 to IMP-8). Apps read the file into rows; the server matches each row to a match (by LawFive id, else match number), finds or adds the venues, competitions and referees it names, and saves only what the row fills in. Check first with dryRun."
    },
    {
      "name": "Calendar",
      "description": "A private calendar feed of the user's matches (CAL-1 to CAL-5), for calendar apps to subscribe to."
    },
    {
      "name": "Stats",
      "description": "Reports on the user's games (STAT-1 to STAT-4), worked out by the server so every app shows the same."
    },
    {
      "name": "Devices",
      "description": "Where the account is signed in."
    },
    {
      "name": "Attachments",
      "description": "Photos on matches, venues and referees (FILE-1 to FILE-5), kept by the server on its disk or in S3-compatible storage. Upload the file as multipart/form-data, ideally already made smaller (the web app sends at most 2000 pixels on the longest side, as a JPEG without its location and camera details) with a thumbnail; the server keeps what it's sent. The files are private: fetch them with the same sign-in. Deleting the record deletes its attachments. Only a signed-in session can use them for now."
    },
    {
      "name": "Export",
      "description": "Everything in the account as one zip (DATA-2), and restoring one into an account (DATA-4), for backups and moving between servers. The zip has lawfive.json (what it is), README.md, records/<collection>.json (each an array of records as the API gives them), tracks/<match record id>.json, attachments.json and the photos in attachments/. Only a signed-in session can use them."
    },
    {
      "name": "Sharing",
      "description": "A match's share link (DATA-7): an address nobody can guess that shows the match, its log and its track to anyone who has it, without signing in. Never the notes, fee, crew, contacts, player names or photos."
    },
    {
      "name": "Webhooks",
      "description": "URLs that hear about changes to the account's records (HOOK-1 to HOOK-5): matches, match reports, venues, referees, competitions and templates, however they were made (the web app, the API, a watch, an import). Each delivery is a signed JSON POST (WebhookPayload), retried up to five times over about three hours when it doesn't get a 2xx answer. Only a signed-in session can manage them."
    },
    {
      "name": "API keys",
      "description": "Keys for scripts and integrations (KEY-1 to KEY-4), made in the web app's Settings. A key is sent like a session token, Authorization: Bearer <key>, and can use only the collections it was given (read, or read and write), plus the weather (with matches:read) and address search (with venues:write). Keys can't manage devices or other keys, and only a signed-in session can make or delete them."
    },
    {
      "name": "Watch",
      "description": "What a Garmin watch uses, shaped for its little memory: matches in the form it starts one from, and finished matches as it saved them."
    },
    {
      "name": "Places",
      "description": "Address search for placing venues on the map."
    },
    {
      "name": "Quiz",
      "description": "The Laws of the Game quiz's questions (QUIZ-1)."
    },
    {
      "name": "Weather",
      "description": "Forecasts for the user's matches, and the weather finished matches were played in (WX-1 to WX-8). The server fetches and keeps forecasts; apps never call the weather service themselves."
    }
  ],
  "paths": {
    "/health": {
      "get": {
        "operationId": "getHealth",
        "tags": [
          "Server"
        ],
        "summary": "Is the server up",
        "security": [],
        "responses": {
          "200": {
            "description": "It is.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string",
                  "const": "ok"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/meta": {
      "get": {
        "operationId": "getMeta",
        "tags": [
          "Server"
        ],
        "summary": "What this server offers",
        "description": "The API version and capabilities, the map tiles to use and the data providers to credit. Apps check capabilities before using geocode or weather, which a server can switch off.",
        "security": [],
        "responses": {
          "200": {
            "description": "The server's description.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServerMeta"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/auth/sign-up/email": {
      "post": {
        "operationId": "postAuthSignUpEmail",
        "tags": [
          "Accounts"
        ],
        "summary": "Make an account",
        "description": "Signs the new account in. With SIGN_UP=closed only an invitation (or an address in ADMIN_EMAILS) can make one; with SIGN_UP=approval the account is pending until an admin approves it (USR-2 to USR-4).",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "email": {
                    "type": "string"
                  },
                  "password": {
                    "type": "string",
                    "minLength": 8,
                    "maxLength": 128
                  },
                  "invite": {
                    "description": "The token from an invitation link (USR-3): the account is active straight away, whatever the sign-up setting.",
                    "type": "string"
                  }
                },
                "required": [
                  "name",
                  "email",
                  "password"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Made and signed in. The user's status is pending when it waits for approval.",
            "headers": {
              "set-auth-token": {
                "description": "The session token, for apps to send as Authorization: Bearer <token>.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "token": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "user": {
                      "$ref": "#/components/schemas/User"
                    }
                  },
                  "required": [
                    "token",
                    "user"
                  ],
                  "additionalProperties": {}
                }
              }
            }
          },
          "400": {
            "description": "Not allowed: an invalid email, a password that's too short or long, sign-up is closed (SIGN_UP_CLOSED), or the invitation isn't valid (INVITE_INVALID) or is for another address (INVITE_EMAIL_MISMATCH).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthError"
                }
              }
            }
          },
          "403": {
            "description": "A browser request whose Origin isn't the server's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "An account with this email already exists.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/auth/sign-in/email": {
      "post": {
        "operationId": "postAuthSignInEmail",
        "tags": [
          "Accounts"
        ],
        "summary": "Sign in",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string"
                  },
                  "password": {
                    "type": "string"
                  },
                  "rememberMe": {
                    "type": "boolean"
                  }
                },
                "required": [
                  "email",
                  "password"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Signed in.",
            "headers": {
              "set-auth-token": {
                "description": "The session token, for apps to send as Authorization: Bearer <token>.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "redirect": {
                      "type": "boolean"
                    },
                    "token": {
                      "type": "string"
                    },
                    "user": {
                      "$ref": "#/components/schemas/User"
                    }
                  },
                  "required": [
                    "redirect",
                    "token",
                    "user"
                  ],
                  "additionalProperties": {}
                }
              }
            }
          },
          "401": {
            "description": "Wrong email or password.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthError"
                }
              }
            }
          },
          "403": {
            "description": "A browser request whose Origin isn't the server's, or the account is suspended (ACCOUNT_SUSPENDED, USR-7).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/auth/request-password-reset": {
      "post": {
        "operationId": "postAuthRequestPasswordReset",
        "tags": [
          "Accounts"
        ],
        "summary": "Email a password reset link",
        "description": "When the server can send email (the email capability), emails a link to the web app's page for choosing a new password; it works once, for an hour (USR-7). The answer is the same whether or not there's an account with the address. Without email, an admin makes a link instead (POST /v1/people/{id}/reset-link).",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string"
                  },
                  "redirectTo": {
                    "description": "Ignored: the link always opens the web app's reset page.",
                    "type": "string"
                  }
                },
                "required": [
                  "email"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Emailed, if there's an account with the address.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "boolean",
                      "const": true
                    },
                    "message": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "status",
                    "message"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "description": "The server can't send email (RESET_PASSWORD_DISABLED), or the address isn't valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthError"
                }
              }
            }
          },
          "403": {
            "description": "A browser request whose Origin isn't the server's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/auth/reset-password": {
      "post": {
        "operationId": "postAuthResetPassword",
        "tags": [
          "Accounts"
        ],
        "summary": "Choose a new password",
        "description": "With the token from a reset link, emailed or made by an admin (USR-7). Devices already signed in stay signed in.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "newPassword": {
                    "type": "string",
                    "minLength": 8,
                    "maxLength": 128
                  },
                  "token": {
                    "type": "string"
                  }
                },
                "required": [
                  "newPassword",
                  "token"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Changed; sign in with it.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "boolean",
                      "const": true
                    }
                  },
                  "required": [
                    "status"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "description": "The link was used, has expired or is mistyped (INVALID_TOKEN), or the password is too short or long.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthError"
                }
              }
            }
          },
          "403": {
            "description": "A browser request whose Origin isn't the server's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/auth/change-password": {
      "post": {
        "operationId": "postAuthChangePassword",
        "tags": [
          "Accounts"
        ],
        "summary": "Change the password",
        "description": "Needs the current password (USR-7).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "currentPassword": {
                    "type": "string"
                  },
                  "newPassword": {
                    "type": "string",
                    "minLength": 8,
                    "maxLength": 128
                  },
                  "revokeOtherSessions": {
                    "description": "Sign out everywhere else too, linked watches included.",
                    "type": "boolean"
                  }
                },
                "required": [
                  "currentPassword",
                  "newPassword"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Changed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "token": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "user": {
                      "$ref": "#/components/schemas/User"
                    }
                  },
                  "required": [
                    "token",
                    "user"
                  ],
                  "additionalProperties": {}
                }
              }
            }
          },
          "400": {
            "description": "The current password is wrong (INVALID_PASSWORD), or the new one is too short or long.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthError"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthError"
                }
              }
            }
          },
          "403": {
            "description": "A browser request whose Origin isn't the server's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/auth/sign-out": {
      "post": {
        "operationId": "postAuthSignOut",
        "tags": [
          "Accounts"
        ],
        "summary": "Sign out",
        "description": "Ends this session. To sign out another device, use DELETE /v1/devices/{id}.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {},
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Signed out.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    }
                  },
                  "required": [
                    "success"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/auth/get-session": {
      "get": {
        "operationId": "getAuthGetSession",
        "tags": [
          "Accounts"
        ],
        "summary": "Who is signed in",
        "security": [],
        "responses": {
          "200": {
            "description": "The session and account, or null when not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "session": {
                          "$ref": "#/components/schemas/Session"
                        },
                        "user": {
                          "$ref": "#/components/schemas/User"
                        }
                      },
                      "required": [
                        "session",
                        "user"
                      ],
                      "additionalProperties": false
                    },
                    {
                      "type": "null"
                    }
                  ]
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/auth/device/code": {
      "post": {
        "operationId": "postAuthDeviceCode",
        "tags": [
          "Linking a watch"
        ],
        "summary": "Ask for a linking code",
        "description": "Step 1, from the watch. Codes last 10 minutes.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "client_id": {
                    "type": "string",
                    "const": "lawfive-garmin",
                    "description": "The app asking; only known apps can link."
                  },
                  "scope": {
                    "type": "string"
                  }
                },
                "required": [
                  "client_id"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A code to show.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeviceCode"
                }
              }
            }
          },
          "400": {
            "description": "Not a known app (invalid_client).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/auth/device": {
      "get": {
        "operationId": "getAuthDevice",
        "tags": [
          "Linking a watch"
        ],
        "summary": "Look up a code",
        "description": "Step 2, from the Link page. Signed in, it also claims the code for this session, which approving or refusing it needs.",
        "security": [],
        "parameters": [
          {
            "name": "user_code",
            "in": "query",
            "required": true,
            "description": "The code the watch shows.",
            "schema": {
              "type": "string",
              "description": "The code the watch shows."
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The code is waiting.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "user_code": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "pending",
                        "approved",
                        "denied"
                      ]
                    },
                    "client_id": {
                      "description": "The app asking to link (lawfive-garmin), when signed in.",
                      "type": "string"
                    },
                    "device_name": {
                      "description": "The watch asking, \"Garmin Venu 2\", when it said which it is (its X-LawFive-Device header).",
                      "type": "string"
                    },
                    "scope": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  },
                  "required": [
                    "user_code",
                    "status"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "description": "No such code, or it has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/auth/device/approve": {
      "post": {
        "operationId": "postAuthDeviceApprove",
        "tags": [
          "Linking a watch"
        ],
        "summary": "Approve a code",
        "description": "Step 2, after the lookup: links the watch to the signed-in account.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "userCode": {
                    "type": "string"
                  }
                },
                "required": [
                  "userCode"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Approved; the watch's next poll gets its token.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    }
                  },
                  "required": [
                    "success"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "description": "No such code, it has expired, or it wasn't looked up first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthError"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthError"
                }
              }
            }
          },
          "403": {
            "description": "The account is waiting for approval (ACCOUNT_PENDING).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/auth/device/deny": {
      "post": {
        "operationId": "postAuthDeviceDeny",
        "tags": [
          "Linking a watch"
        ],
        "summary": "Refuse a code",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "userCode": {
                    "type": "string"
                  }
                },
                "required": [
                  "userCode"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Refused; the watch's next poll gets access_denied.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    }
                  },
                  "required": [
                    "success"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "description": "No such code, it has expired, or it wasn't looked up first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthError"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/auth/device/token": {
      "post": {
        "operationId": "postAuthDeviceToken",
        "tags": [
          "Linking a watch"
        ],
        "summary": "Poll for the token",
        "description": "Step 3, from the watch, every interval seconds until it's approved, refused or expired.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "grant_type": {
                    "type": "string",
                    "const": "urn:ietf:params:oauth:grant-type:device_code"
                  },
                  "device_code": {
                    "type": "string"
                  },
                  "client_id": {
                    "type": "string"
                  }
                },
                "required": [
                  "grant_type",
                  "device_code",
                  "client_id"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Approved: the watch's token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeviceToken"
                }
              }
            }
          },
          "400": {
            "description": "Not yet (authorization_pending), too often (slow_down), refused (access_denied) or expired (expired_token).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/matches": {
      "get": {
        "operationId": "getMatches",
        "tags": [
          "Records"
        ],
        "summary": "The user's matches",
        "description": "Every match that isn't deleted, oldest change first. API keys: need matches:read.",
        "responses": {
          "200": {
            "description": "The matches.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Match"
                      }
                    }
                  },
                  "required": [
                    "items"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key without matches:read; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/matches/{id}": {
      "get": {
        "operationId": "getMatchesById",
        "tags": [
          "Records"
        ],
        "summary": "One match",
        "description": "Deleted ones too, with deletedAt set. API keys: need matches:read.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID made by the device that created the record.",
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The match.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Match"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key without matches:read; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not this user's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "putMatchesById",
        "tags": [
          "Records"
        ],
        "summary": "Save a match",
        "description": "Creates the match (send no version) or updates it (send the version you last saw). Saving a deleted record brings it back. API keys: need matches:write.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID made by the device that created the record.",
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MatchInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated; version is raised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Match"
                }
              }
            }
          },
          "201": {
            "description": "Created, at version 1.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Match"
                }
              }
            }
          },
          "400": {
            "description": "The id isn't a UUID, the body's id differs from the URL's, or the body doesn't match the schema (issues lists the problems).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "issues": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ValidationIssue"
                      }
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key without matches:write; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "It changed since the version sent (current is the record as it is now), the id belongs to another kind of record, or it would duplicate another (a match's external id, a second settings record).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "current": {
                      "description": "The record as it is now, when it changed since the version sent.",
                      "$ref": "#/components/schemas/Match"
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteMatchesById",
        "tags": [
          "Records"
        ],
        "summary": "Delete a match",
        "description": "Marks it deleted; it stays as a marker so other devices hear of it. Deleting it again changes nothing. API keys: need matches:write.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID made by the device that created the record.",
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          },
          {
            "name": "version",
            "in": "query",
            "required": false,
            "description": "The version last seen; when given, the delete fails if it has changed since.",
            "schema": {
              "description": "The version last seen; when given, the delete fails if it has changed since.",
              "type": "integer",
              "minimum": 1,
              "maximum": 9007199254740991
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted: the record with deletedAt.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Match"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key without matches:write; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not this user's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "It changed since the version sent.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "current": {
                      "description": "The record as it is now, when it changed since the version sent.",
                      "$ref": "#/components/schemas/Match"
                    }
                  },
                  "required": [
                    "error",
                    "current"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/match-templates": {
      "get": {
        "operationId": "getMatchTemplates",
        "tags": [
          "Records"
        ],
        "summary": "The user's match templates",
        "description": "Every match template that isn't deleted, oldest change first. API keys: need match-templates:read.",
        "responses": {
          "200": {
            "description": "The match templates.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/MatchTemplate"
                      }
                    }
                  },
                  "required": [
                    "items"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key without match-templates:read; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/match-templates/{id}": {
      "get": {
        "operationId": "getMatchTemplatesById",
        "tags": [
          "Records"
        ],
        "summary": "One match template",
        "description": "Deleted ones too, with deletedAt set. API keys: need match-templates:read.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID made by the device that created the record.",
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The match template.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MatchTemplate"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key without match-templates:read; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not this user's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "putMatchTemplatesById",
        "tags": [
          "Records"
        ],
        "summary": "Save a match template",
        "description": "Creates the match template (send no version) or updates it (send the version you last saw). Saving a deleted record brings it back. API keys: need match-templates:write.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID made by the device that created the record.",
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MatchTemplateInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated; version is raised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MatchTemplate"
                }
              }
            }
          },
          "201": {
            "description": "Created, at version 1.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MatchTemplate"
                }
              }
            }
          },
          "400": {
            "description": "The id isn't a UUID, the body's id differs from the URL's, or the body doesn't match the schema (issues lists the problems).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "issues": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ValidationIssue"
                      }
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key without match-templates:write; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "It changed since the version sent (current is the record as it is now), the id belongs to another kind of record, or it would duplicate another (a match's external id, a second settings record).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "current": {
                      "description": "The record as it is now, when it changed since the version sent.",
                      "$ref": "#/components/schemas/MatchTemplate"
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteMatchTemplatesById",
        "tags": [
          "Records"
        ],
        "summary": "Delete a match template",
        "description": "Marks it deleted; it stays as a marker so other devices hear of it. Deleting it again changes nothing. API keys: need match-templates:write.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID made by the device that created the record.",
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          },
          {
            "name": "version",
            "in": "query",
            "required": false,
            "description": "The version last seen; when given, the delete fails if it has changed since.",
            "schema": {
              "description": "The version last seen; when given, the delete fails if it has changed since.",
              "type": "integer",
              "minimum": 1,
              "maximum": 9007199254740991
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted: the record with deletedAt.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MatchTemplate"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key without match-templates:write; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not this user's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "It changed since the version sent.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "current": {
                      "description": "The record as it is now, when it changed since the version sent.",
                      "$ref": "#/components/schemas/MatchTemplate"
                    }
                  },
                  "required": [
                    "error",
                    "current"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/match-records": {
      "get": {
        "operationId": "getMatchRecords",
        "tags": [
          "Records"
        ],
        "summary": "The user's match records",
        "description": "Every match record that isn't deleted, oldest change first. API keys: need match-records:read.",
        "responses": {
          "200": {
            "description": "The match records.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/MatchRecord"
                      }
                    }
                  },
                  "required": [
                    "items"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key without match-records:read; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/match-records/{id}": {
      "get": {
        "operationId": "getMatchRecordsById",
        "tags": [
          "Records"
        ],
        "summary": "One match record",
        "description": "Deleted ones too, with deletedAt set. API keys: need match-records:read.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID made by the device that created the record.",
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The match record.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MatchRecord"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key without match-records:read; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not this user's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "putMatchRecordsById",
        "tags": [
          "Records"
        ],
        "summary": "Save a match record",
        "description": "Creates the match record (send no version) or updates it (send the version you last saw). Watches upload theirs through POST /v1/watch/records instead. Saving a deleted record brings it back. API keys: need match-records:write.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID made by the device that created the record.",
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MatchRecordInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated; version is raised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MatchRecord"
                }
              }
            }
          },
          "201": {
            "description": "Created, at version 1.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MatchRecord"
                }
              }
            }
          },
          "400": {
            "description": "The id isn't a UUID, the body's id differs from the URL's, or the body doesn't match the schema (issues lists the problems).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "issues": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ValidationIssue"
                      }
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key without match-records:write; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "It changed since the version sent (current is the record as it is now), the id belongs to another kind of record, or it would duplicate another (a match's external id, a second settings record).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "current": {
                      "description": "The record as it is now, when it changed since the version sent.",
                      "$ref": "#/components/schemas/MatchRecord"
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteMatchRecordsById",
        "tags": [
          "Records"
        ],
        "summary": "Delete a match record",
        "description": "Marks it deleted; it stays as a marker so other devices hear of it. Deleting it again changes nothing. API keys: need match-records:write.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID made by the device that created the record.",
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          },
          {
            "name": "version",
            "in": "query",
            "required": false,
            "description": "The version last seen; when given, the delete fails if it has changed since.",
            "schema": {
              "description": "The version last seen; when given, the delete fails if it has changed since.",
              "type": "integer",
              "minimum": 1,
              "maximum": 9007199254740991
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted: the record with deletedAt.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MatchRecord"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key without match-records:write; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not this user's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "It changed since the version sent.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "current": {
                      "description": "The record as it is now, when it changed since the version sent.",
                      "$ref": "#/components/schemas/MatchRecord"
                    }
                  },
                  "required": [
                    "error",
                    "current"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/venues": {
      "get": {
        "operationId": "getVenues",
        "tags": [
          "Records"
        ],
        "summary": "The user's venues",
        "description": "Every venue that isn't deleted, oldest change first. API keys: need venues:read.",
        "responses": {
          "200": {
            "description": "The venues.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Venue"
                      }
                    }
                  },
                  "required": [
                    "items"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key without venues:read; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/venues/{id}": {
      "get": {
        "operationId": "getVenuesById",
        "tags": [
          "Records"
        ],
        "summary": "One venue",
        "description": "Deleted ones too, with deletedAt set. API keys: need venues:read.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID made by the device that created the record.",
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The venue.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Venue"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key without venues:read; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not this user's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "putVenuesById",
        "tags": [
          "Records"
        ],
        "summary": "Save a venue",
        "description": "Creates the venue (send no version) or updates it (send the version you last saw). Saving a deleted record brings it back. API keys: need venues:write.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID made by the device that created the record.",
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VenueInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated; version is raised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Venue"
                }
              }
            }
          },
          "201": {
            "description": "Created, at version 1.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Venue"
                }
              }
            }
          },
          "400": {
            "description": "The id isn't a UUID, the body's id differs from the URL's, or the body doesn't match the schema (issues lists the problems).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "issues": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ValidationIssue"
                      }
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key without venues:write; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "It changed since the version sent (current is the record as it is now), the id belongs to another kind of record, or it would duplicate another (a match's external id, a second settings record).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "current": {
                      "description": "The record as it is now, when it changed since the version sent.",
                      "$ref": "#/components/schemas/Venue"
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteVenuesById",
        "tags": [
          "Records"
        ],
        "summary": "Delete a venue",
        "description": "Marks it deleted; it stays as a marker so other devices hear of it. Deleting it again changes nothing. API keys: need venues:write.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID made by the device that created the record.",
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          },
          {
            "name": "version",
            "in": "query",
            "required": false,
            "description": "The version last seen; when given, the delete fails if it has changed since.",
            "schema": {
              "description": "The version last seen; when given, the delete fails if it has changed since.",
              "type": "integer",
              "minimum": 1,
              "maximum": 9007199254740991
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted: the record with deletedAt.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Venue"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key without venues:write; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not this user's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "It changed since the version sent.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "current": {
                      "description": "The record as it is now, when it changed since the version sent.",
                      "$ref": "#/components/schemas/Venue"
                    }
                  },
                  "required": [
                    "error",
                    "current"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/competitions": {
      "get": {
        "operationId": "getCompetitions",
        "tags": [
          "Records"
        ],
        "summary": "The user's competitions",
        "description": "Every competition that isn't deleted, oldest change first. API keys: need competitions:read.",
        "responses": {
          "200": {
            "description": "The competitions.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Competition"
                      }
                    }
                  },
                  "required": [
                    "items"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key without competitions:read; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/competitions/{id}": {
      "get": {
        "operationId": "getCompetitionsById",
        "tags": [
          "Records"
        ],
        "summary": "One competition",
        "description": "Deleted ones too, with deletedAt set. API keys: need competitions:read.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID made by the device that created the record.",
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The competition.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Competition"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key without competitions:read; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not this user's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "putCompetitionsById",
        "tags": [
          "Records"
        ],
        "summary": "Save a competition",
        "description": "Creates the competition (send no version) or updates it (send the version you last saw). Saving a deleted record brings it back. API keys: need competitions:write.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID made by the device that created the record.",
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CompetitionInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated; version is raised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Competition"
                }
              }
            }
          },
          "201": {
            "description": "Created, at version 1.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Competition"
                }
              }
            }
          },
          "400": {
            "description": "The id isn't a UUID, the body's id differs from the URL's, or the body doesn't match the schema (issues lists the problems).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "issues": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ValidationIssue"
                      }
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key without competitions:write; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "It changed since the version sent (current is the record as it is now), the id belongs to another kind of record, or it would duplicate another (a match's external id, a second settings record).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "current": {
                      "description": "The record as it is now, when it changed since the version sent.",
                      "$ref": "#/components/schemas/Competition"
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteCompetitionsById",
        "tags": [
          "Records"
        ],
        "summary": "Delete a competition",
        "description": "Marks it deleted; it stays as a marker so other devices hear of it. Deleting it again changes nothing. API keys: need competitions:write.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID made by the device that created the record.",
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          },
          {
            "name": "version",
            "in": "query",
            "required": false,
            "description": "The version last seen; when given, the delete fails if it has changed since.",
            "schema": {
              "description": "The version last seen; when given, the delete fails if it has changed since.",
              "type": "integer",
              "minimum": 1,
              "maximum": 9007199254740991
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted: the record with deletedAt.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Competition"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key without competitions:write; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not this user's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "It changed since the version sent.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "current": {
                      "description": "The record as it is now, when it changed since the version sent.",
                      "$ref": "#/components/schemas/Competition"
                    }
                  },
                  "required": [
                    "error",
                    "current"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/referees": {
      "get": {
        "operationId": "getReferees",
        "tags": [
          "Records"
        ],
        "summary": "The user's referees",
        "description": "Every referee that isn't deleted, oldest change first. API keys: need referees:read.",
        "responses": {
          "200": {
            "description": "The referees.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Referee"
                      }
                    }
                  },
                  "required": [
                    "items"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key without referees:read; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/referees/{id}": {
      "get": {
        "operationId": "getRefereesById",
        "tags": [
          "Records"
        ],
        "summary": "One referee",
        "description": "Deleted ones too, with deletedAt set. API keys: need referees:read.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID made by the device that created the record.",
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The referee.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Referee"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key without referees:read; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not this user's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "putRefereesById",
        "tags": [
          "Records"
        ],
        "summary": "Save a referee",
        "description": "Creates the referee (send no version) or updates it (send the version you last saw). Saving a deleted record brings it back. API keys: need referees:write.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID made by the device that created the record.",
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RefereeInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated; version is raised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Referee"
                }
              }
            }
          },
          "201": {
            "description": "Created, at version 1.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Referee"
                }
              }
            }
          },
          "400": {
            "description": "The id isn't a UUID, the body's id differs from the URL's, or the body doesn't match the schema (issues lists the problems).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "issues": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ValidationIssue"
                      }
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key without referees:write; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "It changed since the version sent (current is the record as it is now), the id belongs to another kind of record, or it would duplicate another (a match's external id, a second settings record).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "current": {
                      "description": "The record as it is now, when it changed since the version sent.",
                      "$ref": "#/components/schemas/Referee"
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteRefereesById",
        "tags": [
          "Records"
        ],
        "summary": "Delete a referee",
        "description": "Marks it deleted; it stays as a marker so other devices hear of it. Deleting it again changes nothing. API keys: need referees:write.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID made by the device that created the record.",
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          },
          {
            "name": "version",
            "in": "query",
            "required": false,
            "description": "The version last seen; when given, the delete fails if it has changed since.",
            "schema": {
              "description": "The version last seen; when given, the delete fails if it has changed since.",
              "type": "integer",
              "minimum": 1,
              "maximum": 9007199254740991
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted: the record with deletedAt.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Referee"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key without referees:write; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not this user's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "It changed since the version sent.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "current": {
                      "description": "The record as it is now, when it changed since the version sent.",
                      "$ref": "#/components/schemas/Referee"
                    }
                  },
                  "required": [
                    "error",
                    "current"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/settings": {
      "get": {
        "operationId": "getSettings",
        "tags": [
          "Records"
        ],
        "summary": "The user's settings records",
        "description": "Every settings record that isn't deleted, oldest change first. API keys: need settings:read.",
        "responses": {
          "200": {
            "description": "The settings records.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/UserSettings"
                      }
                    }
                  },
                  "required": [
                    "items"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key without settings:read; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/settings/{id}": {
      "get": {
        "operationId": "getSettingsById",
        "tags": [
          "Records"
        ],
        "summary": "One settings record",
        "description": "Deleted ones too, with deletedAt set. API keys: need settings:read.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID made by the device that created the record.",
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The settings record.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserSettings"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key without settings:read; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not this user's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "putSettingsById",
        "tags": [
          "Records"
        ],
        "summary": "Save a settings record",
        "description": "Creates the settings record (send no version) or updates it (send the version you last saw). There's one per user: create it once, then update it. Saving a deleted record brings it back. API keys: need settings:write.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID made by the device that created the record.",
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UserSettingsInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated; version is raised.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserSettings"
                }
              }
            }
          },
          "201": {
            "description": "Created, at version 1.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserSettings"
                }
              }
            }
          },
          "400": {
            "description": "The id isn't a UUID, the body's id differs from the URL's, or the body doesn't match the schema (issues lists the problems).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "issues": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ValidationIssue"
                      }
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key without settings:write; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "It changed since the version sent (current is the record as it is now), the id belongs to another kind of record, or it would duplicate another (a match's external id, a second settings record).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "current": {
                      "description": "The record as it is now, when it changed since the version sent.",
                      "$ref": "#/components/schemas/UserSettings"
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteSettingsById",
        "tags": [
          "Records"
        ],
        "summary": "Delete a settings record",
        "description": "Marks it deleted; it stays as a marker so other devices hear of it. Deleting it again changes nothing. API keys: need settings:write.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID made by the device that created the record.",
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          },
          {
            "name": "version",
            "in": "query",
            "required": false,
            "description": "The version last seen; when given, the delete fails if it has changed since.",
            "schema": {
              "description": "The version last seen; when given, the delete fails if it has changed since.",
              "type": "integer",
              "minimum": 1,
              "maximum": 9007199254740991
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted: the record with deletedAt.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserSettings"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key without settings:write; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not this user's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "It changed since the version sent.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "current": {
                      "description": "The record as it is now, when it changed since the version sent.",
                      "$ref": "#/components/schemas/UserSettings"
                    }
                  },
                  "required": [
                    "error",
                    "current"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/match-records/{id}/track": {
      "get": {
        "operationId": "getMatchRecordsByIdTrack",
        "tags": [
          "Tracks"
        ],
        "summary": "A match report's track",
        "description": "With its figures, over the periods played when the match report has their start times. API keys: need match-records:read.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID made by the device that created the record.",
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The track and its figures.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "track": {
                      "$ref": "#/components/schemas/MatchTrack"
                    },
                    "summary": {
                      "$ref": "#/components/schemas/TrackSummary"
                    },
                    "pitch": {
                      "description": "The pitch: the track's own (marked by the referee, or from RefSix), else from corners walked on the watch (TRK-13), else one found from where the referee went (TRK-9), when there's enough GPS.",
                      "$ref": "#/components/schemas/PitchOutline"
                    },
                    "heat": {
                      "description": "Where the time was spent on the pitch, turned as the match page draws it (TRK-9), for apps that draw it rather than work it out. None without a pitch, or for a fourth official.",
                      "type": "object",
                      "properties": {
                        "cols": {
                          "type": "integer",
                          "minimum": -9007199254740991,
                          "maximum": 9007199254740991
                        },
                        "rows": {
                          "type": "integer",
                          "minimum": -9007199254740991,
                          "maximum": 9007199254740991
                        },
                        "lengthM": {
                          "type": "number"
                        },
                        "widthM": {
                          "type": "number"
                        },
                        "total": {
                          "type": "array",
                          "items": {
                            "type": "number"
                          },
                          "description": "Seconds in each cell, row by row from the top of the drawing, left to right."
                        },
                        "periods": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "period": {
                                "type": "integer",
                                "minimum": -9007199254740991,
                                "maximum": 9007199254740991
                              },
                              "cells": {
                                "type": "array",
                                "items": {
                                  "type": "number"
                                }
                              }
                            },
                            "required": [
                              "period",
                              "cells"
                            ],
                            "additionalProperties": false
                          }
                        }
                      },
                      "required": [
                        "cols",
                        "rows",
                        "lengthM",
                        "widthM",
                        "total",
                        "periods"
                      ],
                      "additionalProperties": false
                    },
                    "updatedAt": {
                      "$ref": "#/components/schemas/Timestamp"
                    }
                  },
                  "required": [
                    "track",
                    "summary",
                    "updatedAt"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key without match-records:read; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No track for this match report, or no such match report.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "putMatchRecordsByIdTrack",
        "tags": [
          "Tracks"
        ],
        "summary": "Upload a match report's track",
        "description": "Replaces any track the match report had. Up to 43,200 samples (12 hours at one a second) in each series. API keys: need match-records:write.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID made by the device that created the record.",
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MatchTrack"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Replaced the track it had.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "summary": {
                      "$ref": "#/components/schemas/TrackSummary"
                    },
                    "updatedAt": {
                      "$ref": "#/components/schemas/Timestamp"
                    }
                  },
                  "required": [
                    "summary",
                    "updatedAt"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "201": {
            "description": "Stored.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "summary": {
                      "$ref": "#/components/schemas/TrackSummary"
                    },
                    "updatedAt": {
                      "$ref": "#/components/schemas/Timestamp"
                    }
                  },
                  "required": [
                    "summary",
                    "updatedAt"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "description": "The body doesn't match the schema (issues lists the problems).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key without match-records:write; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such match report: save it first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteMatchRecordsByIdTrack",
        "tags": [
          "Tracks"
        ],
        "summary": "Delete a match report's track",
        "description": "The match report stays. API keys: need match-records:write.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID made by the device that created the record.",
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    }
                  },
                  "required": [
                    "ok"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key without match-records:write; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No track for this match report.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/match-records/{id}/track/pitch": {
      "put": {
        "operationId": "putMatchRecordsByIdTrackPitch",
        "tags": [
          "Tracks"
        ],
        "summary": "Mark where the pitch is",
        "description": "The two corners of one touchline and the centre spot, as the referee placed them (TRK-9); kept with the track, marked by: referee. Heatmaps and Stats use it. API keys: need match-records:write.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID made by the device that created the record.",
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "corners": {
                    "type": "array",
                    "prefixItems": [
                      {
                        "$ref": "#/components/schemas/Position"
                      },
                      {
                        "$ref": "#/components/schemas/Position"
                      }
                    ],
                    "items": false,
                    "minItems": 2,
                    "maxItems": 2,
                    "description": "The corners at the two ends of one touchline."
                  },
                  "centre": {
                    "description": "The centre spot.",
                    "$ref": "#/components/schemas/Position"
                  }
                },
                "required": [
                  "corners",
                  "centre"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Kept.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "pitch": {
                      "$ref": "#/components/schemas/PitchOutline"
                    },
                    "updatedAt": {
                      "$ref": "#/components/schemas/Timestamp"
                    }
                  },
                  "required": [
                    "pitch",
                    "updatedAt"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "description": "The body doesn't match the schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key without match-records:write; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No track for this match report.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteMatchRecordsByIdTrackPitch",
        "tags": [
          "Tracks"
        ],
        "summary": "Forget where the pitch is",
        "description": "Back to the pitch found from the track (TRK-9). API keys: need match-records:write.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID made by the device that created the record.",
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Forgotten; pitch is the one found from the track, when there's enough GPS.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "pitch": {
                      "$ref": "#/components/schemas/PitchOutline"
                    },
                    "updatedAt": {
                      "$ref": "#/components/schemas/Timestamp"
                    }
                  },
                  "required": [
                    "updatedAt"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key without match-records:write; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No track for this match report.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/changes": {
      "get": {
        "operationId": "getChanges",
        "tags": [
          "Sync"
        ],
        "summary": "Changes since a cursor",
        "description": "Every record written after the cursor, deletions included, oldest first, across all collections (for an API key, the ones it can read). Start with since=0, keep the cursor, and ask again while more is true. Changes made at the same time are never skipped. API keys: any read scope; only those collections are included.",
        "parameters": [
          {
            "name": "since",
            "in": "query",
            "required": false,
            "description": "The cursor from the last page; 0 (the default) for everything.",
            "schema": {
              "description": "The cursor from the last page; 0 (the default) for everything.",
              "type": "integer",
              "minimum": 0,
              "maximum": 9007199254740991
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Changes per page: 500 by default, at most 1000.",
            "schema": {
              "description": "Changes per page: 500 by default, at most 1000.",
              "type": "integer",
              "minimum": 1,
              "maximum": 9007199254740991
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of changes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChangesPage"
                }
              }
            }
          },
          "400": {
            "description": "since or limit isn't a whole number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key without *:read; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/stats": {
      "get": {
        "operationId": "getStats",
        "tags": [
          "Stats"
        ],
        "summary": "Stats on the user's games",
        "description": "Games are matches played or abandoned. Every figure follows the filters; options lists what the filters can choose from. Not open to API keys yet: the answer includes referee and venue names. Not with API keys.",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "The first day, in each match's own time zone; none for the first game.",
            "schema": {
              "description": "The first day, in each match's own time zone; none for the first game.",
              "$ref": "#/components/schemas/LocalDate"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "The last day, inclusive; none for today and on.",
            "schema": {
              "description": "The last day, inclusive; none for today and on.",
              "$ref": "#/components/schemas/LocalDate"
            }
          },
          {
            "name": "gender",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/Gender"
            }
          },
          {
            "name": "competitionId",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          },
          {
            "name": "role",
            "in": "query",
            "required": false,
            "description": "Your role: referee, assistant (AR1 or AR2) or fourth.",
            "schema": {
              "description": "Your role: referee, assistant (AR1 or AR2) or fourth.",
              "type": "string",
              "enum": [
                "referee",
                "assistant",
                "fourth"
              ]
            }
          },
          {
            "name": "tag",
            "in": "query",
            "required": false,
            "description": "Games with this tag (ignoring case).",
            "schema": {
              "description": "Games with this tag (ignoring case).",
              "type": "string",
              "maxLength": 30
            }
          },
          {
            "name": "venueId",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The stats.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Stats"
                }
              }
            }
          },
          "400": {
            "description": "A filter isn't valid: from or to isn't a date, an unknown gender or role, or an id isn't a UUID.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key: this needs signing in; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/stats/games.csv": {
      "get": {
        "operationId": "getStatsGames.csv",
        "tags": [
          "Stats"
        ],
        "summary": "The games, as a spreadsheet",
        "description": "The games matching the filters (and, optionally, one slice of a chart), oldest first, as CSV with a header row: date, kickoff, competition, home, away, score, role, venue, pitch, age group, gender, distance (m), average heart rate, kickoff temperature (°C), rain (mm), cautions, send-offs, added time (s), fee, currency and paid (STAT-12). Not with API keys.",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "The first day, in each match's own time zone; none for the first game.",
            "schema": {
              "description": "The first day, in each match's own time zone; none for the first game.",
              "$ref": "#/components/schemas/LocalDate"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "The last day, inclusive; none for today and on.",
            "schema": {
              "description": "The last day, inclusive; none for today and on.",
              "$ref": "#/components/schemas/LocalDate"
            }
          },
          {
            "name": "gender",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/Gender"
            }
          },
          {
            "name": "competitionId",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          },
          {
            "name": "role",
            "in": "query",
            "required": false,
            "description": "Your role: referee, assistant (AR1 or AR2) or fourth.",
            "schema": {
              "description": "Your role: referee, assistant (AR1 or AR2) or fourth.",
              "type": "string",
              "enum": [
                "referee",
                "assistant",
                "fourth"
              ]
            }
          },
          {
            "name": "tag",
            "in": "query",
            "required": false,
            "description": "Games with this tag (ignoring case).",
            "schema": {
              "description": "Games with this tag (ignoring case).",
              "type": "string",
              "maxLength": 30
            }
          },
          {
            "name": "venueId",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          },
          {
            "name": "pitchId",
            "in": "query",
            "required": false,
            "description": "At this pitch of the venue, or none for games at the venue without a pitch.",
            "schema": {
              "description": "At this pitch of the venue, or none for games at the venue without a pitch.",
              "anyOf": [
                {
                  "$ref": "#/components/schemas/Id"
                },
                {
                  "type": "string",
                  "const": "none"
                }
              ]
            }
          },
          {
            "name": "refereeId",
            "in": "query",
            "required": false,
            "description": "With this saved referee on the crew.",
            "schema": {
              "description": "With this saved referee on the crew.",
              "$ref": "#/components/schemas/Id"
            }
          },
          {
            "name": "refereeName",
            "in": "query",
            "required": false,
            "description": "With this name typed in on the crew (ignoring case).",
            "schema": {
              "description": "With this name typed in on the crew (ignoring case).",
              "type": "string",
              "maxLength": 100
            }
          },
          {
            "name": "ageGroup",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 20
            }
          },
          {
            "name": "month",
            "in": "query",
            "required": false,
            "description": "Kicking off in this month, 2026-04, locally.",
            "schema": {
              "description": "Kicking off in this month, 2026-04, locally.",
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The games, as a download (Content-Disposition: attachment).",
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "A filter isn't valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key: this needs signing in; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/account": {
      "get": {
        "operationId": "getAccount",
        "tags": [
          "Accounts"
        ],
        "summary": "The signed-in account",
        "description": "Its status and whether it's an admin. The one endpoint a pending account (USR-4) can use; every other gets 403. Not with API keys.",
        "responses": {
          "200": {
            "description": "The account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Account"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key: this needs signing in; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/account/email": {
      "put": {
        "operationId": "putAccountEmail",
        "tags": [
          "Accounts"
        ],
        "summary": "Change the account's email",
        "description": "With the password, to confirm it's them (USR-7). An address in the server's ADMIN_EMAILS can't be changed here, and can't be changed to. Not with API keys.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmailChange"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Account"
                }
              }
            }
          },
          "400": {
            "description": "The address isn't valid, or the password is wrong.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "issues": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ValidationIssue"
                      }
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key: this needs signing in; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "Another account has the address, or ADMIN_EMAILS lists the old or new one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/account/delete": {
      "post": {
        "operationId": "postAccountDelete",
        "tags": [
          "Accounts"
        ],
        "summary": "Delete the account",
        "description": "With the password, to confirm it's them (USR-7). Deletes the account and everything in it, signs out every device and stops its API keys and calendar feed. Refused for the only admin while there are other accounts. Not with API keys.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "password": {
                    "type": "string"
                  }
                },
                "required": [
                  "password"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    }
                  },
                  "required": [
                    "ok"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "description": "The password is wrong.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key: this needs signing in; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "It's the server's only admin; make someone else an admin first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/people": {
      "get": {
        "operationId": "getPeople",
        "tags": [
          "People"
        ],
        "summary": "Everyone on the server",
        "description": "Waiting for approval first, then everyone else by name. Not with API keys.",
        "responses": {
          "200": {
            "description": "The accounts.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "people": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Person"
                      }
                    }
                  },
                  "required": [
                    "people"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "Not an admin.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/people/{id}/approve": {
      "post": {
        "operationId": "postPeopleByIdApprove",
        "tags": [
          "People"
        ],
        "summary": "Approve an account",
        "description": "It can use LawFive straight away; they're emailed when the server can send email. Not with API keys.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {},
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Approved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Person"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "Not an admin.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/people/{id}/decline": {
      "post": {
        "operationId": "postPeopleByIdDecline",
        "tags": [
          "People"
        ],
        "summary": "Decline an account",
        "description": "Deletes an account that's waiting for approval. Approved accounts can't be declined. Not with API keys.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {},
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Declined and deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    }
                  },
                  "required": [
                    "ok"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "Not an admin.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "The account isn't waiting for approval.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/people/{id}/admin": {
      "put": {
        "operationId": "putPeopleByIdAdmin",
        "tags": [
          "People"
        ],
        "summary": "Make or stop being an admin",
        "description": "Not with API keys.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "admin": {
                    "type": "boolean"
                  }
                },
                "required": [
                  "admin"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Person"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "Not an admin.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "Your own account, an account waiting for approval, or one that ADMIN_EMAILS makes an admin.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/people/{id}/suspended": {
      "put": {
        "operationId": "putPeopleByIdSuspended",
        "tags": [
          "People"
        ],
        "summary": "Suspend an account, or undo it",
        "description": "A suspended account can't sign in, and is signed out everywhere (linked watches need linking again afterwards); its API keys and calendar feed stop working. Its data is kept (USR-7). Not with API keys.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "suspended": {
                    "type": "boolean"
                  }
                },
                "required": [
                  "suspended"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Person"
                }
              }
            }
          },
          "400": {
            "description": "The body isn't { suspended: true } or { suspended: false }.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "Not an admin.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "Your own account, an account waiting for approval, or one that ADMIN_EMAILS makes an admin.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/people/{id}/reset-link": {
      "post": {
        "operationId": "postPeopleByIdResetLink",
        "tags": [
          "People"
        ],
        "summary": "Make a password reset link",
        "description": "For someone who forgot their password, on any server: the admin sends it to them. It works once, for 24 hours (USR-7). Not with API keys.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {},
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Made.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResetLink"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "Not an admin.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "Your own account (change it in Settings), or one waiting for approval.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/people/{id}": {
      "delete": {
        "operationId": "deletePeopleById",
        "tags": [
          "People"
        ],
        "summary": "Delete an account",
        "description": "Deletes the account and everything in it (USR-7). It can't be undone. Not with API keys.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    }
                  },
                  "required": [
                    "ok"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "Not an admin.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "Your own account (delete it in Settings), or one that ADMIN_EMAILS makes an admin.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/server-settings": {
      "get": {
        "operationId": "getServerSettings",
        "tags": [
          "People"
        ],
        "summary": "The server's settings",
        "description": "Each setting an admin can change (ADM-1, ADM-2), where its value comes from, and the value (not for secrets). Not with API keys.",
        "responses": {
          "200": {
            "description": "The settings.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "settings": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ServerSettingView"
                      }
                    }
                  },
                  "required": [
                    "settings"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "Not an admin.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "putServerSettings",
        "tags": [
          "People"
        ],
        "summary": "Change the server's settings",
        "description": "Sets or clears the settings sent (ADM-1); one the environment sets can't be changed, and if any can't be, none are. Takes effect straight away. GET /v1/meta says what's offered. Not with API keys.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ServerSettings"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Changed: the settings as they are now.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "settings": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ServerSettingView"
                      }
                    }
                  },
                  "required": [
                    "settings"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "description": "The body doesn't match, a value isn't one the setting takes, or the environment sets it.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "issues": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ValidationIssue"
                      }
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "Not an admin.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/setup/check": {
      "post": {
        "operationId": "postSetupCheck",
        "tags": [
          "People"
        ],
        "summary": "Check a new server's setup code",
        "description": "While nobody has an account, whether this is the code in the server's logs (ADM-5). The first account is made through sign-up, with the code as setupCode.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "code": {
                    "type": "string"
                  }
                },
                "required": [
                  "code"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "It's the code.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    }
                  },
                  "required": [
                    "ok"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "description": "It isn't.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "The server is already set up, or doesn't need a code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/setup/finish": {
      "post": {
        "operationId": "postSetupFinish",
        "tags": [
          "People"
        ],
        "summary": "Finish setting the server up",
        "description": "The first account finished or skipped the setup wizard, so it doesn't open again (ADM-5). Not with API keys.",
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    }
                  },
                  "required": [
                    "ok"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "Not an admin.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/server-settings/test-email": {
      "post": {
        "operationId": "postServerSettingsTestEmail",
        "tags": [
          "People"
        ],
        "summary": "Send a test email",
        "description": "To the signed-in admin, through the email settings in use (ADM-4). Not with API keys.",
        "responses": {
          "200": {
            "description": "Sent.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "to": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "to"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "Not an admin.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "Email isn't set up.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "502": {
            "description": "The SMTP server didn't take it; the error says why.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/app-sign-in": {
      "post": {
        "operationId": "postAppSignIn",
        "tags": [
          "App sign-in"
        ],
        "summary": "A code to sign a phone in",
        "description": "It works once, for two minutes. Not with API keys.",
        "responses": {
          "201": {
            "description": "Made.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AppSignIn"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key: this needs signing in; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/app-sign-in/{id}": {
      "get": {
        "operationId": "getAppSignInById",
        "tags": [
          "App sign-in"
        ],
        "summary": "Whether a phone has used a code",
        "description": "So the web app can say when the phone is signed in. Expired codes go when a new one is made, and then this answers 404. Not with API keys.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID made by the device that created the record.",
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "How it stands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AppSignInStatus"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key: this needs signing in; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such code, or not this account's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/app-sign-in/check": {
      "post": {
        "operationId": "postAppSignInCheck",
        "tags": [
          "App sign-in"
        ],
        "summary": "Whose a code is",
        "description": "For the phone to ask before signing in. It doesn't use the code up.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AppSignInCode"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "It can be used.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AppSignInCheck"
                }
              }
            }
          },
          "404": {
            "description": "No such code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "410": {
            "description": "It was used, or it has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/app-sign-in/redeem": {
      "post": {
        "operationId": "postAppSignInRedeem",
        "tags": [
          "App sign-in"
        ],
        "summary": "Swap a code for a session",
        "description": "The session belongs to the device asking, and shows under Devices.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AppSignInCode"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AppSignInResult"
                }
              }
            }
          },
          "403": {
            "description": "The account is suspended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "410": {
            "description": "It was used, or it has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/invites": {
      "get": {
        "operationId": "getInvites",
        "tags": [
          "Invitations"
        ],
        "summary": "The invitations",
        "description": "Newest first, including used and expired ones from the last 90 days. Not with API keys.",
        "responses": {
          "200": {
            "description": "The invitations.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "invites": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Invite"
                      }
                    }
                  },
                  "required": [
                    "invites"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "Not an admin.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "postInvites",
        "tags": [
          "Invitations"
        ],
        "summary": "Invite someone",
        "description": "With send, it's emailed to them too, when the server can send email. Not with API keys.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewInvite"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Made.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "invite": {
                      "$ref": "#/components/schemas/Invite"
                    },
                    "emailed": {
                      "type": "boolean",
                      "description": "Whether it was emailed; when not, send the link yourself."
                    }
                  },
                  "required": [
                    "invite",
                    "emailed"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "description": "The body doesn't match.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "issues": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ValidationIssue"
                      }
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "Not an admin.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "There's already an account with this email.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/invites/{id}": {
      "delete": {
        "operationId": "deleteInvitesById",
        "tags": [
          "Invitations"
        ],
        "summary": "Cancel an invitation",
        "description": "Its link stops working straight away. Not with API keys.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID made by the device that created the record.",
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Cancelled.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    }
                  },
                  "required": [
                    "ok"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "Not an admin.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such invitation, or it was used.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/invite/{token}": {
      "get": {
        "operationId": "getInviteByToken",
        "tags": [
          "Invitations"
        ],
        "summary": "Look up an invitation",
        "description": "For the invitation page, before signing up with it.",
        "security": [],
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "It can be used.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteLookup"
                }
              }
            }
          },
          "404": {
            "description": "No such invitation: the link is mistyped, or it was cancelled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "410": {
            "description": "It was used, or it has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/referees/{id}/merge": {
      "post": {
        "operationId": "postRefereesByIdMerge",
        "tags": [
          "Records"
        ],
        "summary": "Merge referees into this one",
        "description": "For duplicates (DUP-3): the matches whose crew names any of them, or has one of their names typed in, are changed to this referee; the others are deleted, and their names kept as this one's other names. Details are combined: the chosen name, email and phone (else whichever there is), tags together, notes joined, and who they're not. An API key needs matches:write too. API keys: need referees:write.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID made by the device that created the record.",
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RefereeMerge"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Merged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RefereeMergeResult"
                }
              }
            }
          },
          "400": {
            "description": "The body doesn't match, the name isn't one of theirs, or it lists this referee.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "issues": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ValidationIssue"
                      }
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key without referees:write and matches:write.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "This referee, or one to merge, doesn't exist.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/import/matches": {
      "post": {
        "operationId": "postImportMatches",
        "tags": [
          "Import"
        ],
        "summary": "Import matches",
        "description": "Each row adds a match or updates one: the row's ID column (a LawFive match id) wins, else its match number (any source). Only the columns a row fills in change an existing match. Venues, competitions and referees are found by name, ignoring case, and added when there's none (an API key then needs write access to those too). Rows with problems are skipped and the rest are saved; nothing is saved on a dry run.\n\nThe columns, by their headings (any case or spacing, so home_colour and homeColour work too, as do common other headings like Field for Pitch):\n\n| Column | JSON | What it's for | Example | Needed |\n|---|---|---|---|---|\n| ID | `id` | LawFive's own ID for a match, as in a LawFive export. Leave it empty for a new match; with it, the row updates that match. | 5c1e2a9b-… | Optional |\n| Match number | `matchNumber` | The league's or assignor's number for the match. Importing a file again with the same number updates the match instead of adding a copy. | BYL-1142 | Optional |\n| Status | `status` | scheduled, played, abandoned, canceled or postponed. A new match is scheduled when it's empty. | scheduled | Optional |\n| Date | `date` | The day of the match: 2026-11-07, or the way your computer writes dates (11/7/2026 in the US, 7/11/2026 in most other places). | 2026-11-07 | New matches* |\n| Time | `time` | The kickoff time: 14:30 or 2:30 PM. A new match with a date needs one. | 2:30 PM | Optional |\n| Kickoff | `kickoff` | The date and time together, instead of Date and Time. Add a UTC offset (2026-11-07T14:30-08:00) to fix the moment exactly. | 2026-11-07 14:30 | New matches* |\n| Time zone | `timeZone` | Where the kickoff time is, by its name in the time zone database. Empty: the match's own time zone, or your computer's. | America/Los_Angeles | Optional |\n| Home | `home` | The home team's name. | Strikers U12 | New matches* |\n| Home short name | `homeShortName` | Up to 4 letters for the home team on the watch. Empty: the watch uses the first three letters of the name. | STR | Optional |\n| Home color | `homeColour` | The home team's color on the watch: black, white, red, orange, yellow, green, blue or purple, or a hex code like #D71920. | red | Optional |\n| Away | `away` | The away team's name. | Rovers U12 | New matches* |\n| Away short name | `awayShortName` | Up to 4 letters for the away team on the watch. Empty: the watch uses the first three letters of the name. | ROV | Optional |\n| Away color | `awayColour` | The away team's color on the watch, written like Home color. | #0057B8 | Optional |\n| Competition | `competition` | The league, cup or tournament, by name. One you don't have yet is added, as a league. | Bay Youth League U12 Boys | Optional |\n| Venue | `venue` | Where it's played, by name. One you don't have yet is added; give it an address and a place on the map in Venues. | Twin Creeks Sports Complex | Optional |\n| Pitch | `pitch` | The pitch at the venue. It's added to the venue if the venue doesn't have it yet. | Field 3 | Optional |\n| Match name | `matchName` | A name for the match itself, shown with the competition: a round, a final, a friendly's occasion. | Semi-final | Optional |\n| Age group | `ageGroup` | As the league writes it: U12, U19, Adult, O35. | U12 | Optional |\n| Gender | `gender` | Who plays: male, female or co-ed (boys, girls, men, women and mixed work too). | female | Optional |\n| Role | `role` | Your role: referee, AR1, AR2 or fourth official. Empty: taken from a crew column with your name in it, if there is one. | AR1 | Optional |\n| Referee | `referee` | The referee's name, or one of their other names. Your own name makes it your role; someone you haven't saved yet is added to Referees. TBD or empty: nobody yet. | Priya Natarajan | Optional |\n| AR1 | `ar1` | The first assistant referee's name, written like Referee. | Sam Rivera | Optional |\n| AR2 | `ar2` | The second assistant referee's name, written like Referee. | Jordan Lee | Optional |\n| Fourth official | `fourthOfficial` | The fourth official's name, written like Referee. | Sam Rivera | Optional |\n| Template | `template` | One of your match templates, by name, for its format (periods, their length, breaks). It must already exist. Empty for a new match: the competition's template, or 2 × 45 minutes. | U12 league: 2×30 | Optional |\n| Periods | `periods` | How many periods: 2 for halves, 4 for quarters. Overrides the template's. | 2 | Optional |\n| Period length (min) | `periodLength` | Minutes in each period. Overrides the template's. | 30 | Optional |\n| Half-time (min) | `halfTime` | Minutes of break between periods. Overrides the template's. | 5 | Optional |\n| Team size | `teamSize` | Players a side on the field, 1 to 11. Overrides the template's. | 9 | Optional |\n| Fee | `fee` | What you're paid for the match, as a number. | 40.50 | Optional |\n| Currency | `currency` | The fee's currency, as a three-letter code (USD, GBP, EUR). Empty: your currency in Settings. | USD | Optional |\n| Paid | `paid` | Whether you've been paid the fee: yes or no. | no | Optional |\n| Tags | `tags` | Labels to find and filter matches by, separated by commas. | league, needs AR | Optional |\n| Notes | `notes` | Anything to remember for the match. | Bring the spare flags. | Optional |\n| Outlook | `outlook` | What to expect: how competitive it'll be, the teams' strengths, history between them. | Top of the table meets second. | Optional |\n\n\\* A new match needs a date (Date and Time, or Kickoff) or its teams; everything else is optional, and an empty value never changes a match. API keys: need matches:write.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MatchImport"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "What happened to each row (or would, on a dry run).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MatchImportResult"
                }
              }
            }
          },
          "400": {
            "description": "The body doesn't match: no rows, more than 1000, or not rows of values.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "issues": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ValidationIssue"
                      }
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key without matches:write; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/import/file": {
      "post": {
        "operationId": "postImportFile",
        "tags": [
          "Import"
        ],
        "summary": "Import matches from a file",
        "description": "The same import as POST /v1/import/matches, from a file the server reads: CSV (commas, semicolons or tabs), Excel (.xlsx, its Matches sheet or else the first) or JSON (a list of objects, or an object with a matches list). The web app and phone apps use this; send it with dryRun first to check every row, then again without to save. API keys: need matches:write.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/MatchImportFileFields"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "file": {
                        "type": "string",
                        "format": "binary",
                        "description": "required: the file, at most 5 MB and 1000 rows"
                      }
                    },
                    "required": [
                      "file"
                    ]
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "What happened to each row (or would, on a dry run), and where each is in the file.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MatchImportFileResult"
                }
              }
            }
          },
          "400": {
            "description": "The file is missing, too big, or can't be read; error says why, in words to show the person who chose it.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "issues": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ValidationIssue"
                      }
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key without matches:write; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/calendar": {
      "get": {
        "operationId": "getCalendar",
        "tags": [
          "Calendar"
        ],
        "summary": "The calendar feed's address",
        "description": "Not with API keys.",
        "responses": {
          "200": {
            "description": "The feed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CalendarFeed"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key: this needs signing in; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "There's no feed; make one with POST.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "postCalendar",
        "tags": [
          "Calendar"
        ],
        "summary": "Make the calendar feed, or a new address for it",
        "description": "A new address replaces the old one, which stops working (calendars subscribed to it stop updating). Not with API keys.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {},
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Made.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CalendarFeed"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key: this needs signing in; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteCalendar",
        "tags": [
          "Calendar"
        ],
        "summary": "Turn the calendar feed off",
        "description": "Not with API keys.",
        "responses": {
          "200": {
            "description": "Its address stops working.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    }
                  },
                  "required": [
                    "ok"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key: this needs signing in; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "There's no feed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/calendar/{file}": {
      "get": {
        "operationId": "getCalendarByFile",
        "tags": [
          "Calendar"
        ],
        "summary": "The calendar feed",
        "description": "What calendar apps fetch: the user's matches as events (CAL-2 to CAL-4), past and future, cancelled ones marked so. The address is the secret; there's no other sign-in.",
        "security": [],
        "parameters": [
          {
            "name": "file",
            "in": "path",
            "required": true,
            "description": "The feed's token and .ics.",
            "schema": {
              "type": "string",
              "description": "The feed's token and .ics."
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The calendar (text/calendar).",
            "content": {
              "text/calendar": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "The account is suspended (USR-7).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No feed has this address: it was replaced or turned off.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/devices": {
      "get": {
        "operationId": "getDevices",
        "tags": [
          "Devices"
        ],
        "summary": "Where the account is signed in",
        "description": "Browsers and linked watches, most recently active first. Apps name themselves with an X-LawFive-Device header: garmin, and from the watch app's later versions what it runs on, garmin; model=006-B3703-00; firmware=12.30, which names it (Garmin Venu 2), and its own version, app=2026.10.30 (VER-3). Not with API keys.",
        "responses": {
          "200": {
            "description": "The devices.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "devices": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Device"
                      }
                    }
                  },
                  "required": [
                    "devices"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key: this needs signing in; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/devices/{id}": {
      "delete": {
        "operationId": "deleteDevicesById",
        "tags": [
          "Devices"
        ],
        "summary": "Sign a device out",
        "description": "Unlinks a watch or signs a browser out; its next request gets 401. Not with API keys.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Signed out.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    }
                  },
                  "required": [
                    "ok"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key: this needs signing in; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not this user's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/devices/sign-out-others": {
      "post": {
        "operationId": "postDevicesSignOutOthers",
        "tags": [
          "Devices"
        ],
        "summary": "Sign out everywhere else",
        "description": "Ends the account's other sessions, keeping the one asking (SEC-7): other browsers only, or every other device, which unlinks watches and signs phones out. Not with API keys.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SignOutOthers"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "How many were signed out.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "signedOut": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 9007199254740991
                    }
                  },
                  "required": [
                    "signedOut"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "description": "Not browsers or all.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key: this needs signing in; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/devices/current/push": {
      "put": {
        "operationId": "putDevicesCurrentPush",
        "tags": [
          "Devices"
        ],
        "summary": "Say how to reach this phone",
        "description": "Keeps the handle the push relay gave this phone with its session, so signing out removes it (PUSH-4). Needs an X-LawFive-Device header. Not with API keys. Only when GET /v1/meta lists the push capability. Not with API keys.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PushHandle"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Kept.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    }
                  },
                  "required": [
                    "ok"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "description": "The body doesn't match, or there's no device header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key: this needs signing in; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/account/storage": {
      "get": {
        "operationId": "getAccountStorage",
        "tags": [
          "Devices"
        ],
        "summary": "How much of its storage the account is using",
        "description": "Photos and tracks, and the limit if this server has one (SEC-6). Not with API keys.",
        "responses": {
          "200": {
            "description": "The figures.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountStorage"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key: this needs signing in; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/api-keys": {
      "get": {
        "operationId": "getApiKeys",
        "tags": [
          "API keys"
        ],
        "summary": "The account's API keys",
        "description": "Newest first. The keys themselves aren't kept, only their first characters. Not with API keys.",
        "responses": {
          "200": {
            "description": "The keys.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "keys": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ApiKey"
                      }
                    }
                  },
                  "required": [
                    "keys"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key: this needs signing in; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "postApiKeys",
        "tags": [
          "API keys"
        ],
        "summary": "Make an API key",
        "description": "At most 20 per account. Not with API keys.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewApiKey"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Made: the answer has the key, which isn't shown again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreatedApiKey"
                }
              }
            }
          },
          "400": {
            "description": "The body doesn't match, or the account already has 20 keys.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "issues": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ValidationIssue"
                      }
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key: this needs signing in; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/api-keys/{id}": {
      "delete": {
        "operationId": "deleteApiKeysById",
        "tags": [
          "API keys"
        ],
        "summary": "Delete an API key",
        "description": "It stops working straight away. Not with API keys.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID made by the device that created the record.",
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    }
                  },
                  "required": [
                    "ok"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key: this needs signing in; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not this user's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/export": {
      "get": {
        "operationId": "getExport",
        "tags": [
          "Export"
        ],
        "summary": "Download everything",
        "description": "Every record, track and photo in the account, as a zip (Content-Disposition: attachment). It's made as it's sent, so it starts straight away however big it is. Not with API keys.",
        "responses": {
          "200": {
            "description": "The zip.",
            "content": {
              "application/zip": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key: this needs signing in; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/restore": {
      "post": {
        "operationId": "postRestore",
        "tags": [
          "Export"
        ],
        "summary": "Restore from an export",
        "description": "Merges an export into the account: records keep their ids, anything missing is added, and where the account has a record too, whichever was changed last is kept. Tracks are added to match reports without one; photos the account doesn't have are added. At most 2 GB. Not with API keys.",
        "requestBody": {
          "required": true,
          "description": "A zip made by GET /v1/export",
          "content": {
            "application/zip": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Restored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RestoreResult"
                }
              }
            }
          },
          "400": {
            "description": "It isn't a LawFive export, or it's too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key: this needs signing in; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/matches/{id}/share": {
      "get": {
        "operationId": "getMatchesByIdShare",
        "tags": [
          "Sharing"
        ],
        "summary": "A match's share link",
        "description": "Not with API keys.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID made by the device that created the record.",
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Its link.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MatchShare"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key: this needs signing in; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "It isn't shared, or there's no such match.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "postMatchesByIdShare",
        "tags": [
          "Sharing"
        ],
        "summary": "Share a match",
        "description": "Makes its link, or a new one in place of the old, which stops working. Not with API keys.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID made by the device that created the record.",
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Its link.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MatchShare"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key: this needs signing in; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such match.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteMatchesByIdShare",
        "tags": [
          "Sharing"
        ],
        "summary": "Stop sharing a match",
        "description": "Its link stops working. Not with API keys.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID made by the device that created the record.",
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Not shared any more.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    }
                  },
                  "required": [
                    "ok"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key: this needs signing in; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "It isn't shared.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/shared/{token}": {
      "get": {
        "operationId": "getSharedByToken",
        "tags": [
          "Sharing"
        ],
        "summary": "A shared match",
        "description": "What the share link's page shows. The token is the secret; there's no other sign-in. Answers carry X-Robots-Tag: noindex.",
        "security": [],
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "description": "The token in the share link.",
            "schema": {
              "type": "string",
              "description": "The token in the share link."
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The match.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SharedMatch"
                }
              }
            }
          },
          "403": {
            "description": "The account is suspended (USR-7).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No match has this link: it was replaced or turned off, or the match was deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/shared/{token}/track": {
      "get": {
        "operationId": "getSharedByTokenTrack",
        "tags": [
          "Sharing"
        ],
        "summary": "A shared match's track",
        "description": "With its figures, as the owner sees them.",
        "security": [],
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "description": "The token in the share link.",
            "schema": {
              "type": "string",
              "description": "The token in the share link."
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The track and its figures.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "track": {
                      "$ref": "#/components/schemas/MatchTrack"
                    },
                    "summary": {
                      "$ref": "#/components/schemas/TrackSummary"
                    },
                    "pitch": {
                      "description": "The pitch: the track's own (marked by the referee, or from RefSix), else from corners walked on the watch (TRK-13), else one found from where the referee went (TRK-9), when there's enough GPS.",
                      "$ref": "#/components/schemas/PitchOutline"
                    },
                    "heat": {
                      "description": "Where the time was spent on the pitch, turned as the match page draws it (TRK-9), for apps that draw it rather than work it out. None without a pitch, or for a fourth official.",
                      "type": "object",
                      "properties": {
                        "cols": {
                          "type": "integer",
                          "minimum": -9007199254740991,
                          "maximum": 9007199254740991
                        },
                        "rows": {
                          "type": "integer",
                          "minimum": -9007199254740991,
                          "maximum": 9007199254740991
                        },
                        "lengthM": {
                          "type": "number"
                        },
                        "widthM": {
                          "type": "number"
                        },
                        "total": {
                          "type": "array",
                          "items": {
                            "type": "number"
                          },
                          "description": "Seconds in each cell, row by row from the top of the drawing, left to right."
                        },
                        "periods": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "period": {
                                "type": "integer",
                                "minimum": -9007199254740991,
                                "maximum": 9007199254740991
                              },
                              "cells": {
                                "type": "array",
                                "items": {
                                  "type": "number"
                                }
                              }
                            },
                            "required": [
                              "period",
                              "cells"
                            ],
                            "additionalProperties": false
                          }
                        }
                      },
                      "required": [
                        "cols",
                        "rows",
                        "lengthM",
                        "widthM",
                        "total",
                        "periods"
                      ],
                      "additionalProperties": false
                    },
                    "updatedAt": {
                      "$ref": "#/components/schemas/Timestamp"
                    }
                  },
                  "required": [
                    "track",
                    "summary",
                    "updatedAt"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "403": {
            "description": "The account is suspended (USR-7).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No match has this link, or it has no track.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/attachments": {
      "get": {
        "operationId": "getAttachments",
        "tags": [
          "Attachments"
        ],
        "summary": "Attachments of a kind of record",
        "description": "In order (by record, then position). With recordId, just that record's; with first, only each record's first (a referee's picture). Not with API keys.",
        "parameters": [
          {
            "name": "recordType",
            "in": "query",
            "required": true,
            "description": "The kinds of record that can have attachments.",
            "schema": {
              "$ref": "#/components/schemas/AttachmentRecordType"
            }
          },
          {
            "name": "recordId",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          },
          {
            "name": "first",
            "in": "query",
            "required": false,
            "description": "Only each record's first attachment.",
            "schema": {
              "description": "Only each record's first attachment.",
              "type": "string",
              "enum": [
                "true"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The attachments.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "attachments": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Attachment"
                      }
                    }
                  },
                  "required": [
                    "attachments"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "description": "recordType or recordId isn't valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key: this needs signing in; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "postAttachments",
        "tags": [
          "Attachments"
        ],
        "summary": "Upload an attachment",
        "description": "Added after the record's others. At most 30 on a record. Not with API keys.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/AttachmentUpload"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "file": {
                        "type": "string",
                        "format": "binary",
                        "description": "required: the photo, a JPEG, PNG or WebP of at most 10 MB"
                      },
                      "thumb": {
                        "type": "string",
                        "format": "binary",
                        "description": "optional: a small version, at most 1 MB, for lists and galleries"
                      }
                    },
                    "required": [
                      "file"
                    ]
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Uploaded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Attachment"
                }
              }
            }
          },
          "400": {
            "description": "With storage ({ usedBytes, limitBytes }) when it doesn't fit the account's room (FILE-7). A field isn't valid, the file is missing, too big or not a JPEG, PNG or WebP, or the record has 30 already.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "issues": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ValidationIssue"
                      }
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key: this needs signing in; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such record: save it first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/attachments/{id}": {
      "put": {
        "operationId": "putAttachmentsById",
        "tags": [
          "Attachments"
        ],
        "summary": "Change an attachment",
        "description": "Its caption, or its place among the record's attachments (moving a referee's photo to 0 makes it their picture). Not with API keys.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID made by the device that created the record.",
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AttachmentChange"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Attachment"
                }
              }
            }
          },
          "400": {
            "description": "The body doesn't match.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "issues": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ValidationIssue"
                      }
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key: this needs signing in; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not this user's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteAttachmentsById",
        "tags": [
          "Attachments"
        ],
        "summary": "Delete an attachment",
        "description": "With its files, for good. Not with API keys.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID made by the device that created the record.",
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    }
                  },
                  "required": [
                    "ok"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key: this needs signing in; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not this user's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/attachments/{id}/file": {
      "get": {
        "operationId": "getAttachmentsByIdFile",
        "tags": [
          "Attachments"
        ],
        "summary": "An attachment's file",
        "description": "As uploaded. Files never change, so they can be kept (Cache-Control: private, immutable). Not with API keys.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID made by the device that created the record.",
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The image.",
            "content": {
              "image/*": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key: this needs signing in; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not this user's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/attachments/{id}/thumb": {
      "get": {
        "operationId": "getAttachmentsByIdThumb",
        "tags": [
          "Attachments"
        ],
        "summary": "An attachment's thumbnail",
        "description": "The small version, or the file itself when none was uploaded. Not with API keys.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID made by the device that created the record.",
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The image.",
            "content": {
              "image/*": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key: this needs signing in; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not this user's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/webhooks": {
      "get": {
        "operationId": "getWebhooks",
        "tags": [
          "Webhooks"
        ],
        "summary": "The account's webhooks",
        "description": "Newest first, each with its latest delivery. Not with API keys.",
        "responses": {
          "200": {
            "description": "The webhooks.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "webhooks": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Webhook"
                      }
                    }
                  },
                  "required": [
                    "webhooks"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key: this needs signing in; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "postWebhooks",
        "tags": [
          "Webhooks"
        ],
        "summary": "Add a webhook",
        "description": "At most 10 per account. The answer has the secret deliveries are signed with, which isn't shown again. Not with API keys.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Added.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreatedWebhook"
                }
              }
            }
          },
          "400": {
            "description": "The body doesn't match, the address isn't allowed (a private network, unless the server allows it), or the account already has 10.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "issues": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ValidationIssue"
                      }
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key: this needs signing in; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/webhooks/{id}": {
      "put": {
        "operationId": "putWebhooksById",
        "tags": [
          "Webhooks"
        ],
        "summary": "Change a webhook",
        "description": "Its address, topics, or pausing it (active: false). Not with API keys.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID made by the device that created the record.",
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Webhook"
                }
              }
            }
          },
          "400": {
            "description": "The body doesn't match, or the address isn't allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "issues": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ValidationIssue"
                      }
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key: this needs signing in; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not this user's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteWebhooksById",
        "tags": [
          "Webhooks"
        ],
        "summary": "Delete a webhook",
        "description": "With its deliveries; nothing more is sent. Not with API keys.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID made by the device that created the record.",
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    }
                  },
                  "required": [
                    "ok"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key: this needs signing in; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not this user's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/webhooks/{id}/deliveries": {
      "get": {
        "operationId": "getWebhooksByIdDeliveries",
        "tags": [
          "Webhooks"
        ],
        "summary": "A webhook's deliveries",
        "description": "The latest 50, newest first. Not with API keys.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID made by the device that created the record.",
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The deliveries.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deliveries": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WebhookDelivery"
                      }
                    }
                  },
                  "required": [
                    "deliveries"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key: this needs signing in; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not this user's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/webhooks/{id}/test": {
      "post": {
        "operationId": "postWebhooksByIdTest",
        "tags": [
          "Webhooks"
        ],
        "summary": "Send a test",
        "description": "Sends an example delivery now, once, and says how it went. The example is the account's latest record of the topic, or a made-up one; test is true in it. Not with API keys.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "UUID made by the device that created the record.",
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "topic": {
                    "description": "match when left out.",
                    "$ref": "#/components/schemas/WebhookTopic"
                  }
                },
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Sent (whatever the answer).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "delivery": {
                      "$ref": "#/components/schemas/WebhookDelivery"
                    },
                    "payload": {
                      "$ref": "#/components/schemas/WebhookPayload"
                    },
                    "durationMs": {
                      "type": "integer",
                      "minimum": -9007199254740991,
                      "maximum": 9007199254740991
                    }
                  },
                  "required": [
                    "delivery",
                    "payload",
                    "durationMs"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key: this needs signing in; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not this user's.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/watch/matches": {
      "get": {
        "operationId": "getWatchMatches",
        "tags": [
          "Watch"
        ],
        "summary": "The next matches, for the watch",
        "description": "Scheduled matches from three hours ago on, soonest first, so one that's about to start or running late is still there. Not with API keys.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "At most 10 (the default).",
            "schema": {
              "description": "At most 10 (the default).",
              "type": "integer",
              "minimum": -9007199254740991,
              "maximum": 9007199254740991
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The matches.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "matches": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WatchMatch"
                      }
                    },
                    "serverVersion": {
                      "description": "The server's version (VER-3), for the watch's About screen.",
                      "type": "string"
                    },
                    "screen": {
                      "description": "The match screen chosen for the account (UI-24), once one has been.",
                      "$ref": "#/components/schemas/WatchScreenChange"
                    }
                  },
                  "required": [
                    "matches"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key: this needs signing in; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/watch/records": {
      "post": {
        "operationId": "postWatchRecords",
        "tags": [
          "Watch"
        ],
        "summary": "Upload a finished match",
        "description": "Stored as a match record, and its planned match marked played. A quick match (no matchId) gets a match made for it from what the watch knew. Sending the same match again updates it. Not with API keys.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WatchUpload"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated: this match was sent before.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WatchUploadResult"
                }
              }
            }
          },
          "201": {
            "description": "Stored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WatchUploadResult"
                }
              }
            }
          },
          "400": {
            "description": "The body doesn't match.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key: this needs signing in; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "It would duplicate another record.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/watch/tracks": {
      "post": {
        "operationId": "postWatchTracks",
        "tags": [
          "Watch"
        ],
        "summary": "Upload part of a match's track",
        "description": "The watch sends the track it recorded (TRK-7) a chunk at a time, after the match. Once every chunk and the match are in, the chunks become the match report's track, unless it has one already (from a file, say), which is kept. Sending a chunk again replaces it. Not with API keys.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WatchTrackChunk"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Received.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WatchTrackResult"
                }
              }
            }
          },
          "400": {
            "description": "The body doesn't match, or the samples can't be read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key: this needs signing in; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/watch/pitch": {
      "post": {
        "operationId": "postWatchPitch",
        "tags": [
          "Watch"
        ],
        "summary": "Pitch corners from the watch",
        "description": "Corners walked on the watch (TRK-13), sent at its next sync (SYNC-12). They're kept with the match, replacing any it had, and with its venue's pitch when it has one, for its matches from then on. Not with API keys.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WatchPitchMarking"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Kept.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    },
                    "pitch": {
                      "type": "boolean",
                      "description": "Whether they were kept with a venue's pitch too."
                    }
                  },
                  "required": [
                    "ok",
                    "pitch"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "description": "The body doesn't match, or it names neither matchId nor created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "issues": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ValidationIssue"
                      }
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key: this needs signing in; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such match.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/watch/screen": {
      "post": {
        "operationId": "postWatchScreen",
        "tags": [
          "Watch"
        ],
        "summary": "The match screen chosen on the watch",
        "description": "Settings > Match screen on the watch (UI-24), sent at its next sync. It becomes the account's choice unless the website or a phone has changed it since (the latest change wins). An account with no settings yet keeps nothing. Not with API keys.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WatchScreenChange"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Whether this change is now the account's choice.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "applied": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "applied"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "description": "The body doesn't match.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key: this needs signing in; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/watch/fee": {
      "post": {
        "operationId": "postWatchFee",
        "tags": [
          "Watch"
        ],
        "summary": "Mark a fee paid from the watch",
        "description": "The watch's Paid toggle (SYNC-11), sent at its next sync. It changes only whether the fee is paid, and only if the match hasn't been changed since (the latest change wins). Not with API keys.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WatchFeeChange"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Whether the fee is paid now, and whether this change made it so.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "paid": {
                      "type": "boolean"
                    },
                    "applied": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "paid",
                    "applied"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "description": "The body doesn't match.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key: this needs signing in; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such match, or it has no fee.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/geocode": {
      "get": {
        "operationId": "getGeocode",
        "tags": [
          "Places"
        ],
        "summary": "Find an address",
        "description": "Through Nominatim (OpenStreetMap) by default, at most one lookup a second for the whole server, with answers cached. Only when GET /v1/meta lists the geocode capability. API keys: need venues:write.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "The address.",
            "schema": {
              "type": "string",
              "minLength": 3,
              "maxLength": 200,
              "description": "The address."
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Up to five places.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "results": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Place"
                      }
                    }
                  },
                  "required": [
                    "results"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "description": "q is shorter than 3 or longer than 200 characters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key without venues:write; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Address search is off on this server (an admin can switch it on, ADM-2).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "502": {
            "description": "The address search service didn't answer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/quiz": {
      "get": {
        "operationId": "getQuiz",
        "tags": [
          "Quiz"
        ],
        "summary": "The quiz's questions",
        "description": "Every active question in a language, with the Laws and protocols they refer to, for the app to pick from. The phone apps keep the last copy, for no signal. Only when GET /v1/meta lists the quiz capability. Not with API keys.",
        "parameters": [
          {
            "name": "language",
            "in": "query",
            "required": false,
            "description": "A two-letter language code; en by default.",
            "schema": {
              "description": "A two-letter language code; en by default.",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The questions.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuizBank"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key: this needs signing in; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "There are no questions in that language.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/weather": {
      "get": {
        "operationId": "getWeather",
        "tags": [
          "Weather"
        ],
        "summary": "Weather for matches in a window",
        "description": "For the user's matches kicking off between from and to that have a place on the map and a forecast (up to 16 days ahead) or kept weather (once played). Only when GET /v1/meta lists the weather capability. API keys: need matches:read.",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "ISO 8601 time; 30 days ago by default.",
            "schema": {
              "description": "ISO 8601 time; 30 days ago by default.",
              "type": "string"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "ISO 8601 time; 17 days from now by default. At most 400 days after from.",
            "schema": {
              "description": "ISO 8601 time; 17 days from now by default. At most 400 days after from.",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The weather, one per match that has any.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/MatchWeather"
                      }
                    }
                  },
                  "required": [
                    "items"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "description": "from or to isn't a time, or they're the wrong way round or too far apart.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key without matches:read; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/weather/{matchId}": {
      "get": {
        "operationId": "getWeatherByMatchId",
        "tags": [
          "Weather"
        ],
        "summary": "One match's weather",
        "description": "Only when GET /v1/meta lists the weather capability. API keys: need matches:read.",
        "parameters": [
          {
            "name": "matchId",
            "in": "path",
            "required": true,
            "description": "UUID made by the device that created the record.",
            "schema": {
              "$ref": "#/components/schemas/Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The weather.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MatchWeather"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, the session has ended, or the API key is unknown or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "An API key without matches:read; or the account is waiting for approval (USR-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No weather for this match: not found, no place on the map, or too far ahead.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "413": {
            "description": "The request is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many tries from this address; the Retry-After header says how many seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Id": {
        "$id": "#/components/schemas/Id",
        "type": "string",
        "format": "uuid",
        "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
        "description": "UUID made by the device that created the record."
      },
      "Timestamp": {
        "$id": "#/components/schemas/Timestamp",
        "type": "string",
        "format": "date-time",
        "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$",
        "description": "ISO 8601 date and time with a UTC offset."
      },
      "LocalDate": {
        "$id": "#/components/schemas/LocalDate",
        "type": "string",
        "format": "date",
        "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$",
        "description": "ISO 8601 date."
      },
      "Position": {
        "$id": "#/components/schemas/Position",
        "type": "object",
        "properties": {
          "lat": {
            "type": "number",
            "minimum": -90,
            "maximum": 90
          },
          "lon": {
            "type": "number",
            "minimum": -180,
            "maximum": 180
          }
        },
        "required": [
          "lat",
          "lon"
        ],
        "additionalProperties": false,
        "description": "GPS position in decimal degrees (WGS 84)."
      },
      "Side": {
        "$id": "#/components/schemas/Side",
        "type": "string",
        "enum": [
          "home",
          "away"
        ]
      },
      "Role": {
        "$id": "#/components/schemas/Role",
        "type": "string",
        "enum": [
          "referee",
          "ar1",
          "ar2",
          "fourth"
        ]
      },
      "ClockStyle": {
        "$id": "#/components/schemas/ClockStyle",
        "type": "string",
        "enum": [
          "up",
          "upEach",
          "down"
        ],
        "description": "up: counts up through the match (second half 45:00 to 90:00); upEach: from 0:00 each period; down: counts down each period (TMR-11)."
      },
      "MatchFormat": {
        "$id": "#/components/schemas/MatchFormat",
        "type": "object",
        "properties": {
          "periods": {
            "type": "integer",
            "minimum": 2,
            "maximum": 4
          },
          "periodSec": {
            "type": "integer",
            "minimum": 60,
            "maximum": 3600
          },
          "breakSec": {
            "type": "integer",
            "minimum": 0,
            "maximum": 1800,
            "description": "Between periods, and before and between extra-time periods."
          },
          "halfwayBreak": {
            "type": "boolean",
            "description": "A break halfway through each period of normal time (TMR-7)."
          },
          "halfwayBreakSec": {
            "type": "integer",
            "minimum": 0,
            "maximum": 900
          },
          "teamSize": {
            "type": "integer",
            "minimum": 1,
            "maximum": 11
          },
          "clock": {
            "$ref": "#/components/schemas/ClockStyle"
          },
          "trackStoppage": {
            "type": "boolean"
          },
          "breakStoppage": {
            "description": "With stoppage tracked, the halfway break's time is added on as stoppage (TMR-12). Missing means on: formats saved before the setting.",
            "type": "boolean"
          },
          "goalScorers": {
            "description": "Ask the scorer's number for each goal (EVT-10). Missing means on: formats saved before this always asked.",
            "type": "boolean"
          },
          "goalTypes": {
            "type": "boolean",
            "description": "Ask the goal type (goal, DFK, penalty, own goal) for each goal (EVT-8)."
          },
          "extraTime": {
            "type": "boolean"
          },
          "extraTimePeriods": {
            "type": "integer",
            "minimum": 1,
            "maximum": 2
          },
          "extraTimePeriodSec": {
            "type": "integer",
            "minimum": 60,
            "maximum": 1800
          },
          "penalties": {
            "type": "boolean"
          }
        },
        "required": [
          "periods",
          "periodSec",
          "breakSec",
          "halfwayBreak",
          "halfwayBreakSec",
          "teamSize",
          "clock",
          "trackStoppage",
          "goalTypes",
          "extraTime",
          "extraTimePeriods",
          "extraTimePeriodSec",
          "penalties"
        ],
        "additionalProperties": false,
        "description": "How a match is played: periods, breaks, extra time, penalties and what the watch records."
      },
      "SchemeId": {
        "$id": "#/components/schemas/SchemeId",
        "type": "string",
        "pattern": "^[a-z0-9-]+$",
        "description": "A misconduct scheme's ID, the name of its file in schema/misconduct/ (ifab, ussf, fa)."
      },
      "MisconductScheme": {
        "$id": "#/components/schemas/MisconductScheme",
        "type": "object",
        "properties": {
          "id": {
            "$ref": "#/components/schemas/SchemeId"
          },
          "name": {
            "type": "string",
            "minLength": 1
          },
          "source": {
            "type": "string",
            "description": "Where the codes come from."
          },
          "cautions": {
            "minItems": 1,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "code": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 12
                },
                "description": {
                  "type": "string",
                  "minLength": 1
                }
              },
              "required": [
                "code",
                "description"
              ],
              "additionalProperties": false
            }
          },
          "sendOffs": {
            "minItems": 1,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "code": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 12
                },
                "description": {
                  "type": "string",
                  "minLength": 1
                }
              },
              "required": [
                "code",
                "description"
              ],
              "additionalProperties": false
            }
          },
          "secondCaution": {
            "type": "string",
            "description": "The send-off code for a second caution in the same match (2CT, S7)."
          }
        },
        "required": [
          "id",
          "name",
          "source",
          "cautions",
          "sendOffs",
          "secondCaution"
        ],
        "additionalProperties": false,
        "description": "A set of caution and send-off codes."
      },
      "MisconductCode": {
        "$id": "#/components/schemas/MisconductCode",
        "type": "object",
        "properties": {
          "scheme": {
            "$ref": "#/components/schemas/SchemeId"
          },
          "code": {
            "type": "string",
            "minLength": 1,
            "maxLength": 12
          }
        },
        "required": [
          "scheme",
          "code"
        ],
        "additionalProperties": false
      },
      "Tags": {
        "$id": "#/components/schemas/Tags",
        "maxItems": 20,
        "type": "array",
        "items": {
          "type": "string",
          "minLength": 1,
          "maxLength": 30
        }
      },
      "Team": {
        "$id": "#/components/schemas/Team",
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 60
          },
          "alias": {
            "description": "Shown on the watch; with none, the first three letters of the name in capitals, or HOM/AWY (SET-5).",
            "type": "string",
            "minLength": 1,
            "maxLength": 4
          },
          "colour": {
            "description": "The team's primary colour; the watch picks black or white text for contrast.",
            "type": "string",
            "pattern": "^#[0-9A-Fa-f]{6}$"
          }
        },
        "additionalProperties": false,
        "description": "A team as typed in for one match. Saved teams come later, tied to a competition."
      },
      "Fee": {
        "$id": "#/components/schemas/Fee",
        "type": "object",
        "properties": {
          "amount": {
            "type": "number",
            "minimum": 0
          },
          "currency": {
            "type": "string",
            "pattern": "^[A-Z]{3}$",
            "description": "ISO 4217 currency code, for example USD."
          },
          "paid": {
            "type": "boolean"
          }
        },
        "required": [
          "amount",
          "currency",
          "paid"
        ],
        "additionalProperties": false
      },
      "CrewMember": {
        "$id": "#/components/schemas/CrewMember",
        "type": "object",
        "properties": {
          "role": {
            "$ref": "#/components/schemas/Role"
          },
          "refereeId": {
            "description": "A saved referee.",
            "$ref": "#/components/schemas/Id"
          },
          "name": {
            "description": "A name typed in, when no saved referee is linked.",
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          }
        },
        "required": [
          "role"
        ],
        "additionalProperties": false,
        "description": "Another official on the match."
      },
      "ExternalRef": {
        "$id": "#/components/schemas/ExternalRef",
        "type": "object",
        "properties": {
          "source": {
            "type": "string",
            "maxLength": 40,
            "pattern": "^[a-z0-9-]+$",
            "description": "Where the ID comes from: manual, csv, or an assignor system."
          },
          "id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          }
        },
        "required": [
          "source",
          "id"
        ],
        "additionalProperties": false,
        "description": "A match ID from outside LawFive, unique per user and source; a re-import with the same one updates the match."
      },
      "Gender": {
        "$id": "#/components/schemas/Gender",
        "type": "string",
        "enum": [
          "male",
          "female",
          "coed"
        ],
        "description": "Who plays: male, female or co-ed (mixed)."
      },
      "MatchStatus": {
        "$id": "#/components/schemas/MatchStatus",
        "type": "string",
        "enum": [
          "scheduled",
          "played",
          "abandoned",
          "cancelled",
          "postponed"
        ]
      },
      "PitchMarking": {
        "$id": "#/components/schemas/PitchMarking",
        "type": "object",
        "properties": {
          "corners": {
            "minItems": 2,
            "maxItems": 4,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Position"
            },
            "description": "Two to four of the pitch's corners, in any order, as walked on the watch."
          },
          "at": {
            "description": "When they were marked.",
            "$ref": "#/components/schemas/Timestamp"
          }
        },
        "required": [
          "corners",
          "at"
        ],
        "additionalProperties": false,
        "description": "Pitch corners marked on the watch (TRK-13)."
      },
      "Match": {
        "$id": "#/components/schemas/Match",
        "type": "object",
        "properties": {
          "id": {
            "$ref": "#/components/schemas/Id"
          },
          "version": {
            "type": "integer",
            "minimum": 1,
            "maximum": 9007199254740991,
            "description": "Raised by the server on every change; clients send the version they last saw so conflicts can be spotted."
          },
          "updatedAt": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "deletedAt": {
            "description": "Set when the record is deleted; deleted records are kept so other devices learn of the deletion.",
            "$ref": "#/components/schemas/Timestamp"
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "status": {
            "$ref": "#/components/schemas/MatchStatus"
          },
          "kickoff": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "timeZone": {
            "description": "The venue's time zone, for showing the kickoff in local time.",
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          },
          "format": {
            "description": "A copy of the format, so editing the template later doesn't change this match.",
            "$ref": "#/components/schemas/MatchFormat"
          },
          "templateId": {
            "description": "The template the format was copied from.",
            "$ref": "#/components/schemas/Id"
          },
          "home": {
            "$ref": "#/components/schemas/Team"
          },
          "away": {
            "$ref": "#/components/schemas/Team"
          },
          "gender": {
            "$ref": "#/components/schemas/Gender"
          },
          "ageGroup": {
            "description": "Free text, as leagues write it: U12, U19, Adult, O35.",
            "type": "string",
            "minLength": 1,
            "maxLength": 30
          },
          "competitionId": {
            "$ref": "#/components/schemas/Id"
          },
          "venueId": {
            "$ref": "#/components/schemas/Id"
          },
          "pitchId": {
            "description": "One of the venue's pitches.",
            "$ref": "#/components/schemas/Id"
          },
          "role": {
            "description": "The user's role in this match.",
            "$ref": "#/components/schemas/Role"
          },
          "crew": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CrewMember"
            },
            "description": "The other officials."
          },
          "notes": {
            "type": "string",
            "maxLength": 2000
          },
          "outlook": {
            "description": "What to expect: competitive, one-sided, team strengths and so on.",
            "type": "string",
            "maxLength": 2000
          },
          "fee": {
            "$ref": "#/components/schemas/Fee"
          },
          "external": {
            "$ref": "#/components/schemas/ExternalRef"
          },
          "tags": {
            "$ref": "#/components/schemas/Tags"
          },
          "pitchMarking": {
            "description": "The pitch's corners as marked on the watch for this match (TRK-13).",
            "$ref": "#/components/schemas/PitchMarking"
          }
        },
        "required": [
          "id",
          "version",
          "updatedAt",
          "status",
          "format",
          "home",
          "away",
          "crew"
        ],
        "additionalProperties": false,
        "description": "One referee's own copy of a match."
      },
      "MatchTemplate": {
        "$id": "#/components/schemas/MatchTemplate",
        "type": "object",
        "properties": {
          "id": {
            "$ref": "#/components/schemas/Id"
          },
          "version": {
            "type": "integer",
            "minimum": 1,
            "maximum": 9007199254740991,
            "description": "Raised by the server on every change; clients send the version they last saw so conflicts can be spotted."
          },
          "updatedAt": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "deletedAt": {
            "description": "Set when the record is deleted; deleted records are kept so other devices learn of the deletion.",
            "$ref": "#/components/schemas/Timestamp"
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 60
          },
          "format": {
            "$ref": "#/components/schemas/MatchFormat"
          },
          "defaults": {
            "type": "object",
            "properties": {
              "fee": {
                "$ref": "#/components/schemas/Fee"
              }
            },
            "additionalProperties": false,
            "description": "Filled into a match made from this template, or from a competition that uses it, when the match has none. Not the user's role: that depends on the match."
          }
        },
        "required": [
          "id",
          "version",
          "updatedAt",
          "name",
          "format",
          "defaults"
        ],
        "additionalProperties": false,
        "description": "A reusable match format, such as \"U12 league: 2×30, 5 min half-time\"."
      },
      "Pitch": {
        "$id": "#/components/schemas/Pitch",
        "type": "object",
        "properties": {
          "id": {
            "$ref": "#/components/schemas/Id"
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 60
          },
          "position": {
            "description": "Where the pitch itself is, which can be some way from the venue's address.",
            "$ref": "#/components/schemas/Position"
          },
          "markings": {
            "description": "Its corners as marked on the watch, oldest first (TRK-13). Each is used for matches kicking off after it, until a later one.",
            "maxItems": 20,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PitchMarking"
            }
          }
        },
        "required": [
          "id",
          "name"
        ],
        "additionalProperties": false
      },
      "Venue": {
        "$id": "#/components/schemas/Venue",
        "type": "object",
        "properties": {
          "id": {
            "$ref": "#/components/schemas/Id"
          },
          "version": {
            "type": "integer",
            "minimum": 1,
            "maximum": 9007199254740991,
            "description": "Raised by the server on every change; clients send the version they last saw so conflicts can be spotted."
          },
          "updatedAt": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "deletedAt": {
            "description": "Set when the record is deleted; deleted records are kept so other devices learn of the deletion.",
            "$ref": "#/components/schemas/Timestamp"
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "address": {
            "description": "Street address; often where you park.",
            "type": "string",
            "maxLength": 300
          },
          "position": {
            "description": "The address's position (parking), for directions.",
            "$ref": "#/components/schemas/Position"
          },
          "pitches": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Pitch"
            },
            "description": "Named pitches (\"Field 3\"), each with its own position."
          },
          "notes": {
            "type": "string",
            "maxLength": 2000
          },
          "tags": {
            "$ref": "#/components/schemas/Tags"
          }
        },
        "required": [
          "id",
          "version",
          "updatedAt",
          "name",
          "pitches"
        ],
        "additionalProperties": false
      },
      "CompetitionType": {
        "$id": "#/components/schemas/CompetitionType",
        "type": "string",
        "enum": [
          "league",
          "tournament",
          "cup",
          "friendly"
        ]
      },
      "Competition": {
        "$id": "#/components/schemas/Competition",
        "type": "object",
        "properties": {
          "id": {
            "$ref": "#/components/schemas/Id"
          },
          "version": {
            "type": "integer",
            "minimum": 1,
            "maximum": 9007199254740991,
            "description": "Raised by the server on every change; clients send the version they last saw so conflicts can be spotted."
          },
          "updatedAt": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "deletedAt": {
            "description": "Set when the record is deleted; deleted records are kept so other devices learn of the deletion.",
            "$ref": "#/components/schemas/Timestamp"
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "organiser": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "type": {
            "$ref": "#/components/schemas/CompetitionType"
          },
          "season": {
            "description": "Season label, for example 2026-27.",
            "type": "string",
            "minLength": 1,
            "maxLength": 30
          },
          "startDate": {
            "$ref": "#/components/schemas/LocalDate"
          },
          "endDate": {
            "$ref": "#/components/schemas/LocalDate"
          },
          "ageGroup": {
            "type": "string",
            "minLength": 1,
            "maxLength": 30
          },
          "level": {
            "type": "string",
            "minLength": 1,
            "maxLength": 60
          },
          "templateId": {
            "description": "The match template for this competition's matches: picking the competition fills the format (and fee).",
            "$ref": "#/components/schemas/Id"
          },
          "misconductScheme": {
            "description": "Overrides the user's default scheme for this competition's matches.",
            "$ref": "#/components/schemas/SchemeId"
          },
          "notes": {
            "type": "string",
            "maxLength": 2000
          }
        },
        "required": [
          "id",
          "version",
          "updatedAt",
          "name",
          "type"
        ],
        "additionalProperties": false,
        "description": "One competition for one season; next season is a new record."
      },
      "Referee": {
        "$id": "#/components/schemas/Referee",
        "type": "object",
        "properties": {
          "id": {
            "$ref": "#/components/schemas/Id"
          },
          "version": {
            "type": "integer",
            "minimum": 1,
            "maximum": 9007199254740991,
            "description": "Raised by the server on every change; clients send the version they last saw so conflicts can be spotted."
          },
          "updatedAt": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "deletedAt": {
            "description": "Set when the record is deleted; deleted records are kept so other devices learn of the deletion.",
            "$ref": "#/components/schemas/Timestamp"
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "aliases": {
            "description": "Other names they go by, such as the ones merged into this referee (DUP-3); imports and searches find the referee by them.",
            "maxItems": 50,
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 100
            }
          },
          "email": {
            "type": "string",
            "format": "email",
            "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
          },
          "phone": {
            "type": "string",
            "minLength": 1,
            "maxLength": 30
          },
          "notes": {
            "type": "string",
            "maxLength": 2000
          },
          "tags": {
            "$ref": "#/components/schemas/Tags"
          },
          "distinctFrom": {
            "description": "Referees the user said aren't the same person, so they aren't suggested as duplicates again (DUP-5).",
            "maxItems": 500,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Id"
            }
          }
        },
        "required": [
          "id",
          "version",
          "updatedAt",
          "name"
        ],
        "additionalProperties": false,
        "description": "Someone the user officiates with."
      },
      "DistanceUnit": {
        "$id": "#/components/schemas/DistanceUnit",
        "type": "string",
        "enum": [
          "km",
          "mi"
        ]
      },
      "TemperatureUnit": {
        "$id": "#/components/schemas/TemperatureUnit",
        "type": "string",
        "enum": [
          "c",
          "f"
        ]
      },
      "WatchLayout": {
        "$id": "#/components/schemas/WatchLayout",
        "type": "string",
        "enum": [
          "default",
          "stacked",
          "minimal",
          "scoreboard",
          "sideBySide",
          "ring"
        ],
        "description": "The watch's running screen (UI-21). stacked and sideBySide show a second clock; ring is round watches only, others show default."
      },
      "WatchScreen": {
        "$id": "#/components/schemas/WatchScreen",
        "type": "object",
        "properties": {
          "layout": {
            "$ref": "#/components/schemas/WatchLayout"
          },
          "secondClock": {
            "description": "What the second clock shows (UI-22); down is the time remaining.",
            "$ref": "#/components/schemas/ClockStyle"
          },
          "changedAt": {
            "description": "When it was last changed, wherever that was: the latest change wins (UI-24).",
            "$ref": "#/components/schemas/Timestamp"
          }
        },
        "required": [
          "layout",
          "secondClock",
          "changedAt"
        ],
        "additionalProperties": false,
        "description": "How the watch's running screen looks, for all the account's watches (UI-24)."
      },
      "UserSettings": {
        "$id": "#/components/schemas/UserSettings",
        "type": "object",
        "properties": {
          "id": {
            "$ref": "#/components/schemas/Id"
          },
          "version": {
            "type": "integer",
            "minimum": 1,
            "maximum": 9007199254740991,
            "description": "Raised by the server on every change; clients send the version they last saw so conflicts can be spotted."
          },
          "updatedAt": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "deletedAt": {
            "description": "Set when the record is deleted; deleted records are kept so other devices learn of the deletion.",
            "$ref": "#/components/schemas/Timestamp"
          },
          "misconductScheme": {
            "description": "The default scheme for cards; a competition can override it.",
            "$ref": "#/components/schemas/SchemeId"
          },
          "currency": {
            "type": "string",
            "pattern": "^[A-Z]{3}$",
            "description": "Default currency for fees."
          },
          "distanceUnit": {
            "$ref": "#/components/schemas/DistanceUnit"
          },
          "temperatureUnit": {
            "$ref": "#/components/schemas/TemperatureUnit"
          },
          "maxHeartRate": {
            "description": "Beats a minute, for heart-rate zones (TRK-4).",
            "type": "integer",
            "minimum": 100,
            "maximum": 240
          },
          "sprintKph": {
            "description": "Where sprinting starts, in km/h, for the speed bands and sprints (TRK-11); the other bands scale with it. 21 when not set.",
            "type": "number",
            "minimum": 15,
            "maximum": 35
          },
          "calendarArriveMin": {
            "description": "Minutes before kickoff the calendar feed's events start, for travel and warming up (CAL-3); 0 starts them at kickoff. 30 when not set.",
            "type": "integer",
            "minimum": 0,
            "maximum": 180
          },
          "calendarReminderMin": {
            "description": "Before several reminders (CAL-3): minutes before kickoff for the calendar feed's one reminder; 0 for none. Used only when none of calendarReminders, calendarDayBefore and calendarDayOf is set; 60 when nothing is.",
            "type": "integer",
            "minimum": 0,
            "maximum": 2880
          },
          "calendarReminders": {
            "description": "The calendar feed's reminders, in minutes before kickoff (CAL-3): 15-minute steps up to 3 hours.",
            "maxItems": 12,
            "type": "array",
            "items": {
              "type": "integer",
              "minimum": 15,
              "maximum": 180
            }
          },
          "calendarDayBefore": {
            "description": "A reminder the day before a match at this time (HH:MM, in the match's time zone); none when not set.",
            "type": "string",
            "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$"
          },
          "watchScreen": {
            "description": "Missing until it's first chosen: watches then keep Default, or their own choice.",
            "$ref": "#/components/schemas/WatchScreen"
          },
          "calendarDayOf": {
            "description": "A reminder on the day of a match at this time (HH:MM, in the match's time zone), when that's before the event starts; none when not set.",
            "type": "string",
            "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$"
          }
        },
        "required": [
          "id",
          "version",
          "updatedAt",
          "misconductScheme",
          "currency",
          "distanceUnit",
          "temperatureUnit"
        ],
        "additionalProperties": false,
        "description": "The user's preferences; one per user, first set from the browser's language when the account is created."
      },
      "GoalType": {
        "$id": "#/components/schemas/GoalType",
        "type": "string",
        "enum": [
          "goal",
          "directFreeKick",
          "penalty",
          "ownGoal"
        ]
      },
      "TeamOfficialRole": {
        "$id": "#/components/schemas/TeamOfficialRole",
        "type": "string",
        "enum": [
          "headCoach",
          "assistantCoach",
          "other"
        ]
      },
      "Offender": {
        "$id": "#/components/schemas/Offender",
        "oneOf": [
          {
            "type": "object",
            "properties": {
              "kind": {
                "type": "string",
                "const": "player"
              },
              "number": {
                "type": "integer",
                "minimum": 1,
                "maximum": 99
              }
            },
            "required": [
              "kind"
            ],
            "additionalProperties": false
          },
          {
            "type": "object",
            "properties": {
              "kind": {
                "type": "string",
                "const": "teamOfficial"
              },
              "role": {
                "$ref": "#/components/schemas/TeamOfficialRole"
              },
              "name": {
                "type": "string",
                "maxLength": 100
              }
            },
            "required": [
              "kind",
              "role"
            ],
            "additionalProperties": false
          }
        ]
      },
      "AbandonReason": {
        "$id": "#/components/schemas/AbandonReason",
        "type": "string",
        "enum": [
          "weather",
          "lightning",
          "crowdTrouble",
          "notEnoughPlayers",
          "other"
        ]
      },
      "Event": {
        "$id": "#/components/schemas/Event",
        "oneOf": [
          {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "const": "goal"
              },
              "when": {
                "type": "string",
                "enum": [
                  "play",
                  "beforeKickoff",
                  "break",
                  "afterFullTime",
                  "penalties"
                ]
              },
              "period": {
                "description": "1-based; extra-time periods follow the normal ones.",
                "type": "integer",
                "minimum": 1,
                "maximum": 9007199254740991
              },
              "periodSec": {
                "description": "Seconds since the period's kickoff, including stoppage; only during play.",
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "at": {
                "description": "Wall-clock time, when the device knows it.",
                "$ref": "#/components/schemas/Timestamp"
              },
              "team": {
                "description": "The team the goal counts for; for an own goal, the player is from the other team.",
                "$ref": "#/components/schemas/Side"
              },
              "player": {
                "type": "integer",
                "minimum": 1,
                "maximum": 99
              },
              "goalType": {
                "description": "When the match records goal types (EVT-8).",
                "$ref": "#/components/schemas/GoalType"
              }
            },
            "required": [
              "type",
              "when",
              "team"
            ],
            "additionalProperties": false
          },
          {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "const": "caution"
              },
              "when": {
                "type": "string",
                "enum": [
                  "play",
                  "beforeKickoff",
                  "break",
                  "afterFullTime",
                  "penalties"
                ]
              },
              "period": {
                "description": "1-based; extra-time periods follow the normal ones.",
                "type": "integer",
                "minimum": 1,
                "maximum": 9007199254740991
              },
              "periodSec": {
                "description": "Seconds since the period's kickoff, including stoppage; only during play.",
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "at": {
                "description": "Wall-clock time, when the device knows it.",
                "$ref": "#/components/schemas/Timestamp"
              },
              "team": {
                "$ref": "#/components/schemas/Side"
              },
              "offender": {
                "$ref": "#/components/schemas/Offender"
              },
              "misconduct": {
                "description": "For a second caution, the reason for the second caution.",
                "$ref": "#/components/schemas/MisconductCode"
              }
            },
            "required": [
              "type",
              "when",
              "team",
              "offender"
            ],
            "additionalProperties": false
          },
          {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "const": "secondCaution"
              },
              "when": {
                "type": "string",
                "enum": [
                  "play",
                  "beforeKickoff",
                  "break",
                  "afterFullTime",
                  "penalties"
                ]
              },
              "period": {
                "description": "1-based; extra-time periods follow the normal ones.",
                "type": "integer",
                "minimum": 1,
                "maximum": 9007199254740991
              },
              "periodSec": {
                "description": "Seconds since the period's kickoff, including stoppage; only during play.",
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "at": {
                "description": "Wall-clock time, when the device knows it.",
                "$ref": "#/components/schemas/Timestamp"
              },
              "team": {
                "$ref": "#/components/schemas/Side"
              },
              "offender": {
                "$ref": "#/components/schemas/Offender"
              },
              "misconduct": {
                "description": "For a second caution, the reason for the second caution.",
                "$ref": "#/components/schemas/MisconductCode"
              }
            },
            "required": [
              "type",
              "when",
              "team",
              "offender"
            ],
            "additionalProperties": false,
            "description": "Counts as a send-off (EVT-5)."
          },
          {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "const": "sendOff"
              },
              "when": {
                "type": "string",
                "enum": [
                  "play",
                  "beforeKickoff",
                  "break",
                  "afterFullTime",
                  "penalties"
                ]
              },
              "period": {
                "description": "1-based; extra-time periods follow the normal ones.",
                "type": "integer",
                "minimum": 1,
                "maximum": 9007199254740991
              },
              "periodSec": {
                "description": "Seconds since the period's kickoff, including stoppage; only during play.",
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "at": {
                "description": "Wall-clock time, when the device knows it.",
                "$ref": "#/components/schemas/Timestamp"
              },
              "team": {
                "$ref": "#/components/schemas/Side"
              },
              "offender": {
                "$ref": "#/components/schemas/Offender"
              },
              "misconduct": {
                "description": "For a second caution, the reason for the second caution.",
                "$ref": "#/components/schemas/MisconductCode"
              }
            },
            "required": [
              "type",
              "when",
              "team",
              "offender"
            ],
            "additionalProperties": false
          },
          {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "const": "stoppage"
              },
              "when": {
                "type": "string",
                "enum": [
                  "play",
                  "beforeKickoff",
                  "break",
                  "afterFullTime",
                  "penalties"
                ]
              },
              "period": {
                "description": "1-based; extra-time periods follow the normal ones.",
                "type": "integer",
                "minimum": 1,
                "maximum": 9007199254740991
              },
              "periodSec": {
                "description": "Seconds since the period's kickoff, including stoppage; only during play.",
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "at": {
                "description": "Wall-clock time, when the device knows it.",
                "$ref": "#/components/schemas/Timestamp"
              },
              "lengthSec": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              }
            },
            "required": [
              "type",
              "when",
              "lengthSec"
            ],
            "additionalProperties": false,
            "description": "Time added for a stoppage, when stoppage is tracked (EVT-4); periodSec is when it began."
          },
          {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "const": "periodEnd"
              },
              "when": {
                "type": "string",
                "enum": [
                  "play",
                  "beforeKickoff",
                  "break",
                  "afterFullTime",
                  "penalties"
                ]
              },
              "period": {
                "description": "1-based; extra-time periods follow the normal ones.",
                "type": "integer",
                "minimum": 1,
                "maximum": 9007199254740991
              },
              "periodSec": {
                "description": "Seconds since the period's kickoff, including stoppage; only during play.",
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "at": {
                "description": "Wall-clock time, when the device knows it.",
                "$ref": "#/components/schemas/Timestamp"
              }
            },
            "required": [
              "type",
              "when"
            ],
            "additionalProperties": false
          },
          {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "const": "penaltyKick"
              },
              "when": {
                "type": "string",
                "enum": [
                  "play",
                  "beforeKickoff",
                  "break",
                  "afterFullTime",
                  "penalties"
                ]
              },
              "period": {
                "description": "1-based; extra-time periods follow the normal ones.",
                "type": "integer",
                "minimum": 1,
                "maximum": 9007199254740991
              },
              "periodSec": {
                "description": "Seconds since the period's kickoff, including stoppage; only during play.",
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "at": {
                "description": "Wall-clock time, when the device knows it.",
                "$ref": "#/components/schemas/Timestamp"
              },
              "team": {
                "$ref": "#/components/schemas/Side"
              },
              "scored": {
                "type": "boolean"
              },
              "player": {
                "type": "integer",
                "minimum": 1,
                "maximum": 99
              }
            },
            "required": [
              "type",
              "when",
              "team",
              "scored"
            ],
            "additionalProperties": false
          },
          {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "const": "injury"
              },
              "when": {
                "type": "string",
                "enum": [
                  "play",
                  "beforeKickoff",
                  "break",
                  "afterFullTime",
                  "penalties"
                ]
              },
              "period": {
                "description": "1-based; extra-time periods follow the normal ones.",
                "type": "integer",
                "minimum": 1,
                "maximum": 9007199254740991
              },
              "periodSec": {
                "description": "Seconds since the period's kickoff, including stoppage; only during play.",
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "at": {
                "description": "Wall-clock time, when the device knows it.",
                "$ref": "#/components/schemas/Timestamp"
              },
              "team": {
                "$ref": "#/components/schemas/Side"
              },
              "player": {
                "type": "integer",
                "minimum": 1,
                "maximum": 99
              },
              "lengthSec": {
                "description": "How long play was stopped.",
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "note": {
                "type": "string",
                "maxLength": 500
              }
            },
            "required": [
              "type",
              "when",
              "team"
            ],
            "additionalProperties": false
          },
          {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "const": "suspension"
              },
              "when": {
                "type": "string",
                "enum": [
                  "play",
                  "beforeKickoff",
                  "break",
                  "afterFullTime",
                  "penalties"
                ]
              },
              "period": {
                "description": "1-based; extra-time periods follow the normal ones.",
                "type": "integer",
                "minimum": 1,
                "maximum": 9007199254740991
              },
              "periodSec": {
                "description": "Seconds since the period's kickoff, including stoppage; only during play.",
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "at": {
                "description": "Wall-clock time, when the device knows it.",
                "$ref": "#/components/schemas/Timestamp"
              },
              "reason": {
                "type": "string",
                "enum": [
                  "weather",
                  "lightning",
                  "crowdTrouble",
                  "other"
                ]
              },
              "lengthSec": {
                "description": "How long until play resumed; absent if it never did (an abandonment follows).",
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "note": {
                "type": "string",
                "maxLength": 500
              }
            },
            "required": [
              "type",
              "when",
              "reason"
            ],
            "additionalProperties": false,
            "description": "Play stopped for a while, for example a lightning delay, and the match continued."
          },
          {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "const": "abandonment"
              },
              "when": {
                "type": "string",
                "enum": [
                  "play",
                  "beforeKickoff",
                  "break",
                  "afterFullTime",
                  "penalties"
                ]
              },
              "period": {
                "description": "1-based; extra-time periods follow the normal ones.",
                "type": "integer",
                "minimum": 1,
                "maximum": 9007199254740991
              },
              "periodSec": {
                "description": "Seconds since the period's kickoff, including stoppage; only during play.",
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "at": {
                "description": "Wall-clock time, when the device knows it.",
                "$ref": "#/components/schemas/Timestamp"
              },
              "reason": {
                "$ref": "#/components/schemas/AbandonReason"
              },
              "note": {
                "type": "string",
                "maxLength": 500
              }
            },
            "required": [
              "type",
              "when",
              "reason"
            ],
            "additionalProperties": false,
            "description": "The match was abandoned; the score at that point is kept."
          }
        ]
      },
      "PeriodPlayed": {
        "$id": "#/components/schemas/PeriodPlayed",
        "type": "object",
        "properties": {
          "period": {
            "type": "integer",
            "minimum": 1,
            "maximum": 9007199254740991
          },
          "extraTime": {
            "type": "boolean"
          },
          "startedAt": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "playedSec": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991,
            "description": "From kickoff to the end of the period, including added time."
          }
        },
        "required": [
          "period",
          "extraTime",
          "playedSec"
        ],
        "additionalProperties": false
      },
      "MatchDiagnostics": {
        "$id": "#/components/schemas/MatchDiagnostics",
        "type": "object",
        "properties": {
          "battery": {
            "description": "The watch's battery at each period's kickoff and end, so the last is full time.",
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "period": {
                  "type": "integer",
                  "minimum": 1,
                  "maximum": 9007199254740991
                },
                "at": {
                  "type": "string",
                  "enum": [
                    "kickoff",
                    "end"
                  ]
                },
                "percent": {
                  "type": "number",
                  "minimum": 0,
                  "maximum": 100
                }
              },
              "required": [
                "period",
                "at",
                "percent"
              ],
              "additionalProperties": false
            }
          },
          "app": {
            "description": "The version of LawFive that recorded it.",
            "type": "string",
            "maxLength": 40
          },
          "firmware": {
            "description": "The watch's own software.",
            "type": "string",
            "maxLength": 40
          },
          "gpsFirstFixSec": {
            "description": "Seconds from kickoff to the first GPS fix.",
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "gpsSamples": {
            "description": "Track samples taken.",
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "gpsMissed": {
            "description": "Of those, how many had no GPS fix.",
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "restarts": {
            "description": "How many times the app started again mid-match.",
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          }
        },
        "additionalProperties": false,
        "description": "How the recording went, for tracing problems. Never on a shared match's page."
      },
      "MatchRecord": {
        "$id": "#/components/schemas/MatchRecord",
        "type": "object",
        "properties": {
          "id": {
            "$ref": "#/components/schemas/Id"
          },
          "version": {
            "type": "integer",
            "minimum": 1,
            "maximum": 9007199254740991,
            "description": "Raised by the server on every change; clients send the version they last saw so conflicts can be spotted."
          },
          "updatedAt": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "deletedAt": {
            "description": "Set when the record is deleted; deleted records are kept so other devices learn of the deletion.",
            "$ref": "#/components/schemas/Timestamp"
          },
          "matchId": {
            "description": "The planned match; absent for a quick match started on the watch, for which the server makes a match.",
            "$ref": "#/components/schemas/Id"
          },
          "device": {
            "description": "What recorded it, for example garmin:venu2.",
            "type": "string",
            "maxLength": 60
          },
          "outcome": {
            "type": "string",
            "enum": [
              "completed",
              "abandoned"
            ]
          },
          "periods": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PeriodPlayed"
            }
          },
          "score": {
            "type": "object",
            "properties": {
              "home": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "away": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              }
            },
            "required": [
              "home",
              "away"
            ],
            "additionalProperties": false
          },
          "penalties": {
            "description": "Shoot-out score, when there was one.",
            "type": "object",
            "properties": {
              "home": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "away": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              }
            },
            "required": [
              "home",
              "away"
            ],
            "additionalProperties": false
          },
          "distanceM": {
            "description": "Distance the referee covered during the match, in metres, from the watch's GPS.",
            "type": "number",
            "minimum": 0
          },
          "kickoffDirection": {
            "description": "Which way the first period's kickoff team attacked, when the referee recorded it on the watch (PRE-8); heatmaps use it (TRK-9).",
            "type": "object",
            "properties": {
              "team": {
                "type": "string",
                "enum": [
                  "home",
                  "away"
                ],
                "description": "The team that kicked off the first period."
              },
              "bearingDeg": {
                "type": "number",
                "minimum": 0,
                "exclusiveMaximum": 360,
                "description": "The compass bearing of the goal it attacked, in degrees from true north."
              }
            },
            "required": [
              "team",
              "bearingDeg"
            ],
            "additionalProperties": false
          },
          "events": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Event"
            }
          },
          "diagnostics": {
            "description": "How the recording went (DIAG-1).",
            "$ref": "#/components/schemas/MatchDiagnostics"
          }
        },
        "required": [
          "id",
          "version",
          "updatedAt",
          "outcome",
          "periods",
          "score",
          "events"
        ],
        "additionalProperties": false,
        "description": "What happened in a match, as recorded on a watch."
      },
      "WeatherCondition": {
        "$id": "#/components/schemas/WeatherCondition",
        "type": "string",
        "enum": [
          "clear",
          "partlyCloudy",
          "cloudy",
          "fog",
          "drizzle",
          "rain",
          "snow",
          "storm"
        ],
        "description": "What the sky is doing, for the icon: the most significant condition while the match is played."
      },
      "WeatherRiskKind": {
        "$id": "#/components/schemas/WeatherRiskKind",
        "type": "string",
        "enum": [
          "wetField",
          "rain",
          "storm",
          "snow",
          "heat"
        ],
        "description": "wetField: rain in the 24 hours before (fields close); rain, storm, snow: during the match or the hour before; heat: feels-like temperature while playing."
      },
      "WeatherRisk": {
        "$id": "#/components/schemas/WeatherRisk",
        "type": "object",
        "properties": {
          "kind": {
            "$ref": "#/components/schemas/WeatherRiskKind"
          },
          "level": {
            "type": "string",
            "enum": [
              "possible",
              "likely"
            ],
            "description": "possible: worth watching; likely: the match may well be called off or stopped."
          },
          "amount": {
            "description": "What the level is based on: millimetres of rain (wetField: in the 24 hours before; rain: during the match), centimetres of snow, or the highest feels-like temperature in °C (heat).",
            "type": "number"
          }
        },
        "required": [
          "kind",
          "level"
        ],
        "additionalProperties": false,
        "description": "Something in the forecast that could stop or cancel the match (WX-4). Rough rules, not a prediction."
      },
      "WeatherHour": {
        "$id": "#/components/schemas/WeatherHour",
        "type": "object",
        "properties": {
          "time": {
            "description": "The start of the hour.",
            "$ref": "#/components/schemas/Timestamp"
          },
          "condition": {
            "$ref": "#/components/schemas/WeatherCondition"
          },
          "isDay": {
            "type": "boolean"
          },
          "temperatureC": {
            "type": "number"
          },
          "precipitationMm": {
            "type": "number",
            "minimum": 0,
            "description": "Rain (or melted snow) during the hour."
          },
          "precipitationChance": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          }
        },
        "required": [
          "time",
          "condition",
          "isDay",
          "temperatureC",
          "precipitationMm"
        ],
        "additionalProperties": false
      },
      "MatchWeather": {
        "$id": "#/components/schemas/MatchWeather",
        "type": "object",
        "properties": {
          "matchId": {
            "$ref": "#/components/schemas/Id"
          },
          "final": {
            "type": "boolean",
            "description": "true once the match is over: the weather it was played in, which no longer changes. false: a forecast, updated as kickoff gets closer."
          },
          "condition": {
            "$ref": "#/components/schemas/WeatherCondition"
          },
          "isDay": {
            "type": "boolean",
            "description": "Daylight at kickoff, for a sun or moon icon."
          },
          "temperatureC": {
            "type": "number",
            "description": "At kickoff."
          },
          "feelsLikeC": {
            "type": "number",
            "description": "At kickoff: the temperature with humidity, wind and sun taken into account."
          },
          "precipitationChance": {
            "description": "The highest chance of rain in any hour of the match.",
            "type": "number",
            "minimum": 0,
            "maximum": 1
          },
          "precipitationMm": {
            "type": "number",
            "minimum": 0,
            "description": "Expected rain from an hour before kickoff to the end of the match."
          },
          "precipitationBeforeMm": {
            "type": "number",
            "minimum": 0,
            "description": "Rain in the 24 hours before that."
          },
          "windGustKph": {
            "description": "The strongest gust during the match.",
            "type": "number",
            "minimum": 0
          },
          "hours": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WeatherHour"
            },
            "description": "Hour by hour from kickoff to the end of the match."
          },
          "risks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WeatherRisk"
            }
          },
          "updatedAt": {
            "description": "When the forecast was fetched.",
            "$ref": "#/components/schemas/Timestamp"
          },
          "source": {
            "type": "string",
            "description": "Whose data it is, to credit beside it (Open-Meteo)."
          }
        },
        "required": [
          "matchId",
          "final",
          "condition",
          "isDay",
          "temperatureC",
          "feelsLikeC",
          "precipitationMm",
          "precipitationBeforeMm",
          "hours",
          "risks",
          "updatedAt",
          "source"
        ],
        "additionalProperties": false,
        "description": "The weather for one match, from the forecast for its pitch (or venue) location."
      },
      "TrackSourceKind": {
        "$id": "#/components/schemas/TrackSourceKind",
        "type": "string",
        "enum": [
          "lawfive",
          "refsix",
          "fit",
          "gpx",
          "tcx",
          "appleHealth",
          "healthConnect",
          "strava",
          "other"
        ],
        "description": "lawfive: a LawFive watch app; refsix: imported from RefSix; fit, gpx, tcx: an activity file; appleHealth, healthConnect: a phone's health store; strava: from Strava."
      },
      "TrackSource": {
        "$id": "#/components/schemas/TrackSource",
        "type": "object",
        "properties": {
          "kind": {
            "$ref": "#/components/schemas/TrackSourceKind"
          },
          "device": {
            "description": "What recorded it, for example garmin:venu2.",
            "type": "string",
            "maxLength": 60
          },
          "app": {
            "description": "The app that recorded it, with its version: RefSix 1.9.1.",
            "type": "string",
            "maxLength": 60
          }
        },
        "required": [
          "kind"
        ],
        "additionalProperties": false
      },
      "GpsSeries": {
        "$id": "#/components/schemas/GpsSeries",
        "type": "object",
        "properties": {
          "t": {
            "maxItems": 43200,
            "type": "array",
            "items": {
              "type": "integer",
              "minimum": 0,
              "maximum": 9007199254740991
            },
            "description": "Milliseconds after the track's start, always increasing."
          },
          "lat": {
            "maxItems": 43200,
            "type": "array",
            "items": {
              "type": "number",
              "minimum": -90,
              "maximum": 90
            }
          },
          "lon": {
            "maxItems": 43200,
            "type": "array",
            "items": {
              "type": "number",
              "minimum": -180,
              "maximum": 180
            }
          },
          "accuracyM": {
            "description": "How far off each position may be, in metres, when the source says.",
            "maxItems": 43200,
            "type": "array",
            "items": {
              "type": "number",
              "minimum": 0
            }
          },
          "speedMps": {
            "description": "The speed the watch measured at each position (m/s), when the source records it (TRK-3); null where it didn't, and the speed then comes from the positions.",
            "maxItems": 43200,
            "type": "array",
            "items": {
              "anyOf": [
                {
                  "type": "number",
                  "minimum": 0
                },
                {
                  "type": "null"
                }
              ]
            }
          }
        },
        "required": [
          "t",
          "lat",
          "lon"
        ],
        "additionalProperties": false,
        "description": "Positions in decimal degrees (WGS 84), one per time; leave out samples without a position."
      },
      "HeartRateSeries": {
        "$id": "#/components/schemas/HeartRateSeries",
        "type": "object",
        "properties": {
          "t": {
            "maxItems": 43200,
            "type": "array",
            "items": {
              "type": "integer",
              "minimum": 0,
              "maximum": 9007199254740991
            },
            "description": "Milliseconds after the track's start, always increasing."
          },
          "bpm": {
            "maxItems": 43200,
            "type": "array",
            "items": {
              "type": "integer",
              "minimum": 20,
              "maximum": 250
            }
          }
        },
        "required": [
          "t",
          "bpm"
        ],
        "additionalProperties": false,
        "description": "Heart rate in beats a minute, one per time."
      },
      "PitchOutline": {
        "$id": "#/components/schemas/PitchOutline",
        "type": "object",
        "properties": {
          "corners": {
            "type": "array",
            "prefixItems": [
              {
                "$ref": "#/components/schemas/Position"
              },
              {
                "$ref": "#/components/schemas/Position"
              }
            ],
            "items": false,
            "minItems": 2,
            "maxItems": 2,
            "description": "The corners at the two ends of one touchline."
          },
          "centre": {
            "description": "The centre spot.",
            "$ref": "#/components/schemas/Position"
          },
          "by": {
            "type": "string",
            "enum": [
              "referee",
              "corners",
              "auto"
            ],
            "description": "Marked by the referee on the map, from corners walked on the watch (TRK-13), or found automatically."
          }
        },
        "required": [
          "corners",
          "centre",
          "by"
        ],
        "additionalProperties": false,
        "description": "Where the pitch is and which way it lies, for heatmaps (TRK-9)."
      },
      "MatchTrack": {
        "$id": "#/components/schemas/MatchTrack",
        "type": "object",
        "properties": {
          "source": {
            "$ref": "#/components/schemas/TrackSource"
          },
          "start": {
            "description": "The time the series' times count from.",
            "$ref": "#/components/schemas/Timestamp"
          },
          "gps": {
            "$ref": "#/components/schemas/GpsSeries"
          },
          "heartRate": {
            "$ref": "#/components/schemas/HeartRateSeries"
          },
          "pitch": {
            "$ref": "#/components/schemas/PitchOutline"
          }
        },
        "required": [
          "source",
          "start"
        ],
        "additionalProperties": false,
        "description": "GPS and heart rate for one match report, each on its own times, kept as recorded (up to about one sample a second)."
      },
      "SpeedBand": {
        "$id": "#/components/schemas/SpeedBand",
        "type": "string",
        "enum": [
          "walking",
          "jogging",
          "running",
          "highSpeed",
          "sprinting"
        ],
        "description": "With the default sprint speed: walking under 7 km/h; jogging 7–13; running 13–18; highSpeed 18–21; sprinting 21 and over. A user's own sprint speed scales them all (TRK-11)."
      },
      "TrackSummary": {
        "$id": "#/components/schemas/TrackSummary",
        "type": "object",
        "properties": {
          "covers": {
            "type": "string",
            "enum": [
              "periods",
              "track"
            ],
            "description": "periods: only the periods played, from the match report's period start times; track: the whole track (the report has no start times)."
          },
          "total": {
            "type": "object",
            "properties": {
              "distanceM": {
                "description": "The GPS figures (distance, top speed, sprints, speed bands) are left out where the track has no GPS, for example before the watch had a fix.",
                "type": "number",
                "minimum": 0
              },
              "topSpeedMps": {
                "description": "The watch's own speed when the track has it, otherwise from positions averaged over 3 seconds.",
                "type": "number",
                "minimum": 0
              },
              "sprints": {
                "description": "At sprinting speed (21 km/h or more by default, TRK-11) for 2 to 6 seconds.",
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "speedBands": {
                "description": "Time and distance in each band, slowest first.",
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "band": {
                      "$ref": "#/components/schemas/SpeedBand"
                    },
                    "sec": {
                      "type": "number",
                      "minimum": 0
                    },
                    "distanceM": {
                      "type": "number",
                      "minimum": 0
                    }
                  },
                  "required": [
                    "band",
                    "sec",
                    "distanceM"
                  ],
                  "additionalProperties": false
                }
              },
              "avgBpm": {
                "type": "integer",
                "minimum": -9007199254740991,
                "maximum": 9007199254740991
              },
              "maxBpm": {
                "type": "integer",
                "minimum": -9007199254740991,
                "maximum": 9007199254740991
              },
              "zones": {
                "description": "Time in each heart-rate zone; only with a maximum heart rate in the user's settings.",
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "zone": {
                      "type": "integer",
                      "minimum": 1,
                      "maximum": 5
                    },
                    "fromBpm": {
                      "type": "integer",
                      "minimum": -9007199254740991,
                      "maximum": 9007199254740991
                    },
                    "sec": {
                      "type": "number",
                      "minimum": 0
                    }
                  },
                  "required": [
                    "zone",
                    "fromBpm",
                    "sec"
                  ],
                  "additionalProperties": false
                }
              }
            },
            "additionalProperties": false
          },
          "periods": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "distanceM": {
                  "description": "The GPS figures (distance, top speed, sprints, speed bands) are left out where the track has no GPS, for example before the watch had a fix.",
                  "type": "number",
                  "minimum": 0
                },
                "topSpeedMps": {
                  "description": "The watch's own speed when the track has it, otherwise from positions averaged over 3 seconds.",
                  "type": "number",
                  "minimum": 0
                },
                "sprints": {
                  "description": "At sprinting speed (21 km/h or more by default, TRK-11) for 2 to 6 seconds.",
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 9007199254740991
                },
                "speedBands": {
                  "description": "Time and distance in each band, slowest first.",
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "band": {
                        "$ref": "#/components/schemas/SpeedBand"
                      },
                      "sec": {
                        "type": "number",
                        "minimum": 0
                      },
                      "distanceM": {
                        "type": "number",
                        "minimum": 0
                      }
                    },
                    "required": [
                      "band",
                      "sec",
                      "distanceM"
                    ],
                    "additionalProperties": false
                  }
                },
                "avgBpm": {
                  "type": "integer",
                  "minimum": -9007199254740991,
                  "maximum": 9007199254740991
                },
                "maxBpm": {
                  "type": "integer",
                  "minimum": -9007199254740991,
                  "maximum": 9007199254740991
                },
                "zones": {
                  "description": "Time in each heart-rate zone; only with a maximum heart rate in the user's settings.",
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "zone": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 5
                      },
                      "fromBpm": {
                        "type": "integer",
                        "minimum": -9007199254740991,
                        "maximum": 9007199254740991
                      },
                      "sec": {
                        "type": "number",
                        "minimum": 0
                      }
                    },
                    "required": [
                      "zone",
                      "fromBpm",
                      "sec"
                    ],
                    "additionalProperties": false
                  }
                },
                "period": {
                  "type": "integer",
                  "minimum": 1,
                  "maximum": 9007199254740991
                }
              },
              "required": [
                "period"
              ],
              "additionalProperties": false
            },
            "description": "Each period with a start time; empty when covers is track."
          },
          "maxHeartRate": {
            "description": "The maximum heart rate the zones are worked out from (Settings).",
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "sprintKph": {
            "type": "number",
            "description": "The sprint speed the speed bands and sprints are worked out from (Settings, TRK-11; 21 by default)."
          }
        },
        "required": [
          "covers",
          "total",
          "periods",
          "sprintKph"
        ],
        "additionalProperties": false,
        "description": "What the server works out from a track (TRK-3): GPS figures when it has GPS, heart-rate figures when it has heart rate."
      },
      "Stats": {
        "$id": "#/components/schemas/Stats",
        "type": "object",
        "properties": {
          "options": {
            "type": "object",
            "properties": {
              "years": {
                "type": "array",
                "items": {
                  "type": "integer",
                  "minimum": -9007199254740991,
                  "maximum": 9007199254740991
                },
                "description": "Years with games, newest first, for the When filter."
              },
              "genders": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Gender"
                },
                "description": "Genders your games have."
              },
              "competitions": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "$ref": "#/components/schemas/Id"
                    },
                    "name": {
                      "type": "string"
                    },
                    "season": {
                      "type": "string"
                    },
                    "games": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 9007199254740991
                    }
                  },
                  "required": [
                    "id",
                    "name",
                    "games"
                  ],
                  "additionalProperties": false
                },
                "description": "Competitions with games, most games first."
              },
              "tags": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Tags on your games, alphabetically."
              },
              "venues": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "$ref": "#/components/schemas/Id"
                    },
                    "name": {
                      "type": "string"
                    },
                    "games": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 9007199254740991
                    }
                  },
                  "required": [
                    "id",
                    "name",
                    "games"
                  ],
                  "additionalProperties": false
                },
                "description": "Venues with games, most games first."
              }
            },
            "required": [
              "years",
              "genders",
              "competitions",
              "tags",
              "venues"
            ],
            "additionalProperties": false,
            "description": "What the filters can choose from: from all your games, whatever is filtered."
          },
          "games": {
            "type": "object",
            "properties": {
              "total": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "abandoned": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "first": {
                "$ref": "#/components/schemas/LocalDate"
              },
              "last": {
                "$ref": "#/components/schemas/LocalDate"
              }
            },
            "required": [
              "total",
              "abandoned"
            ],
            "additionalProperties": false
          },
          "roles": {
            "type": "object",
            "properties": {
              "referee": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "ar1": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "ar2": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "fourth": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "notSet": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              }
            },
            "required": [
              "referee",
              "ar1",
              "ar2",
              "fourth",
              "notSet"
            ],
            "additionalProperties": false
          },
          "distance": {
            "type": "object",
            "properties": {
              "totalM": {
                "type": "number",
                "minimum": 0
              },
              "games": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991,
                "description": "Games with a distance."
              },
              "tracked": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991,
                "description": "Of those, games whose distance comes from a track."
              },
              "byRole": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "role": {
                      "type": "string",
                      "enum": [
                        "referee",
                        "assistant",
                        "fourth"
                      ]
                    },
                    "games": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 9007199254740991
                    },
                    "averageM": {
                      "type": "number",
                      "minimum": 0
                    }
                  },
                  "required": [
                    "role",
                    "games",
                    "averageM"
                  ],
                  "additionalProperties": false
                }
              },
              "longest": {
                "type": "object",
                "properties": {
                  "matchId": {
                    "$ref": "#/components/schemas/Id"
                  },
                  "kickoff": {
                    "$ref": "#/components/schemas/Timestamp"
                  },
                  "title": {
                    "type": "string",
                    "description": "Home v Away, or the match's name."
                  },
                  "role": {
                    "$ref": "#/components/schemas/Role"
                  },
                  "distanceM": {
                    "type": "number"
                  }
                },
                "required": [
                  "matchId",
                  "kickoff",
                  "title",
                  "distanceM"
                ],
                "additionalProperties": false
              },
              "shortest": {
                "type": "object",
                "properties": {
                  "matchId": {
                    "$ref": "#/components/schemas/Id"
                  },
                  "kickoff": {
                    "$ref": "#/components/schemas/Timestamp"
                  },
                  "title": {
                    "type": "string",
                    "description": "Home v Away, or the match's name."
                  },
                  "role": {
                    "$ref": "#/components/schemas/Role"
                  },
                  "distanceM": {
                    "type": "number"
                  }
                },
                "required": [
                  "matchId",
                  "kickoff",
                  "title",
                  "distanceM"
                ],
                "additionalProperties": false
              },
              "overTime": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "matchId": {
                      "$ref": "#/components/schemas/Id"
                    },
                    "kickoff": {
                      "$ref": "#/components/schemas/Timestamp"
                    },
                    "distanceM": {
                      "type": "number"
                    },
                    "totalM": {
                      "type": "number"
                    }
                  },
                  "required": [
                    "matchId",
                    "kickoff",
                    "distanceM",
                    "totalM"
                  ],
                  "additionalProperties": false
                },
                "description": "Each game with a distance, oldest first, with the running total."
              }
            },
            "required": [
              "totalM",
              "games",
              "tracked",
              "byRole",
              "overTime"
            ],
            "additionalProperties": false
          },
          "months": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "month": {
                  "type": "string",
                  "description": "2026-04"
                },
                "referee": {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 9007199254740991
                },
                "assistant": {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 9007199254740991
                },
                "fourth": {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 9007199254740991
                },
                "notSet": {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 9007199254740991
                }
              },
              "required": [
                "month",
                "referee",
                "assistant",
                "fourth",
                "notSet"
              ],
              "additionalProperties": false
            },
            "description": "Every month from the first game to the last, by role."
          },
          "busiest": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "day": {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 6,
                  "description": "0 is Monday."
                },
                "hour": {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 23,
                  "description": "Kickoff hour, local time."
                },
                "games": {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 9007199254740991
                }
              },
              "required": [
                "day",
                "hour",
                "games"
              ],
              "additionalProperties": false
            },
            "description": "Only the day and hour pairs with games."
          },
          "ageGroups": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "ageGroup": {
                  "type": "string"
                },
                "male": {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 9007199254740991
                },
                "female": {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 9007199254740991
                },
                "coed": {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 9007199254740991
                },
                "notSet": {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 9007199254740991
                }
              },
              "required": [
                "ageGroup",
                "male",
                "female",
                "coed",
                "notSet"
              ],
              "additionalProperties": false
            },
            "description": "Youngest first; games without an age group are left out."
          },
          "places": {
            "type": "object",
            "properties": {
              "venues": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "pitches": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991,
                "description": "Distinct pitches, counting a venue as one when a match has no pitch."
              },
              "top": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "venueId": {
                      "$ref": "#/components/schemas/Id"
                    },
                    "pitchId": {
                      "$ref": "#/components/schemas/Id"
                    },
                    "venue": {
                      "type": "string"
                    },
                    "pitch": {
                      "type": "string"
                    },
                    "games": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 9007199254740991
                    }
                  },
                  "required": [
                    "venueId",
                    "venue",
                    "games"
                  ],
                  "additionalProperties": false
                },
                "description": "Up to ten, most games first."
              }
            },
            "required": [
              "venues",
              "pitches",
              "top"
            ],
            "additionalProperties": false
          },
          "referees": {
            "type": "object",
            "properties": {
              "count": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991,
                "description": "Distinct people on your crews: saved referees, and names typed in."
              },
              "top": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "refereeId": {
                      "$ref": "#/components/schemas/Id"
                    },
                    "name": {
                      "type": "string"
                    },
                    "games": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 9007199254740991
                    }
                  },
                  "required": [
                    "name",
                    "games"
                  ],
                  "additionalProperties": false
                },
                "description": "Up to ten, most games first."
              }
            },
            "required": [
              "count",
              "top"
            ],
            "additionalProperties": false
          },
          "weather": {
            "type": "object",
            "properties": {
              "games": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991,
                "description": "Games with weather."
              },
              "rainy": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991,
                "description": "1 mm of rain or more from an hour before kickoff to the end."
              },
              "averageTemperatureC": {
                "description": "At kickoff.",
                "type": "number"
              },
              "hottest": {
                "type": "object",
                "properties": {
                  "matchId": {
                    "$ref": "#/components/schemas/Id"
                  },
                  "kickoff": {
                    "$ref": "#/components/schemas/Timestamp"
                  },
                  "title": {
                    "type": "string",
                    "description": "Home v Away, or the match's name."
                  },
                  "role": {
                    "$ref": "#/components/schemas/Role"
                  },
                  "temperatureC": {
                    "type": "number"
                  }
                },
                "required": [
                  "matchId",
                  "kickoff",
                  "title",
                  "temperatureC"
                ],
                "additionalProperties": false
              },
              "coldest": {
                "type": "object",
                "properties": {
                  "matchId": {
                    "$ref": "#/components/schemas/Id"
                  },
                  "kickoff": {
                    "$ref": "#/components/schemas/Timestamp"
                  },
                  "title": {
                    "type": "string",
                    "description": "Home v Away, or the match's name."
                  },
                  "role": {
                    "$ref": "#/components/schemas/Role"
                  },
                  "temperatureC": {
                    "type": "number"
                  }
                },
                "required": [
                  "matchId",
                  "kickoff",
                  "title",
                  "temperatureC"
                ],
                "additionalProperties": false
              },
              "windiest": {
                "type": "object",
                "properties": {
                  "matchId": {
                    "$ref": "#/components/schemas/Id"
                  },
                  "kickoff": {
                    "$ref": "#/components/schemas/Timestamp"
                  },
                  "title": {
                    "type": "string",
                    "description": "Home v Away, or the match's name."
                  },
                  "role": {
                    "$ref": "#/components/schemas/Role"
                  },
                  "windGustKph": {
                    "type": "number"
                  }
                },
                "required": [
                  "matchId",
                  "kickoff",
                  "title",
                  "windGustKph"
                ],
                "additionalProperties": false
              },
              "temperaturesC": {
                "type": "array",
                "items": {
                  "type": "number"
                },
                "description": "Each game's kickoff temperature, for apps to put into bands in their unit."
              }
            },
            "required": [
              "games",
              "rainy",
              "temperaturesC"
            ],
            "additionalProperties": false
          },
          "fees": {
            "type": "object",
            "properties": {
              "currency": {
                "description": "The currency the amounts are in: the Settings one.",
                "type": "string"
              },
              "games": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991,
                "description": "Games with a fee in that currency."
              },
              "earnedAmount": {
                "type": "number",
                "minimum": 0
              },
              "paidAmount": {
                "type": "number",
                "minimum": 0
              },
              "owedAmount": {
                "type": "number",
                "minimum": 0
              },
              "otherCurrencies": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "currency": {
                      "type": "string"
                    },
                    "games": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 9007199254740991
                    },
                    "amount": {
                      "type": "number"
                    }
                  },
                  "required": [
                    "currency",
                    "games",
                    "amount"
                  ],
                  "additionalProperties": false
                },
                "description": "Fees in other currencies, not in the figures above."
              },
              "months": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "month": {
                      "type": "string"
                    },
                    "paidAmount": {
                      "type": "number"
                    },
                    "owedAmount": {
                      "type": "number"
                    }
                  },
                  "required": [
                    "month",
                    "paidAmount",
                    "owedAmount"
                  ],
                  "additionalProperties": false
                },
                "description": "Every month from the first game to the last."
              },
              "competitions": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "competitionId": {
                      "$ref": "#/components/schemas/Id"
                    },
                    "name": {
                      "type": "string"
                    },
                    "games": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 9007199254740991
                    },
                    "amount": {
                      "type": "number"
                    }
                  },
                  "required": [
                    "name",
                    "games",
                    "amount"
                  ],
                  "additionalProperties": false
                },
                "description": "Up to ten, most earned first; games without a competition as one."
              }
            },
            "required": [
              "games",
              "earnedAmount",
              "paidAmount",
              "owedAmount",
              "otherCurrencies",
              "months",
              "competitions"
            ],
            "additionalProperties": false
          },
          "discipline": {
            "type": "object",
            "properties": {
              "games": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991,
                "description": "Games with a match report, which the figures come from."
              },
              "cautions": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991,
                "description": "Yellow cards: cautions and second cautions."
              },
              "sendOffs": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991,
                "description": "Red cards: send-offs and second cautions."
              },
              "secondCautions": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "goals": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "ageGroups": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "ageGroup": {
                      "type": "string"
                    },
                    "games": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 9007199254740991
                    },
                    "cautions": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 9007199254740991
                    },
                    "sendOffs": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 9007199254740991
                    }
                  },
                  "required": [
                    "ageGroup",
                    "games",
                    "cautions",
                    "sendOffs"
                  ],
                  "additionalProperties": false
                },
                "description": "Youngest first."
              },
              "competitions": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "competitionId": {
                      "$ref": "#/components/schemas/Id"
                    },
                    "name": {
                      "type": "string"
                    },
                    "games": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 9007199254740991
                    },
                    "cautions": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 9007199254740991
                    },
                    "sendOffs": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 9007199254740991
                    }
                  },
                  "required": [
                    "name",
                    "games",
                    "cautions",
                    "sendOffs"
                  ],
                  "additionalProperties": false
                },
                "description": "Up to ten, most games first."
              },
              "codes": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "misconduct": {
                      "$ref": "#/components/schemas/MisconductCode"
                    },
                    "cautions": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 9007199254740991
                    },
                    "sendOffs": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 9007199254740991
                    }
                  },
                  "required": [
                    "misconduct",
                    "cautions",
                    "sendOffs"
                  ],
                  "additionalProperties": false
                },
                "description": "Up to ten, most used first."
              }
            },
            "required": [
              "games",
              "cautions",
              "sendOffs",
              "secondCautions",
              "goals",
              "ageGroups",
              "competitions",
              "codes"
            ],
            "additionalProperties": false
          },
          "fitness": {
            "type": "object",
            "properties": {
              "games": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "matchId": {
                      "$ref": "#/components/schemas/Id"
                    },
                    "kickoff": {
                      "$ref": "#/components/schemas/Timestamp"
                    },
                    "distanceM": {
                      "type": "number"
                    },
                    "avgBpm": {
                      "type": "integer",
                      "minimum": -9007199254740991,
                      "maximum": 9007199254740991
                    },
                    "maxBpm": {
                      "type": "integer",
                      "minimum": -9007199254740991,
                      "maximum": 9007199254740991
                    },
                    "sprints": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 9007199254740991
                    },
                    "topSpeedMps": {
                      "type": "number"
                    }
                  },
                  "required": [
                    "matchId",
                    "kickoff"
                  ],
                  "additionalProperties": false
                },
                "description": "Games with a track, oldest first, with the track's figures (TRK-3)."
              },
              "maxHeartRate": {
                "description": "From Settings; zones only with it.",
                "type": "integer",
                "minimum": -9007199254740991,
                "maximum": 9007199254740991
              },
              "zones": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "zone": {
                      "type": "integer",
                      "minimum": 1,
                      "maximum": 5
                    },
                    "fromBpm": {
                      "type": "integer",
                      "minimum": -9007199254740991,
                      "maximum": 9007199254740991
                    },
                    "sec": {
                      "type": "number",
                      "minimum": 0
                    }
                  },
                  "required": [
                    "zone",
                    "fromBpm",
                    "sec"
                  ],
                  "additionalProperties": false
                }
              },
              "speedBands": {
                "description": "Time and distance in each speed band across the games with GPS, slowest first (TRK-9).",
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "band": {
                      "$ref": "#/components/schemas/SpeedBand"
                    },
                    "sec": {
                      "type": "number",
                      "minimum": 0
                    },
                    "distanceM": {
                      "type": "number",
                      "minimum": 0
                    }
                  },
                  "required": [
                    "band",
                    "sec",
                    "distanceM"
                  ],
                  "additionalProperties": false
                }
              },
              "sprintKph": {
                "description": "The sprint speed the bands start from (Settings, TRK-11).",
                "type": "number"
              },
              "heatmaps": {
                "description": "Where you spent the periods across the games, one heatmap a role, on a grid over a pitch drawing (its margin included). A referee's games are turned so the first period's kickoff team attacks left to right when the kickoff direction is known; an assistant's so their touchline is at the bottom (TRK-9)",
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "role": {
                      "type": "string",
                      "enum": [
                        "referee",
                        "assistant"
                      ],
                      "description": "Games as referee (and games with no role), or as an assistant; a fourth official's have none."
                    },
                    "cols": {
                      "type": "integer",
                      "exclusiveMinimum": 0,
                      "maximum": 9007199254740991
                    },
                    "rows": {
                      "type": "integer",
                      "exclusiveMinimum": 0,
                      "maximum": 9007199254740991
                    },
                    "cells": {
                      "type": "array",
                      "items": {
                        "type": "number",
                        "minimum": 0
                      },
                      "description": "Seconds in each cell, row by row from the top of the drawing (the far touchline), left to right."
                    },
                    "games": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 9007199254740991,
                      "description": "Games in it: those with GPS whose pitch is known or could be found."
                    }
                  },
                  "required": [
                    "role",
                    "cols",
                    "rows",
                    "cells",
                    "games"
                  ],
                  "additionalProperties": false
                }
              }
            },
            "required": [
              "games"
            ],
            "additionalProperties": false
          },
          "addedTime": {
            "type": "object",
            "properties": {
              "periods": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991,
                "description": "Periods counted: of completed games, at most 15 minutes over."
              },
              "averageSec": {
                "type": "number",
                "minimum": 0
              },
              "byPeriod": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "period": {
                      "type": "integer",
                      "minimum": 1,
                      "maximum": 9007199254740991
                    },
                    "periods": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 9007199254740991
                    },
                    "averageSec": {
                      "type": "number",
                      "minimum": 0
                    }
                  },
                  "required": [
                    "period",
                    "periods",
                    "averageSec"
                  ],
                  "additionalProperties": false
                },
                "description": "Regular periods: first half, second half, or each quarter."
              },
              "ageGroups": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "ageGroup": {
                      "type": "string"
                    },
                    "periods": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 9007199254740991
                    },
                    "averageSec": {
                      "type": "number",
                      "minimum": 0
                    }
                  },
                  "required": [
                    "ageGroup",
                    "periods",
                    "averageSec"
                  ],
                  "additionalProperties": false
                }
              },
              "competitions": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "competitionId": {
                      "$ref": "#/components/schemas/Id"
                    },
                    "name": {
                      "type": "string"
                    },
                    "periods": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 9007199254740991
                    },
                    "averageSec": {
                      "type": "number",
                      "minimum": 0
                    }
                  },
                  "required": [
                    "name",
                    "periods",
                    "averageSec"
                  ],
                  "additionalProperties": false
                },
                "description": "Up to ten, most periods first."
              }
            },
            "required": [
              "periods",
              "byPeriod",
              "ageGroups",
              "competitions"
            ],
            "additionalProperties": false
          },
          "years": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "year": {
                  "type": "integer",
                  "minimum": -9007199254740991,
                  "maximum": 9007199254740991
                },
                "games": {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 9007199254740991
                },
                "asCentre": {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 9007199254740991
                },
                "distanceM": {
                  "type": "number",
                  "minimum": 0
                },
                "distanceGames": {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 9007199254740991
                },
                "referees": {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 9007199254740991
                },
                "venues": {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 9007199254740991
                },
                "feesAmount": {
                  "type": "number",
                  "minimum": 0
                }
              },
              "required": [
                "year",
                "games",
                "asCentre",
                "distanceM",
                "distanceGames",
                "referees",
                "venues",
                "feesAmount"
              ],
              "additionalProperties": false
            },
            "description": "Each year with games, newest first."
          }
        },
        "required": [
          "options",
          "games",
          "roles",
          "distance",
          "months",
          "busiest",
          "ageGroups",
          "places",
          "referees",
          "weather",
          "fees",
          "discipline",
          "fitness",
          "addedTime",
          "years"
        ],
        "additionalProperties": false,
        "description": "Stats on the user's games (STAT-1 to STAT-3), for the filters in the query."
      },
      "MatchInput": {
        "$id": "#/components/schemas/MatchInput",
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "status": {
            "$ref": "#/components/schemas/MatchStatus"
          },
          "kickoff": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "timeZone": {
            "description": "The venue's time zone, for showing the kickoff in local time.",
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          },
          "format": {
            "description": "A copy of the format, so editing the template later doesn't change this match.",
            "$ref": "#/components/schemas/MatchFormat"
          },
          "templateId": {
            "description": "The template the format was copied from.",
            "$ref": "#/components/schemas/Id"
          },
          "home": {
            "$ref": "#/components/schemas/Team"
          },
          "away": {
            "$ref": "#/components/schemas/Team"
          },
          "gender": {
            "$ref": "#/components/schemas/Gender"
          },
          "ageGroup": {
            "description": "Free text, as leagues write it: U12, U19, Adult, O35.",
            "type": "string",
            "minLength": 1,
            "maxLength": 30
          },
          "competitionId": {
            "$ref": "#/components/schemas/Id"
          },
          "venueId": {
            "$ref": "#/components/schemas/Id"
          },
          "pitchId": {
            "description": "One of the venue's pitches.",
            "$ref": "#/components/schemas/Id"
          },
          "role": {
            "description": "The user's role in this match.",
            "$ref": "#/components/schemas/Role"
          },
          "crew": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CrewMember"
            },
            "description": "The other officials."
          },
          "notes": {
            "type": "string",
            "maxLength": 2000
          },
          "outlook": {
            "description": "What to expect: competitive, one-sided, team strengths and so on.",
            "type": "string",
            "maxLength": 2000
          },
          "fee": {
            "$ref": "#/components/schemas/Fee"
          },
          "external": {
            "$ref": "#/components/schemas/ExternalRef"
          },
          "tags": {
            "$ref": "#/components/schemas/Tags"
          },
          "pitchMarking": {
            "description": "The pitch's corners as marked on the watch for this match (TRK-13).",
            "$ref": "#/components/schemas/PitchMarking"
          },
          "id": {
            "description": "The id from the URL; can be left out.",
            "$ref": "#/components/schemas/Id"
          },
          "version": {
            "description": "The version last seen, when updating. Leave it out to create the record.",
            "type": "integer",
            "minimum": 1,
            "maximum": 9007199254740991
          }
        },
        "required": [
          "status",
          "format",
          "home",
          "away",
          "crew"
        ],
        "additionalProperties": false,
        "description": "A Match as a client sends it to save it: the server sets updatedAt and deletedAt, and version only goes with an update."
      },
      "MatchTemplateInput": {
        "$id": "#/components/schemas/MatchTemplateInput",
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 60
          },
          "format": {
            "$ref": "#/components/schemas/MatchFormat"
          },
          "defaults": {
            "type": "object",
            "properties": {
              "fee": {
                "$ref": "#/components/schemas/Fee"
              }
            },
            "additionalProperties": false,
            "description": "Filled into a match made from this template, or from a competition that uses it, when the match has none. Not the user's role: that depends on the match."
          },
          "id": {
            "description": "The id from the URL; can be left out.",
            "$ref": "#/components/schemas/Id"
          },
          "version": {
            "description": "The version last seen, when updating. Leave it out to create the record.",
            "type": "integer",
            "minimum": 1,
            "maximum": 9007199254740991
          }
        },
        "required": [
          "name",
          "format",
          "defaults"
        ],
        "additionalProperties": false,
        "description": "A MatchTemplate as a client sends it to save it: the server sets updatedAt and deletedAt, and version only goes with an update."
      },
      "MatchRecordInput": {
        "$id": "#/components/schemas/MatchRecordInput",
        "type": "object",
        "properties": {
          "matchId": {
            "description": "The planned match; absent for a quick match started on the watch, for which the server makes a match.",
            "$ref": "#/components/schemas/Id"
          },
          "device": {
            "description": "What recorded it, for example garmin:venu2.",
            "type": "string",
            "maxLength": 60
          },
          "outcome": {
            "type": "string",
            "enum": [
              "completed",
              "abandoned"
            ]
          },
          "periods": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PeriodPlayed"
            }
          },
          "score": {
            "type": "object",
            "properties": {
              "home": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "away": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              }
            },
            "required": [
              "home",
              "away"
            ],
            "additionalProperties": false
          },
          "penalties": {
            "description": "Shoot-out score, when there was one.",
            "type": "object",
            "properties": {
              "home": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "away": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              }
            },
            "required": [
              "home",
              "away"
            ],
            "additionalProperties": false
          },
          "distanceM": {
            "description": "Distance the referee covered during the match, in metres, from the watch's GPS.",
            "type": "number",
            "minimum": 0
          },
          "kickoffDirection": {
            "description": "Which way the first period's kickoff team attacked, when the referee recorded it on the watch (PRE-8); heatmaps use it (TRK-9).",
            "type": "object",
            "properties": {
              "team": {
                "type": "string",
                "enum": [
                  "home",
                  "away"
                ],
                "description": "The team that kicked off the first period."
              },
              "bearingDeg": {
                "type": "number",
                "minimum": 0,
                "exclusiveMaximum": 360,
                "description": "The compass bearing of the goal it attacked, in degrees from true north."
              }
            },
            "required": [
              "team",
              "bearingDeg"
            ],
            "additionalProperties": false
          },
          "events": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Event"
            }
          },
          "diagnostics": {
            "description": "How the recording went (DIAG-1).",
            "$ref": "#/components/schemas/MatchDiagnostics"
          },
          "id": {
            "description": "The id from the URL; can be left out.",
            "$ref": "#/components/schemas/Id"
          },
          "version": {
            "description": "The version last seen, when updating. Leave it out to create the record.",
            "type": "integer",
            "minimum": 1,
            "maximum": 9007199254740991
          }
        },
        "required": [
          "outcome",
          "periods",
          "score",
          "events"
        ],
        "additionalProperties": false,
        "description": "A MatchRecord as a client sends it to save it: the server sets updatedAt and deletedAt, and version only goes with an update."
      },
      "VenueInput": {
        "$id": "#/components/schemas/VenueInput",
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "address": {
            "description": "Street address; often where you park.",
            "type": "string",
            "maxLength": 300
          },
          "position": {
            "description": "The address's position (parking), for directions.",
            "$ref": "#/components/schemas/Position"
          },
          "pitches": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Pitch"
            },
            "description": "Named pitches (\"Field 3\"), each with its own position."
          },
          "notes": {
            "type": "string",
            "maxLength": 2000
          },
          "tags": {
            "$ref": "#/components/schemas/Tags"
          },
          "id": {
            "description": "The id from the URL; can be left out.",
            "$ref": "#/components/schemas/Id"
          },
          "version": {
            "description": "The version last seen, when updating. Leave it out to create the record.",
            "type": "integer",
            "minimum": 1,
            "maximum": 9007199254740991
          }
        },
        "required": [
          "name",
          "pitches"
        ],
        "additionalProperties": false,
        "description": "A Venue as a client sends it to save it: the server sets updatedAt and deletedAt, and version only goes with an update."
      },
      "CompetitionInput": {
        "$id": "#/components/schemas/CompetitionInput",
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "organiser": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "type": {
            "$ref": "#/components/schemas/CompetitionType"
          },
          "season": {
            "description": "Season label, for example 2026-27.",
            "type": "string",
            "minLength": 1,
            "maxLength": 30
          },
          "startDate": {
            "$ref": "#/components/schemas/LocalDate"
          },
          "endDate": {
            "$ref": "#/components/schemas/LocalDate"
          },
          "ageGroup": {
            "type": "string",
            "minLength": 1,
            "maxLength": 30
          },
          "level": {
            "type": "string",
            "minLength": 1,
            "maxLength": 60
          },
          "templateId": {
            "description": "The match template for this competition's matches: picking the competition fills the format (and fee).",
            "$ref": "#/components/schemas/Id"
          },
          "misconductScheme": {
            "description": "Overrides the user's default scheme for this competition's matches.",
            "$ref": "#/components/schemas/SchemeId"
          },
          "notes": {
            "type": "string",
            "maxLength": 2000
          },
          "id": {
            "description": "The id from the URL; can be left out.",
            "$ref": "#/components/schemas/Id"
          },
          "version": {
            "description": "The version last seen, when updating. Leave it out to create the record.",
            "type": "integer",
            "minimum": 1,
            "maximum": 9007199254740991
          }
        },
        "required": [
          "name",
          "type"
        ],
        "additionalProperties": false,
        "description": "A Competition as a client sends it to save it: the server sets updatedAt and deletedAt, and version only goes with an update."
      },
      "RefereeInput": {
        "$id": "#/components/schemas/RefereeInput",
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "aliases": {
            "description": "Other names they go by, such as the ones merged into this referee (DUP-3); imports and searches find the referee by them.",
            "maxItems": 50,
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 100
            }
          },
          "email": {
            "type": "string",
            "format": "email",
            "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
          },
          "phone": {
            "type": "string",
            "minLength": 1,
            "maxLength": 30
          },
          "notes": {
            "type": "string",
            "maxLength": 2000
          },
          "tags": {
            "$ref": "#/components/schemas/Tags"
          },
          "distinctFrom": {
            "description": "Referees the user said aren't the same person, so they aren't suggested as duplicates again (DUP-5).",
            "maxItems": 500,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Id"
            }
          },
          "id": {
            "description": "The id from the URL; can be left out.",
            "$ref": "#/components/schemas/Id"
          },
          "version": {
            "description": "The version last seen, when updating. Leave it out to create the record.",
            "type": "integer",
            "minimum": 1,
            "maximum": 9007199254740991
          }
        },
        "required": [
          "name"
        ],
        "additionalProperties": false,
        "description": "A Referee as a client sends it to save it: the server sets updatedAt and deletedAt, and version only goes with an update."
      },
      "UserSettingsInput": {
        "$id": "#/components/schemas/UserSettingsInput",
        "type": "object",
        "properties": {
          "misconductScheme": {
            "description": "The default scheme for cards; a competition can override it.",
            "$ref": "#/components/schemas/SchemeId"
          },
          "currency": {
            "type": "string",
            "pattern": "^[A-Z]{3}$",
            "description": "Default currency for fees."
          },
          "distanceUnit": {
            "$ref": "#/components/schemas/DistanceUnit"
          },
          "temperatureUnit": {
            "$ref": "#/components/schemas/TemperatureUnit"
          },
          "maxHeartRate": {
            "description": "Beats a minute, for heart-rate zones (TRK-4).",
            "type": "integer",
            "minimum": 100,
            "maximum": 240
          },
          "sprintKph": {
            "description": "Where sprinting starts, in km/h, for the speed bands and sprints (TRK-11); the other bands scale with it. 21 when not set.",
            "type": "number",
            "minimum": 15,
            "maximum": 35
          },
          "calendarArriveMin": {
            "description": "Minutes before kickoff the calendar feed's events start, for travel and warming up (CAL-3); 0 starts them at kickoff. 30 when not set.",
            "type": "integer",
            "minimum": 0,
            "maximum": 180
          },
          "calendarReminderMin": {
            "description": "Before several reminders (CAL-3): minutes before kickoff for the calendar feed's one reminder; 0 for none. Used only when none of calendarReminders, calendarDayBefore and calendarDayOf is set; 60 when nothing is.",
            "type": "integer",
            "minimum": 0,
            "maximum": 2880
          },
          "calendarReminders": {
            "description": "The calendar feed's reminders, in minutes before kickoff (CAL-3): 15-minute steps up to 3 hours.",
            "maxItems": 12,
            "type": "array",
            "items": {
              "type": "integer",
              "minimum": 15,
              "maximum": 180
            }
          },
          "calendarDayBefore": {
            "description": "A reminder the day before a match at this time (HH:MM, in the match's time zone); none when not set.",
            "type": "string",
            "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$"
          },
          "watchScreen": {
            "description": "Missing until it's first chosen: watches then keep Default, or their own choice.",
            "$ref": "#/components/schemas/WatchScreen"
          },
          "calendarDayOf": {
            "description": "A reminder on the day of a match at this time (HH:MM, in the match's time zone), when that's before the event starts; none when not set.",
            "type": "string",
            "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$"
          },
          "id": {
            "description": "The id from the URL; can be left out.",
            "$ref": "#/components/schemas/Id"
          },
          "version": {
            "description": "The version last seen, when updating. Leave it out to create the record.",
            "type": "integer",
            "minimum": 1,
            "maximum": 9007199254740991
          }
        },
        "required": [
          "misconductScheme",
          "currency",
          "distanceUnit",
          "temperatureUnit"
        ],
        "additionalProperties": false,
        "description": "A UserSettings as a client sends it to save it: the server sets updatedAt and deletedAt, and version only goes with an update."
      },
      "ApiKeyScope": {
        "$id": "#/components/schemas/ApiKeyScope",
        "type": "string",
        "enum": [
          "matches:read",
          "matches:write",
          "match-templates:read",
          "match-templates:write",
          "match-records:read",
          "match-records:write",
          "venues:read",
          "venues:write",
          "competitions:read",
          "competitions:write",
          "referees:read",
          "referees:write",
          "settings:read",
          "settings:write"
        ],
        "description": "<collection>:read, or <collection>:write (which includes reading it)."
      },
      "ApiKey": {
        "$id": "#/components/schemas/ApiKey",
        "type": "object",
        "properties": {
          "id": {
            "$ref": "#/components/schemas/Id"
          },
          "name": {
            "type": "string",
            "description": "What it's for, \"RefSix import\"."
          },
          "prefix": {
            "type": "string",
            "description": "The start of the key, to recognise it: lf_ and 8 characters."
          },
          "scopes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ApiKeyScope"
            }
          },
          "createdAt": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "lastUsedAt": {
            "description": "To within five minutes; none until it's used.",
            "$ref": "#/components/schemas/Timestamp"
          },
          "expiresAt": {
            "description": "None for a key that doesn't expire.",
            "$ref": "#/components/schemas/Timestamp"
          }
        },
        "required": [
          "id",
          "name",
          "prefix",
          "scopes",
          "createdAt"
        ],
        "additionalProperties": false,
        "description": "A key for scripts and integrations, with only the access it was given (KEY-1)."
      },
      "NewApiKey": {
        "$id": "#/components/schemas/NewApiKey",
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 60
          },
          "scopes": {
            "minItems": 1,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ApiKeyScope"
            },
            "description": "At least one."
          },
          "expiresInDays": {
            "description": "Leave out for a key that doesn't expire.",
            "type": "integer",
            "minimum": 1,
            "maximum": 3650
          }
        },
        "required": [
          "name",
          "scopes"
        ],
        "additionalProperties": false
      },
      "WebhookTopic": {
        "$id": "#/components/schemas/WebhookTopic",
        "type": "string",
        "enum": [
          "match",
          "matchRecord",
          "venue",
          "referee",
          "competition",
          "matchTemplate"
        ],
        "description": "The kinds of record a webhook can hear about: matches, match reports (results), venues, referees, competitions and match templates."
      },
      "Webhook": {
        "$id": "#/components/schemas/Webhook",
        "type": "object",
        "properties": {
          "id": {
            "$ref": "#/components/schemas/Id"
          },
          "url": {
            "type": "string",
            "description": "Where deliveries are POSTed."
          },
          "topics": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WebhookTopic"
            },
            "description": "Which records' changes it hears about."
          },
          "active": {
            "type": "boolean",
            "description": "Paused webhooks get no deliveries."
          },
          "createdAt": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "lastDelivery": {
            "description": "The latest delivery and how it went.",
            "type": "object",
            "properties": {
              "at": {
                "$ref": "#/components/schemas/Timestamp"
              },
              "status": {
                "type": "string",
                "enum": [
                  "pending",
                  "delivered",
                  "failed"
                ]
              },
              "responseCode": {
                "type": "integer",
                "minimum": -9007199254740991,
                "maximum": 9007199254740991
              }
            },
            "required": [
              "at",
              "status"
            ],
            "additionalProperties": false
          }
        },
        "required": [
          "id",
          "url",
          "topics",
          "active",
          "createdAt"
        ],
        "additionalProperties": false,
        "description": "A URL that hears about changes to the account's records (HOOK-1)."
      },
      "WebhookInput": {
        "$id": "#/components/schemas/WebhookInput",
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "maxLength": 2000,
            "format": "uri",
            "description": "http or https."
          },
          "topics": {
            "minItems": 1,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WebhookTopic"
            }
          },
          "active": {
            "description": "true when left out.",
            "type": "boolean"
          }
        },
        "required": [
          "url",
          "topics"
        ],
        "additionalProperties": false
      },
      "CreatedWebhook": {
        "$id": "#/components/schemas/CreatedWebhook",
        "type": "object",
        "properties": {
          "id": {
            "$ref": "#/components/schemas/Id"
          },
          "url": {
            "type": "string",
            "description": "Where deliveries are POSTed."
          },
          "topics": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WebhookTopic"
            },
            "description": "Which records' changes it hears about."
          },
          "active": {
            "type": "boolean",
            "description": "Paused webhooks get no deliveries."
          },
          "createdAt": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "lastDelivery": {
            "description": "The latest delivery and how it went.",
            "type": "object",
            "properties": {
              "at": {
                "$ref": "#/components/schemas/Timestamp"
              },
              "status": {
                "type": "string",
                "enum": [
                  "pending",
                  "delivered",
                  "failed"
                ]
              },
              "responseCode": {
                "type": "integer",
                "minimum": -9007199254740991,
                "maximum": 9007199254740991
              }
            },
            "required": [
              "at",
              "status"
            ],
            "additionalProperties": false
          },
          "secret": {
            "type": "string",
            "description": "For checking a delivery's X-LawFive-Signature, shown only this once."
          }
        },
        "required": [
          "id",
          "url",
          "topics",
          "active",
          "createdAt",
          "secret"
        ],
        "additionalProperties": false,
        "description": "A new webhook, with the secret its deliveries are signed with."
      },
      "WebhookDelivery": {
        "$id": "#/components/schemas/WebhookDelivery",
        "type": "object",
        "properties": {
          "id": {
            "description": "Also sent as X-LawFive-Delivery.",
            "$ref": "#/components/schemas/Id"
          },
          "event": {
            "type": "string",
            "description": "match.created, matchRecord.updated, venue.deleted…"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "delivered",
              "failed"
            ],
            "description": "pending: waiting for its next try; failed: gave up after five tries."
          },
          "attempts": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "responseCode": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "error": {
            "description": "Why the last try failed, when it didn't get an answer.",
            "type": "string"
          },
          "test": {
            "type": "boolean",
            "description": "Sent with Send test."
          },
          "createdAt": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "deliveredAt": {
            "$ref": "#/components/schemas/Timestamp"
          }
        },
        "required": [
          "id",
          "event",
          "status",
          "attempts",
          "test",
          "createdAt"
        ],
        "additionalProperties": false,
        "description": "One change sent to a webhook, with how it went."
      },
      "WebhookPayload": {
        "$id": "#/components/schemas/WebhookPayload",
        "type": "object",
        "properties": {
          "id": {
            "description": "The delivery's id; the same on every retry, so a receiver can ignore one it has had.",
            "$ref": "#/components/schemas/Id"
          },
          "event": {
            "type": "string",
            "description": "<topic>.<created|updated|deleted>"
          },
          "createdAt": {
            "description": "When the change was made.",
            "$ref": "#/components/schemas/Timestamp"
          },
          "test": {
            "description": "true for Send test; data is then an example.",
            "type": "boolean"
          },
          "data": {
            "type": "object",
            "propertyNames": {
              "type": "string"
            },
            "additionalProperties": {},
            "description": "The record as stored (as GET returns it); for deleted, its id, version and deletedAt."
          }
        },
        "required": [
          "id",
          "event",
          "createdAt",
          "data"
        ],
        "additionalProperties": false,
        "description": "What each delivery POSTs, as JSON. It's signed: X-LawFive-Signature is t=<unix seconds>,v1=<hex HMAC-SHA256 of \"<t>.<body>\" with the webhook's secret>."
      },
      "RestoreResult": {
        "$id": "#/components/schemas/RestoreResult",
        "type": "object",
        "properties": {
          "records": {
            "type": "object",
            "propertyNames": {
              "type": "string"
            },
            "additionalProperties": {
              "type": "object",
              "properties": {
                "added": {
                  "type": "integer",
                  "minimum": -9007199254740991,
                  "maximum": 9007199254740991
                },
                "updated": {
                  "type": "integer",
                  "minimum": -9007199254740991,
                  "maximum": 9007199254740991
                },
                "unchanged": {
                  "type": "integer",
                  "minimum": -9007199254740991,
                  "maximum": 9007199254740991
                }
              },
              "required": [
                "added",
                "updated",
                "unchanged"
              ],
              "additionalProperties": false
            },
            "description": "By collection: matches, venues, referees…"
          },
          "tracks": {
            "type": "object",
            "properties": {
              "added": {
                "type": "integer",
                "minimum": -9007199254740991,
                "maximum": 9007199254740991
              },
              "updated": {
                "type": "integer",
                "minimum": -9007199254740991,
                "maximum": 9007199254740991
              },
              "unchanged": {
                "type": "integer",
                "minimum": -9007199254740991,
                "maximum": 9007199254740991
              }
            },
            "required": [
              "added",
              "updated",
              "unchanged"
            ],
            "additionalProperties": false
          },
          "photos": {
            "type": "object",
            "properties": {
              "added": {
                "type": "integer",
                "minimum": -9007199254740991,
                "maximum": 9007199254740991
              },
              "updated": {
                "type": "integer",
                "minimum": -9007199254740991,
                "maximum": 9007199254740991
              },
              "unchanged": {
                "type": "integer",
                "minimum": -9007199254740991,
                "maximum": 9007199254740991
              }
            },
            "required": [
              "added",
              "updated",
              "unchanged"
            ],
            "additionalProperties": false
          },
          "problems": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What couldn't be restored and why, at most 50."
          }
        },
        "required": [
          "records",
          "tracks",
          "photos",
          "problems"
        ],
        "additionalProperties": false,
        "description": "What restoring an export did (DATA-4). Records the account already has a newer copy of are unchanged; running it again adds nothing."
      },
      "MatchShare": {
        "$id": "#/components/schemas/MatchShare",
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "description": "The public page: PUBLIC_URL/s/<token>. Anyone with it can see the match."
          },
          "createdAt": {
            "$ref": "#/components/schemas/Timestamp"
          }
        },
        "required": [
          "url",
          "createdAt"
        ],
        "additionalProperties": false,
        "description": "A match's share link (DATA-7)."
      },
      "SharedMatch": {
        "$id": "#/components/schemas/SharedMatch",
        "type": "object",
        "properties": {
          "match": {
            "description": "Without its notes, outlook, fee, crew, tags, match number or the ids of its venue and competition.",
            "$ref": "#/components/schemas/Match"
          },
          "record": {
            "description": "Its match report, when it's been played, without notes on events or team officials' names.",
            "$ref": "#/components/schemas/MatchRecord"
          },
          "competition": {
            "description": "The competition's name.",
            "type": "string"
          },
          "venue": {
            "description": "The venue's name.",
            "type": "string"
          },
          "pitch": {
            "description": "The pitch's name.",
            "type": "string"
          },
          "distanceUnit": {
            "description": "The sharer's, for the figures.",
            "$ref": "#/components/schemas/DistanceUnit"
          },
          "track": {
            "type": "boolean",
            "description": "Whether GET /v1/shared/{token}/track has a track."
          }
        },
        "required": [
          "match",
          "distanceUnit",
          "track"
        ],
        "additionalProperties": false,
        "description": "What a match's share link shows (DATA-7): never its notes, fee, crew, contacts, player names or photos."
      },
      "AttachmentRecordType": {
        "$id": "#/components/schemas/AttachmentRecordType",
        "type": "string",
        "enum": [
          "match",
          "venue",
          "referee"
        ],
        "description": "The kinds of record that can have attachments."
      },
      "Attachment": {
        "$id": "#/components/schemas/Attachment",
        "type": "object",
        "properties": {
          "id": {
            "$ref": "#/components/schemas/Id"
          },
          "recordType": {
            "$ref": "#/components/schemas/AttachmentRecordType"
          },
          "recordId": {
            "description": "The match, venue or referee it's on.",
            "$ref": "#/components/schemas/Id"
          },
          "contentType": {
            "type": "string",
            "enum": [
              "image/jpeg",
              "image/png",
              "image/webp"
            ],
            "description": "What the file is, worked out from its contents."
          },
          "size": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991,
            "description": "The file's size in bytes."
          },
          "width": {
            "description": "Its width in pixels, when the upload gave it.",
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "height": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "caption": {
            "type": "string"
          },
          "position": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991,
            "description": "Its place among the record's attachments, from 0. A referee's first photo is their picture."
          },
          "createdAt": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "url": {
            "type": "string",
            "description": "Where to fetch the file: /v1/attachments/{id}/file, with the same sign-in."
          },
          "thumbUrl": {
            "type": "string",
            "description": "A small version for lists and galleries: /v1/attachments/{id}/thumb (the file itself when none was uploaded)."
          }
        },
        "required": [
          "id",
          "recordType",
          "recordId",
          "contentType",
          "size",
          "position",
          "createdAt",
          "url",
          "thumbUrl"
        ],
        "additionalProperties": false,
        "description": "A photo on a match, venue or referee (FILE-1). The files are private: fetching them needs the same sign-in as the rest of the API."
      },
      "AttachmentUpload": {
        "$id": "#/components/schemas/AttachmentUpload",
        "type": "object",
        "properties": {
          "recordType": {
            "$ref": "#/components/schemas/AttachmentRecordType"
          },
          "recordId": {
            "$ref": "#/components/schemas/Id"
          },
          "caption": {
            "type": "string",
            "maxLength": 500
          },
          "width": {
            "description": "The image's width in pixels.",
            "type": "integer",
            "minimum": 1,
            "maximum": 20000
          },
          "height": {
            "type": "integer",
            "minimum": 1,
            "maximum": 20000
          }
        },
        "required": [
          "recordType",
          "recordId"
        ],
        "additionalProperties": false,
        "description": "The form fields of an upload (multipart/form-data), beside the files: file (a JPEG, PNG or WebP of at most 10 MB) and, optionally, thumb (a small version, at most 1 MB)."
      },
      "AttachmentChange": {
        "$id": "#/components/schemas/AttachmentChange",
        "type": "object",
        "properties": {
          "caption": {
            "description": "The new caption; null or an empty string removes it.",
            "anyOf": [
              {
                "type": "string",
                "maxLength": 500
              },
              {
                "type": "null"
              }
            ]
          },
          "position": {
            "description": "Move it to this place among the record's attachments (0 is first; a larger number moves it to the end).",
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          }
        },
        "additionalProperties": false,
        "description": "What to change about an attachment; leave out what stays the same."
      },
      "CreatedApiKey": {
        "$id": "#/components/schemas/CreatedApiKey",
        "type": "object",
        "properties": {
          "id": {
            "$ref": "#/components/schemas/Id"
          },
          "name": {
            "type": "string",
            "description": "What it's for, \"RefSix import\"."
          },
          "prefix": {
            "type": "string",
            "description": "The start of the key, to recognise it: lf_ and 8 characters."
          },
          "scopes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ApiKeyScope"
            }
          },
          "createdAt": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "lastUsedAt": {
            "description": "To within five minutes; none until it's used.",
            "$ref": "#/components/schemas/Timestamp"
          },
          "expiresAt": {
            "description": "None for a key that doesn't expire.",
            "$ref": "#/components/schemas/Timestamp"
          },
          "key": {
            "type": "string",
            "description": "The key itself, shown only this once: send it as Authorization: Bearer <key>."
          }
        },
        "required": [
          "id",
          "name",
          "prefix",
          "scopes",
          "createdAt",
          "key"
        ],
        "additionalProperties": false,
        "description": "A new key, with the key itself. The server keeps only a hash, so it can't show it again."
      },
      "Change": {
        "$id": "#/components/schemas/Change",
        "oneOf": [
          {
            "type": "object",
            "properties": {
              "collection": {
                "type": "string",
                "const": "matches"
              },
              "record": {
                "$ref": "#/components/schemas/Match"
              }
            },
            "required": [
              "collection",
              "record"
            ],
            "additionalProperties": false
          },
          {
            "type": "object",
            "properties": {
              "collection": {
                "type": "string",
                "const": "match-templates"
              },
              "record": {
                "$ref": "#/components/schemas/MatchTemplate"
              }
            },
            "required": [
              "collection",
              "record"
            ],
            "additionalProperties": false
          },
          {
            "type": "object",
            "properties": {
              "collection": {
                "type": "string",
                "const": "match-records"
              },
              "record": {
                "$ref": "#/components/schemas/MatchRecord"
              }
            },
            "required": [
              "collection",
              "record"
            ],
            "additionalProperties": false
          },
          {
            "type": "object",
            "properties": {
              "collection": {
                "type": "string",
                "const": "venues"
              },
              "record": {
                "$ref": "#/components/schemas/Venue"
              }
            },
            "required": [
              "collection",
              "record"
            ],
            "additionalProperties": false
          },
          {
            "type": "object",
            "properties": {
              "collection": {
                "type": "string",
                "const": "competitions"
              },
              "record": {
                "$ref": "#/components/schemas/Competition"
              }
            },
            "required": [
              "collection",
              "record"
            ],
            "additionalProperties": false
          },
          {
            "type": "object",
            "properties": {
              "collection": {
                "type": "string",
                "const": "referees"
              },
              "record": {
                "$ref": "#/components/schemas/Referee"
              }
            },
            "required": [
              "collection",
              "record"
            ],
            "additionalProperties": false
          },
          {
            "type": "object",
            "properties": {
              "collection": {
                "type": "string",
                "const": "settings"
              },
              "record": {
                "$ref": "#/components/schemas/UserSettings"
              }
            },
            "required": [
              "collection",
              "record"
            ],
            "additionalProperties": false
          }
        ],
        "description": "A record as it is now, after a change: created, updated or deleted (deletedAt set)."
      },
      "ChangesPage": {
        "$id": "#/components/schemas/ChangesPage",
        "type": "object",
        "properties": {
          "changes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Change"
            },
            "description": "Oldest first. A record changed several times appears once, as it is now."
          },
          "cursor": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991,
            "description": "Send as since next time. Unchanged when there's nothing new."
          },
          "more": {
            "type": "boolean",
            "description": "true when there are more changes after this page; ask again straight away."
          }
        },
        "required": [
          "changes",
          "cursor",
          "more"
        ],
        "additionalProperties": false,
        "description": "One page of the changes after a cursor, for sync."
      },
      "ApiError": {
        "$id": "#/components/schemas/ApiError",
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "What went wrong, in words a person can read."
          }
        },
        "required": [
          "error"
        ],
        "additionalProperties": false,
        "description": "Every LawFive error (outside /v1/auth) has this shape; some add fields."
      },
      "ValidationIssue": {
        "$id": "#/components/schemas/ValidationIssue",
        "type": "object",
        "properties": {
          "code": {
            "type": "string"
          },
          "path": {
            "type": "array",
            "items": {
              "type": [
                "string",
                "number"
              ]
            },
            "description": "Where in the body, for example [\"home\", \"colour\"]."
          },
          "message": {
            "type": "string"
          }
        },
        "required": [
          "code",
          "path",
          "message"
        ],
        "additionalProperties": {},
        "description": "One problem with a request body, as Zod reports it."
      },
      "ValidationError": {
        "$id": "#/components/schemas/ValidationError",
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "issues": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ValidationIssue"
            }
          }
        },
        "required": [
          "error",
          "issues"
        ],
        "additionalProperties": false,
        "description": "The body doesn't match the schema; issues lists each problem."
      },
      "SignUpMode": {
        "$id": "#/components/schemas/SignUpMode",
        "type": "string",
        "enum": [
          "open",
          "approval",
          "closed"
        ],
        "description": "Who can make an account (USR-2): open, anyone; approval, anyone, but an admin approves the account before it can be used; closed, only people with an invitation (USR-3)."
      },
      "ServerMeta": {
        "$id": "#/components/schemas/ServerMeta",
        "type": "object",
        "properties": {
          "product": {
            "type": "string",
            "const": "LawFive"
          },
          "apiUrl": {
            "description": "The address apps should use for the API, which may differ from the one they came through (APP-13).",
            "type": "string"
          },
          "apiVersion": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991,
            "description": "Raised when the API changes in a way older apps can't follow."
          },
          "version": {
            "description": "The server's version (VER-1): the day of its last change, \"2026.10.30\" (\"2026.10.30.2\" for a second change that day), or \"dev\" when run from a checkout without git. Missing on servers from before it.",
            "type": "string"
          },
          "commit": {
            "description": "The commit it was built from, \"f5de635\".",
            "type": "string"
          },
          "capabilities": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What this server offers: accounts, records, changes, tracks and stats (missing on servers from before them), devices, link, watch, people, invites, calendar and import, then email, geocode, push and weather when they're switched on. Ignore names you don't know."
          },
          "maps": {
            "type": "object",
            "properties": {
              "style": {
                "description": "A vector map, which apps that can draw one use instead of tileUrl (OpenFreeMap's by default).",
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "description": "A MapLibre style for the map (vector tiles)."
                  },
                  "darkUrl": {
                    "description": "The style to use in dark mode, when there is one.",
                    "type": "string"
                  },
                  "attribution": {
                    "type": "string",
                    "description": "The credit the map provider asks for, as HTML."
                  }
                },
                "required": [
                  "url",
                  "attribution"
                ],
                "additionalProperties": false
              },
              "tileUrl": {
                "type": "string",
                "description": "Map tiles (raster images), with {z}, {x} and {y} in the URL: the map when there's no style, and for apps that can't draw vector maps."
              },
              "attribution": {
                "type": "string",
                "description": "The credit the tile provider asks for, as HTML."
              },
              "satellite": {
                "description": "Satellite imagery, when the server has a provider.",
                "type": "object",
                "properties": {
                  "tileUrl": {
                    "type": "string"
                  },
                  "attribution": {
                    "type": "string"
                  }
                },
                "required": [
                  "tileUrl",
                  "attribution"
                ],
                "additionalProperties": false
              }
            },
            "required": [
              "tileUrl",
              "attribution"
            ],
            "additionalProperties": false
          },
          "geocoder": {
            "description": "The address search service behind GET /v1/geocode, to credit it.",
            "type": "object",
            "properties": {
              "url": {
                "type": "string"
              }
            },
            "required": [
              "url"
            ],
            "additionalProperties": false
          },
          "weather": {
            "description": "Whose weather data GET /v1/weather uses, to credit it.",
            "type": "object",
            "properties": {
              "source": {
                "type": "string"
              }
            },
            "required": [
              "source"
            ],
            "additionalProperties": false
          },
          "firstAccount": {
            "description": "Nobody has an account on this server yet (USR-8): the first one can be made whatever signUp says, and unless ADMIN_EMAILS is set it's the server's admin.",
            "type": "boolean",
            "const": true
          },
          "setupCodeNeeded": {
            "description": "The first account needs the code in the server's logs (ADM-5).",
            "type": "boolean",
            "const": true
          },
          "setupPending": {
            "description": "The first account hasn't finished setting the server up (ADM-5).",
            "type": "boolean",
            "const": true
          },
          "publicUrl": {
            "description": "The server's own address (PUBLIC_URL), for the website to say if it isn't HTTPS (ADM-5, ADM-6).",
            "type": "string"
          },
          "signUp": {
            "description": "Who can make an account (USR-2); missing on servers from before it, which were open or closed.",
            "$ref": "#/components/schemas/SignUpMode"
          }
        },
        "required": [
          "product",
          "apiVersion",
          "capabilities",
          "maps"
        ],
        "additionalProperties": false,
        "description": "What this server is and offers, so apps can adapt to older or self-hosted servers."
      },
      "Device": {
        "$id": "#/components/schemas/Device",
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The session's id, for DELETE /v1/devices/{id}."
          },
          "kind": {
            "type": "string",
            "description": "browser, or the app's name from its X-LawFive-Device header (garmin)."
          },
          "name": {
            "type": "string",
            "description": "For people: \"Garmin Venu 2\" (\"Garmin watch\" when it didn't say which), \"Chrome on macOS\"."
          },
          "model": {
            "description": "A watch's Garmin part number, \"006-B3703-00\", when it gives one.",
            "type": "string"
          },
          "firmware": {
            "description": "A watch's firmware version, \"12.30\", when it gives one.",
            "type": "string"
          },
          "appVersion": {
            "description": "The version of LawFive on it (VER-3), \"2026.10.30\", from the app= in its X-LawFive-Device header; missing for browsers and apps from before it.",
            "type": "string"
          },
          "linkedAt": {
            "description": "When it signed in or was linked.",
            "$ref": "#/components/schemas/Timestamp"
          },
          "lastActiveAt": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "current": {
            "type": "boolean",
            "description": "true for the device making this request."
          }
        },
        "required": [
          "id",
          "kind",
          "name",
          "linkedAt",
          "lastActiveAt",
          "current"
        ],
        "additionalProperties": false,
        "description": "Somewhere the account is signed in: a browser or a linked watch."
      },
      "WatchMatch": {
        "$id": "#/components/schemas/WatchMatch",
        "type": "object",
        "properties": {
          "id": {
            "$ref": "#/components/schemas/Id"
          },
          "kickoff": {
            "anyOf": [
              {
                "type": "integer",
                "minimum": -9007199254740991,
                "maximum": 9007199254740991
              },
              {
                "type": "null"
              }
            ],
            "description": "Seconds since 1970 (UTC)."
          },
          "title": {
            "type": "string",
            "description": "\"STR v ROV\", for the watch's list."
          },
          "name": {
            "description": "The match's name, or its competition's.",
            "type": [
              "string",
              "null"
            ]
          },
          "where": {
            "description": "Venue and pitch, \"Twin Creeks, Field 3\".",
            "type": [
              "string",
              "null"
            ]
          },
          "role": {
            "description": "Referee, AR1, AR2 or 4th official.",
            "type": [
              "string",
              "null"
            ]
          },
          "weather": {
            "description": "A short forecast in the user's temperature unit, \"54° rain\" (WX-7).",
            "type": [
              "string",
              "null"
            ]
          },
          "fee": {
            "description": "The fee, for the match's screen and its Paid toggle (UI-19); absent from servers before it.",
            "anyOf": [
              {
                "type": "object",
                "properties": {
                  "text": {
                    "type": "string",
                    "description": "\"$25\", in the fee's currency."
                  },
                  "paid": {
                    "type": "boolean"
                  }
                },
                "required": [
                  "text",
                  "paid"
                ],
                "additionalProperties": false
              },
              {
                "type": "null"
              }
            ]
          },
          "crew": {
            "description": "The other officials, [role, name]: [\"AR1\", \"Sam Rivera\"].",
            "type": "array",
            "items": {
              "type": "array",
              "prefixItems": [
                {
                  "type": "string"
                },
                {
                  "type": "string"
                }
              ],
              "items": false,
              "minItems": 2,
              "maxItems": 2
            }
          },
          "notes": {
            "description": "The match's notes, shortened to 300 characters for the watch's memory.",
            "type": [
              "string",
              "null"
            ]
          },
          "home": {
            "type": "string",
            "maxLength": 4,
            "description": "Short name shown on the watch."
          },
          "away": {
            "type": "string",
            "maxLength": 4
          },
          "homeName": {
            "type": "string"
          },
          "awayName": {
            "type": "string"
          },
          "homeColor": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991,
            "description": "0xRRGGBB, or -1 for none."
          },
          "awayColor": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "numPeriods": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "periodSec": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "halftimeSec": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991,
            "description": "The break between periods."
          },
          "teamSize": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "midBreak": {
            "type": "boolean",
            "description": "A break halfway through each period (for example a water break)."
          },
          "breakLenSec": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "extraTime": {
            "type": "boolean"
          },
          "etPeriods": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "etPeriodSec": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "penalties": {
            "type": "boolean"
          },
          "clockStyle": {
            "anyOf": [
              {
                "type": "number",
                "const": 0
              },
              {
                "type": "number",
                "const": 1
              },
              {
                "type": "number",
                "const": 2
              }
            ],
            "description": "0 counts up, 1 counts up from zero each period, 2 counts down."
          },
          "trackStoppage": {
            "type": "boolean"
          },
          "breakStoppage": {
            "type": "boolean",
            "description": "The halfway break's time is added on as stoppage (TMR-12)."
          },
          "goalScorers": {
            "type": "boolean",
            "description": "Ask the scorer's number for each goal (EVT-10)."
          },
          "goalTypes": {
            "type": "boolean"
          }
        },
        "required": [
          "id",
          "kickoff",
          "title",
          "name",
          "where",
          "role",
          "weather",
          "home",
          "away",
          "homeName",
          "awayName",
          "homeColor",
          "awayColor",
          "numPeriods",
          "periodSec",
          "halftimeSec",
          "teamSize",
          "midBreak",
          "breakLenSec",
          "extraTime",
          "etPeriods",
          "etPeriodSec",
          "penalties",
          "clockStyle",
          "trackStoppage",
          "breakStoppage",
          "goalScorers",
          "goalTypes"
        ],
        "additionalProperties": false,
        "description": "A match in the form the Garmin watch starts one from (its setup keys), plus what its list shows. Watches have little memory, so the server does the converting."
      },
      "WatchEvent": {
        "$id": "#/components/schemas/WatchEvent",
        "type": "array",
        "prefixItems": [
          {
            "type": "string"
          },
          {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          {
            "anyOf": [
              {
                "type": "integer",
                "minimum": -9007199254740991,
                "maximum": 9007199254740991
              },
              {
                "type": "null"
              }
            ]
          },
          {
            "anyOf": [
              {
                "type": "integer",
                "minimum": -9007199254740991,
                "maximum": 9007199254740991
              },
              {
                "type": "null"
              }
            ]
          }
        ],
        "items": false,
        "minItems": 6,
        "maxItems": 6,
        "description": "[code, team, period, seconds, player, extra]. code: G goal, GO own goal, GP penalty, GF direct free kick, Y caution, Y2 second caution, R send-off, S stoppage, E end of period, A abandoned, with the reason in extra (0 weather, 1 lightning, 2 crowd trouble, 3 not enough players, 4 other), PG and PM shoot-out kick scored and missed. team: 0 home, 1 away, -1 neither. period counts from 0. seconds: into the period, or -1 before kickoff, -2 at a break, -3 after full time, -4 in the shoot-out. player: shirt number or null. extra: a stoppage's length in seconds."
      },
      "WatchFeeChange": {
        "$id": "#/components/schemas/WatchFeeChange",
        "type": "object",
        "properties": {
          "matchId": {
            "$ref": "#/components/schemas/Id"
          },
          "paid": {
            "type": "boolean"
          },
          "at": {
            "type": "integer",
            "exclusiveMinimum": 0,
            "maximum": 9007199254740991,
            "description": "When it was changed on the watch, in seconds since 1970."
          }
        },
        "required": [
          "matchId",
          "paid",
          "at"
        ],
        "additionalProperties": false,
        "description": "A match's fee marked paid or not paid on the watch (SYNC-11)."
      },
      "WatchScreenChange": {
        "$id": "#/components/schemas/WatchScreenChange",
        "type": "object",
        "properties": {
          "layout": {
            "$ref": "#/components/schemas/WatchLayout"
          },
          "secondClock": {
            "$ref": "#/components/schemas/ClockStyle"
          },
          "at": {
            "type": "integer",
            "exclusiveMinimum": 0,
            "maximum": 9007199254740991,
            "description": "When it was changed on the watch, in seconds since 1970."
          }
        },
        "required": [
          "layout",
          "secondClock",
          "at"
        ],
        "additionalProperties": false,
        "description": "The match screen as chosen on the watch, or as it's sent to one (UI-24); the latest change wins."
      },
      "WatchPitchMarking": {
        "$id": "#/components/schemas/WatchPitchMarking",
        "type": "object",
        "properties": {
          "matchId": {
            "description": "A planned match's id.",
            "$ref": "#/components/schemas/Id"
          },
          "created": {
            "description": "Or, for a quick match, when it was started on the watch (it has no id until it's uploaded).",
            "type": "integer",
            "exclusiveMinimum": 0,
            "maximum": 9007199254740991
          },
          "corners": {
            "minItems": 2,
            "maxItems": 4,
            "type": "array",
            "items": {
              "type": "array",
              "prefixItems": [
                {
                  "type": "integer",
                  "minimum": -90000000,
                  "maximum": 90000000
                },
                {
                  "type": "integer",
                  "minimum": -180000000,
                  "maximum": 180000000
                }
              ],
              "items": false,
              "minItems": 2,
              "maxItems": 2
            },
            "description": "[latitude, longitude] in millionths of a degree, as the watch's tracks are: its JSON keeps only six significant figures of a decimal."
          },
          "at": {
            "type": "integer",
            "exclusiveMinimum": 0,
            "maximum": 9007199254740991,
            "description": "When they were marked, in seconds since 1970."
          }
        },
        "required": [
          "corners",
          "at"
        ],
        "additionalProperties": false,
        "description": "A pitch's corners walked on the watch (TRK-13, SYNC-12)."
      },
      "WatchUpload": {
        "$id": "#/components/schemas/WatchUpload",
        "type": "object",
        "properties": {
          "created": {
            "type": "integer",
            "exclusiveMinimum": 0,
            "maximum": 9007199254740991,
            "description": "When the match was started on the watch, in seconds since 1970."
          },
          "matchId": {
            "description": "The planned match it was started from; none for a quick match.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/Id"
              },
              {
                "type": "null"
              }
            ]
          },
          "device": {
            "description": "The watch model, \"venu2\".",
            "type": "string",
            "maxLength": 40
          },
          "home": {
            "type": "string",
            "maxLength": 4
          },
          "away": {
            "type": "string",
            "maxLength": 4
          },
          "homeName": {
            "type": "string",
            "maxLength": 60
          },
          "awayName": {
            "type": "string",
            "maxLength": 60
          },
          "homeColor": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "awayColor": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "numPeriods": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "periodSec": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "halftimeSec": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "teamSize": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "breakAtSec": {
            "description": "When the halfway break comes, or -1 for none.",
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "breakLenSec": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "extraTime": {
            "type": "boolean"
          },
          "etPeriods": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "etPeriodSec": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "penalties": {
            "type": "boolean"
          },
          "clockStyle": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "trackStoppage": {
            "type": "boolean"
          },
          "breakStoppage": {
            "type": "boolean"
          },
          "goalScorers": {
            "type": "boolean"
          },
          "goalTypes": {
            "type": "boolean"
          },
          "score": {
            "type": "array",
            "prefixItems": [
              {
                "type": "integer",
                "minimum": -9007199254740991,
                "maximum": 9007199254740991
              },
              {
                "type": "integer",
                "minimum": -9007199254740991,
                "maximum": 9007199254740991
              }
            ],
            "items": false,
            "minItems": 2,
            "maxItems": 2,
            "description": "[home, away]."
          },
          "pens": {
            "description": "Shoot-out score, [home, away].",
            "anyOf": [
              {
                "type": "array",
                "prefixItems": [
                  {
                    "type": "integer",
                    "minimum": -9007199254740991,
                    "maximum": 9007199254740991
                  },
                  {
                    "type": "integer",
                    "minimum": -9007199254740991,
                    "maximum": 9007199254740991
                  }
                ],
                "items": false,
                "minItems": 2,
                "maxItems": 2
              },
              {
                "type": "null"
              }
            ]
          },
          "distanceM": {
            "description": "How far the referee ran, in metres.",
            "anyOf": [
              {
                "type": "number",
                "minimum": 0
              },
              {
                "type": "null"
              }
            ]
          },
          "startedAt": {
            "description": "When each period kicked off, in seconds since 1970, for the track's figures (TRK-3).",
            "maxItems": 8,
            "type": "array",
            "items": {
              "type": "integer",
              "exclusiveMinimum": 0,
              "maximum": 9007199254740991
            }
          },
          "kickoff": {
            "description": "The team that kicked off the first period: 0 home, 1 away, -1 not chosen.",
            "type": "integer",
            "minimum": -1,
            "maximum": 1
          },
          "homeHeading": {
            "description": "The compass heading of the goal the home team attacked in the first period, in radians from true north, when the referee recorded it (PRE-8).",
            "type": [
              "number",
              "null"
            ]
          },
          "events": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WatchEvent"
            }
          },
          "diagnostics": {
            "description": "How the recording went, for tracing problems (DIAG-1).",
            "type": "object",
            "properties": {
              "battery": {
                "description": "[period from 0, 0 kickoff or 1 its end, percent].",
                "maxItems": 24,
                "type": "array",
                "items": {
                  "type": "array",
                  "prefixItems": [
                    {
                      "type": "integer",
                      "minimum": -9007199254740991,
                      "maximum": 9007199254740991
                    },
                    {
                      "type": "integer",
                      "minimum": -9007199254740991,
                      "maximum": 9007199254740991
                    },
                    {
                      "type": "number"
                    }
                  ],
                  "items": false,
                  "minItems": 3,
                  "maxItems": 3
                }
              },
              "app": {
                "type": "string",
                "maxLength": 40
              },
              "firmware": {
                "type": "string",
                "maxLength": 40
              },
              "gpsFirstFixSec": {
                "description": "Seconds from kickoff to the first GPS fix.",
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "gpsSamples": {
                "description": "Track samples taken.",
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "gpsMissed": {
                "description": "Of those, how many had no GPS fix.",
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "restarts": {
                "description": "How many times the app started again mid-match (R-5).",
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              }
            },
            "additionalProperties": false
          }
        },
        "required": [
          "created",
          "home",
          "away",
          "numPeriods",
          "periodSec",
          "halftimeSec",
          "score",
          "events"
        ],
        "additionalProperties": false,
        "description": "A finished match as the watch saved it. The server stores it as a match record."
      },
      "WatchTrackChunk": {
        "$id": "#/components/schemas/WatchTrackChunk",
        "type": "object",
        "properties": {
          "created": {
            "type": "integer",
            "exclusiveMinimum": 0,
            "maximum": 9007199254740991,
            "description": "The match's created time, as in its upload: which match the track is for."
          },
          "start": {
            "type": "integer",
            "exclusiveMinimum": 0,
            "maximum": 9007199254740991,
            "description": "When the track starts (the first kickoff), in seconds since 1970."
          },
          "index": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991,
            "description": "Which chunk this is, from 0."
          },
          "count": {
            "type": "integer",
            "minimum": 1,
            "maximum": 200,
            "description": "How many chunks the track has."
          },
          "device": {
            "description": "The watch model, \"venu2\".",
            "type": "string",
            "maxLength": 40
          },
          "data": {
            "type": "string",
            "maxLength": 32768,
            "description": "The chunk's samples as the watch packs them, \"dt,dlat,dlon,speed,hr;\" each: tenths of a second since the sample before (empty for 10; the chunk's first counts from start), millionths of a degree moved since the chunk's position before (the first is whole; both empty without a new fix), the speed the watch measured in cm/s, and beats a minute. Empty fields mean unknown; trailing ones are left off."
          }
        },
        "required": [
          "created",
          "start",
          "index",
          "count",
          "data"
        ],
        "additionalProperties": false,
        "description": "Part of a match's track as the watch recorded it (TRK-7): GPS and heart rate about once a second during the periods."
      },
      "WatchTrackResult": {
        "$id": "#/components/schemas/WatchTrackResult",
        "type": "object",
        "properties": {
          "received": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991,
            "description": "How many of the track's chunks the server has."
          },
          "count": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "track": {
            "type": "string",
            "enum": [
              "waiting",
              "saved",
              "kept",
              "dropped"
            ],
            "description": "waiting: for more chunks or the match; saved: the track is on the match report; kept: the match report already had a track (from a file, say), which stays; dropped: nothing in it could be used (no positions or heart rate)."
          }
        },
        "required": [
          "received",
          "count",
          "track"
        ],
        "additionalProperties": false
      },
      "WatchUploadResult": {
        "$id": "#/components/schemas/WatchUploadResult",
        "type": "object",
        "properties": {
          "recordId": {
            "description": "The match record. The same every time this match is sent, so sending it again updates it.",
            "$ref": "#/components/schemas/Id"
          },
          "matchId": {
            "description": "The planned match, or the one made for a quick match.",
            "$ref": "#/components/schemas/Id"
          }
        },
        "required": [
          "recordId",
          "matchId"
        ],
        "additionalProperties": false
      },
      "Place": {
        "$id": "#/components/schemas/Place",
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "The full address."
          },
          "lat": {
            "type": "number"
          },
          "lon": {
            "type": "number"
          }
        },
        "required": [
          "name",
          "lat",
          "lon"
        ],
        "additionalProperties": false,
        "description": "A place found by address search."
      },
      "User": {
        "$id": "#/components/schemas/User",
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "emailVerified": {
            "type": "boolean"
          },
          "image": {
            "type": [
              "string",
              "null"
            ]
          },
          "createdAt": {
            "type": "string"
          },
          "updatedAt": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "name",
          "email",
          "emailVerified",
          "createdAt",
          "updatedAt"
        ],
        "additionalProperties": {},
        "description": "A LawFive account."
      },
      "Session": {
        "$id": "#/components/schemas/Session",
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "userId": {
            "type": "string"
          },
          "token": {
            "type": "string",
            "description": "The bearer token."
          },
          "expiresAt": {
            "type": "string",
            "description": "90 days after it was last renewed; a session is renewed once a day while it's used."
          },
          "createdAt": {
            "type": "string"
          },
          "updatedAt": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "userId",
          "token",
          "expiresAt",
          "createdAt",
          "updatedAt"
        ],
        "additionalProperties": {},
        "description": "A signed-in device."
      },
      "AuthError": {
        "$id": "#/components/schemas/AuthError",
        "type": "object",
        "properties": {
          "message": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "description": "For example INVALID_EMAIL_OR_PASSWORD or INVALID_ORIGIN."
          }
        },
        "required": [
          "message",
          "code"
        ],
        "additionalProperties": {},
        "description": "An error from the account endpoints."
      },
      "OAuthError": {
        "$id": "#/components/schemas/OAuthError",
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "authorization_pending, slow_down, expired_token, access_denied, invalid_request, invalid_client, invalid_grant or unauthorized."
          },
          "error_description": {
            "type": "string"
          }
        },
        "required": [
          "error"
        ],
        "additionalProperties": {},
        "description": "An error from the linking endpoints, as RFC 8628 has them."
      },
      "DeviceCode": {
        "$id": "#/components/schemas/DeviceCode",
        "type": "object",
        "properties": {
          "device_code": {
            "type": "string",
            "description": "Secret; the watch polls with it."
          },
          "user_code": {
            "type": "string",
            "description": "Eight letters and digits without look-alikes, shown on the watch."
          },
          "verification_uri": {
            "type": "string",
            "description": "The web app's Link page."
          },
          "verification_uri_complete": {
            "type": "string",
            "description": "The Link page with the code filled in, for a QR code."
          },
          "expires_in": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991,
            "description": "Seconds (600)."
          },
          "interval": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991,
            "description": "Seconds to wait between polls (5)."
          }
        },
        "required": [
          "device_code",
          "user_code",
          "verification_uri",
          "verification_uri_complete",
          "expires_in",
          "interval"
        ],
        "additionalProperties": false,
        "description": "A code for linking a watch (RFC 8628)."
      },
      "DeviceToken": {
        "$id": "#/components/schemas/DeviceToken",
        "type": "object",
        "properties": {
          "access_token": {
            "type": "string",
            "description": "Send as Authorization: Bearer <token>."
          },
          "token_type": {
            "type": "string",
            "const": "Bearer"
          },
          "expires_in": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "scope": {
            "type": "string"
          }
        },
        "required": [
          "access_token",
          "token_type",
          "expires_in",
          "scope"
        ],
        "additionalProperties": {},
        "description": "The watch's token once someone has approved its code."
      },
      "AccountStatus": {
        "$id": "#/components/schemas/AccountStatus",
        "type": "string",
        "enum": [
          "active",
          "pending",
          "suspended"
        ],
        "description": "pending: made on a server that approves new accounts, and not approved yet (USR-4). It can sign in, but only GET /v1/account answers. suspended: an admin suspended it (USR-7); it can't sign in, and its API keys and calendar feed stop working, but its data is kept."
      },
      "Account": {
        "$id": "#/components/schemas/Account",
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "status": {
            "$ref": "#/components/schemas/AccountStatus"
          },
          "admin": {
            "type": "boolean",
            "description": "Can invite and approve people (USR-1)."
          }
        },
        "required": [
          "id",
          "name",
          "email",
          "status",
          "admin"
        ],
        "additionalProperties": false,
        "description": "The signed-in account, with what it may do."
      },
      "AccountStorage": {
        "$id": "#/components/schemas/AccountStorage",
        "type": "object",
        "properties": {
          "usedBytes": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991,
            "description": "Photos and tracks on this account."
          },
          "limitBytes": {
            "description": "How much it may keep (SEC-6); absent when this server sets no limit.",
            "type": "integer",
            "exclusiveMinimum": 0,
            "maximum": 9007199254740991
          }
        },
        "required": [
          "usedBytes"
        ],
        "additionalProperties": false
      },
      "PushHandle": {
        "$id": "#/components/schemas/PushHandle",
        "type": "object",
        "properties": {
          "handle": {
            "anyOf": [
              {
                "type": "string",
                "minLength": 1,
                "maxLength": 4096
              },
              {
                "type": "null"
              }
            ],
            "description": "What the push relay gave this phone (PUSH-2), which only it can read; null to stop notifications."
          }
        },
        "required": [
          "handle"
        ],
        "additionalProperties": false
      },
      "SignOutOthers": {
        "$id": "#/components/schemas/SignOutOthers",
        "type": "object",
        "properties": {
          "scope": {
            "type": "string",
            "enum": [
              "browsers",
              "all"
            ],
            "description": "browsers: other browsers only, so watches and phones stay linked. all: every other device too."
          }
        },
        "required": [
          "scope"
        ],
        "additionalProperties": false
      },
      "Person": {
        "$id": "#/components/schemas/Person",
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "status": {
            "$ref": "#/components/schemas/AccountStatus"
          },
          "admin": {
            "type": "boolean"
          },
          "adminFromSetting": {
            "type": "boolean",
            "description": "An admin because the server's ADMIN_EMAILS lists them, which the web app can't change."
          },
          "joinedAt": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "lastActiveAt": {
            "description": "When one of their devices last used the account; none if never signed in since joining.",
            "$ref": "#/components/schemas/Timestamp"
          },
          "invitedBy": {
            "description": "The name of the admin whose invitation they joined with.",
            "type": "string"
          }
        },
        "required": [
          "id",
          "name",
          "email",
          "status",
          "admin",
          "adminFromSetting",
          "joinedAt"
        ],
        "additionalProperties": false,
        "description": "An account on this server, as an admin sees it (USR-1)."
      },
      "Invite": {
        "$id": "#/components/schemas/Invite",
        "type": "object",
        "properties": {
          "id": {
            "$ref": "#/components/schemas/Id"
          },
          "email": {
            "description": "Who it's for; only this address can use it. None for a link anyone can use once.",
            "type": "string"
          },
          "name": {
            "description": "Their name, to fill in the sign-up form.",
            "type": "string"
          },
          "link": {
            "type": "string",
            "description": "The link to send: the web app's invitation page."
          },
          "createdBy": {
            "type": "string",
            "description": "The name of the admin who made it."
          },
          "createdAt": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "expiresAt": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "usedAt": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "usedBy": {
            "description": "The name of the account made with it.",
            "type": "string"
          },
          "emailedAt": {
            "description": "When it was emailed, if it was.",
            "$ref": "#/components/schemas/Timestamp"
          }
        },
        "required": [
          "id",
          "link",
          "createdBy",
          "createdAt",
          "expiresAt"
        ],
        "additionalProperties": false,
        "description": "An invitation to make an account, even when sign-up is closed (USR-3). It works once, for 14 days."
      },
      "NewInvite": {
        "$id": "#/components/schemas/NewInvite",
        "type": "object",
        "properties": {
          "email": {
            "description": "Who it's for; only this address can use it.",
            "type": "string",
            "maxLength": 254,
            "format": "email",
            "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
          },
          "name": {
            "description": "Their name, to fill in the sign-up form.",
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "send": {
            "description": "Email it to them (needs email set up on the server, and an email).",
            "type": "boolean"
          }
        },
        "additionalProperties": false
      },
      "ResetLink": {
        "$id": "#/components/schemas/ResetLink",
        "type": "object",
        "properties": {
          "link": {
            "type": "string",
            "description": "The web app's page for choosing a new password. It works once."
          },
          "expiresAt": {
            "$ref": "#/components/schemas/Timestamp"
          }
        },
        "required": [
          "link",
          "expiresAt"
        ],
        "additionalProperties": false,
        "description": "A link an admin sends someone who forgot their password, for servers without email (USR-7)."
      },
      "ServerSettings": {
        "$id": "#/components/schemas/ServerSettings",
        "type": "object",
        "properties": {
          "signUp": {
            "description": "Who can make an account (USR-2, SIGN_UP).",
            "anyOf": [
              {
                "$ref": "#/components/schemas/SignUpMode"
              },
              {
                "type": "null"
              }
            ]
          },
          "smtpUrl": {
            "description": "Email through an SMTP server: smtp://user:password@host:587 or smtps://…:465. A secret: never sent back (SMTP_URL). null or \"\" clears it, back to the default.",
            "anyOf": [
              {
                "type": "string",
                "maxLength": 2000
              },
              {
                "type": "null"
              }
            ]
          },
          "mailFrom": {
            "description": "The sender, \"LawFive <lawfive@example.com>\" (MAIL_FROM). null or \"\" clears it, back to the default.",
            "anyOf": [
              {
                "type": "string",
                "maxLength": 2000
              },
              {
                "type": "null"
              }
            ]
          },
          "mapStyleUrl": {
            "description": "A MapLibre style for the map, or off for raster tiles (MAP_STYLE_URL). null or \"\" clears it, back to the default.",
            "anyOf": [
              {
                "type": "string",
                "maxLength": 2000
              },
              {
                "type": "null"
              }
            ]
          },
          "mapStyleDarkUrl": {
            "description": "The style for dark mode (MAP_STYLE_DARK_URL). null or \"\" clears it, back to the default.",
            "anyOf": [
              {
                "type": "string",
                "maxLength": 2000
              },
              {
                "type": "null"
              }
            ]
          },
          "mapStyleAttribution": {
            "description": "The style's credit, as HTML (MAP_STYLE_ATTRIBUTION). null or \"\" clears it, back to the default.",
            "anyOf": [
              {
                "type": "string",
                "maxLength": 2000
              },
              {
                "type": "null"
              }
            ]
          },
          "mapTileUrl": {
            "description": "Raster tiles, with {z}, {x} and {y} (MAP_TILE_URL). null or \"\" clears it, back to the default.",
            "anyOf": [
              {
                "type": "string",
                "maxLength": 2000
              },
              {
                "type": "null"
              }
            ]
          },
          "mapAttribution": {
            "description": "The tiles' credit, as HTML (MAP_ATTRIBUTION). null or \"\" clears it, back to the default.",
            "anyOf": [
              {
                "type": "string",
                "maxLength": 2000
              },
              {
                "type": "null"
              }
            ]
          },
          "mapSatelliteUrl": {
            "description": "Satellite tiles, with the provider's key (MAP_SATELLITE_URL). null or \"\" clears it, back to the default.",
            "anyOf": [
              {
                "type": "string",
                "maxLength": 2000
              },
              {
                "type": "null"
              }
            ]
          },
          "mapSatelliteAttribution": {
            "description": "Their credit, as HTML (MAP_SATELLITE_ATTRIBUTION). null or \"\" clears it, back to the default.",
            "anyOf": [
              {
                "type": "string",
                "maxLength": 2000
              },
              {
                "type": "null"
              }
            ]
          },
          "geocoderUrl": {
            "description": "Address search (Nominatim-compatible), or off (GEOCODER_URL). null or \"\" clears it, back to the default.",
            "anyOf": [
              {
                "type": "string",
                "maxLength": 2000
              },
              {
                "type": "null"
              }
            ]
          },
          "weatherUrl": {
            "description": "Forecasts (Open-Meteo-compatible), or off (WEATHER_URL). null or \"\" clears it, back to the default.",
            "anyOf": [
              {
                "type": "string",
                "maxLength": 2000
              },
              {
                "type": "null"
              }
            ]
          },
          "weatherHistoryUrl": {
            "description": "Past weather for played matches, or off (WEATHER_HISTORY_URL). null or \"\" clears it, back to the default.",
            "anyOf": [
              {
                "type": "string",
                "maxLength": 2000
              },
              {
                "type": "null"
              }
            ]
          },
          "weatherApiKey": {
            "description": "For a paid plan. A secret: never sent back (WEATHER_API_KEY). null or \"\" clears it, back to the default.",
            "anyOf": [
              {
                "type": "string",
                "maxLength": 2000
              },
              {
                "type": "null"
              }
            ]
          },
          "weatherDailyLimit": {
            "description": "Weather calls a day at most (WEATHER_DAILY_LIMIT). null or \"\" clears it, back to the default.",
            "anyOf": [
              {
                "type": "string",
                "maxLength": 2000
              },
              {
                "type": "null"
              }
            ]
          },
          "webhookAllowPrivate": {
            "description": "true lets webhooks reach private and local addresses (WEBHOOK_ALLOW_PRIVATE). null or \"\" clears it, back to the default.",
            "anyOf": [
              {
                "type": "string",
                "maxLength": 2000
              },
              {
                "type": "null"
              }
            ]
          },
          "accountStorageLimitMb": {
            "description": "How many megabytes of photos and tracks each account may keep; none means no limit (ACCOUNT_STORAGE_LIMIT_MB). null or \"\" clears it, back to the default.",
            "anyOf": [
              {
                "type": "string",
                "maxLength": 2000
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "additionalProperties": false,
        "description": "Settings for the whole server that admins change in the web app (ADM-1, ADM-2), sending only those that change. One the environment sets is fixed there and can't be changed."
      },
      "ServerSettingView": {
        "$id": "#/components/schemas/ServerSettingView",
        "type": "object",
        "properties": {
          "key": {
            "type": "string",
            "enum": [
              "signUp",
              "smtpUrl",
              "mailFrom",
              "mapStyleUrl",
              "mapStyleDarkUrl",
              "mapStyleAttribution",
              "mapTileUrl",
              "mapAttribution",
              "mapSatelliteUrl",
              "mapSatelliteAttribution",
              "geocoderUrl",
              "weatherUrl",
              "weatherHistoryUrl",
              "weatherApiKey",
              "weatherDailyLimit",
              "webhookAllowPrivate",
              "accountStorageLimitMb"
            ]
          },
          "env": {
            "type": "string",
            "description": "Its environment variable."
          },
          "source": {
            "type": "string",
            "enum": [
              "environment",
              "admin",
              "default"
            ],
            "description": "environment: set there, so fixed; admin: set in the web app; default: neither."
          },
          "value": {
            "description": "The value in use, when it's set and not a secret.",
            "type": "string"
          },
          "secret": {
            "description": "Never sent back: set says whether it has a value.",
            "type": "boolean",
            "const": true
          },
          "set": {
            "type": "boolean"
          }
        },
        "required": [
          "key",
          "env",
          "source"
        ],
        "additionalProperties": false,
        "description": "A server setting as the admin page shows it (ADM-1)."
      },
      "EmailChange": {
        "$id": "#/components/schemas/EmailChange",
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "maxLength": 254,
            "format": "email",
            "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
          },
          "password": {
            "type": "string",
            "description": "The account's password, to confirm it's them."
          }
        },
        "required": [
          "email",
          "password"
        ],
        "additionalProperties": false
      },
      "InviteLookup": {
        "$id": "#/components/schemas/InviteLookup",
        "type": "object",
        "properties": {
          "email": {
            "description": "The address it's for; the sign-up form uses it and doesn't let it be changed.",
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "invitedBy": {
            "type": "string"
          },
          "expiresAt": {
            "$ref": "#/components/schemas/Timestamp"
          }
        },
        "required": [
          "invitedBy",
          "expiresAt"
        ],
        "additionalProperties": false,
        "description": "What the invitation page shows before someone signs up with it."
      },
      "AppSignIn": {
        "$id": "#/components/schemas/AppSignIn",
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "For GET /v1/app-sign-in/{id}."
          },
          "code": {
            "type": "string",
            "description": "Eight letters and digits."
          },
          "link": {
            "type": "string",
            "description": "What the QR code holds: the web app's Get the app page with the code in its fragment, which a phone's camera can open."
          },
          "appLink": {
            "type": "string",
            "description": "The same as a lawfive:// link, which opens the app."
          },
          "server": {
            "type": "string",
            "description": "The address to sign in at, for someone typing the code."
          },
          "expiresAt": {
            "$ref": "#/components/schemas/Timestamp"
          }
        },
        "required": [
          "id",
          "code",
          "link",
          "appLink",
          "server",
          "expiresAt"
        ],
        "additionalProperties": false,
        "description": "A code that signs a phone in to this account. It works once, for two minutes."
      },
      "AppSignInStatus": {
        "$id": "#/components/schemas/AppSignInStatus",
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "waiting",
              "used",
              "expired"
            ]
          },
          "deviceName": {
            "description": "The phone that used it, when it said which.",
            "type": "string"
          }
        },
        "required": [
          "status"
        ],
        "additionalProperties": false
      },
      "AppSignInCode": {
        "$id": "#/components/schemas/AppSignInCode",
        "type": "object",
        "properties": {
          "code": {
            "type": "string"
          }
        },
        "required": [
          "code"
        ],
        "additionalProperties": false
      },
      "AppSignInCheck": {
        "$id": "#/components/schemas/AppSignInCheck",
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "expiresAt": {
            "$ref": "#/components/schemas/Timestamp"
          }
        },
        "required": [
          "name",
          "email",
          "expiresAt"
        ],
        "additionalProperties": false,
        "description": "Whose the code is, so the phone can ask before signing in."
      },
      "AppSignInResult": {
        "$id": "#/components/schemas/AppSignInResult",
        "type": "object",
        "properties": {
          "token": {
            "type": "string",
            "description": "The session token, sent from then on as Authorization: Bearer <token>."
          },
          "user": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "email": {
                "type": "string"
              },
              "status": {
                "$ref": "#/components/schemas/AccountStatus"
              }
            },
            "required": [
              "id",
              "name",
              "email",
              "status"
            ],
            "additionalProperties": false
          }
        },
        "required": [
          "token",
          "user"
        ],
        "additionalProperties": false
      },
      "CalendarFeed": {
        "$id": "#/components/schemas/CalendarFeed",
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "description": "The feed (https), for calendar apps that ask for an address."
          },
          "webcalUrl": {
            "type": "string",
            "description": "The same feed as webcal://, which opens the calendar app to subscribe."
          },
          "createdAt": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "lastFetchedAt": {
            "description": "When a calendar app last fetched it, to within an hour.",
            "$ref": "#/components/schemas/Timestamp"
          }
        },
        "required": [
          "url",
          "webcalUrl",
          "createdAt"
        ],
        "additionalProperties": false,
        "description": "The account's private calendar address (CAL-1). Anyone with it can see the matches in it; make a new one to stop the old one working."
      },
      "ImportRow": {
        "$id": "#/components/schemas/ImportRow",
        "type": "object",
        "propertyNames": {
          "type": "string",
          "maxLength": 60
        },
        "additionalProperties": {
          "anyOf": [
            {
              "type": "string",
              "maxLength": 2000
            },
            {
              "type": "number"
            },
            {
              "type": "boolean"
            },
            {
              "type": "null"
            }
          ]
        },
        "description": "A row as a spreadsheet has it: column names (Home, home_colour and homeColour are the same column) and text, number or true/false values. Empty values change nothing."
      },
      "MatchImport": {
        "$id": "#/components/schemas/MatchImport",
        "type": "object",
        "properties": {
          "rows": {
            "minItems": 1,
            "maxItems": 1000,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ImportRow"
            }
          },
          "timeZone": {
            "description": "For kickoffs written without a UTC offset, when the row has no Time zone column; the web app sends the browser's. UTC when not given.",
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          },
          "dateOrder": {
            "description": "How to read dates written with slashes or dots (7/11/2026); ISO dates (2026-11-07) need none. mdy when not given.",
            "type": "string",
            "enum": [
              "dmy",
              "mdy"
            ]
          },
          "dryRun": {
            "description": "Check every row and say what would happen, without saving anything (IMP-5).",
            "type": "boolean"
          }
        },
        "required": [
          "rows"
        ],
        "additionalProperties": false,
        "description": "Matches to add or update, from a spreadsheet, CSV or JSON file (IMP-1 to IMP-8)."
      },
      "MatchImportFileFields": {
        "$id": "#/components/schemas/MatchImportFileFields",
        "type": "object",
        "properties": {
          "timeZone": {
            "description": "For kickoffs written without a UTC offset, when the row has no Time zone column.",
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          },
          "dateOrder": {
            "description": "How to read dates written with slashes or dots (7/11/2026); ISO dates (2026-11-07) are always understood.",
            "type": "string",
            "enum": [
              "dmy",
              "mdy"
            ]
          },
          "dryRun": {
            "description": "true: check every row and say what would happen, without saving anything (IMP-5).",
            "type": "string",
            "enum": [
              "true",
              "false"
            ]
          }
        },
        "additionalProperties": false,
        "description": "The form fields of a file import (multipart/form-data), beside the file."
      },
      "ImportedRow": {
        "$id": "#/components/schemas/ImportedRow",
        "type": "object",
        "properties": {
          "row": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991,
            "description": "The row's place in rows, from 1."
          },
          "action": {
            "type": "string",
            "enum": [
              "add",
              "update",
              "skip"
            ],
            "description": "skip: it has problems, so it wasn't imported."
          },
          "matchId": {
            "description": "The match it added or updated (or would).",
            "$ref": "#/components/schemas/Id"
          },
          "title": {
            "description": "The match in a few words, for showing the row: home v away.",
            "type": "string"
          },
          "kickoff": {
            "description": "Its kickoff, once imported.",
            "$ref": "#/components/schemas/Timestamp"
          },
          "problems": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Why it's skipped, naming the column; empty when it isn't."
          }
        },
        "required": [
          "row",
          "action",
          "problems"
        ],
        "additionalProperties": false
      },
      "MatchImportResult": {
        "$id": "#/components/schemas/MatchImportResult",
        "type": "object",
        "properties": {
          "rows": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ImportedRow"
            },
            "description": "Every row that had anything in it, in order."
          },
          "added": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "updated": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "skipped": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "created": {
            "type": "object",
            "properties": {
              "venues": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "competitions": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "referees": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            },
            "required": [
              "venues",
              "competitions",
              "referees"
            ],
            "additionalProperties": false,
            "description": "Names the user had no record for, added (or, on a dry run, to be added) (IMP-4)."
          },
          "ignoredColumns": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Columns the import doesn't know, as written in the file."
          }
        },
        "required": [
          "rows",
          "added",
          "updated",
          "skipped",
          "created",
          "ignoredColumns"
        ],
        "additionalProperties": false
      },
      "MatchImportFileResult": {
        "$id": "#/components/schemas/MatchImportFileResult",
        "type": "object",
        "properties": {
          "rows": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ImportedRow"
            },
            "description": "Every row that had anything in it, in order."
          },
          "added": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "updated": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "skipped": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "created": {
            "type": "object",
            "properties": {
              "venues": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "competitions": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "referees": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            },
            "required": [
              "venues",
              "competitions",
              "referees"
            ],
            "additionalProperties": false,
            "description": "Names the user had no record for, added (or, on a dry run, to be added) (IMP-4)."
          },
          "ignoredColumns": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Columns the import doesn't know, as written in the file."
          },
          "file": {
            "type": "object",
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "table",
                  "json"
                ],
                "description": "table: a row of a CSV or Excel file; json: an item of a JSON list."
              },
              "lines": {
                "type": "array",
                "items": {
                  "type": "integer",
                  "minimum": -9007199254740991,
                  "maximum": 9007199254740991
                },
                "description": "Where each row is in the file, in the order of rows' row numbers: its line, or its item number."
              }
            },
            "required": [
              "kind",
              "lines"
            ],
            "additionalProperties": false,
            "description": "How to point at a row in the file."
          }
        },
        "required": [
          "rows",
          "added",
          "updated",
          "skipped",
          "created",
          "ignoredColumns",
          "file"
        ],
        "additionalProperties": false,
        "description": "MatchImportResult, for a file (IMP-2)."
      },
      "RefereeMerge": {
        "$id": "#/components/schemas/RefereeMerge",
        "type": "object",
        "properties": {
          "from": {
            "minItems": 1,
            "maxItems": 20,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Id"
            },
            "description": "The referees to merge into this one; they're deleted."
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100,
            "description": "The name to keep: one of the referees' names or other names. The rest become other names."
          },
          "email": {
            "description": "When they have different emails, the one to keep; otherwise the one there is.",
            "type": "string",
            "format": "email",
            "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
          },
          "phone": {
            "description": "When they have different phone numbers, the one to keep; otherwise the one there is.",
            "type": "string",
            "minLength": 1,
            "maxLength": 30
          }
        },
        "required": [
          "from",
          "name"
        ],
        "additionalProperties": false,
        "description": "Referees to merge into one (DUP-3, DUP-4)."
      },
      "RefereeMergeResult": {
        "$id": "#/components/schemas/RefereeMergeResult",
        "type": "object",
        "properties": {
          "referee": {
            "description": "The referee they were merged into, as saved.",
            "$ref": "#/components/schemas/Referee"
          },
          "merged": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991,
            "description": "How many referees were merged into it (and deleted)."
          },
          "matchesUpdated": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991,
            "description": "Matches whose crew now names it, including crew typed as one of its names."
          }
        },
        "required": [
          "referee",
          "merged",
          "matchesUpdated"
        ],
        "additionalProperties": false
      },
      "QuizOption": {
        "$id": "#/components/schemas/QuizOption",
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1
          },
          "text": {
            "type": "string",
            "minLength": 1
          },
          "correct": {
            "type": "boolean"
          },
          "explanation": {
            "type": "string",
            "description": "Why this option is right or wrong; shown after answering."
          }
        },
        "required": [
          "id",
          "text",
          "correct",
          "explanation"
        ],
        "additionalProperties": false
      },
      "QuizRef": {
        "$id": "#/components/schemas/QuizRef",
        "type": "object",
        "properties": {
          "law": {
            "type": "integer",
            "minimum": 1,
            "maximum": 17
          },
          "section": {
            "type": [
              "string",
              "null"
            ]
          },
          "protocol": {
            "type": "string"
          }
        },
        "additionalProperties": false,
        "description": "What a question is based on: a Law and section, or a protocol."
      },
      "QuizQuestion": {
        "$id": "#/components/schemas/QuizQuestion",
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1
          },
          "status": {
            "type": "string",
            "description": "active, draft or retired; the API only sends active ones."
          },
          "type": {
            "type": "string",
            "description": "single, multiple or true_false."
          },
          "difficulty": {
            "type": "string",
            "description": "easy, medium or hard."
          },
          "refs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/QuizRef"
            }
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "question": {
            "type": "string",
            "minLength": 1
          },
          "options": {
            "minItems": 2,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/QuizOption"
            }
          },
          "editionAdded": {
            "type": "string"
          },
          "lastReviewed": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "status",
          "type",
          "difficulty",
          "refs",
          "tags",
          "question",
          "options",
          "editionAdded",
          "lastReviewed"
        ],
        "additionalProperties": false
      },
      "QuizLaw": {
        "$id": "#/components/schemas/QuizLaw",
        "type": "object",
        "properties": {
          "law": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "title": {
            "type": "string",
            "minLength": 1
          },
          "url": {
            "type": "string"
          }
        },
        "required": [
          "law",
          "title",
          "url"
        ],
        "additionalProperties": false
      },
      "QuizProtocol": {
        "$id": "#/components/schemas/QuizProtocol",
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1
          },
          "title": {
            "type": "string",
            "minLength": 1
          },
          "relatedLaws": {
            "type": "array",
            "items": {
              "type": "integer",
              "minimum": -9007199254740991,
              "maximum": 9007199254740991
            }
          },
          "url": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "title",
          "relatedLaws",
          "url"
        ],
        "additionalProperties": false
      },
      "QuizBank": {
        "$id": "#/components/schemas/QuizBank",
        "type": "object",
        "properties": {
          "schemaVersion": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "language": {
            "type": "string"
          },
          "edition": {
            "type": "string"
          },
          "effectiveDate": {
            "type": "string"
          },
          "source": {
            "type": "object",
            "properties": {
              "title": {
                "type": "string"
              },
              "url": {
                "type": "string"
              }
            },
            "required": [
              "title",
              "url"
            ],
            "additionalProperties": false
          },
          "laws": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/QuizLaw"
            }
          },
          "protocols": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/QuizProtocol"
            }
          },
          "questions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/QuizQuestion"
            }
          }
        },
        "required": [
          "schemaVersion",
          "language",
          "edition",
          "effectiveDate",
          "source",
          "laws",
          "protocols",
          "questions"
        ],
        "additionalProperties": false,
        "description": "The Laws of the Game quiz's questions in one language."
      }
    },
    "securitySchemes": {
      "bearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "Apps: the token from set-auth-token (signing in) or access_token (linking a watch)."
      },
      "session": {
        "type": "apiKey",
        "in": "cookie",
        "name": "better-auth.session_token",
        "description": "Browsers: the session cookie set by signing in."
      }
    }
  }
}
