{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://openteesheet.com/schema/openteesheet-v1.json",
  "title": "OpenTeeSheet open data format v1",
  "description": "Every object of the OpenTeeSheet open format in one document (each $defs entry is the same schema as its own file under https://openteesheet.com/schemas/). Licence: free to implement, no permission needed. Docs: https://openteesheet.com/docs/open-format",
  "$defs": {
    "venue": {
      "$schema": "https://json-schema.org/draft/2020-12/schema",
      "$id": "https://openteesheet.com/schemas/venue.schema.json",
      "title": "OpenTeeSheet venue (open format v1)",
      "description": "A golf simulator studio or course on OpenTeeSheet. The full record is what GET /api/v1/venue and the venue export emit; the public GET /api/v1/venues/{slug} emits the public subset (no created_at, no active flag). Never carries payment-processor ids or secrets.",
      "type": "object",
      "properties": {
        "object": {
          "const": "venue"
        },
        "id": {
          "type": "string",
          "format": "uuid"
        },
        "slug": {
          "type": [
            "string",
            "null"
          ],
          "description": "URL slug of the public booking page, https://openteesheet.com/book/<slug>"
        },
        "name": {
          "type": "string",
          "minLength": 1
        },
        "location": {
          "type": [
            "string",
            "null"
          ],
          "description": "Street address"
        },
        "city": {
          "type": [
            "string",
            "null"
          ]
        },
        "state": {
          "type": [
            "string",
            "null"
          ]
        },
        "phone": {
          "type": [
            "string",
            "null"
          ]
        },
        "email": {
          "type": [
            "string",
            "null"
          ]
        },
        "website": {
          "type": [
            "string",
            "null"
          ]
        },
        "description": {
          "type": [
            "string",
            "null"
          ]
        },
        "timezone": {
          "type": "string",
          "minLength": 1,
          "description": "IANA time zone of the venue, e.g. America/New_York"
        },
        "open_24_7": {
          "type": "boolean"
        },
        "operating_hours": {
          "type": [
            "object",
            "null"
          ],
          "description": "Weekly hours, one entry per day (sun..sat); null = never set (the default 08:00-21:00 applies). Ignored when open_24_7 is true.",
          "properties": {
            "sun": {
              "type": "object",
              "properties": {
                "open": {
                  "type": "string",
                  "pattern": "^([01][0-9]|2[0-4]):[0-5][0-9]$"
                },
                "close": {
                  "type": "string",
                  "pattern": "^([01][0-9]|2[0-4]):[0-5][0-9]$"
                },
                "closed": {
                  "type": "boolean"
                }
              },
              "required": [
                "open",
                "close",
                "closed"
              ],
              "additionalProperties": false
            },
            "mon": {
              "type": "object",
              "properties": {
                "open": {
                  "type": "string",
                  "pattern": "^([01][0-9]|2[0-4]):[0-5][0-9]$"
                },
                "close": {
                  "type": "string",
                  "pattern": "^([01][0-9]|2[0-4]):[0-5][0-9]$"
                },
                "closed": {
                  "type": "boolean"
                }
              },
              "required": [
                "open",
                "close",
                "closed"
              ],
              "additionalProperties": false
            },
            "tue": {
              "type": "object",
              "properties": {
                "open": {
                  "type": "string",
                  "pattern": "^([01][0-9]|2[0-4]):[0-5][0-9]$"
                },
                "close": {
                  "type": "string",
                  "pattern": "^([01][0-9]|2[0-4]):[0-5][0-9]$"
                },
                "closed": {
                  "type": "boolean"
                }
              },
              "required": [
                "open",
                "close",
                "closed"
              ],
              "additionalProperties": false
            },
            "wed": {
              "type": "object",
              "properties": {
                "open": {
                  "type": "string",
                  "pattern": "^([01][0-9]|2[0-4]):[0-5][0-9]$"
                },
                "close": {
                  "type": "string",
                  "pattern": "^([01][0-9]|2[0-4]):[0-5][0-9]$"
                },
                "closed": {
                  "type": "boolean"
                }
              },
              "required": [
                "open",
                "close",
                "closed"
              ],
              "additionalProperties": false
            },
            "thu": {
              "type": "object",
              "properties": {
                "open": {
                  "type": "string",
                  "pattern": "^([01][0-9]|2[0-4]):[0-5][0-9]$"
                },
                "close": {
                  "type": "string",
                  "pattern": "^([01][0-9]|2[0-4]):[0-5][0-9]$"
                },
                "closed": {
                  "type": "boolean"
                }
              },
              "required": [
                "open",
                "close",
                "closed"
              ],
              "additionalProperties": false
            },
            "fri": {
              "type": "object",
              "properties": {
                "open": {
                  "type": "string",
                  "pattern": "^([01][0-9]|2[0-4]):[0-5][0-9]$"
                },
                "close": {
                  "type": "string",
                  "pattern": "^([01][0-9]|2[0-4]):[0-5][0-9]$"
                },
                "closed": {
                  "type": "boolean"
                }
              },
              "required": [
                "open",
                "close",
                "closed"
              ],
              "additionalProperties": false
            },
            "sat": {
              "type": "object",
              "properties": {
                "open": {
                  "type": "string",
                  "pattern": "^([01][0-9]|2[0-4]):[0-5][0-9]$"
                },
                "close": {
                  "type": "string",
                  "pattern": "^([01][0-9]|2[0-4]):[0-5][0-9]$"
                },
                "closed": {
                  "type": "boolean"
                }
              },
              "required": [
                "open",
                "close",
                "closed"
              ],
              "additionalProperties": false
            }
          },
          "required": [
            "sun",
            "mon",
            "tue",
            "wed",
            "thu",
            "fri",
            "sat"
          ],
          "additionalProperties": false
        },
        "payment_mode": {
          "type": "string",
          "enum": [
            "pay_at_venue",
            "prepay"
          ],
          "description": "prepay = card at booking (live only once the venue's own Stripe account can take charges)"
        },
        "prepay_enabled": {
          "type": "boolean",
          "description": "Public view only: card at booking is live for this venue right now"
        },
        "active": {
          "type": "boolean"
        },
        "created_at": {
          "type": [
            "string",
            "null"
          ],
          "format": "date-time"
        }
      },
      "required": [
        "object",
        "id",
        "name"
      ],
      "additionalProperties": false
    },
    "bay": {
      "$schema": "https://json-schema.org/draft/2020-12/schema",
      "$id": "https://openteesheet.com/schemas/bay.schema.json",
      "title": "OpenTeeSheet bay (open format v1)",
      "description": "One bookable simulator bay (or tee sheet) at a venue.",
      "type": "object",
      "properties": {
        "object": {
          "const": "bay"
        },
        "id": {
          "type": "string",
          "format": "uuid"
        },
        "venue_id": {
          "type": "string",
          "format": "uuid"
        },
        "name": {
          "type": "string",
          "minLength": 1
        },
        "simulator": {
          "type": [
            "string",
            "null"
          ],
          "description": "Free text the owner typed, e.g. 'Trackman iO'"
        },
        "launch_monitor": {
          "type": [
            "string",
            "null"
          ],
          "enum": [
            "trackman",
            "foresight",
            "uneekor",
            "full_swing",
            "aboutgolf",
            "golfzon",
            "flightscope",
            "skytrak",
            "garmin",
            "other",
            null
          ]
        },
        "session_control": {
          "type": "string",
          "enum": [
            "off",
            "bay_agent"
          ],
          "description": "bay_agent = the OpenTeeSheet bay agent on the sim PC starts/stops each paid session"
        },
        "session_start": {
          "type": "string",
          "enum": [
            "booking_start",
            "door_open"
          ],
          "description": "When the bay agent starts the sim: booking_start = on the booking's clock; door_open = when the door lets the golfer in (a no-show never runs the sim)"
        },
        "rate_cents": {
          "type": "integer",
          "minimum": 0,
          "description": "Hourly rate in cents (whole dollars on every OpenTeeSheet price)"
        },
        "max_players": {
          "type": [
            "integer",
            "null"
          ],
          "minimum": 1
        },
        "sort_order": {
          "type": [
            "integer",
            "null"
          ]
        },
        "buffer_minutes": {
          "type": [
            "integer",
            "null"
          ],
          "minimum": 0,
          "maximum": 120,
          "description": "Cleanup gap kept free after each booking"
        },
        "active": {
          "type": "boolean"
        }
      },
      "required": [
        "object",
        "id",
        "venue_id",
        "name",
        "rate_cents"
      ],
      "additionalProperties": false
    },
    "booking": {
      "$schema": "https://json-schema.org/draft/2020-12/schema",
      "$id": "https://openteesheet.com/schemas/booking.schema.json",
      "title": "OpenTeeSheet booking (open format v1)",
      "description": "One booking of one bay for one time window. The door code itself is not part of the format: credentials are door grants (door-grant.schema.json) and their secrets never leave OpenTeeSheet.",
      "type": "object",
      "properties": {
        "object": {
          "const": "booking"
        },
        "id": {
          "type": "string",
          "format": "uuid"
        },
        "venue_id": {
          "type": "string",
          "format": "uuid"
        },
        "bay_id": {
          "type": "string",
          "format": "uuid"
        },
        "start_time": {
          "type": "string",
          "format": "date-time"
        },
        "end_time": {
          "type": "string",
          "format": "date-time"
        },
        "duration_minutes": {
          "type": [
            "integer",
            "null"
          ],
          "minimum": 0
        },
        "players": {
          "type": [
            "integer",
            "null"
          ],
          "minimum": 1
        },
        "customer_name": {
          "type": [
            "string",
            "null"
          ]
        },
        "customer_email": {
          "type": [
            "string",
            "null"
          ]
        },
        "customer_phone": {
          "type": [
            "string",
            "null"
          ]
        },
        "status": {
          "type": "string",
          "minLength": 1,
          "description": "confirmed, pending_payment, checked_in, completed, cancelled, no_show, ..."
        },
        "payment_status": {
          "type": [
            "string",
            "null"
          ],
          "description": "unpaid, paid, refunded, ..."
        },
        "amount_cents": {
          "type": [
            "integer",
            "null"
          ],
          "minimum": 0
        },
        "extra_players": {
          "type": [
            "integer",
            "null"
          ],
          "minimum": 0
        },
        "guest_fee_cents": {
          "type": [
            "integer",
            "null"
          ],
          "minimum": 0
        },
        "created_at": {
          "type": [
            "string",
            "null"
          ],
          "format": "date-time"
        }
      },
      "required": [
        "object",
        "id",
        "venue_id",
        "bay_id",
        "start_time",
        "end_time",
        "status"
      ],
      "additionalProperties": false
    },
    "door-grant": {
      "$schema": "https://json-schema.org/draft/2020-12/schema",
      "$id": "https://openteesheet.com/schemas/door-grant.schema.json",
      "title": "OpenTeeSheet door grant (open format v1)",
      "description": "One door credential issued for one booking on one lock (keypad code, card, mobile key or remote unlock). The secret (the code itself) is never exported: a grant says which lock, which window and whether it is live.",
      "type": "object",
      "properties": {
        "object": {
          "const": "door_grant"
        },
        "id": {
          "type": "string",
          "format": "uuid"
        },
        "booking_id": {
          "type": "string",
          "format": "uuid"
        },
        "venue_id": {
          "type": "string",
          "format": "uuid"
        },
        "method": {
          "type": "string",
          "enum": [
            "keypad",
            "card",
            "mobile",
            "remote"
          ]
        },
        "provider": {
          "type": "string",
          "minLength": 1,
          "description": "Lock provider key, e.g. igloohome, remotelock, kisi, seam, custom"
        },
        "lock_id": {
          "type": [
            "string",
            "null"
          ],
          "format": "uuid"
        },
        "external_id": {
          "type": [
            "string",
            "null"
          ],
          "description": "The credential's id at the lock vendor"
        },
        "window_start": {
          "type": [
            "string",
            "null"
          ],
          "format": "date-time"
        },
        "window_end": {
          "type": [
            "string",
            "null"
          ],
          "format": "date-time"
        },
        "status": {
          "type": "string",
          "enum": [
            "active",
            "revoked",
            "failed"
          ]
        },
        "created_at": {
          "type": [
            "string",
            "null"
          ],
          "format": "date-time"
        },
        "revoked_at": {
          "type": [
            "string",
            "null"
          ],
          "format": "date-time"
        }
      },
      "required": [
        "object",
        "id",
        "booking_id",
        "venue_id",
        "method",
        "provider",
        "status"
      ],
      "additionalProperties": false
    },
    "bay-session": {
      "$schema": "https://json-schema.org/draft/2020-12/schema",
      "$id": "https://openteesheet.com/schemas/bay-session.schema.json",
      "title": "OpenTeeSheet bay session (open format v1)",
      "description": "A booking's simulator session on the bay: scheduled, armed when the door lets the golfer in, started and stopped as the bay agent (or the owner) reports, and reconciled after the booking ends with how it ended.",
      "type": "object",
      "properties": {
        "object": {
          "const": "bay_session"
        },
        "booking_id": {
          "type": "string",
          "format": "uuid"
        },
        "bay_id": {
          "type": "string",
          "format": "uuid"
        },
        "venue_id": {
          "type": "string",
          "format": "uuid"
        },
        "state": {
          "type": "string",
          "enum": [
            "scheduled",
            "armed",
            "started",
            "stopped",
            "failed",
            "missed_start",
            "reconciled"
          ]
        },
        "started_at": {
          "type": [
            "string",
            "null"
          ],
          "format": "date-time"
        },
        "stopped_at": {
          "type": [
            "string",
            "null"
          ],
          "format": "date-time"
        },
        "last_error": {
          "type": [
            "string",
            "null"
          ]
        },
        "updated_at": {
          "type": [
            "string",
            "null"
          ],
          "format": "date-time"
        },
        "armed_at": {
          "type": [
            "string",
            "null"
          ],
          "format": "date-time"
        },
        "reconciled_at": {
          "type": [
            "string",
            "null"
          ],
          "format": "date-time"
        },
        "end_reason": {
          "type": [
            "string",
            "null"
          ],
          "enum": [
            "booking_end",
            "early_stop",
            "no_stop_report",
            "never_started",
            "no_show",
            "not_started",
            "failed",
            "missed_start",
            null
          ]
        }
      },
      "required": [
        "object",
        "booking_id",
        "bay_id",
        "venue_id",
        "state"
      ],
      "additionalProperties": false
    },
    "venue-export": {
      "$schema": "https://json-schema.org/draft/2020-12/schema",
      "$id": "https://openteesheet.com/schemas/venue-export.schema.json",
      "title": "OpenTeeSheet venue export (open format v1)",
      "description": "Everything a venue owner's account holds about their venue in the open format: the venue, every bay, every booking, every door grant and every bay session. The same document from GET /api/v1/venue/export, `openteesheet export` and the MCP tool export_venue_data. Every record is complete (all fields present, null when unknown).",
      "type": "object",
      "properties": {
        "format": {
          "const": "openteesheet.venue-export"
        },
        "version": {
          "const": 1
        },
        "schema": {
          "const": "https://openteesheet.com/schemas/venue-export.schema.json"
        },
        "exported_at": {
          "type": "string",
          "format": "date-time"
        },
        "venue": {
          "allOf": [
            {
              "$ref": "https://openteesheet.com/schemas/venue.schema.json"
            },
            {
              "type": "object",
              "required": [
                "active",
                "city",
                "created_at",
                "description",
                "email",
                "id",
                "location",
                "name",
                "object",
                "open_24_7",
                "operating_hours",
                "payment_mode",
                "phone",
                "slug",
                "state",
                "timezone",
                "website"
              ]
            }
          ]
        },
        "bays": {
          "type": "array",
          "items": {
            "allOf": [
              {
                "$ref": "https://openteesheet.com/schemas/bay.schema.json"
              },
              {
                "type": "object",
                "required": [
                  "active",
                  "buffer_minutes",
                  "id",
                  "launch_monitor",
                  "max_players",
                  "name",
                  "object",
                  "rate_cents",
                  "session_control",
                  "simulator",
                  "sort_order",
                  "venue_id"
                ]
              }
            ]
          }
        },
        "bookings": {
          "type": "array",
          "items": {
            "allOf": [
              {
                "$ref": "https://openteesheet.com/schemas/booking.schema.json"
              },
              {
                "type": "object",
                "required": [
                  "amount_cents",
                  "bay_id",
                  "created_at",
                  "customer_email",
                  "customer_name",
                  "customer_phone",
                  "duration_minutes",
                  "end_time",
                  "extra_players",
                  "guest_fee_cents",
                  "id",
                  "object",
                  "payment_status",
                  "players",
                  "start_time",
                  "status",
                  "venue_id"
                ]
              }
            ]
          }
        },
        "door_grants": {
          "type": "array",
          "items": {
            "allOf": [
              {
                "$ref": "https://openteesheet.com/schemas/door-grant.schema.json"
              },
              {
                "type": "object",
                "required": [
                  "booking_id",
                  "created_at",
                  "external_id",
                  "id",
                  "lock_id",
                  "method",
                  "object",
                  "provider",
                  "revoked_at",
                  "status",
                  "venue_id",
                  "window_end",
                  "window_start"
                ]
              }
            ]
          }
        },
        "bay_sessions": {
          "type": "array",
          "items": {
            "allOf": [
              {
                "$ref": "https://openteesheet.com/schemas/bay-session.schema.json"
              },
              {
                "type": "object",
                "required": [
                  "bay_id",
                  "booking_id",
                  "last_error",
                  "object",
                  "started_at",
                  "state",
                  "stopped_at",
                  "updated_at",
                  "venue_id"
                ]
              }
            ]
          }
        },
        "counts": {
          "type": "object",
          "properties": {
            "bays": {
              "type": "integer",
              "minimum": 0
            },
            "bookings": {
              "type": "integer",
              "minimum": 0
            },
            "door_grants": {
              "type": "integer",
              "minimum": 0
            },
            "bay_sessions": {
              "type": "integer",
              "minimum": 0
            }
          },
          "required": [
            "bays",
            "bookings",
            "door_grants",
            "bay_sessions"
          ],
          "additionalProperties": false
        }
      },
      "required": [
        "format",
        "version",
        "schema",
        "exported_at",
        "venue",
        "bays",
        "bookings",
        "door_grants",
        "bay_sessions",
        "counts"
      ],
      "additionalProperties": false
    },
    "golfer-sessions": {
      "$schema": "https://json-schema.org/draft/2020-12/schema",
      "$id": "https://openteesheet.com/schemas/golfer-sessions.schema.json",
      "title": "OpenTeeSheet golfer sessions export (open format v1)",
      "description": "A golfer's OWN session history across every OpenTeeSheet venue: per session the venue, bay, launch-monitor brand, how the session started, the real start and stop from the bay where the bay reports them (else the booked time, and which), minutes played and how it ended, with per-venue and per-month totals. Shot and round data is NOT included: it stays with the launch-monitor maker (shot_data.included is always false). The same document from the account page's download, GET /api/v1/me/sessions, `openteesheet my-sessions` and the MCP tool export_my_sessions.",
      "type": "object",
      "properties": {
        "format": {
          "const": "openteesheet.golfer-sessions"
        },
        "version": {
          "const": 1
        },
        "schema": {
          "const": "https://openteesheet.com/schemas/golfer-sessions.schema.json"
        },
        "exported_at": {
          "type": "string",
          "format": "date-time"
        },
        "shot_data": {
          "type": "object",
          "properties": {
            "included": {
              "const": false
            },
            "why": {
              "type": "string",
              "minLength": 1
            }
          },
          "required": [
            "included",
            "why"
          ],
          "additionalProperties": false
        },
        "sessions": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "object": {
                "const": "golfer_session"
              },
              "booking_id": {
                "type": "string",
                "format": "uuid"
              },
              "venue_name": {
                "type": "string",
                "minLength": 1
              },
              "venue_slug": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "venue_city": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "venue_timezone": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "bay_name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "launch_monitor": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The brand key (trackman, foresight, uneekor, full_swing, aboutgolf, golfzon, flightscope, skytrak, garmin, other); null when the venue did not name one."
              },
              "launch_monitor_label": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "session_start": {
                "type": [
                  "string",
                  "null"
                ],
                "enum": [
                  "booking_start",
                  "door_open",
                  null
                ],
                "description": "How the bay starts a session: at the booking time, or when the door opens. null = this bay does not report sessions, so only the booked time is known."
              },
              "booked_start": {
                "type": "string",
                "format": "date-time"
              },
              "booked_end": {
                "type": "string",
                "format": "date-time"
              },
              "started_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time",
                "description": "When the bay reported the session started; null when it did not."
              },
              "stopped_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time",
                "description": "When the bay reported the session stopped; null when it did not."
              },
              "start_from": {
                "type": "string",
                "enum": [
                  "bay",
                  "booking"
                ]
              },
              "stop_from": {
                "type": "string",
                "enum": [
                  "bay",
                  "booking"
                ]
              },
              "minutes_played": {
                "type": "integer",
                "minimum": 0
              },
              "ending": {
                "type": "string",
                "enum": [
                  "ended",
                  "stopped_early",
                  "failed_to_start",
                  "no_show"
                ]
              }
            },
            "required": [
              "object",
              "booking_id",
              "venue_name",
              "venue_slug",
              "venue_city",
              "venue_timezone",
              "bay_name",
              "launch_monitor",
              "launch_monitor_label",
              "session_start",
              "booked_start",
              "booked_end",
              "started_at",
              "stopped_at",
              "start_from",
              "stop_from",
              "minutes_played",
              "ending"
            ],
            "additionalProperties": false
          }
        },
        "rollups": {
          "type": "object",
          "properties": {
            "by_venue": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "venue_name": {
                    "type": "string"
                  },
                  "venue_slug": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "sessions": {
                    "type": "integer",
                    "minimum": 0
                  },
                  "played": {
                    "type": "integer",
                    "minimum": 0
                  },
                  "minutes_played": {
                    "type": "integer",
                    "minimum": 0
                  }
                },
                "required": [
                  "venue_name",
                  "venue_slug",
                  "sessions",
                  "played",
                  "minutes_played"
                ],
                "additionalProperties": false
              }
            },
            "by_month": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "month": {
                    "type": "string",
                    "pattern": "^\\d{4}-\\d{2}$"
                  },
                  "sessions": {
                    "type": "integer",
                    "minimum": 0
                  },
                  "played": {
                    "type": "integer",
                    "minimum": 0
                  },
                  "minutes_played": {
                    "type": "integer",
                    "minimum": 0
                  }
                },
                "required": [
                  "month",
                  "sessions",
                  "played",
                  "minutes_played"
                ],
                "additionalProperties": false
              }
            }
          },
          "required": [
            "by_venue",
            "by_month"
          ],
          "additionalProperties": false
        },
        "counts": {
          "type": "object",
          "properties": {
            "sessions": {
              "type": "integer",
              "minimum": 0
            },
            "played": {
              "type": "integer",
              "minimum": 0
            },
            "minutes_played": {
              "type": "integer",
              "minimum": 0
            }
          },
          "required": [
            "sessions",
            "played",
            "minutes_played"
          ],
          "additionalProperties": false
        }
      },
      "required": [
        "format",
        "version",
        "schema",
        "exported_at",
        "shot_data",
        "sessions",
        "rollups",
        "counts"
      ],
      "additionalProperties": false
    },
    "league-season": {
      "$schema": "https://json-schema.org/draft/2020-12/schema",
      "$id": "https://openteesheet.com/schemas/league-season.schema.json",
      "title": "OpenTeeSheet league season (open format v1)",
      "description": "A league season at a sim studio: a weekly night on named bays for N weeks, teams of a fixed size, an entry fee per golfer and a league-only bay rate per team per night (whole dollars, in cents), and one of three scoring rules. Making a season reserves every league night on those bays as a real league booking.",
      "type": "object",
      "properties": {
        "object": {
          "const": "league_season"
        },
        "id": {
          "type": "string",
          "format": "uuid"
        },
        "venue_id": {
          "type": "string",
          "format": "uuid"
        },
        "name": {
          "type": "string",
          "minLength": 1,
          "maxLength": 80
        },
        "starts_on": {
          "type": "string",
          "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
          "description": "The first league night, venue-local date; the season plays that weekday every week."
        },
        "start_time": {
          "type": "string",
          "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$",
          "description": "Venue-local start, HH:MM."
        },
        "duration_minutes": {
          "type": "integer",
          "minimum": 60,
          "maximum": 300
        },
        "weeks": {
          "type": "integer",
          "minimum": 1,
          "maximum": 26
        },
        "bay_ids": {
          "type": "array",
          "items": {
            "type": "string",
            "format": "uuid"
          }
        },
        "team_size": {
          "type": "integer",
          "minimum": 1,
          "maximum": 4
        },
        "roster_cap": {
          "type": "integer",
          "minimum": 1,
          "maximum": 8
        },
        "substitutes_allowed": {
          "type": "boolean"
        },
        "entry_fee_cents": {
          "type": "integer",
          "minimum": 0,
          "description": "Per golfer, paid once at sign-up. Whole dollars."
        },
        "league_rate_cents": {
          "type": "integer",
          "minimum": 0,
          "description": "What one team pays per league night for its share of the bay, instead of the walk-in rate; the captain pays the season's total at sign-up. Whole dollars."
        },
        "scoring": {
          "type": "string",
          "enum": [
            "match_play",
            "field_rank",
            "low_total"
          ]
        },
        "max_teams": {
          "type": "integer",
          "minimum": 2
        },
        "status": {
          "type": "string",
          "enum": [
            "open",
            "closed"
          ]
        },
        "created_at": {
          "type": [
            "string",
            "null"
          ],
          "format": "date-time"
        }
      },
      "required": [
        "object",
        "id",
        "venue_id",
        "name",
        "starts_on",
        "start_time",
        "duration_minutes",
        "weeks",
        "bay_ids",
        "team_size",
        "roster_cap",
        "substitutes_allowed",
        "entry_fee_cents",
        "league_rate_cents",
        "scoring",
        "max_teams",
        "status"
      ],
      "additionalProperties": false
    },
    "league-team": {
      "$schema": "https://json-schema.org/draft/2020-12/schema",
      "$id": "https://openteesheet.com/schemas/league-team.schema.json",
      "title": "OpenTeeSheet league team (open format v1)",
      "description": "A team in a league season and its roster: the golfers who paid (or pay at the venue). A golfer who deletes their account leaves every roster; the team's posted scores stay.",
      "type": "object",
      "properties": {
        "object": {
          "const": "league_team"
        },
        "id": {
          "type": "string",
          "format": "uuid"
        },
        "season_id": {
          "type": "string",
          "format": "uuid"
        },
        "venue_id": {
          "type": "string",
          "format": "uuid"
        },
        "name": {
          "type": "string",
          "minLength": 1,
          "maxLength": 40
        },
        "roster": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "name": {
                "type": "string"
              },
              "email": {
                "type": "string"
              },
              "role": {
                "type": "string",
                "enum": [
                  "captain",
                  "player"
                ]
              },
              "payment_status": {
                "type": "string",
                "enum": [
                  "paid",
                  "at_venue",
                  "free"
                ]
              }
            },
            "required": [
              "id",
              "name",
              "email",
              "role",
              "payment_status"
            ],
            "additionalProperties": false
          }
        },
        "created_at": {
          "type": [
            "string",
            "null"
          ],
          "format": "date-time"
        }
      },
      "required": [
        "object",
        "id",
        "season_id",
        "venue_id",
        "name",
        "roster"
      ],
      "additionalProperties": false
    },
    "league-standing": {
      "$schema": "https://json-schema.org/draft/2020-12/schema",
      "$id": "https://openteesheet.com/schemas/league-standing.schema.json",
      "title": "OpenTeeSheet league standing (open format v1)",
      "description": "One team's line in a season's standings, computed from the posted weekly scores by the season's rule: match_play (2 for a win, 1 for a tie, lower score wins), field_rank (each night's best score earns one point per team that posted, ties share the higher) or low_total (fewest strokes over the weeks played; points is null).",
      "type": "object",
      "properties": {
        "object": {
          "const": "league_standing"
        },
        "season_id": {
          "type": "string",
          "format": "uuid"
        },
        "team_id": {
          "type": "string",
          "format": "uuid"
        },
        "team": {
          "type": "string"
        },
        "rank": {
          "type": "integer",
          "minimum": 1
        },
        "played": {
          "type": "integer",
          "minimum": 0
        },
        "wins": {
          "type": "integer",
          "minimum": 0
        },
        "ties": {
          "type": "integer",
          "minimum": 0
        },
        "losses": {
          "type": "integer",
          "minimum": 0
        },
        "points": {
          "type": [
            "integer",
            "null"
          ],
          "minimum": 0
        },
        "total_strokes": {
          "type": "integer",
          "minimum": 0
        }
      },
      "required": [
        "object",
        "season_id",
        "team_id",
        "team",
        "rank",
        "played",
        "wins",
        "ties",
        "losses",
        "points",
        "total_strokes"
      ],
      "additionalProperties": false
    }
  }
}
