{
  "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. https://lawfive.dev/llms.txt lists the same reference as smaller pages, with worked examples.",
    "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. That token only reaches the watch's endpoints (/v1/watch/), and of the account endpoints only sign-out and get-session (SEC-13); anything else answers 403."
    },
    {
      "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); and make plans, named sets of limits, and put each account in one (PLAN-1 to PLAN-5). 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": "Assignor calendars",
      "description": "An assignor system's private calendar link (ArbiterSports, Assignr, HorizonWebRef, RefTown and others give each official one), which the server reads every 30 minutes into the account's matches through the import (FEED-1 to FEED-10): new games added, moved ones changed, ones gone before kickoff canceled. The link is a password, so it's kept encrypted and never sent back. Only a signed-in session can manage them."
    },
    {
      "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",
        "description": "Answers 503 if the server can't reach its database within 2 seconds, for Docker's healthcheck (ADM-7). Other services, such as weather or email, don't affect it.",
        "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"
                }
              }
            }
          },
          "503": {
            "description": "It can't reach its database.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string",
                  "const": "database unavailable"
                }
              }
            }
          }
        }
      }
    },
    "/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/openapi.json": {
      "get": {
        "operationId": "getOpenapi.json",
        "tags": [
          "Server"
        ],
        "summary": "This server's API, as OpenAPI",
        "description": "The whole API as OpenAPI 3.1, for this server's version and address, so a client or an AI agent can work from what this server really does. About 340 KB, or much less compressed; https://lawfive.dev/llms.txt lists the same as smaller pages (AGT-3).",
        "security": [],
        "responses": {
          "200": {
            "description": "The description.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "openapi": {
                      "type": "string",
                      "const": "3.1.0"
                    }
                  },
                  "required": [
                    "openapi"
                  ],
                  "additionalProperties": {}
                }
              }
            }
          },
          "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 can make one; with SIGN_UP=approval the account is pending until an admin approves it (USR-2 to USR-4). An address in ADMIN_EMAILS without an invitation is pending even when sign-up is open (SEC-8). A new server's first account needs the setupCode from its logs (ADM-5).",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "$ref": "#/components/schemas/AccountName"
                  },
                  "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 name that's empty or over 100 characters (INVALID_NAME, USR-10), 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, the account is suspended (ACCOUNT_SUSPENDED, USR-7), or a phone signing in to an account that already has as many watches and phones as its plan allows (DEVICE_LIMIT, PLAN-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 (SEC-2), or too many wrong passwords for this email address (ACCOUNT_LOCKED, SEC-12): after 5 in a row it's locked for a minute, doubling to at most an hour, and even the right password waits. Retry-After says how long; resetting the password ends it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthError"
                }
              }
            }
          }
        }
      }
    },
    "/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. It also ends a lock after too many wrong passwords (SEC-12).",
        "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/update-user": {
      "post": {
        "operationId": "postAuthUpdateUser",
        "tags": [
          "Accounts"
        ],
        "summary": "Change the account's name",
        "description": "Better Auth's own endpoint, open only to the name (USR-10): the same rule as signing up, 1 to 100 characters with spaces at either end trimmed. GET /v1/account and get-session give the new name straight away.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NameChange"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Changed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "boolean",
                      "const": true
                    }
                  },
                  "required": [
                    "status"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "description": "The name is empty or longer than 100 characters, or the body has something besides the name (INVALID_NAME).",
            "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, or a watch's link (WATCH_ONLY).",
            "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), or already has as many watches and phones as its plan allows (DEVICE_LIMIT, PLAN-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/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, which only reaches the watch's endpoints (SEC-13).",
            "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 waiting for approval; or a new match over the plan's limit (PLAN-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlanRefusal"
                }
              }
            }
          },
          "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. Its match records go with it, marked deleted the same way, and their tracks are deleted (RES-8); saving the match again doesn't bring them 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"
            }
          },
          {
            "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 waiting for approval; or a new venue over the plan's limit (PLAN-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlanRefusal"
                }
              }
            }
          },
          "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 waiting for approval; or a new referee over the plan's limit (PLAN-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlanRefusal"
                }
              }
            }
          },
          "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 center 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 center 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. An API key gets only what its scopes can read: without match-records:read there are no figures from match reports or tracks (distance, discipline, added time, fitness, heatmaps); without settings:read, no heart-rate zones and the standard sprint speed; without venues:read, referees:read or competitions:read, the lists naming them are empty, while the counts stay. API keys: need matches:read.",
        "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 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/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). For an API key, the columns its scopes can't read are left empty, as for /v1/stats. API keys: need matches:read.",
        "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 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/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. When the server can send email (the email capability), the new address is emailed a link that changes it when followed, once, within 24 hours (USR-9), and the old address keeps working until then; asking again replaces the link. Without email, it changes straight away. Not with API keys.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmailChange"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Changed (the server can't send email), or already the account's address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Account"
                }
              }
            }
          },
          "202": {
            "description": "A link was emailed to the new address: pendingEmail says which, and pendingEmailUntil until when.",
            "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"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteAccountEmail",
        "tags": [
          "Accounts"
        ],
        "summary": "Cancel a change of email",
        "description": "The link emailed to the new address stops working (USR-9). Nothing to cancel is fine too. Not with API keys.",
        "responses": {
          "200": {
            "description": "Canceled, or there was nothing waiting.",
            "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/confirm": {
      "post": {
        "operationId": "postAccountEmailConfirm",
        "tags": [
          "Accounts"
        ],
        "summary": "Confirm a new email address",
        "description": "What the web app's /confirm-email/<token> page sends, signed in or not (USR-9): the account's address becomes the new one, and the old address is told. It doesn't sign anyone in. The link works once, for 24 hours.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmailConfirmation"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Changed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "email": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "email"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "description": "The link was used, canceled, replaced by a newer one, has expired or is mistyped.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "Another account took the address meanwhile, 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}/plan": {
      "put": {
        "operationId": "putPeopleByIdPlan",
        "tags": [
          "People"
        ],
        "summary": "Put an account in a plan",
        "description": "With an optional last day (in the account's time zone, PLAN-9) and the plan after it, the default when not given (PLAN-3, PLAN-5). Nothing in the account is lost if it's over the new plan's limits; only adding more is refused (PLAN-4). Not with API keys.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PlanChange"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Person"
                }
              }
            }
          },
          "400": {
            "description": "The body doesn't match, there's no such plan, or the last day has passed.",
            "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"
                }
              }
            }
          },
          "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}/plan-history": {
      "get": {
        "operationId": "getPeopleByIdPlanHistory",
        "tags": [
          "People"
        ],
        "summary": "An account's plans so far",
        "description": "Each change of plan, newest first (PLAN-5): whether someone has had a trial, say. Not with API keys.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The changes.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "changes": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PlanHistoryEntry"
                      }
                    }
                  },
                  "required": [
                    "changes"
                  ],
                  "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"
                }
              }
            }
          },
          "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 afterward); 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}/unlock": {
      "post": {
        "operationId": "postPeopleByIdUnlock",
        "tags": [
          "People"
        ],
        "summary": "Unlock signing in",
        "description": "After too many wrong passwords an account's address is locked for a while (SEC-12); this ends it now. A password reset ends it too. 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": "Unlocked.",
            "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}/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/plans": {
      "get": {
        "operationId": "getPlans",
        "tags": [
          "People"
        ],
        "summary": "The plans",
        "description": "Every plan, with how many accounts are in each, and the sign-up trial (PLAN-1, PLAN-5). Not with API keys.",
        "responses": {
          "200": {
            "description": "The plans.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Plans"
                }
              }
            }
          },
          "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": "postPlans",
        "tags": [
          "People"
        ],
        "summary": "Make a plan",
        "description": "Not with API keys.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PlanInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Made.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Plan"
                }
              }
            }
          },
          "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": "Another plan has this 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/plans/{code}": {
      "put": {
        "operationId": "putPlansByCode",
        "tags": [
          "People"
        ],
        "summary": "Change a plan",
        "description": "Its name, code or limits; everyone in it gets the new limits straight away. A new code breaks anything outside LawFive that refers to the old one. Not with API keys.",
        "parameters": [
          {
            "name": "code",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PlanInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Plan"
                }
              }
            }
          },
          "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"
                }
              }
            }
          },
          "404": {
            "description": "No such plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "Another plan has the new 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"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deletePlansByCode",
        "tags": [
          "People"
        ],
        "summary": "Delete a plan",
        "description": "Not with API keys.",
        "parameters": [
          {
            "name": "code",
            "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 plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "It's in use (Plan.inUse): move its accounts to another plan 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/plan-settings": {
      "put": {
        "operationId": "putPlanSettings",
        "tags": [
          "People"
        ],
        "summary": "Choose the default plan and the sign-up trial",
        "description": "Not with API keys.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PlanSettings"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Changed: the plans as they are now.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Plans"
                }
              }
            }
          },
          "400": {
            "description": "The body doesn't match, or there's no such plan.",
            "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/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/network": {
      "get": {
        "operationId": "getServerSettingsNetwork",
        "tags": [
          "People"
        ],
        "summary": "How the server sees visitors",
        "description": "The address this server sees the asking admin at, and the TRUSTED_PROXIES and TRUSTED_IP_HEADER in use (SEC-14), so Settings → Server can show whether a proxy in front of it is believed. Both are environment values, not changed here. Not with API keys.",
        "responses": {
          "200": {
            "description": "As it is now.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NetworkView"
                }
              }
            }
          },
          "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, or already has as many watches and phones as its plan allows (PLAN-7).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlanRefusal"
                }
              }
            }
          },
          "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": "Canceled.",
            "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 canceled.",
            "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), the short name the officials line uses (WEB-20; this one's, else another's), tags together, notes joined, and who they're not. A match saved later naming a merged referee is quietly corrected to this one. Apps send the merge from POST /v1/referees/{id}/merge/preview. 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/venues/{id}/merge": {
      "post": {
        "operationId": "postVenuesByIdMerge",
        "tags": [
          "Records"
        ],
        "summary": "Merge venues into this one",
        "description": "For duplicates (DUP-7): the matches at any of them move to this venue; the others are deleted, and their names kept as this one's other names. Each of their pitches is either kept, under a name no other pitch of the venue has, or combined into a pitch that's the same, whose matches it takes, whose corners walked it adds to (the newest 20, in date order), whose position it fills when there's none, and whose other names get its name. Details are combined: the chosen name, the address and parking from the chosen venue (else whichever there is), tags together, notes joined, and which venues it's not. A match saved later with a merged venue's or pitch's id is quietly corrected to where it went. Apps send the merge from POST /v1/venues/{id}/merge/preview. An API key needs matches:write too. 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/VenueMerge"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Merged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VenueMergeResult"
                }
              }
            }
          },
          "400": {
            "description": "The body doesn't match; the name isn't one of theirs; a pitch is missing, listed twice or combined into one that isn't kept; two kept pitches share a name; or it lists this venue.",
            "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 and matches:write.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "This venue, 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/duplicates/referees": {
      "get": {
        "operationId": "getDuplicatesReferees",
        "tags": [
          "Records"
        ],
        "summary": "Likely duplicate referees",
        "description": "For Find duplicates (DUP-1, DUP-2): groups of referees who are probably one person, with why, surest first; referees marked not the same (DUP-5) aren't grouped. The server works this out so every app shows the same (DUP-11). An API key needs matches:read too, for the counts. Only when GET /v1/meta lists the merging capability. API keys: need referees:read.",
        "responses": {
          "200": {
            "description": "The groups.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DuplicateGroups"
                }
              }
            }
          },
          "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 and matches:read.",
            "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/duplicates/venues": {
      "get": {
        "operationId": "getDuplicatesVenues",
        "tags": [
          "Records"
        ],
        "summary": "Likely duplicate venues",
        "description": "For Find duplicates (DUP-7): groups of venues that are probably one place (the same address, places within 150 m, or similar names), with why, surest first; venues marked not the same aren't grouped. An API key needs matches:read too, for the counts. Only when GET /v1/meta lists the merging capability. API keys: need venues:read.",
        "responses": {
          "200": {
            "description": "The groups.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DuplicateGroups"
                }
              }
            }
          },
          "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 and matches:read.",
            "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}/duplicates": {
      "get": {
        "operationId": "getRefereesByIdDuplicates",
        "tags": [
          "Records"
        ],
        "summary": "Referees likely the same as this one",
        "description": "For Merge with… (DUP-1, DUP-2): the likeliest few, with why. Search the referees list for anyone else. An API key needs matches:read too. Only when GET /v1/meta lists the merging capability. 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 likeliest, best first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LikelyDuplicates"
                }
              }
            }
          },
          "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 and matches:read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such referee.",
            "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}/duplicates": {
      "get": {
        "operationId": "getVenuesByIdDuplicates",
        "tags": [
          "Records"
        ],
        "summary": "Venues likely the same as this one",
        "description": "For Merge with… (DUP-7): the likeliest few, with why. Search the venues list for any other. An API key needs matches:read too. Only when GET /v1/meta lists the merging capability. 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 likeliest, best first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LikelyDuplicates"
                }
              }
            }
          },
          "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 and matches:read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such venue.",
            "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/preview": {
      "post": {
        "operationId": "postRefereesByIdMergePreview",
        "tags": [
          "Records"
        ],
        "summary": "Preview merging referees",
        "description": "What merging them into this one would make, from the choices so far (DUP-4): the names, emails and phones to choose from and those chosen, and their matches together. Nothing is saved. Apps ask again whenever a choice changes, and send the answer's merge to POST /v1/referees/{id}/merge. An API key needs matches:read too. Only when GET /v1/meta lists the merging capability. 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"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RefereeMergeDraft"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The preview.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RefereeMergePreview"
                }
              }
            }
          },
          "400": {
            "description": "The body doesn't match, 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:read and matches:read.",
            "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/venues/{id}/merge/preview": {
      "post": {
        "operationId": "postVenuesByIdMergePreview",
        "tags": [
          "Records"
        ],
        "summary": "Preview merging venues",
        "description": "What merging them into this one would make, from the choices so far (DUP-7, DUP-8): the names and addresses to choose from and those chosen; every pitch of the others, riskiest first, with its suggestion or the user's choice, why, and any problem; notes; and their matches together. Nothing is saved. Apps ask again whenever a choice changes, and send the answer's merge (there when nothing needs a look) to POST /v1/venues/{id}/merge. An API key needs matches:read too. Only when GET /v1/meta lists the merging capability. 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"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VenueMergeDraft"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The preview.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VenueMergePreview"
                }
              }
            }
          },
          "400": {
            "description": "The body doesn't match, or it lists this venue.",
            "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:read and matches:read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "This venue, 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/referees/{id}/not-the-same": {
      "post": {
        "operationId": "postRefereesByIdNotTheSame",
        "tags": [
          "Records"
        ],
        "summary": "Say referees aren't the same person",
        "description": "Not the same person (DUP-5): each of them remembers the others in distinctFrom, so they aren't suggested together again. They can still be merged by hand. Only when GET /v1/meta lists the merging capability. 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/NotTheSame"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    }
                  },
                  "required": [
                    "ok"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "description": "The body doesn't match, 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.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "This referee, or one of the others, 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/venues/{id}/not-the-same": {
      "post": {
        "operationId": "postVenuesByIdNotTheSame",
        "tags": [
          "Records"
        ],
        "summary": "Say venues aren't the same place",
        "description": "Not the same venue (DUP-7): each of them remembers the others in distinctFrom, so they aren't suggested together again. They can still be merged by hand. Only when GET /v1/meta lists the merging capability. 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/NotTheSame"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    }
                  },
                  "required": [
                    "ok"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "description": "The body doesn't match, or it lists this venue.",
            "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.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "This venue, or one of the others, 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, pitches, competitions and referees are found by name, ignoring case (venues, pitches and referees by their other names too), 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| Assignor | `assignor` | Who assigned you the match: a person, or the assigning group's name. | Dana Whitfield | 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: made from both team names (the first three letters or digits of each, skipping the words both names start with), and it follows them when they change. | 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: made from both team names (the first three letters or digits of each, skipping the words both names start with), and it follows them when they change. | 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| Venue address | `venueAddress` | The venue's street address, for directions: given to a venue that's added, or one that has no address yet. | 969 Caribbean Dr, Sunnyvale, CA 94089 | 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, 1 to 4: 2 for halves, 4 for quarters, 1 for a tournament game of one period. 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 matches, venues or referees it would add are over the plan's limits (PLAN-4), so nothing is saved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlanRefusal"
                }
              }
            }
          },
          "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 (5 MB, or the plan's fileMb), 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 matches, venues or referees it would add are over the plan's limits (PLAN-4), so nothing is saved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlanRefusal"
                }
              }
            }
          },
          "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/assignor-calendars": {
      "get": {
        "operationId": "getAssignorCalendars",
        "tags": [
          "Assignor calendars"
        ],
        "summary": "The account's assignor calendars",
        "description": "Oldest first, each with how its latest read went. Only when GET /v1/meta lists the assignor-calendars capability. Not with API keys.",
        "responses": {
          "200": {
            "description": "The calendars.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "calendars": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AssignorCalendar"
                      }
                    }
                  },
                  "required": [
                    "calendars"
                  ],
                  "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": "postAssignorCalendars",
        "tags": [
          "Assignor calendars"
        ],
        "summary": "Connect an assignor calendar",
        "description": "Reads it straight away, adding its games still to come, then every 30 minutes. As many as the account's plan allows (FEED-10). Only when GET /v1/meta lists the assignor-calendars capability. Not with API keys.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewAssignorCalendar"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Connected and read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssignorCalendar"
                }
              }
            }
          },
          "400": {
            "description": "The body doesn't match, or the link can't be read; error says why.",
            "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, or the account already has as many assignor calendars as its plan allows (PLAN-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlanRefusal"
                }
              }
            }
          },
          "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/assignor-calendars/preview": {
      "post": {
        "operationId": "postAssignorCalendarsPreview",
        "tags": [
          "Assignor calendars"
        ],
        "summary": "Preview a calendar link",
        "description": "Reads the link once and says what it holds: the games still to come, how many are already in LawFive, and the first three as they'd be saved. Nothing is saved (FEED-1). Only when GET /v1/meta lists the assignor-calendars capability. Not with API keys.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AssignorCalendarLink"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "What it holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssignorCalendarPreview"
                }
              }
            }
          },
          "400": {
            "description": "The body doesn't match, or the link can't be read (not a web address, a private address, no answer, or not a calendar); error says why, in words to show the person.",
            "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/assignor-calendars/{id}": {
      "patch": {
        "operationId": "patchAssignorCalendarsById",
        "tags": [
          "Assignor calendars"
        ],
        "summary": "Change an assignor calendar",
        "description": "Its name, notifications, pausing or resuming it, or a new link. Resuming it or a new link reads it straight away. Only when GET /v1/meta lists the assignor-calendars capability. 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/AssignorCalendarChange"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssignorCalendar"
                }
              }
            }
          },
          "400": {
            "description": "The body doesn't match, or the new link can't be read; error says why.",
            "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": "deleteAssignorCalendarsById",
        "tags": [
          "Assignor calendars"
        ],
        "summary": "Remove an assignor calendar",
        "description": "It's no longer read; the matches it added stay, as ordinary matches. Only when GET /v1/meta lists the assignor-calendars capability. 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": "Removed.",
            "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/assignor-calendars/{id}/read": {
      "post": {
        "operationId": "postAssignorCalendarsByIdRead",
        "tags": [
          "Assignor calendars"
        ],
        "summary": "Read an assignor calendar now",
        "description": "Instead of waiting for its next read; a paused one stays paused. Only when GET /v1/meta lists the assignor-calendars capability. 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": "Read, with how it went.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssignorCalendar"
                }
              }
            }
          },
          "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/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, canceled 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/matches": {
      "get": {
        "operationId": "getDevicesMatches",
        "tags": [
          "Devices"
        ],
        "summary": "Which matches are on which watch",
        "description": "For a match's page to say whether it's on a watch (SYNC-14): each match in each linked watch's latest download of its next matches (GET /v1/watch/matches, from the watch or through the phone app with the watch's link), and whether the watch has it as it is now. A watch unlinked drops out; a match deleted isn't listed. Phones fetch it as they sync and keep the last answer for when there's no signal. Not with API keys. Not with API keys.",
        "responses": {
          "200": {
            "description": "The matches on watches, newest download first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "matches": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/MatchOnWatch"
                      }
                    }
                  },
                  "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/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/notices/{id}": {
      "get": {
        "operationId": "getNoticesById",
        "tags": [
          "Devices"
        ],
        "summary": "What an integration did",
        "description": "The matches an API key added or changed, from a match-changed notification's id (NOTE-3), for the phone to write the notification itself. Kept for 30 days. Not with API keys. Only when GET /v1/meta lists the push capability. 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 notice.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MatchNotice"
                }
              }
            }
          },
          "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, not this user's, or more than 30 days old.",
            "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 the account's plan has one (SEC-6, PLAN-2). 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/account/plan": {
      "get": {
        "operationId": "getAccountPlan",
        "tags": [
          "Devices"
        ],
        "summary": "The account's plan",
        "description": "Its plan and limits, when it ends and what follows, and how much of each limit it's using (PLAN-6). Only when GET /v1/meta lists the plans capability. Not with API keys.",
        "responses": {
          "200": {
            "description": "The plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountPlan"
                }
              }
            }
          },
          "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": "As many as the account's plan allows (PLAN-2). 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.",
            "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, or the account already has as many keys as its plan allows (PLAN-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlanRefusal"
                }
              }
            }
          },
          "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"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "patchApiKeysById",
        "tags": [
          "API keys"
        ],
        "summary": "Change an API key",
        "description": "Whether the user's phones are told when it adds or changes matches (NOTE-1). Its access and expiry can't change: make a new key instead. 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/ApiKeyChange"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKey"
                }
              }
            }
          },
          "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"
                }
              }
            }
          }
        }
      }
    },
    "/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. Matches, referees and venues over the plan's limits (PLAN-4) aren't added, and problems says so. 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 (2 GB, or the plan's fileMb).",
            "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": "Another restore for the account is under way: one at a time.",
            "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 (or the plan's photoMb)"
                      },
                      "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": {
      "put": {
        "operationId": "putAttachmentsByIdFile",
        "tags": [
          "Attachments"
        ],
        "summary": "Replace an attachment's file",
        "description": "A photo cropped or turned by the app, in place of the old one (FILE-8): it keeps its id, place and caption, and the old files are deleted. Its url and thumbUrl change, so a kept copy of the old one isn't shown. 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": {
            "multipart/form-data": {
              "schema": {
                "allOf": [
                  {
                    "type": "object",
                    "properties": {
                      "width": {
                        "description": "The new width in pixels.",
                        "type": "string"
                      },
                      "height": {
                        "type": "string"
                      }
                    },
                    "additionalProperties": false
                  },
                  {
                    "type": "object",
                    "properties": {
                      "file": {
                        "type": "string",
                        "format": "binary",
                        "description": "required: the photo, a JPEG, PNG or WebP of at most 10 MB (or the plan's photoMb)"
                      },
                      "thumb": {
                        "type": "string",
                        "format": "binary",
                        "description": "optional: a small version, at most 1 MB"
                      }
                    },
                    "required": [
                      "file"
                    ]
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Replaced.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Attachment"
                }
              }
            }
          },
          "400": {
            "description": "With storage ({ usedBytes, limitBytes }) when a bigger file doesn't fit the account's room (FILE-7). The file is missing, too big or not a JPEG, PNG or WebP.",
            "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"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "getAttachmentsByIdFile",
        "tags": [
          "Attachments"
        ],
        "summary": "An attachment's file",
        "description": "As uploaded. A file at an address never changes, so it can be kept (Cache-Control: private, immutable); replacing it (FILE-8) gives the attachment new addresses, with ?v=. 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": "As many as the account's plan allows (PLAN-2). 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, or the address isn't allowed (a private network, unless the server allows 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, or the account already has as many webhooks as its plan allows (PLAN-4).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlanRefusal"
                }
              }
            }
          },
          "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/server-settings/test-push": {
      "post": {
        "operationId": "postServerSettingsTestPush",
        "tags": [
          "People"
        ],
        "summary": "Send a test notification",
        "description": "To the signed-in admin's own phones that have notifications on, through the push relay (PUSH-6). Not with API keys.",
        "responses": {
          "200": {
            "description": "The relay took it.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "sent": {
                      "type": "integer",
                      "exclusiveMinimum": 0,
                      "maximum": 9007199254740991,
                      "description": "How many phones."
                    }
                  },
                  "required": [
                    "sent"
                  ],
                  "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": "Notifications are off on this server, or none of the admin's phones has them on.",
            "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 relay didn't take it; the error says why.",
            "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. A watch's link asking is noted as the watch now having these matches (SYNC-14, GET /v1/devices/matches), unless it's the phone app fetching them ahead for the watch (prefetch). 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
            }
          },
          {
            "name": "prefetch",
            "in": "query",
            "required": false,
            "description": "The phone app fetching the watch's next matches ahead (APP-7), for when the watch asks without signal: not noted as on the watch.",
            "schema": {
              "description": "The phone app fetching the watch's next matches ahead (APP-7), for when the watch asks without signal: not noted as on the watch.",
              "type": "string",
              "enum": [
                "true"
              ]
            }
          }
        ],
        "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"
                    },
                    "teamSwipes": {
                      "description": "How swipes open the team controls (INP-10), once it's been set for the account.",
                      "$ref": "#/components/schemas/TeamSwipes"
                    },
                    "misconductScheme": {
                      "description": "The account's misconduct codes, for a quick match's card reasons on the watch (EVT-14); a planned match carries its own.",
                      "$ref": "#/components/schemas/SchemeId"
                    }
                  },
                  "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, unless its result has been corrected since (RES-4) or it has been deleted since (RES-8): then nothing changes. A planned match deleted before its result first arrives comes back, played. Not with API keys.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WatchUpload"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated: this match was sent before (or kept as it is: corrected or deleted since).",
            "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. Every search counts against the account's plan's address searches a day (PLAN-11), cached or not. 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": "The account's plan allows no more address searches today (PLAN-11), or two of its searches are already waiting; Retry-After 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: Match time, counting up through the match (second half 45:00 to 90:00); upEach: Each half from 0; down: Time left in each period (TMR-11)."
      },
      "MatchFormat": {
        "$id": "#/components/schemas/MatchFormat",
        "type": "object",
        "properties": {
          "periods": {
            "type": "integer",
            "minimum": 1,
            "maximum": 4,
            "description": "Periods of normal time: 1 (SET-16, a tournament game of one period) to 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": {
            "description": "No longer chosen per match: the account's match screen sets it (TMR-11). Kept for older apps.",
            "$ref": "#/components/schemas/ClockStyle"
          },
          "trackStoppage": {
            "type": "boolean"
          },
          "stoppingClock": {
            "description": "The clock (TMR-15): true stops the match clock while play is stopped (a stoppage, the halfway break), as US high school (NFHS) and college (NCAA) games keep time, with no added time; it needs trackStoppage. Missing means it runs on, as the Laws have it.",
            "type": "boolean"
          },
          "stopForGoalsCards": {
            "description": "With a stopping clock, recording a goal or a card stops it, until Start/Stop at the restart (TMR-15). Missing means on.",
            "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
          },
          "goldenGoal": {
            "description": "With extra time, a goal in it ends the match, the scoring team winning (ET-6). Missing means off.",
            "type": "boolean"
          },
          "penalties": {
            "type": "boolean"
          },
          "sinBins": {
            "description": "Temporary dismissals (EVT-12): Sin bin in the watch's team menu, a countdown with the match clock. Missing means off.",
            "type": "boolean"
          },
          "sinBinSec": {
            "description": "A sin bin's length (EVT-12). Missing means 10 minutes.",
            "type": "integer",
            "minimum": 60,
            "maximum": 1200
          }
        },
        "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
                },
                "short": {
                  "description": "A few words for a watch's list of reasons (EVT-14), \"Unsporting behavior\"; the description, cut short, when left out.",
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 22
                }
              },
              "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
                },
                "short": {
                  "description": "A few words for a watch's list of reasons (EVT-14), \"Unsporting behavior\"; the description, cut short, when left out.",
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 22
                }
              },
              "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": "A short name typed for the watch. With none, one is made from the two team names: the first three letters or digits of each in capitals, skipping the words both names start with when those would be the same (L84_Roach and L84_Smith show ROA and SMI), or HOM/AWY with no name (SET-5). Leave it out to have it follow the names.",
            "type": "string",
            "minLength": 1,
            "maxLength": 4
          },
          "colour": {
            "description": "The team's primary color; 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"
          },
          "assignor": {
            "description": "Who assigned the match (SET-15): a name, typed in or set by an integration or import.",
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "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
          },
          "aliases": {
            "description": "Other names it goes by, such as those of pitches combined into it by a merge (DUP-7); imports and pickers find the pitch by them.",
            "maxItems": 20,
            "type": "array",
            "items": {
              "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
          },
          "aliases": {
            "description": "Other names it goes by, such as the ones merged into this venue (DUP-7); imports and searches find the venue by them.",
            "maxItems": 50,
            "type": "array",
            "items": {
              "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"
          },
          "distinctFrom": {
            "description": "Venues the user said aren't the same place, so they aren't suggested as duplicates again (DUP-7).",
            "maxItems": 500,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Id"
            }
          }
        },
        "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
          },
          "shortName": {
            "description": "What the officials line on match cards calls them (WEB-20); without it, their surname as guessed from the name.",
            "type": "string",
            "minLength": 1,
            "maxLength": 30
          },
          "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 running screen as watch builds from before the four choices (UI-21) know it: the nearest of the six old layouts. scoreboard is no longer offered."
      },
      "WatchClocks": {
        "$id": "#/components/schemas/WatchClocks",
        "type": "string",
        "enum": [
          "one",
          "stacked",
          "sideBySide"
        ],
        "description": "One clock, or two: a smaller second clock under the first, or two the same size side by side (UI-21)."
      },
      "BreakClockStyle": {
        "$id": "#/components/schemas/BreakClockStyle",
        "type": "string",
        "enum": [
          "down",
          "up"
        ],
        "description": "How the break clock counts (TMR-4): down is the time left, up the time taken."
      },
      "WatchEdge": {
        "$id": "#/components/schemas/WatchEdge",
        "type": "string",
        "enum": [
          "none",
          "ring",
          "time"
        ],
        "description": "What the edge of the running screen shows (UI-21): nothing, the ring (UI-23) or the time of day (UI-25)."
      },
      "WatchScreen": {
        "$id": "#/components/schemas/WatchScreen",
        "type": "object",
        "properties": {
          "layout": {
            "description": "For older watch builds: the nearest old layout, kept in step with the fields below.",
            "$ref": "#/components/schemas/WatchLayout"
          },
          "secondClock": {
            "description": "What the second clock shows (UI-22); down is the time left. Never the same as mainClock.",
            "$ref": "#/components/schemas/ClockStyle"
          },
          "clocks": {
            "description": "Missing in setups saved before the four choices (UI-21): worked out from layout.",
            "$ref": "#/components/schemas/WatchClocks"
          },
          "mainClock": {
            "description": "How the match clock counts (TMR-11); up (Match time) when missing.",
            "$ref": "#/components/schemas/ClockStyle"
          },
          "breakClock": {
            "description": "How the break clock counts (TMR-4); down when missing.",
            "$ref": "#/components/schemas/BreakClockStyle"
          },
          "edge": {
            "description": "Missing in older setups: worked out from layout.",
            "$ref": "#/components/schemas/WatchEdge"
          },
          "names": {
            "description": "The team names over the clock, or false for scores only. Missing in older setups: worked out from layout.",
            "type": "boolean"
          },
          "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-21, UI-24)."
      },
      "TeamSwipes": {
        "$id": "#/components/schemas/TeamSwipes",
        "type": "string",
        "enum": [
          "upDown",
          "sideways"
        ],
        "description": "How a swipe on the watch's match screen opens a team's controls (INP-10): upDown is down for home and up for away, with sideways swipes for the game menu; sideways is right for home and left for away, with up for the game menu."
      },
      "UsualFormat": {
        "$id": "#/components/schemas/UsualFormat",
        "type": "object",
        "properties": {
          "format": {
            "$ref": "#/components/schemas/MatchFormat"
          },
          "templateId": {
            "description": "The template it came from, if it did, so the form can show its name.",
            "$ref": "#/components/schemas/Id"
          },
          "starterId": {
            "description": "The starter format it came from (starterFormats' id, \"us-u10\"), if it did and is unchanged.",
            "type": "string",
            "maxLength": 30
          },
          "changedAt": {
            "description": "When it was last set.",
            "$ref": "#/components/schemas/Timestamp"
          }
        },
        "required": [
          "format",
          "changedAt"
        ],
        "additionalProperties": false,
        "description": "The format new matches start with (SET-17): the last one saved on a match form, or one chosen in Settings."
      },
      "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"
          },
          "timeZone": {
            "description": "Where the person is (PLAN-9): a plan ends at the end of its last day here. Filled in from the first browser or phone that signs in.",
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          },
          "mapsApp": {
            "description": "Where Directions open (WEB-14): auto (Apple Maps in the iPhone app, Google Maps everywhere else, the same when not set), or always Google Maps or Apple Maps.",
            "type": "string",
            "enum": [
              "auto",
              "google",
              "apple"
            ]
          },
          "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 two clocks stacked (time left over match time) with the time of day and names, or their own choice.",
            "$ref": "#/components/schemas/WatchScreen"
          },
          "teamSwipes": {
            "description": "For Garmin and Apple watches (INP-10); upDown when not set. Wear OS watches always swipe sideways, as a swipe down from the top opens their quick settings.",
            "$ref": "#/components/schemas/TeamSwipes"
          },
          "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$"
          },
          "nextMatchCollapsed": {
            "description": "The match list's next match folded to a single strip (WEB-27); open when not set.",
            "type": "boolean"
          },
          "usualRole": {
            "description": "The role a new match starts with on the match forms (SET-17); referee when not set. An import, an integration or a quick match from the watch keeps its own.",
            "$ref": "#/components/schemas/Role"
          },
          "usualFormat": {
            "description": "The format a new match starts with on the match forms (SET-17): the last one saved on a form, or chosen in Settings. Not set until then: forms start from defaultFormat (2×45), offering the starter formats. A competition's template still wins.",
            "$ref": "#/components/schemas/UsualFormat"
          },
          "getReady": {
            "description": "Get ready on Matches (WEB-32), the same on the website and the phones: what the person chose there, so it follows them to another browser or phone.",
            "type": "object",
            "properties": {
              "hidden": {
                "description": "Hide this list was chosen.",
                "type": "boolean"
              },
              "unitsConfirmed": {
                "description": "They're right was chosen for the units and time zone guessed.",
                "type": "boolean"
              }
            },
            "additionalProperties": false
          }
        },
        "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": "sinBin"
              },
              "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"
              },
              "lengthSec": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991,
                "description": "How long the player was to be off, from the format (EVT-12)."
              }
            },
            "required": [
              "type",
              "when",
              "team",
              "offender",
              "lengthSec"
            ],
            "additionalProperties": false,
            "description": "A temporary dismissal (EVT-12): the player off for lengthSec of the match clock."
          },
          {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "const": "secondSinBin"
              },
              "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"
              },
              "lengthSec": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              }
            },
            "required": [
              "type",
              "when",
              "team",
              "offender",
              "lengthSec"
            ],
            "additionalProperties": false,
            "description": "A second sin bin for the same numbered player, which counts as a send-off (EVT-12)."
          },
          {
            "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. With a stopping clock (TMR-15), how long the clock stood still from periodSec, adding nothing."
          },
          {
            "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": "clockAdjusted"
              },
              "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"
              },
              "clock": {
                "type": "string",
                "enum": [
                  "match",
                  "added"
                ],
                "description": "What was corrected: the match clock (the period's time) or the added time (the stoppage total)."
              },
              "changeSec": {
                "type": "integer",
                "minimum": -6000,
                "maximum": 6000,
                "description": "How far it moved, in seconds: forward (more time played, or more added) or back (negative)."
              }
            },
            "required": [
              "type",
              "when",
              "clock",
              "changeSec"
            ],
            "additionalProperties": false,
            "description": "The referee corrected a clock during play (TMR-14); periodSec is the period's time just after. Events before it keep their times, and the added-time stats leave a period with its match clock corrected out (STAT-9)."
          },
          {
            "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; time play was suspended is left out (TMR-13). With a stopping clock (TMR-15), the clock's time, so the period's length."
          },
          "stoppedSec": {
            "description": "With a stopping clock (TMR-15), how long the clock stood still in the period (its stoppages): the period ran for playedSec plus this, and any suspensions.",
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "suspended": {
            "description": "When play was suspended in the period and resumed (TMR-13), on the clock of day from startedAt: the period ran for playedSec plus these. The track's figures leave them out (TRK-3).",
            "maxItems": 20,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "fromSec": {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 9007199254740991,
                  "description": "Seconds of wall-clock time after startedAt."
                },
                "lengthSec": {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 9007199254740991
                }
              },
              "required": [
                "fromSec",
                "lengthSec"
              ],
              "additionalProperties": false
            }
          }
        },
        "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."
      },
      "RecordedResult": {
        "$id": "#/components/schemas/RecordedResult",
        "type": "object",
        "properties": {
          "outcome": {
            "type": "string",
            "enum": [
              "completed",
              "abandoned"
            ]
          },
          "score": {
            "type": "object",
            "properties": {
              "home": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "away": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              }
            },
            "required": [
              "home",
              "away"
            ],
            "additionalProperties": false
          },
          "penalties": {
            "type": "object",
            "properties": {
              "home": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "away": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              }
            },
            "required": [
              "home",
              "away"
            ],
            "additionalProperties": false
          },
          "goldenGoal": {
            "type": "boolean"
          },
          "events": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Event"
            }
          }
        },
        "required": [
          "outcome",
          "score",
          "events"
        ],
        "additionalProperties": false,
        "description": "A match's result as the watch recorded it, before any correction (RES-3)."
      },
      "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: the watch app and the watch, for example garmin:006-B3703-00 (a Garmin part number), wearos:SM-R860 (the model) or applewatch:Watch7,3 (the hardware identifier; applewatch:Apple Watch from apps before it).",
            "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
          },
          "goldenGoal": {
            "description": "The match ended on a golden goal in extra time (ET-6): the team that scored it won.",
            "type": "boolean"
          },
          "stoppingClock": {
            "description": "The match was played on a clock that stops (TMR-15), as US high school and college games are: events' times are the clock's, its stoppages are when it stood still, and there was no added time, so the added-time stats leave the match out (STAT-9).",
            "type": "boolean"
          },
          "distanceM": {
            "description": "Distance the referee covered during the match, in meters, 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"
          },
          "editedAt": {
            "description": "When its result was last corrected after the match (RES-1); set by the server whenever the outcome, score, shoot-out, golden goal or events change. A watch's later upload of the same match then changes nothing (RES-4).",
            "$ref": "#/components/schemas/Timestamp"
          },
          "original": {
            "description": "The result exactly as the watch first sent it, kept by the server the first time it's corrected (RES-3), so it can be shown or put back. Absent for results entered without a watch.",
            "$ref": "#/components/schemas/RecordedResult"
          }
        },
        "required": [
          "id",
          "version",
          "updatedAt",
          "outcome",
          "periods",
          "score",
          "events"
        ],
        "additionalProperties": false,
        "description": "What happened in a match, as recorded on a watch, or entered or corrected afterward (RES)."
      },
      "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: millimeters of rain (wetField: in the 24 hours before; rain: during the match), centimeters 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 meters, 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 center 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."
              },
              "averageM": {
                "description": "A game's distance (STAT-16): totalM over games, to the meter; the one per-game distance every page shows. Absent without distances.",
                "type": "number",
                "minimum": 0
              },
              "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, second cautions and second sin bins (EVT-12)."
              },
              "secondCautions": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "sinBins": {
                "description": "Temporary dismissals (EVT-12), second sin bins included; missing from servers before them.",
                "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
          },
          "watches": {
            "description": "The watches the games were recorded on and how much battery a match used on each, from the battery readings the watches send with a match. Absent from servers before it, and for an API key that can't read match reports.",
            "type": "object",
            "properties": {
              "games": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991,
                "description": "Games with a match report from a watch that said which watch it was."
              },
              "models": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "device": {
                      "type": "string",
                      "description": "As the watch reported it, for example garmin:006-B3703-00."
                    },
                    "name": {
                      "type": "string",
                      "description": "As people know it (deviceName): Garmin Venu 2, Galaxy Watch4, Apple Watch Series 9, or the device as given."
                    },
                    "platform": {
                      "type": "string",
                      "enum": [
                        "garmin",
                        "wearos",
                        "applewatch",
                        "other"
                      ]
                    },
                    "games": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 9007199254740991
                    },
                    "batteryGames": {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 9007199254740991,
                      "description": "Of those, games with the battery read at the first kickoff and at full time, which the drain comes from."
                    },
                    "drainPerHour": {
                      "description": "Battery used an hour, in percent: from the first kickoff to full time, over that time, halftime included, averaged over the batteryGames.",
                      "type": "number",
                      "minimum": 0
                    },
                    "drainPerGame": {
                      "description": "Battery used a game, in percent, averaged over the batteryGames.",
                      "type": "number",
                      "minimum": 0
                    }
                  },
                  "required": [
                    "device",
                    "name",
                    "platform",
                    "games",
                    "batteryGames"
                  ],
                  "additionalProperties": false
                },
                "description": "Most games first."
              }
            },
            "required": [
              "games",
              "models"
            ],
            "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"
          },
          "assignor": {
            "description": "Who assigned the match (SET-15): a name, typed in or set by an integration or import.",
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "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: the watch app and the watch, for example garmin:006-B3703-00 (a Garmin part number), wearos:SM-R860 (the model) or applewatch:Watch7,3 (the hardware identifier; applewatch:Apple Watch from apps before it).",
            "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
          },
          "goldenGoal": {
            "description": "The match ended on a golden goal in extra time (ET-6): the team that scored it won.",
            "type": "boolean"
          },
          "stoppingClock": {
            "description": "The match was played on a clock that stops (TMR-15), as US high school and college games are: events' times are the clock's, its stoppages are when it stood still, and there was no added time, so the added-time stats leave the match out (STAT-9).",
            "type": "boolean"
          },
          "distanceM": {
            "description": "Distance the referee covered during the match, in meters, 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"
          },
          "editedAt": {
            "description": "When its result was last corrected after the match (RES-1); set by the server whenever the outcome, score, shoot-out, golden goal or events change. A watch's later upload of the same match then changes nothing (RES-4).",
            "$ref": "#/components/schemas/Timestamp"
          },
          "original": {
            "description": "The result exactly as the watch first sent it, kept by the server the first time it's corrected (RES-3), so it can be shown or put back. Absent for results entered without a watch.",
            "$ref": "#/components/schemas/RecordedResult"
          },
          "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
          },
          "aliases": {
            "description": "Other names it goes by, such as the ones merged into this venue (DUP-7); imports and searches find the venue by them.",
            "maxItems": 50,
            "type": "array",
            "items": {
              "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"
          },
          "distinctFrom": {
            "description": "Venues the user said aren't the same place, so they aren't suggested as duplicates again (DUP-7).",
            "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",
          "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
          },
          "shortName": {
            "description": "What the officials line on match cards calls them (WEB-20); without it, their surname as guessed from the name.",
            "type": "string",
            "minLength": 1,
            "maxLength": 30
          },
          "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"
          },
          "timeZone": {
            "description": "Where the person is (PLAN-9): a plan ends at the end of its last day here. Filled in from the first browser or phone that signs in.",
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          },
          "mapsApp": {
            "description": "Where Directions open (WEB-14): auto (Apple Maps in the iPhone app, Google Maps everywhere else, the same when not set), or always Google Maps or Apple Maps.",
            "type": "string",
            "enum": [
              "auto",
              "google",
              "apple"
            ]
          },
          "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 two clocks stacked (time left over match time) with the time of day and names, or their own choice.",
            "$ref": "#/components/schemas/WatchScreen"
          },
          "teamSwipes": {
            "description": "For Garmin and Apple watches (INP-10); upDown when not set. Wear OS watches always swipe sideways, as a swipe down from the top opens their quick settings.",
            "$ref": "#/components/schemas/TeamSwipes"
          },
          "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$"
          },
          "nextMatchCollapsed": {
            "description": "The match list's next match folded to a single strip (WEB-27); open when not set.",
            "type": "boolean"
          },
          "usualRole": {
            "description": "The role a new match starts with on the match forms (SET-17); referee when not set. An import, an integration or a quick match from the watch keeps its own.",
            "$ref": "#/components/schemas/Role"
          },
          "usualFormat": {
            "description": "The format a new match starts with on the match forms (SET-17): the last one saved on a form, or chosen in Settings. Not set until then: forms start from defaultFormat (2×45), offering the starter formats. A competition's template still wins.",
            "$ref": "#/components/schemas/UsualFormat"
          },
          "getReady": {
            "description": "Get ready on Matches (WEB-32), the same on the website and the phones: what the person chose there, so it follows them to another browser or phone.",
            "type": "object",
            "properties": {
              "hidden": {
                "description": "Hide this list was chosen.",
                "type": "boolean"
              },
              "unitsConfirmed": {
                "description": "They're right was chosen for the units and time zone guessed.",
                "type": "boolean"
              }
            },
            "additionalProperties": false
          },
          "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 recognize 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"
          },
          "notify": {
            "type": "boolean",
            "description": "Whether the user's phones are told when this key adds a match, or cancels, postpones, moves or removes one that hasn't kicked off (NOTE-1)."
          }
        },
        "required": [
          "id",
          "name",
          "prefix",
          "scopes",
          "createdAt",
          "notify"
        ],
        "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
          },
          "notify": {
            "description": "Tell the user's phones when this key adds or changes matches (NOTE-1); off when left out.",
            "type": "boolean"
          }
        },
        "required": [
          "name",
          "scopes"
        ],
        "additionalProperties": false
      },
      "ApiKeyChange": {
        "$id": "#/components/schemas/ApiKeyChange",
        "type": "object",
        "properties": {
          "notify": {
            "type": "boolean",
            "description": "Tell the user's phones when this key adds or changes matches (NOTE-1)."
          }
        },
        "required": [
          "notify"
        ],
        "additionalProperties": false,
        "description": "What can change on a key after it's made."
      },
      "MatchNoticeChange": {
        "$id": "#/components/schemas/MatchNoticeChange",
        "type": "string",
        "enum": [
          "added",
          "canceled",
          "postponed",
          "backOn",
          "kickoff",
          "venue",
          "removed"
        ],
        "description": "added: a new match; canceled, postponed; backOn: scheduled again after being canceled or postponed; kickoff: moved to another time; venue: moved to another venue; removed: deleted."
      },
      "MatchNotice": {
        "$id": "#/components/schemas/MatchNotice",
        "type": "object",
        "properties": {
          "id": {
            "$ref": "#/components/schemas/Id"
          },
          "source": {
            "type": "string",
            "description": "Who made the changes: the API key's name, which is usually the integration's, or the assignor calendar's (FEED-8)."
          },
          "createdAt": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "matches": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "matchId": {
                  "$ref": "#/components/schemas/Id"
                },
                "changes": {
                  "minItems": 1,
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/MatchNoticeChange"
                  },
                  "description": "What happened, most important first; added and removed come alone."
                },
                "title": {
                  "type": "string",
                  "description": "Home v Away, or the match's name."
                },
                "kickoff": {
                  "description": "When it kicks off now (or did, for a removed match).",
                  "$ref": "#/components/schemas/Timestamp"
                },
                "previousKickoff": {
                  "description": "With kickoff: when it was going to.",
                  "$ref": "#/components/schemas/Timestamp"
                },
                "timeZone": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 64,
                  "description": "IANA time zone name, for example America/Los_Angeles."
                },
                "venue": {
                  "description": "Where it is now: the venue, and pitch.",
                  "type": "string"
                },
                "previousVenue": {
                  "description": "With venue: where it was going to be.",
                  "type": "string"
                }
              },
              "required": [
                "matchId",
                "changes",
                "title"
              ],
              "additionalProperties": false
            },
            "description": "The matches, soonest first."
          }
        },
        "required": [
          "id",
          "source",
          "createdAt",
          "matches"
        ],
        "additionalProperties": false,
        "description": "What an integration (an API key, or an assignor calendar) did to the user's matches, collected over about a minute, for a phone to write as a notification (NOTE-3)."
      },
      "AssignorSystem": {
        "$id": "#/components/schemas/AssignorSystem",
        "type": "string",
        "enum": [
          "arbiter",
          "assignr",
          "horizon",
          "reftown",
          "gotsport",
          "spordle",
          "other"
        ],
        "description": "Which system a calendar link is from: ArbiterSports, Assignr, HorizonWebRef, RefTown, GotSport, Spordle, or another (FEED-1). Each is read the same way until it has rules of its own (FEED-5)."
      },
      "AssignorCalendarEvent": {
        "$id": "#/components/schemas/AssignorCalendarEvent",
        "type": "object",
        "properties": {
          "title": {
            "type": "string",
            "description": "Home v Away, or the match's name."
          },
          "kickoff": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "timeZone": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64,
            "description": "IANA time zone name, for example America/Los_Angeles."
          },
          "venue": {
            "type": "string"
          },
          "inLawFive": {
            "type": "boolean",
            "description": "A match already in LawFive at this kickoff, which the calendar takes on without changing (FEED-5)."
          }
        },
        "required": [
          "title",
          "kickoff",
          "inLawFive"
        ],
        "additionalProperties": false,
        "description": "A game in an assignor's calendar, as it would be saved."
      },
      "AssignorCalendarSkipped": {
        "$id": "#/components/schemas/AssignorCalendarSkipped",
        "type": "object",
        "properties": {
          "title": {
            "type": "string",
            "description": "The event's summary, or what's known of it."
          },
          "kickoff": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "reason": {
            "type": "string"
          }
        },
        "required": [
          "title",
          "reason"
        ],
        "additionalProperties": false,
        "description": "An event that couldn't be read or saved, and why (FEED-9)."
      },
      "AssignorCalendarPreview": {
        "$id": "#/components/schemas/AssignorCalendarPreview",
        "type": "object",
        "properties": {
          "calendarName": {
            "description": "What the calendar calls itself (X-WR-CALNAME).",
            "type": "string"
          },
          "games": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991,
            "description": "Games still to come."
          },
          "from": {
            "description": "The first one's kickoff.",
            "$ref": "#/components/schemas/Timestamp"
          },
          "to": {
            "description": "The last one's kickoff.",
            "$ref": "#/components/schemas/Timestamp"
          },
          "inLawFive": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991,
            "description": "Of those, how many are already in LawFive at the same kickoff."
          },
          "first": {
            "maxItems": 3,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AssignorCalendarEvent"
            },
            "description": "The first three, as they'd be saved."
          },
          "skipped": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AssignorCalendarSkipped"
            },
            "description": "Events that couldn't be read, at most 20."
          }
        },
        "required": [
          "games",
          "inLawFive",
          "first",
          "skipped"
        ],
        "additionalProperties": false,
        "description": "What a calendar link holds, before connecting it; nothing is saved (FEED-1)."
      },
      "AssignorCalendar": {
        "$id": "#/components/schemas/AssignorCalendar",
        "type": "object",
        "properties": {
          "id": {
            "$ref": "#/components/schemas/Id"
          },
          "name": {
            "type": "string"
          },
          "system": {
            "$ref": "#/components/schemas/AssignorSystem"
          },
          "link": {
            "type": "string",
            "description": "The start of the link, to recognize it by: the whole link is a password, so it's never sent back (FEED-2)."
          },
          "notify": {
            "type": "boolean",
            "description": "Tell the account's phones when it adds, moves or cancels a match (FEED-8, NOTE-1)."
          },
          "active": {
            "type": "boolean",
            "description": "false: paused, by the person or after the link stopped working (FEED-9); not read."
          },
          "createdAt": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "lastReadAt": {
            "description": "When it was last read, whatever happened.",
            "$ref": "#/components/schemas/Timestamp"
          },
          "lastChangedAt": {
            "description": "When a read last added or changed a match.",
            "$ref": "#/components/schemas/Timestamp"
          },
          "lastResult": {
            "description": "What the latest read that worked found and did.",
            "type": "object",
            "properties": {
              "games": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991,
                "description": "Games still to come in it."
              },
              "added": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "updated": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              },
              "canceled": {
                "type": "integer",
                "minimum": 0,
                "maximum": 9007199254740991
              }
            },
            "required": [
              "games",
              "added",
              "updated",
              "canceled"
            ],
            "additionalProperties": false
          },
          "problem": {
            "description": "Why the latest read didn't work, or why it's paused.",
            "type": "string"
          },
          "stoppedAt": {
            "description": "When it was paused because the link stopped working (FEED-9).",
            "$ref": "#/components/schemas/Timestamp"
          },
          "skipped": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AssignorCalendarSkipped"
            },
            "description": "Events the latest read couldn't save, at most 20."
          }
        },
        "required": [
          "id",
          "name",
          "system",
          "link",
          "notify",
          "active",
          "createdAt",
          "skipped"
        ],
        "additionalProperties": false,
        "description": "An assignor's calendar link, read into the account's matches every 30 minutes (FEED-1 to FEED-9)."
      },
      "AssignorCalendarLink": {
        "$id": "#/components/schemas/AssignorCalendarLink",
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "minLength": 1,
            "maxLength": 2000,
            "description": "The calendar link: https, http, or webcal (read as https)."
          },
          "system": {
            "$ref": "#/components/schemas/AssignorSystem"
          }
        },
        "required": [
          "url",
          "system"
        ],
        "additionalProperties": false
      },
      "NewAssignorCalendar": {
        "$id": "#/components/schemas/NewAssignorCalendar",
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "minLength": 1,
            "maxLength": 2000,
            "description": "The calendar link: https, http, or webcal (read as https)."
          },
          "system": {
            "$ref": "#/components/schemas/AssignorSystem"
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 60
          },
          "notify": {
            "description": "true when left out (FEED-8).",
            "type": "boolean"
          }
        },
        "required": [
          "url",
          "system",
          "name"
        ],
        "additionalProperties": false
      },
      "AssignorCalendarChange": {
        "$id": "#/components/schemas/AssignorCalendarChange",
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 60
          },
          "notify": {
            "type": "boolean"
          },
          "active": {
            "description": "false pauses it; true starts reading it again, straight away.",
            "type": "boolean"
          },
          "url": {
            "description": "A new link, such as one the assignor system made again (FEED-9); read straight away.",
            "type": "string",
            "minLength": 1,
            "maxLength": 2000
          }
        },
        "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": "Only what a public page shows: without its notes, outlook, fee, crew, tags, match number, assignor or the ids of its venue, pitch, competition and template.",
            "$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 recognize 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"
          },
          "notify": {
            "type": "boolean",
            "description": "Whether the user's phones are told when this key adds a match, or cancels, postpones, moves or removes one that hasn't kicked off (NOTE-1)."
          },
          "key": {
            "type": "string",
            "description": "The key itself, shown only this once: send it as Authorization: Bearer <key>."
          }
        },
        "required": [
          "id",
          "name",
          "prefix",
          "scopes",
          "createdAt",
          "notify",
          "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."
          },
          "openApiUrl": {
            "description": "Where this server describes its own API as OpenAPI (AGT-3); missing on servers from before it.",
            "type": "string"
          },
          "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, notices (with push, from servers that tell phones what integrations did, NOTE-1) and weather when they're switched on; merging, from servers that find duplicates and preview merges for the apps (DUP-11); plans, from servers with plans and the account's time zone (PLAN-6, PLAN-9); assignor-calendars, from servers that read assignors' calendar links (FEED-1). 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, with the setup code when setupCodeNeeded, and 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."
          },
          "watchOnly": {
            "description": "true for a watch's link, which only syncs matches (SEC-13); absent for everything else.",
            "type": "boolean"
          }
        },
        "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
          },
          "goldenGoal": {
            "type": "boolean",
            "description": "A goal in extra time ends the match (ET-6)."
          },
          "penalties": {
            "type": "boolean"
          },
          "clockStyle": {
            "anyOf": [
              {
                "type": "number",
                "const": 0
              },
              {
                "type": "number",
                "const": 1
              },
              {
                "type": "number",
                "const": 2
              }
            ],
            "description": "For older watch builds: the account's match clock (TMR-11), 0 Match time, 1 each period from 0, 2 time left. Newer ones take it from the match screen."
          },
          "trackStoppage": {
            "type": "boolean"
          },
          "stoppingClock": {
            "description": "The match clock stops while play is stopped, with no added time (TMR-15); trackStoppage is on with it. Absent from servers before it.",
            "type": "boolean"
          },
          "stopForGoalsCards": {
            "description": "With a stopping clock, a goal or card recorded stops it (TMR-15).",
            "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"
          },
          "sinBins": {
            "type": "boolean",
            "description": "Sin bin in the team menu (EVT-12)."
          },
          "sinBinSec": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991,
            "description": "A sin bin's length, 10 minutes unless the format says."
          },
          "misconductScheme": {
            "description": "The codes for a card's reason on the watch (EVT-14): the competition's, else the account's.",
            "$ref": "#/components/schemas/SchemeId"
          }
        },
        "required": [
          "id",
          "kickoff",
          "title",
          "name",
          "where",
          "role",
          "weather",
          "home",
          "away",
          "homeName",
          "awayName",
          "homeColor",
          "awayColor",
          "numPeriods",
          "periodSec",
          "halftimeSec",
          "teamSize",
          "midBreak",
          "breakLenSec",
          "extraTime",
          "etPeriods",
          "etPeriodSec",
          "goldenGoal",
          "penalties",
          "clockStyle",
          "trackStoppage",
          "breakStoppage",
          "goalScorers",
          "goalTypes",
          "sinBins",
          "sinBinSec"
        ],
        "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, SB a sin bin, with its length in seconds in extra, SB2 a second sin bin for the same number, which counts as a send-off (EVT-12), TY and TR a team official's caution and send-off, with the role in extra (0 head coach, 1 assistant coach, 2 other), I injury, S stoppage, CK and CA the match clock and the added time corrected during play (TMR-14), with the change in seconds in extra (negative for back), E end of period, A abandoned, with the reason in extra (0 weather, 1 lightning, 2 crowd trouble, 3 not enough players, 4 other), SU play suspended, with the reason in extra as for A (TMR-13), RS play resumed, with the whole seconds suspended in extra, both at the period time where play stopped, 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": {
            "description": "The nearest old layout. A watch that sends only this and secondClock is a build from before the four choices (UI-21).",
            "$ref": "#/components/schemas/WatchLayout"
          },
          "secondClock": {
            "$ref": "#/components/schemas/ClockStyle"
          },
          "clocks": {
            "$ref": "#/components/schemas/WatchClocks"
          },
          "mainClock": {
            "$ref": "#/components/schemas/ClockStyle"
          },
          "breakClock": {
            "$ref": "#/components/schemas/BreakClockStyle"
          },
          "edge": {
            "$ref": "#/components/schemas/WatchEdge"
          },
          "names": {
            "type": "boolean"
          },
          "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-21, 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
          },
          "goldenGoal": {
            "description": "The format's golden goal (ET-6).",
            "type": "boolean"
          },
          "penalties": {
            "type": "boolean"
          },
          "clockStyle": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "trackStoppage": {
            "type": "boolean"
          },
          "stoppingClock": {
            "description": "The format's stopping clock (TMR-15): every event's seconds are the clock's, which stood still during stoppages (S).",
            "type": "boolean"
          },
          "stopForGoalsCards": {
            "type": "boolean"
          },
          "breakStoppage": {
            "type": "boolean"
          },
          "goalScorers": {
            "type": "boolean"
          },
          "goalTypes": {
            "type": "boolean"
          },
          "sinBins": {
            "description": "The format's sin bins (EVT-12).",
            "type": "boolean"
          },
          "sinBinSec": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "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"
              }
            ]
          },
          "golden": {
            "description": "It ended on a golden goal in extra time (ET-6).",
            "type": "boolean"
          },
          "distanceM": {
            "description": "How far the referee ran, in meters.",
            "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"
            }
          },
          "misconduct": {
            "description": "The cards' reasons chosen on the watch (EVT-14); a code that isn't the scheme's for that card is left out.",
            "type": "object",
            "properties": {
              "scheme": {
                "description": "The codes the watch listed.",
                "$ref": "#/components/schemas/SchemeId"
              },
              "codes": {
                "maxItems": 200,
                "type": "array",
                "items": {
                  "type": "array",
                  "prefixItems": [
                    {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 9007199254740991
                    },
                    {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 12
                    }
                  ],
                  "items": false,
                  "minItems": 2,
                  "maxItems": 2
                },
                "description": "[the card's index in events, its code]."
              }
            },
            "required": [
              "scheme",
              "codes"
            ],
            "additionalProperties": false
          },
          "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)."
          },
          "pendingEmail": {
            "description": "A new address waiting for its owner to follow the link emailed to it (USR-9); email stays the old one until then.",
            "type": "string"
          },
          "pendingEmailUntil": {
            "description": "When that link stops working.",
            "$ref": "#/components/schemas/Timestamp"
          }
        },
        "required": [
          "id",
          "name",
          "email",
          "status",
          "admin"
        ],
        "additionalProperties": false,
        "description": "The signed-in account, with what it may do."
      },
      "AccountName": {
        "$id": "#/components/schemas/AccountName",
        "type": "string",
        "minLength": 1,
        "maxLength": 100,
        "description": "An account's name (USR-10): 1 to 100 characters, spaces at either end trimmed."
      },
      "NameChange": {
        "$id": "#/components/schemas/NameChange",
        "type": "object",
        "properties": {
          "name": {
            "$ref": "#/components/schemas/AccountName"
          }
        },
        "required": [
          "name"
        ],
        "additionalProperties": false,
        "description": "A new name for the signed-in account (USR-10); nothing else can be changed this way."
      },
      "EmailConfirmation": {
        "$id": "#/components/schemas/EmailConfirmation",
        "type": "object",
        "properties": {
          "token": {
            "type": "string",
            "minLength": 10,
            "maxLength": 200,
            "description": "From the link emailed to the new address: /confirm-email/<token>."
          }
        },
        "required": [
          "token"
        ],
        "additionalProperties": false,
        "description": "Following the link that confirms a new email address (USR-9)."
      },
      "MatchOnWatch": {
        "$id": "#/components/schemas/MatchOnWatch",
        "type": "object",
        "properties": {
          "matchId": {
            "$ref": "#/components/schemas/Id"
          },
          "deviceId": {
            "type": "string",
            "description": "The watch's link, as GET /v1/devices gives it (the session's id)."
          },
          "kind": {
            "type": "string",
            "description": "garmin, wearos or applewatch, from the watch's X-LawFive-Device header; watch when it didn't say."
          },
          "name": {
            "type": "string",
            "description": "For people: \"Garmin Venu 2\", \"Galaxy Watch4\", \"Apple Watch Series 9\" (\"Garmin watch\" when it didn't say which)."
          },
          "since": {
            "description": "When the watch first got the match as it has it now.",
            "$ref": "#/components/schemas/Timestamp"
          },
          "syncedAt": {
            "description": "The watch's latest download, which still had the match.",
            "$ref": "#/components/schemas/Timestamp"
          },
          "upToDate": {
            "type": "boolean",
            "description": "false when what the watch shows of the match has changed since (teams, kickoff, format, venue, officials, notes, fee), or the match is no longer scheduled: the watch's next sync brings the change, or takes the match off."
          }
        },
        "required": [
          "matchId",
          "deviceId",
          "kind",
          "name",
          "since",
          "syncedAt",
          "upToDate"
        ],
        "additionalProperties": false,
        "description": "A match on one of the account's linked watches (SYNC-14): in the watch's latest download of its next matches."
      },
      "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
      },
      "PlanLimits": {
        "$id": "#/components/schemas/PlanLimits",
        "type": "object",
        "properties": {
          "matches": {
            "description": "Matches kept, not counting deleted ones; matches recorded on a watch are never refused. Left out: no limit; 0: none at all.",
            "type": "integer",
            "minimum": 0,
            "maximum": 1000000000
          },
          "referees": {
            "description": "Referees kept. Left out: no limit; 0: none at all.",
            "type": "integer",
            "minimum": 0,
            "maximum": 1000000000
          },
          "venues": {
            "description": "Venues kept. Left out: no limit; 0: none at all.",
            "type": "integer",
            "minimum": 0,
            "maximum": 1000000000
          },
          "storageMb": {
            "description": "Megabytes of photos and tracks (SEC-6); a track recorded on a watch is never refused. Left out: no limit; 0: none at all.",
            "type": "integer",
            "minimum": 0,
            "maximum": 1000000000
          },
          "photoMb": {
            "description": "The largest photo, in megabytes; the server's own most, 10 MB, still applies. Left out: no limit; 0: none at all.",
            "type": "integer",
            "minimum": 0,
            "maximum": 1000000000
          },
          "fileMb": {
            "description": "The largest file brought in (an import or a restore), in megabytes; the server's own most for each (5 MB and 2 GB) still applies. Left out: no limit; 0: none at all.",
            "type": "integer",
            "minimum": 0,
            "maximum": 1000000000
          },
          "devices": {
            "description": "Watches and phones signed in at once (PLAN-7); browsers don't count. Left out: no limit; 0: none at all.",
            "type": "integer",
            "minimum": 0,
            "maximum": 1000000000
          },
          "pushPerDay": {
            "description": "Phone notifications a day (from midnight UTC), across all the account's phones (PLAN-8). Left out: no limit; 0: none at all.",
            "type": "integer",
            "minimum": 0,
            "maximum": 1000000000
          },
          "apiKeys": {
            "description": "API keys (KEY-1). Left out: no limit; 0: none at all.",
            "type": "integer",
            "minimum": 0,
            "maximum": 1000000000
          },
          "apiCallsPerMinute": {
            "description": "Requests made with the account's API keys a minute, all its keys together (PLAN-8). Left out: no limit; 0: none at all.",
            "type": "integer",
            "minimum": 0,
            "maximum": 1000000000
          },
          "apiCallsPerDay": {
            "description": "Requests made with the account's API keys a day (from midnight UTC), all its keys together (PLAN-8). Left out: no limit; 0: none at all.",
            "type": "integer",
            "minimum": 0,
            "maximum": 1000000000
          },
          "webhooks": {
            "description": "Webhooks (HOOK-1). Left out: no limit; 0: none at all.",
            "type": "integer",
            "minimum": 0,
            "maximum": 1000000000
          },
          "assignorCalendars": {
            "description": "Assignor calendars (FEED-10). Left out: no limit; 0: none at all.",
            "type": "integer",
            "minimum": 0,
            "maximum": 1000000000
          },
          "addressSearchesPerDay": {
            "description": "Address searches a day (from midnight UTC), for placing venues on the map (PLAN-11). Left out: no limit; 0: none at all.",
            "type": "integer",
            "minimum": 0,
            "maximum": 1000000000
          }
        },
        "additionalProperties": false,
        "description": "What a plan allows (PLAN-2)."
      },
      "PlanName": {
        "$id": "#/components/schemas/PlanName",
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "minLength": 1,
            "maxLength": 40,
            "pattern": "^[a-z0-9-]+$",
            "description": "A plan's short handle, unique on the server, to look it up by (PLAN-1): free, pro, friends-family."
          },
          "name": {
            "type": "string"
          }
        },
        "required": [
          "code",
          "name"
        ],
        "additionalProperties": false
      },
      "Plan": {
        "$id": "#/components/schemas/Plan",
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "minLength": 1,
            "maxLength": 40,
            "pattern": "^[a-z0-9-]+$",
            "description": "A plan's short handle, unique on the server, to look it up by (PLAN-1): free, pro, friends-family."
          },
          "name": {
            "type": "string"
          },
          "limits": {
            "$ref": "#/components/schemas/PlanLimits"
          },
          "default": {
            "type": "boolean",
            "description": "New accounts join it, and accounts given no plan are in it."
          },
          "people": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991,
            "description": "Accounts in it now."
          },
          "inUse": {
            "type": "boolean",
            "description": "Something refers to it (an account, an account's next plan, an unused invitation, the sign-up trial, or it's the default), so it can't be deleted."
          }
        },
        "required": [
          "code",
          "name",
          "limits",
          "default",
          "people",
          "inUse"
        ],
        "additionalProperties": false,
        "description": "A named set of limits (PLAN-1)."
      },
      "PlanInput": {
        "$id": "#/components/schemas/PlanInput",
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "minLength": 1,
            "maxLength": 40,
            "pattern": "^[a-z0-9-]+$",
            "description": "A plan's short handle, unique on the server, to look it up by (PLAN-1): free, pro, friends-family."
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 60
          },
          "limits": {
            "$ref": "#/components/schemas/PlanLimits"
          }
        },
        "required": [
          "code",
          "name",
          "limits"
        ],
        "additionalProperties": false
      },
      "SignUpTrial": {
        "$id": "#/components/schemas/SignUpTrial",
        "type": "object",
        "properties": {
          "plan": {
            "type": "string",
            "minLength": 1,
            "maxLength": 40,
            "pattern": "^[a-z0-9-]+$",
            "description": "A plan's short handle, unique on the server, to look it up by (PLAN-1): free, pro, friends-family."
          },
          "days": {
            "type": "integer",
            "minimum": 1,
            "maximum": 366
          }
        },
        "required": [
          "plan",
          "days"
        ],
        "additionalProperties": false,
        "description": "New accounts start in this plan for these days, then go to the default (PLAN-5)."
      },
      "Plans": {
        "$id": "#/components/schemas/Plans",
        "type": "object",
        "properties": {
          "plans": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Plan"
            },
            "description": "By name."
          },
          "signUpTrial": {
            "$ref": "#/components/schemas/SignUpTrial"
          }
        },
        "required": [
          "plans"
        ],
        "additionalProperties": false
      },
      "PlanSettings": {
        "$id": "#/components/schemas/PlanSettings",
        "type": "object",
        "properties": {
          "default": {
            "description": "The plan new accounts join.",
            "type": "string",
            "minLength": 1,
            "maxLength": 40,
            "pattern": "^[a-z0-9-]+$"
          },
          "signUpTrial": {
            "description": "null: no trial.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/SignUpTrial"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "additionalProperties": false
      },
      "PlanUsage": {
        "$id": "#/components/schemas/PlanUsage",
        "type": "object",
        "properties": {
          "matches": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "referees": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "venues": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "storageMb": {
            "type": "number",
            "description": "Photos and tracks, in megabytes."
          },
          "devices": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991,
            "description": "Watches and phones signed in."
          },
          "pushPerDay": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991,
            "description": "Notifications sent today (UTC)."
          },
          "apiKeys": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "apiCallsPerDay": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991,
            "description": "Requests with the account's API keys today (UTC)."
          },
          "webhooks": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "assignorCalendars": {
            "description": "Absent from older servers.",
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "addressSearchesPerDay": {
            "description": "Address searches today (UTC); absent from older servers.",
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          }
        },
        "required": [
          "matches",
          "referees",
          "venues",
          "storageMb",
          "devices",
          "pushPerDay",
          "apiKeys",
          "apiCallsPerDay",
          "webhooks"
        ],
        "additionalProperties": false,
        "description": "How much of each limit the account is using; per-minute and per-file limits aren't counted."
      },
      "AccountPlan": {
        "$id": "#/components/schemas/AccountPlan",
        "type": "object",
        "properties": {
          "plan": {
            "$ref": "#/components/schemas/PlanName"
          },
          "limits": {
            "$ref": "#/components/schemas/PlanLimits"
          },
          "since": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "endsAt": {
            "description": "When it ends: the end of endsOn in the account's time zone.",
            "$ref": "#/components/schemas/Timestamp"
          },
          "endsOn": {
            "description": "Its last day.",
            "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])))$"
          },
          "then": {
            "description": "The plan that follows when it ends.",
            "$ref": "#/components/schemas/PlanName"
          },
          "usage": {
            "$ref": "#/components/schemas/PlanUsage"
          }
        },
        "required": [
          "plan",
          "limits",
          "since",
          "usage"
        ],
        "additionalProperties": false,
        "description": "The signed-in account's plan, when it ends, and what it's using (PLAN-6)."
      },
      "PlanChange": {
        "$id": "#/components/schemas/PlanChange",
        "type": "object",
        "properties": {
          "plan": {
            "type": "string",
            "minLength": 1,
            "maxLength": 40,
            "pattern": "^[a-z0-9-]+$",
            "description": "A plan's short handle, unique on the server, to look it up by (PLAN-1): free, pro, friends-family."
          },
          "endsOn": {
            "description": "Its last day, in the account's time zone; none: it doesn't end.",
            "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])))$"
          },
          "then": {
            "description": "The plan after it ends; the default when left out.",
            "type": "string",
            "minLength": 1,
            "maxLength": 40,
            "pattern": "^[a-z0-9-]+$"
          }
        },
        "required": [
          "plan"
        ],
        "additionalProperties": false,
        "description": "Putting an account in a plan (PLAN-3)."
      },
      "PlanHistoryEntry": {
        "$id": "#/components/schemas/PlanHistoryEntry",
        "type": "object",
        "properties": {
          "from": {
            "description": "None for the first entry.",
            "$ref": "#/components/schemas/PlanName"
          },
          "to": {
            "$ref": "#/components/schemas/PlanName"
          },
          "at": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "reason": {
            "type": "string",
            "enum": [
              "sign-up",
              "invite",
              "admin",
              "ended"
            ],
            "description": "sign-up: joined (a trial, or the default then); invite: joined with an invitation's plan; admin: an admin changed it; ended: the plan before it ended."
          },
          "by": {
            "description": "The admin's name, for reason admin.",
            "type": "string"
          },
          "endsAt": {
            "description": "When this one was set to end.",
            "$ref": "#/components/schemas/Timestamp"
          }
        },
        "required": [
          "to",
          "at",
          "reason"
        ],
        "additionalProperties": false
      },
      "PersonPlan": {
        "$id": "#/components/schemas/PersonPlan",
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "minLength": 1,
            "maxLength": 40,
            "pattern": "^[a-z0-9-]+$",
            "description": "A plan's short handle, unique on the server, to look it up by (PLAN-1): free, pro, friends-family."
          },
          "name": {
            "type": "string"
          },
          "endsAt": {
            "$ref": "#/components/schemas/Timestamp"
          },
          "endsOn": {
            "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])))$"
          },
          "then": {
            "$ref": "#/components/schemas/PlanName"
          }
        },
        "required": [
          "code",
          "name"
        ],
        "additionalProperties": false
      },
      "PlanRefusal": {
        "$id": "#/components/schemas/PlanRefusal",
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "limit": {
            "description": "The plan's limit that was reached (PLAN-4).",
            "type": "string",
            "enum": [
              "matches",
              "referees",
              "venues",
              "storageMb",
              "photoMb",
              "fileMb",
              "devices",
              "pushPerDay",
              "apiKeys",
              "apiCallsPerMinute",
              "apiCallsPerDay",
              "webhooks",
              "assignorCalendars",
              "addressSearchesPerDay"
            ]
          },
          "allowed": {
            "description": "What the plan allows of it.",
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          }
        },
        "required": [
          "error"
        ],
        "additionalProperties": false,
        "description": "An ApiError; over a plan's limit, it says which (PLAN-4)."
      },
      "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"
          },
          "plan": {
            "description": "Their plan (PLAN-3).",
            "$ref": "#/components/schemas/PersonPlan"
          },
          "lockedUntil": {
            "description": "Signing in is refused until then, after too many wrong passwords (SEC-12); Unlock ends it now.",
            "$ref": "#/components/schemas/Timestamp"
          }
        },
        "required": [
          "id",
          "name",
          "email",
          "status",
          "admin",
          "adminFromSetting",
          "joinedAt",
          "plan"
        ],
        "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"
          },
          "plan": {
            "description": "The plan the account joins (PLAN-5); the default (or the sign-up trial) when there's none.",
            "$ref": "#/components/schemas/PlanName"
          },
          "planDays": {
            "description": "For how many days, then the default plan; none: it doesn't end.",
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          }
        },
        "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"
          },
          "plan": {
            "description": "The plan the account joins (PLAN-5).",
            "type": "string",
            "minLength": 1,
            "maxLength": 40,
            "pattern": "^[a-z0-9-]+$"
          },
          "planDays": {
            "description": "For how many days, then the default plan.",
            "type": "integer",
            "minimum": 1,
            "maximum": 366
          }
        },
        "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"
              }
            ]
          }
        },
        "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."
      },
      "NetworkView": {
        "$id": "#/components/schemas/NetworkView",
        "type": "object",
        "properties": {
          "yourAddress": {
            "type": "string",
            "description": "The address this server sees the request coming from, after believing the trusted proxies."
          },
          "trustedProxies": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "TRUSTED_PROXIES: the proxies believed about the visitor's address; empty, none."
          },
          "trustedIpHeader": {
            "description": "TRUSTED_IP_HEADER, when set.",
            "type": "string"
          }
        },
        "required": [
          "yourAddress",
          "trustedProxies"
        ],
        "additionalProperties": false,
        "description": "How the server tells visitors apart, for its limits on tries (SEC-2, SEC-14). Both settings are environment values only."
      },
      "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"
            ]
          },
          "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)."
      },
      "VenueMerge": {
        "$id": "#/components/schemas/VenueMerge",
        "type": "object",
        "properties": {
          "from": {
            "minItems": 1,
            "maxItems": 20,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Id"
            },
            "description": "The venues to merge into this one; they're deleted."
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100,
            "description": "The name to keep: one of the venues' names or other names. The rest become other names."
          },
          "addressFrom": {
            "description": "When they have different addresses or parking spots, the venue (this one or one of from) whose address and parking to keep; otherwise the ones there are.",
            "$ref": "#/components/schemas/Id"
          },
          "pitches": {
            "maxItems": 200,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "description": "A pitch of one of the venues in from.",
                  "$ref": "#/components/schemas/Id"
                },
                "into": {
                  "description": "The pitch it's the same as: one of this venue's, or one kept from another. Its matches move to that pitch, its corners walked are added to it, and its name becomes another name of it.",
                  "$ref": "#/components/schemas/Id"
                },
                "name": {
                  "description": "Without into, the pitch is kept, under this name (its own, or a new one): no other pitch of the merged venue may have it.",
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 60
                }
              },
              "required": [
                "id"
              ],
              "additionalProperties": false
            },
            "description": "What happens to each pitch of the venues in from: every one, once."
          }
        },
        "required": [
          "from",
          "name",
          "pitches"
        ],
        "additionalProperties": false,
        "description": "Venues to merge into one (DUP-7): the venue's details, and what happens to each of the others' pitches."
      },
      "VenueMergeResult": {
        "$id": "#/components/schemas/VenueMergeResult",
        "type": "object",
        "properties": {
          "venue": {
            "description": "The venue they were merged into, as saved.",
            "$ref": "#/components/schemas/Venue"
          },
          "merged": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991,
            "description": "How many venues were merged into it (and deleted)."
          },
          "matchesUpdated": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991,
            "description": "Matches that now point at it, and at its pitches."
          }
        },
        "required": [
          "venue",
          "merged",
          "matchesUpdated"
        ],
        "additionalProperties": false
      },
      "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
      },
      "MatchCount": {
        "$id": "#/components/schemas/MatchCount",
        "type": "object",
        "properties": {
          "played": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "upcoming": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991,
            "description": "Scheduled matches."
          },
          "last": {
            "description": "The latest played match's kickoff.",
            "$ref": "#/components/schemas/Timestamp"
          }
        },
        "required": [
          "played",
          "upcoming"
        ],
        "additionalProperties": false,
        "description": "How many matches a venue or referee has, or several as if merged."
      },
      "DuplicateMember": {
        "$id": "#/components/schemas/DuplicateMember",
        "type": "object",
        "properties": {
          "id": {
            "$ref": "#/components/schemas/Id"
          },
          "name": {
            "type": "string"
          },
          "aliases": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Its other names (DUP-6, DUP-9)."
          },
          "address": {
            "description": "A venue's.",
            "type": "string"
          },
          "pitches": {
            "description": "A venue's number of pitches.",
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "email": {
            "description": "A referee's.",
            "type": "string"
          },
          "phone": {
            "description": "A referee's.",
            "type": "string"
          },
          "matches": {
            "$ref": "#/components/schemas/MatchCount"
          },
          "reasons": {
            "description": "Why it's likely the same as the one asked about (\"Same address\", \"Same name, in a different order\"), surest first; for the likeliest only.",
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "id",
          "name",
          "aliases",
          "matches"
        ],
        "additionalProperties": false,
        "description": "A venue or referee as the duplicate finder and merge preview show it (DUP-2, DUP-7)."
      },
      "DuplicateGroups": {
        "$id": "#/components/schemas/DuplicateGroups",
        "type": "object",
        "properties": {
          "groups": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "reasons": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  },
                  "description": "Why they're probably one, in a few words each."
                },
                "members": {
                  "minItems": 2,
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/DuplicateMember"
                  },
                  "description": "Most matches first: the one a merge keeps unless the user picks another."
                }
              },
              "required": [
                "reasons",
                "members"
              ],
              "additionalProperties": false
            },
            "description": "Groups that are probably one venue or person, surest first."
          },
          "of": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991,
            "description": "How many venues or referees there are in all."
          }
        },
        "required": [
          "groups",
          "of"
        ],
        "additionalProperties": false,
        "description": "Likely duplicates across all of the user's venues or referees (DUP-1, DUP-7)."
      },
      "LikelyDuplicates": {
        "$id": "#/components/schemas/LikelyDuplicates",
        "type": "object",
        "properties": {
          "likely": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DuplicateMember"
            },
            "description": "At most six, likeliest first, each with its reasons."
          }
        },
        "required": [
          "likely"
        ],
        "additionalProperties": false,
        "description": "The venues or referees most likely the same as one (Merge with…, DUP-1, DUP-7)."
      },
      "NotTheSame": {
        "$id": "#/components/schemas/NotTheSame",
        "type": "object",
        "properties": {
          "others": {
            "minItems": 1,
            "maxItems": 20,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Id"
            },
            "description": "Venues or referees that aren't the same as this one, nor as each other."
          }
        },
        "required": [
          "others"
        ],
        "additionalProperties": false,
        "description": "Venues or referees the user says are different, so they aren't suggested together again (DUP-5)."
      },
      "RefereeMergeDraft": {
        "$id": "#/components/schemas/RefereeMergeDraft",
        "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": {
            "description": "The name chosen so far; left out, or not one of theirs, the kept referee's.",
            "type": "string",
            "maxLength": 100
          },
          "email": {
            "description": "The email chosen so far, where they differ.",
            "type": "string",
            "maxLength": 254
          },
          "phone": {
            "description": "The phone number chosen so far, where they differ.",
            "type": "string",
            "maxLength": 30
          }
        },
        "required": [
          "from"
        ],
        "additionalProperties": false,
        "description": "A referee merge as chosen so far, to preview (DUP-4)."
      },
      "RefereeMergePreview": {
        "$id": "#/components/schemas/RefereeMergePreview",
        "type": "object",
        "properties": {
          "referees": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DuplicateMember"
            },
            "description": "The referee kept, then the others, in the order sent; each with its reasons when it's among the likeliest."
          },
          "names": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Every name to choose from: theirs and their other names, once each. The rest become other names."
          },
          "name": {
            "type": "string",
            "description": "The name that will be kept."
          },
          "emails": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Their emails, once each: a choice when there's more than one."
          },
          "email": {
            "type": "string"
          },
          "phones": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Their phone numbers, once each: a choice when there's more than one."
          },
          "phone": {
            "type": "string"
          },
          "together": {
            "description": "Their matches as one referee, crew typed in under one of their names included; a match counts once.",
            "$ref": "#/components/schemas/MatchCount"
          },
          "merge": {
            "description": "What to send to POST /v1/referees/{id}/merge to merge exactly this.",
            "$ref": "#/components/schemas/RefereeMerge"
          }
        },
        "required": [
          "referees",
          "names",
          "name",
          "emails",
          "phones",
          "together",
          "merge"
        ],
        "additionalProperties": false,
        "description": "What merging these referees would make (DUP-4, DUP-11)."
      },
      "VenueMergeDraft": {
        "$id": "#/components/schemas/VenueMergeDraft",
        "type": "object",
        "properties": {
          "from": {
            "minItems": 1,
            "maxItems": 20,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Id"
            },
            "description": "The venues to merge into this one; they're deleted."
          },
          "name": {
            "description": "The name chosen so far; left out, or not one of theirs, the kept venue's.",
            "type": "string",
            "maxLength": 100
          },
          "addressFrom": {
            "description": "The venue whose address and parking were chosen so far, where they differ.",
            "$ref": "#/components/schemas/Id"
          },
          "pitches": {
            "description": "The pitches the user has chosen for so far; the rest get the suggestion. A name can be empty while it's being typed.",
            "maxItems": 200,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "$ref": "#/components/schemas/Id"
                },
                "into": {
                  "description": "The pitch it's the same as.",
                  "$ref": "#/components/schemas/Id"
                },
                "name": {
                  "description": "Without into, kept, under this name.",
                  "type": "string",
                  "maxLength": 60
                }
              },
              "required": [
                "id"
              ],
              "additionalProperties": false
            }
          }
        },
        "required": [
          "from"
        ],
        "additionalProperties": false,
        "description": "A venue merge as chosen so far, to preview (DUP-7, DUP-8)."
      },
      "VenueMergePreview": {
        "$id": "#/components/schemas/VenueMergePreview",
        "type": "object",
        "properties": {
          "venues": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DuplicateMember"
            },
            "description": "The venue kept, then the others, in the order sent; each with its reasons when it's among the likeliest."
          },
          "names": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Every name to choose from: theirs and their other names, once each. The rest become other names."
          },
          "name": {
            "type": "string",
            "description": "The name that will be kept."
          },
          "places": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "venueId": {
                  "$ref": "#/components/schemas/Id"
                },
                "address": {
                  "type": "string"
                },
                "onMap": {
                  "type": "boolean",
                  "description": "Whether its parking is on the map."
                }
              },
              "required": [
                "venueId",
                "onMap"
              ],
              "additionalProperties": false
            },
            "description": "Their different addresses and parking spots, once each: a choice when there's more than one."
          },
          "addressFrom": {
            "description": "The venue whose address and parking will be kept.",
            "$ref": "#/components/schemas/Id"
          },
          "pitches": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "$ref": "#/components/schemas/Id"
                },
                "name": {
                  "type": "string",
                  "description": "Its name now."
                },
                "venue": {
                  "description": "The venue it's from, when there are several.",
                  "type": "string"
                },
                "matches": {
                  "type": "integer",
                  "minimum": -9007199254740991,
                  "maximum": 9007199254740991
                },
                "corners": {
                  "type": "integer",
                  "minimum": -9007199254740991,
                  "maximum": 9007199254740991,
                  "description": "How many times its corners were walked on the watch (TRK-13)."
                },
                "onMap": {
                  "type": "boolean"
                },
                "check": {
                  "type": "boolean",
                  "description": "Whether to look at it first (shown as Check); the rest are likely fine."
                },
                "why": {
                  "type": "string",
                  "description": "Why it's suggested as it is, in a few words."
                },
                "choice": {
                  "type": "object",
                  "properties": {
                    "into": {
                      "description": "The pitch it's the same as.",
                      "$ref": "#/components/schemas/Id"
                    },
                    "name": {
                      "description": "Without into, kept, under this name.",
                      "type": "string",
                      "maxLength": 60
                    }
                  },
                  "additionalProperties": false,
                  "description": "What happens to it: the user's choice, else the suggestion."
                },
                "targets": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "$ref": "#/components/schemas/Id"
                      },
                      "name": {
                        "type": "string"
                      }
                    },
                    "required": [
                      "id",
                      "name"
                    ],
                    "additionalProperties": false
                  },
                  "description": "The pitches it can be the same as: those the merged venue keeps, under the names they'll have."
                },
                "problem": {
                  "description": "What's wrong with its choice; the merge waits until there's none.",
                  "type": "string"
                }
              },
              "required": [
                "id",
                "name",
                "matches",
                "corners",
                "onMap",
                "check",
                "why",
                "choice",
                "targets"
              ],
              "additionalProperties": false
            },
            "description": "Every pitch of the venues merged away, riskiest first, then more matches and corners walked first (DUP-8). The order depends only on the venues, so rows stay put while the choices change."
          },
          "notes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Things to know, such as corners walked over the 20 a pitch keeps."
          },
          "problems": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991,
            "description": "Pitches that need a look before merging."
          },
          "together": {
            "description": "Their matches, at one venue.",
            "$ref": "#/components/schemas/MatchCount"
          },
          "merge": {
            "description": "What to send to POST /v1/venues/{id}/merge to merge exactly this; none while there are problems.",
            "$ref": "#/components/schemas/VenueMerge"
          }
        },
        "required": [
          "venues",
          "names",
          "name",
          "places",
          "pitches",
          "notes",
          "problems",
          "together"
        ],
        "additionalProperties": false,
        "description": "What merging these venues would make (DUP-7, DUP-8, DUP-11)."
      },
      "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."
      }
    }
  }
}
