{
 "openapi": "3.1.0",
 "info": {
  "title": "TeesDeSalida",
  "version": "1.0.0",
  "summary": "Live tee times, golf clubs and tournaments around Madrid, on the Costa del Sol and around Valencia.",
  "description": "Free tee times, green fees and tournaments at golf clubs around Madrid, on the Costa del Sol and around Valencia, from TeesDeSalida (https://teesdesalida.es). Tee times are read live from each club's own booking system when asked; the club directory and the tournament calendar are static files rebuilt daily.\n\nFree to use, no key. Prices are per player in EUR; times are Madrid local time. TeesDeSalida takes no bookings: link the golfer to the club's bookingUrl, and credit \"Live tee times from TeesDeSalida (https://teesdesalida.es). Book directly with the club.\"\n\nStart with GET /api/tee-times for \"where can I play on <date>\" questions; use /data/courses.json and /data/areas.json to resolve club and area names to ids. The same data is available to AI assistants as a remote MCP server at https://teesdesalida.es/mcp (Streamable HTTP, no auth).\n\nPlease ask only what a golfer would: every /api/tee-times call reads the clubs live. Answers are cached for 60 s, and heavy use may be rate-limited.",
  "contact": {
   "url": "https://teesdesalida.es"
  }
 },
 "servers": [
  {
   "url": "https://teesdesalida.es"
  }
 ],
 "security": [],
 "paths": {
  "/api/tee-times": {
   "get": {
    "operationId": "searchTeeTimes",
    "summary": "Search free tee times on one date",
    "description": "Finds the free tee times on one date that fit a group: in an area or at specific courses, for a party size, 9 or 18 holes, a start-time window and a budget per player. Read live from the clubs (allow up to ~25 s when searching many clubs). Use it when someone asks where or when they can play golf on a given day. Dates run from today to 60 days ahead; for several days, call once per date. Example: https://teesdesalida.es/api/tee-times?date=2026-10-10&area=madrid&players=2&band=morning\n\nArea or courses is required: the area where the golfer will play (madrid, toledo, guadalajara, segovia, avila, burgos, costa-del-sol, valencia; several are allowed, e.g. area=madrid,toledo) or the clubs (courses=olivar,cng). Each club is read live from its booking system, so search only where the golfer would play.\n\nAnswers are cached at the CDN for 60 s by their exact URL: to share the cache with other clients, send only the params you need, in this order: date, area, courses, players, holes, from, to, band, maxPrice, order, lang; lists comma-separated (area=madrid,toledo).",
    "parameters": [
     {
      "name": "date",
      "in": "query",
      "required": true,
      "description": "The day to play, YYYY-MM-DD in Madrid's calendar: from today up to 60 days ahead. One date per call; for a weekend, search each day.",
      "schema": {
       "type": "string",
       "format": "date"
      },
      "example": "2026-10-10"
     },
     {
      "name": "area",
      "in": "query",
      "required": false,
      "description": "Areas to search (list_areas or /data/areas.json). madrid covers the Community of Madrid; toledo, guadalajara, segovia, avila and burgos are the provinces around it; costa-del-sol is the Costa del Sol, Marbella and the coast around it; valencia the Valencia area. Area or courses is required: each club is read live, so pass only where the golfer would play. Several areas are allowed; ask the golfer where rather than searching them all. marbella is the old id of costa-del-sol: it still works, but is deprecated.",
      "schema": {
       "type": "array",
       "items": {
        "type": "string",
        "enum": [
         "madrid",
         "toledo",
         "guadalajara",
         "segovia",
         "avila",
         "cuenca",
         "burgos",
         "costa-del-sol",
         "valencia"
        ]
       }
      },
      "style": "form",
      "explode": false,
      "example": [
       "madrid"
      ]
     },
     {
      "name": "courses",
      "in": "query",
      "required": false,
      "description": "Specific clubs, by id or slug (list_courses or /data/courses.json), e.g. olivar or centro-nacional-de-golf. Area or courses is required; when courses are given, area is ignored.",
      "schema": {
       "type": "array",
       "items": {
        "type": "string"
       }
      },
      "style": "form",
      "explode": false,
      "example": [
       "olivar",
       "cng"
      ]
     },
     {
      "name": "players",
      "in": "query",
      "required": false,
      "description": "How many golfers will play together (1–4). Default 1. Prices are per player.",
      "schema": {
       "type": "integer",
       "minimum": 1,
       "maximum": 4
      },
      "example": 2
     },
     {
      "name": "holes",
      "in": "query",
      "required": false,
      "description": "Round length: 18 (default) or 9 holes.",
      "schema": {
       "type": "integer",
       "enum": [
        9,
        18
       ]
      },
      "example": 18
     },
     {
      "name": "from",
      "in": "query",
      "required": false,
      "description": "Earliest start time, HH:MM 24-hour, Madrid local time (inclusive).",
      "schema": {
       "type": "string",
       "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$"
      },
      "example": "08:00"
     },
     {
      "name": "to",
      "in": "query",
      "required": false,
      "description": "Latest start time, HH:MM 24-hour, Madrid local time (exclusive).",
      "schema": {
       "type": "string",
       "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$"
      },
      "example": "11:00"
     },
     {
      "name": "band",
      "in": "query",
      "required": false,
      "description": "Part of the day, when the golfer says 'morning' etc.: morning (before 11:00), midday (11:00–15:00), afternoon (15:00 on). from/to win over it.",
      "schema": {
       "type": "string",
       "enum": [
        "morning",
        "midday",
        "afternoon"
       ]
      },
      "example": "morning"
     },
     {
      "name": "maxPrice",
      "in": "query",
      "required": false,
      "description": "Highest green fee per player, in euros. Tee times whose club publishes no price are always kept.",
      "schema": {
       "type": "number",
       "exclusiveMinimum": 0
      },
      "example": 60
     },
     {
      "name": "order",
      "in": "query",
      "required": false,
      "description": "time (earliest first, the default) or price (cheapest first).",
      "schema": {
       "type": "string",
       "enum": [
        "time",
        "price"
       ]
      },
      "example": "price"
     },
     {
      "name": "lang",
      "in": "query",
      "required": false,
      "description": "Language of the human-readable text in the answer: es (Spanish, the default) or en (English). Data values never change.",
      "schema": {
       "type": "string",
       "enum": [
        "es",
        "en"
       ]
      },
      "example": "en"
     }
    ],
    "responses": {
     "200": {
      "description": "The courses with matching tee times, and the clubs that couldn't be read or aren't open yet.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/TeeTimeReport"
        }
       }
      }
     },
     "400": {
      "description": "Bad params. `error` says what is wrong and which values are valid.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/course-days": {
   "get": {
    "operationId": "readCourseDays",
    "summary": "Every free tee time at an area's clubs on one date",
    "description": "The raw tee sheet behind the TeesDeSalida app: every free tee time on one date at one area's clubs (Madrid's by default) or at the courses named, unfiltered, with each club's status. Prefer /api/tee-times, which filters by group, time and budget; use this when you need a club's whole day.",
    "parameters": [
     {
      "name": "date",
      "in": "query",
      "required": true,
      "description": "The day, YYYY-MM-DD (Madrid), from today to 60 days ahead.",
      "schema": {
       "type": "string",
       "format": "date"
      },
      "example": "2026-10-10"
     },
     {
      "name": "region",
      "in": "query",
      "required": false,
      "description": "The area whose clubs are read (default madrid). Ignored when courses are given. marbella is the old id of costa-del-sol: it still works, but is deprecated.",
      "schema": {
       "type": "string",
       "enum": [
        "madrid",
        "toledo",
        "guadalajara",
        "segovia",
        "avila",
        "cuenca",
        "burgos",
        "costa-del-sol",
        "valencia"
       ]
      },
      "example": "madrid"
     },
     {
      "name": "courses",
      "in": "query",
      "required": false,
      "description": "Comma-separated course ids (from /data/courses.json), instead of the area's clubs.",
      "schema": {
       "type": "array",
       "items": {
        "type": "string",
        "enum": [
         "airgolf",
         "angelesdesanrafael",
         "aranjuez",
         "arqueros",
         "atalaya",
         "banus",
         "baviera",
         "cabopino",
         "calanova",
         "candeleda",
         "cng",
         "dehesa",
         "elbosque",
         "encin",
         "faisanera",
         "galiana",
         "greenpaddock",
         "higueral",
         "higueron",
         "illescas",
         "laherreria",
         "lasrozas",
         "lauro",
         "layos",
         "lerma",
         "magna",
         "marbellaclub",
         "montealvar",
         "naranjos",
         "noria",
         "olivanova",
         "olivar",
         "palomarejos",
         "paraiso",
         "quinta",
         "retamares",
         "robledal",
         "sanroque",
         "santaclara",
         "santamaria",
         "santana",
         "santander",
         "torrequebrada",
         "valleromano",
         "villapadierna"
        ]
       }
      },
      "style": "form",
      "explode": false,
      "example": [
       "olivar",
       "cng"
      ]
     },
     {
      "name": "fresh",
      "in": "query",
      "required": false,
      "description": "1 to skip every cache and read the clubs now (only right before booking).",
      "schema": {
       "type": "string",
       "enum": [
        "1"
       ]
      },
      "example": "1"
     }
    ],
    "responses": {
     "200": {
      "description": "One course day per course.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/CourseDays"
        }
       }
      }
     },
     "400": {
      "description": "Bad params. `error` says what is wrong and which values are valid.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/data/courses.json": {
   "get": {
    "operationId": "listCourses",
    "summary": "Every golf club, with green fees and facilities",
    "description": "The club directory: every club TeesDeSalida reads, with its id and slug (to pass to /api/tee-times), town, area, location, drive from the area's hub, facilities, the green fees it publishes (Spanish and English), how far ahead it takes bookings, and its booking, website and guide links. Static; rebuilt on every deploy.",
    "responses": {
     "200": {
      "description": "Every club.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/CoursesDocument"
        }
       }
      }
     }
    }
   }
  },
  "/data/areas.json": {
   "get": {
    "operationId": "listAreas",
    "summary": "The areas covered, with their number of clubs",
    "description": "Every area id TeesDeSalida understands (for the area param), with its names, hub city and number of clubs. An area with 0 clubs is understood but has none yet.",
    "responses": {
     "200": {
      "description": "Every area.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/AreasDocument"
        }
       }
      }
     }
    }
   }
  },
  "/data/tournaments.json": {
   "get": {
    "operationId": "listTournaments",
    "summary": "Upcoming amateur tournaments at the clubs covered",
    "description": "Upcoming amateur golf tournaments at the clubs, from the federation calendar (NextCaddy) and the clubs' own: dates, format, entry fee, handicap limits, members-only flag and registration opening and closing times. Rebuilt daily. Popular tournaments fill within minutes of registration opening, so registration.opens matters most.",
    "responses": {
     "200": {
      "description": "The tournament catalog.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/TournamentCatalog"
        }
       }
      }
     }
    }
   }
  }
 },
 "components": {
  "schemas": {
   "Error": {
    "type": "object",
    "properties": {
     "error": {
      "type": "string",
      "description": "What is wrong, and the valid values."
     },
     "example": {
      "type": "string",
      "description": "A request that works."
     },
     "docs": {
      "type": "string",
      "description": "This document."
     }
    },
    "required": [
     "error"
    ]
   },
   "LocalizedText": {
    "type": "object",
    "description": "The same text in Spanish and English.",
    "properties": {
     "es": {
      "type": "string"
     },
     "en": {
      "type": "string"
     }
    },
    "required": [
     "es",
     "en"
    ]
   },
   "TeeTimeQuery": {
    "type": "object",
    "description": "The search as run, with the defaults filled in.",
    "properties": {
     "date": {
      "type": "string",
      "description": "YYYY-MM-DD, Madrid's calendar.",
      "format": "date"
     },
     "from": {
      "type": "string",
      "description": "HH:MM, 24-hour, Madrid local time.",
      "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$"
     },
     "to": {
      "type": "string",
      "description": "HH:MM, 24-hour, Madrid local time.",
      "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$"
     },
     "regions": {
      "type": "array",
      "items": {
       "type": "string",
       "enum": [
        "madrid",
        "toledo",
        "guadalajara",
        "segovia",
        "avila",
        "cuenca",
        "burgos",
        "costa-del-sol",
        "valencia"
       ],
       "description": "Area id."
      },
      "description": "Areas searched; empty means every area."
     },
     "courseIds": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Courses searched by id; they win over regions."
     },
     "partySize": {
      "type": "integer",
      "minimum": 1,
      "maximum": 4
     },
     "holes": {
      "type": "integer",
      "enum": [
       9,
       18
      ]
     },
     "maxPricePerPlayer": {
      "type": "number",
      "description": "Budget per player in EUR."
     },
     "order": {
      "type": "string",
      "enum": [
       "time",
       "price"
      ]
     }
    },
    "required": [
     "date",
     "regions",
     "courseIds",
     "partySize",
     "holes",
     "order"
    ]
   },
   "TeeTime": {
    "type": "object",
    "description": "A free tee time as the club offers it.",
    "properties": {
     "time": {
      "type": "string",
      "description": "HH:MM, 24-hour, Madrid local time.",
      "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$"
     },
     "freeSpots": {
      "type": "integer",
      "description": "Places still free in this tee time (1–4)."
     },
     "partySizes": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "The group sizes the club takes at this time, when it restricts them (e.g. [2, 4])."
     },
     "holes": {
      "type": "integer",
      "enum": [
       9,
       18
      ]
     },
     "startingTee": {
      "type": "string",
      "enum": [
       "1",
       "10"
      ],
      "description": "The tee it starts from, when the club starts from two."
     },
     "pricePerPlayer": {
      "type": "number",
      "description": "Green fee per player in EUR for the searched group size. Absent when the club publishes no price online."
     },
     "pricesByPartySize": {
      "type": "object",
      "additionalProperties": {
       "type": "number"
      },
      "description": "Price per player by group size, when it depends on it."
     }
    },
    "required": [
     "time",
     "freeSpots",
     "holes"
    ]
   },
   "PlayableTeeTime": {
    "type": "object",
    "description": "A tee time that fits the search: right group size, holes, window and budget, and still ahead.",
    "properties": {
     "time": {
      "type": "string",
      "description": "HH:MM, 24-hour, Madrid local time.",
      "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$"
     },
     "freeSpots": {
      "type": "integer",
      "description": "Places still free in this tee time (1–4)."
     },
     "partySizes": {
      "type": "array",
      "items": {
       "type": "integer"
      },
      "description": "The group sizes the club takes at this time, when it restricts them (e.g. [2, 4])."
     },
     "holes": {
      "type": "integer",
      "enum": [
       9,
       18
      ]
     },
     "startingTee": {
      "type": "string",
      "enum": [
       "1",
       "10"
      ],
      "description": "The tee it starts from, when the club starts from two."
     },
     "pricePerPlayer": {
      "type": "number",
      "description": "Green fee per player in EUR for the searched group size. Absent when the club publishes no price online."
     },
     "pricesByPartySize": {
      "type": "object",
      "additionalProperties": {
       "type": "number"
      },
      "description": "Price per player by group size, when it depends on it."
     },
     "shared": {
      "type": "boolean",
      "description": "More places than the group: other golfers may join it."
     },
     "endsAfterSunset": {
      "type": "boolean",
      "description": "At a normal pace (4 h 30 for 18 holes) the round ends after sunset."
     }
    },
    "required": [
     "time",
     "freeSpots",
     "holes",
     "shared",
     "endsAfterSunset"
    ]
   },
   "CourseReference": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string"
     },
     "slug": {
      "type": "string"
     },
     "name": {
      "type": "string"
     },
     "bookingUrl": {
      "type": "string",
      "description": "The club's own booking page."
     }
    },
    "required": [
     "id",
     "slug",
     "name",
     "bookingUrl"
    ]
   },
   "CourseTeeTimes": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string"
     },
     "slug": {
      "type": "string"
     },
     "name": {
      "type": "string"
     },
     "town": {
      "type": "string"
     },
     "area": {
      "type": "string",
      "enum": [
       "madrid",
       "toledo",
       "guadalajara",
       "segovia",
       "avila",
       "cuenca",
       "burgos",
       "costa-del-sol",
       "valencia"
      ],
      "description": "Area id."
     },
     "guideUrl": {
      "type": "string",
      "description": "The course's guide on teesdesalida.es."
     },
     "bookingUrl": {
      "type": "string",
      "description": "The club's own booking page: where the golfer books."
     },
     "websiteUrl": {
      "type": "string"
     },
     "teeTimeCount": {
      "type": "integer",
      "description": "How many tee times match."
     },
     "teeTimes": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/PlayableTeeTime"
      },
      "description": "The matching tee times, in the requested order."
     }
    },
    "required": [
     "id",
     "slug",
     "name",
     "town",
     "area",
     "guideUrl",
     "bookingUrl",
     "websiteUrl",
     "teeTimeCount",
     "teeTimes"
    ]
   },
   "TeeTimeReport": {
    "type": "object",
    "properties": {
     "query": {
      "$ref": "#/components/schemas/TeeTimeQuery"
     },
     "summary": {
      "type": "string",
      "description": "One sentence summing up the answer, in the requested lang."
     },
     "courses": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/CourseTeeTimes"
      },
      "description": "Courses with at least one matching tee time, earliest (or cheapest) first."
     },
     "unavailable": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/CourseReference"
      },
      "description": "Clubs whose booking system didn't answer in time: say so, don't call them full."
     },
     "notYetOpen": {
      "type": "array",
      "items": {
       "allOf": [
        {
         "$ref": "#/components/schemas/CourseReference"
        },
        {
         "type": "object",
         "properties": {
          "bookingHorizonDays": {
           "type": "integer"
          }
         },
         "required": [
          "bookingHorizonDays"
         ]
        }
       ]
      },
      "description": "Clubs that don't take bookings for the date yet (bookingHorizonDays: how many days ahead they open)."
     },
     "appUrl": {
      "type": "string",
      "description": "The day in the TeesDeSalida app, when the courses searched lie in one area."
     },
     "attribution": {
      "type": "string",
      "description": "Credit line to show with the data.",
      "const": "Live tee times from TeesDeSalida (https://teesdesalida.es). Book directly with the club."
     }
    },
    "required": [
     "query",
     "summary",
     "courses",
     "unavailable",
     "notYetOpen",
     "attribution"
    ]
   },
   "CourseDay": {
    "type": "object",
    "properties": {
     "courseId": {
      "type": "string"
     },
     "date": {
      "type": "string",
      "description": "YYYY-MM-DD, Madrid's calendar.",
      "format": "date"
     },
     "status": {
      "type": "string",
      "enum": [
       "open",
       "not-open-yet",
       "unavailable"
      ],
      "description": "open: the club takes bookings that day (it may still be full); not-open-yet: beyond its booking horizon; unavailable: its system couldn't be read."
     },
     "teeTimes": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/TeeTime"
      },
      "description": "Every free tee time, by start time."
     },
     "checkedAt": {
      "type": "integer",
      "description": "When the club was read (epoch ms)."
     },
     "problem": {
      "type": "string",
      "description": "Why the club couldn't be read."
     }
    },
    "required": [
     "courseId",
     "date",
     "status",
     "teeTimes",
     "checkedAt"
    ]
   },
   "CourseDays": {
    "type": "object",
    "properties": {
     "date": {
      "type": "string",
      "description": "YYYY-MM-DD, Madrid's calendar.",
      "format": "date"
     },
     "courseDays": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/CourseDay"
      },
      "description": "One per course asked."
     }
    },
    "required": [
     "date",
     "courseDays"
    ]
   },
   "Course": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string"
     },
     "slug": {
      "type": "string",
      "description": "The course's address on the site."
     },
     "name": {
      "type": "string"
     },
     "shortName": {
      "type": "string"
     },
     "town": {
      "type": "string"
     },
     "area": {
      "type": "string",
      "enum": [
       "madrid",
       "toledo",
       "guadalajara",
       "segovia",
       "avila",
       "cuenca",
       "burgos",
       "costa-del-sol",
       "valencia"
      ],
      "description": "Area id."
     },
     "location": {
      "type": "object",
      "description": "The clubhouse (WGS 84).",
      "properties": {
       "latitude": {
        "type": "number"
       },
       "longitude": {
        "type": "number"
       }
      },
      "required": [
       "latitude",
       "longitude"
      ]
     },
     "driveMinutes": {
      "type": "integer",
      "description": "Typical drive from the area's hub."
     },
     "hub": {
      "type": "string",
      "description": "The city drives are measured from."
     },
     "facilities": {
      "type": "object",
      "description": "Only verified facts; an unknown one is absent.",
      "properties": {
       "layout": {
        "$ref": "#/components/schemas/LocalizedText"
       },
       "drivingRange": {
        "type": "boolean"
       },
       "practiceGreen": {
        "type": "boolean"
       },
       "restaurant": {
        "type": "boolean"
       },
       "buggyIncluded": {
        "type": "boolean",
        "description": "Whether the visitor green fee includes a buggy."
       },
       "buggyPrice": {
        "type": "number",
        "description": "Buggy price in EUR, when extra."
       }
      },
      "required": [
       "layout"
      ]
     },
     "greenFeeNotes": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/LocalizedText"
      },
      "description": "Green fees and who pays what, as the club publishes them."
     },
     "bookingHorizonDays": {
      "type": "integer",
      "description": "How many days ahead the club takes visitor bookings."
     },
     "bookingUrl": {
      "type": "string"
     },
     "websiteUrl": {
      "type": "string"
     },
     "guideUrl": {
      "$ref": "#/components/schemas/LocalizedText"
     }
    },
    "required": [
     "id",
     "slug",
     "name",
     "shortName",
     "town",
     "area",
     "location",
     "driveMinutes",
     "hub",
     "facilities",
     "greenFeeNotes",
     "bookingHorizonDays",
     "bookingUrl",
     "websiteUrl",
     "guideUrl"
    ]
   },
   "CoursesDocument": {
    "type": "object",
    "properties": {
     "attribution": {
      "type": "string"
     },
     "courseCount": {
      "type": "integer"
     },
     "courses": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/Course"
      },
      "description": "By area, then by drive."
     }
    },
    "required": [
     "attribution",
     "courseCount",
     "courses"
    ]
   },
   "Area": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "enum": [
       "madrid",
       "toledo",
       "guadalajara",
       "segovia",
       "avila",
       "cuenca",
       "burgos",
       "costa-del-sol",
       "valencia"
      ],
      "description": "Area id."
     },
     "name": {
      "$ref": "#/components/schemas/LocalizedText"
     },
     "hub": {
      "type": "string"
     },
     "clubCount": {
      "type": "integer",
      "description": "Clubs in the directory; 0 means none yet."
     },
     "appUrl": {
      "type": "string",
      "description": "The area in the app."
     }
    },
    "required": [
     "id",
     "name",
     "hub",
     "clubCount",
     "appUrl"
    ]
   },
   "AreasDocument": {
    "type": "object",
    "properties": {
     "attribution": {
      "type": "string"
     },
     "areas": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/Area"
      },
      "description": "Every area."
     }
    },
    "required": [
     "attribution",
     "areas"
    ]
   },
   "LocalMoment": {
    "type": "object",
    "description": "A Madrid date, with the time when the club gives one.",
    "properties": {
     "date": {
      "type": "string",
      "description": "YYYY-MM-DD, Madrid's calendar.",
      "format": "date"
     },
     "time": {
      "type": "string",
      "description": "HH:MM, 24-hour, Madrid local time.",
      "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$"
     }
    },
    "required": [
     "date"
    ]
   },
   "Tournament": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string",
      "description": "Stable id, e.g. nextcaddy:68327."
     },
     "courseId": {
      "type": "string"
     },
     "name": {
      "type": "string"
     },
     "startDate": {
      "type": "string",
      "description": "YYYY-MM-DD, Madrid's calendar.",
      "format": "date"
     },
     "endDate": {
      "type": "string",
      "description": "YYYY-MM-DD, Madrid's calendar.",
      "format": "date"
     },
     "format": {
      "type": "string",
      "description": "e.g. Stableford, Medal Play."
     },
     "access": {
      "type": "string",
      "enum": [
       "open",
       "members-only",
       "unknown"
      ]
     },
     "handicap": {
      "type": "object",
      "description": "Handicap limits; absent when anyone can enter.",
      "properties": {
       "min": {
        "type": "number"
       },
       "max": {
        "type": "number"
       }
      },
      "required": [
       "min",
       "max"
      ]
     },
     "registration": {
      "type": "object",
      "properties": {
       "opens": {
        "$ref": "#/components/schemas/LocalMoment"
       },
       "closes": {
        "$ref": "#/components/schemas/LocalMoment"
       }
      },
      "required": []
     },
     "entryFee": {
      "type": "number",
      "description": "Visitor entry fee in EUR."
     },
     "placesLeft": {
      "type": "integer"
     },
     "source": {
      "type": "string",
      "enum": [
       "nextcaddy",
       "club-calendar"
      ]
     },
     "url": {
      "type": "string",
      "description": "Where to read about it and enter."
     },
     "needsReview": {
      "type": "boolean",
      "description": "Some details were read from free text with low confidence."
     },
     "firstSeenAt": {
      "type": "integer",
      "description": "When it first appeared in the catalog (epoch ms)."
     }
    },
    "required": [
     "id",
     "courseId",
     "name",
     "startDate",
     "access",
     "registration",
     "source",
     "url",
     "needsReview",
     "firstSeenAt"
    ]
   },
   "TournamentCatalog": {
    "type": "object",
    "properties": {
     "generatedAt": {
      "type": "integer",
      "description": "When the catalog was compiled (epoch ms)."
     },
     "tournaments": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/Tournament"
      },
      "description": "Upcoming and recent tournaments; skip those whose last day is past."
     },
     "sources": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "source": {
         "type": "string"
        },
        "courseId": {
         "type": "string"
        },
        "found": {
         "type": "integer"
        },
        "problem": {
         "type": "string"
        }
       },
       "required": [
        "source",
        "courseId",
        "found"
       ]
      },
      "description": "How each source fared in the last compilation."
     }
    },
    "required": [
     "generatedAt",
     "tournaments",
     "sources"
    ]
   }
  }
 }
}
