{
  "components": {
    "parameters": {
      "bookingId": {
        "in": "path",
        "name": "bookingId",
        "required": true,
        "schema": {
          "format": "uuid",
          "type": "string"
        }
      },
      "clubId": {
        "in": "path",
        "name": "clubId",
        "required": true,
        "schema": {
          "format": "uuid",
          "type": "string"
        }
      },
      "event": {
        "in": "path",
        "name": "event",
        "required": true,
        "schema": {
          "enum": [
            "booking.created",
            "booking.confirmed",
            "booking.cancelled",
            "booking.updated",
            "payment.succeeded",
            "match.filled",
            "membership.started",
            "membership.cancelled",
            "program.booked",
            "user.signup",
            "club.player.registered",
            "court.session.starting",
            "court.session.ended",
            "court.door.unlock_requested",
            "checkin.recorded"
          ],
          "type": "string"
        }
      },
      "hookId": {
        "in": "path",
        "name": "hookId",
        "required": true,
        "schema": {
          "format": "uuid",
          "type": "string"
        }
      },
      "webhookDelivery": {
        "description": "Stable across retries of one delivery — de-duplicate on it.",
        "in": "header",
        "name": "x-map-delivery",
        "schema": {
          "format": "uuid",
          "type": "string"
        }
      },
      "webhookEvent": {
        "in": "header",
        "name": "x-map-event",
        "required": true,
        "schema": {
          "enum": [
            "booking.created",
            "booking.confirmed",
            "booking.cancelled",
            "booking.updated",
            "payment.succeeded",
            "match.filled",
            "membership.started",
            "membership.cancelled",
            "program.booked",
            "user.signup",
            "club.player.registered",
            "court.session.starting",
            "court.session.ended",
            "court.door.unlock_requested",
            "checkin.recorded",
            "webhook.ping"
          ],
          "type": "string"
        }
      },
      "webhookSignature": {
        "description": "`t=<unix seconds>,v1=<hex>` where hex = HMAC-SHA256(secret, \"<t>.<raw body>\"). Reject if |now − t| > 300s. Several v1 values may appear during a secret rotation.",
        "in": "header",
        "name": "x-map-signature",
        "required": true,
        "schema": {
          "type": "string"
        }
      }
    },
    "responses": {
      "BadRequest": {
        "content": {
          "application/json": {
            "schema": {
              "properties": {
                "error": {
                  "enum": [
                    "bad_request",
                    "conflict",
                    "internal_error",
                    "invalid_client",
                    "invalid_key",
                    "missing_key",
                    "not_configured",
                    "not_found",
                    "rate_limited",
                    "scope_denied"
                  ],
                  "type": "string"
                },
                "issues": {
                  "items": {
                    "properties": {
                      "message": {
                        "type": "string"
                      },
                      "path": {
                        "type": "string"
                      }
                    },
                    "required": [
                      "message",
                      "path"
                    ],
                    "type": "object"
                  },
                  "type": "array"
                },
                "ok": {
                  "const": false
                },
                "reason": {
                  "description": "A stable machine-readable refusal reason, when there is one.",
                  "type": "string"
                },
                "required": {
                  "enum": [
                    "read:bookings",
                    "read:courts",
                    "read:availability",
                    "read:customers",
                    "read:payments",
                    "write:bookings",
                    "read:events",
                    "write:webhooks"
                  ],
                  "type": "string"
                },
                "retryAfterSeconds": {
                  "type": "integer"
                }
              },
              "required": [
                "ok",
                "error"
              ],
              "type": "object"
            }
          }
        },
        "description": "The request body or query was invalid."
      },
      "Conflict": {
        "content": {
          "application/json": {
            "schema": {
              "properties": {
                "error": {
                  "enum": [
                    "bad_request",
                    "conflict",
                    "internal_error",
                    "invalid_client",
                    "invalid_key",
                    "missing_key",
                    "not_configured",
                    "not_found",
                    "rate_limited",
                    "scope_denied"
                  ],
                  "type": "string"
                },
                "issues": {
                  "items": {
                    "properties": {
                      "message": {
                        "type": "string"
                      },
                      "path": {
                        "type": "string"
                      }
                    },
                    "required": [
                      "message",
                      "path"
                    ],
                    "type": "object"
                  },
                  "type": "array"
                },
                "ok": {
                  "const": false
                },
                "reason": {
                  "description": "A stable machine-readable refusal reason, when there is one.",
                  "type": "string"
                },
                "required": {
                  "enum": [
                    "read:bookings",
                    "read:courts",
                    "read:availability",
                    "read:customers",
                    "read:payments",
                    "write:bookings",
                    "read:events",
                    "write:webhooks"
                  ],
                  "type": "string"
                },
                "retryAfterSeconds": {
                  "type": "integer"
                }
              },
              "required": [
                "ok",
                "error"
              ],
              "type": "object"
            }
          }
        },
        "description": "The court is already taken or blocked at that time."
      },
      "Forbidden": {
        "content": {
          "application/json": {
            "schema": {
              "properties": {
                "error": {
                  "enum": [
                    "bad_request",
                    "conflict",
                    "internal_error",
                    "invalid_client",
                    "invalid_key",
                    "missing_key",
                    "not_configured",
                    "not_found",
                    "rate_limited",
                    "scope_denied"
                  ],
                  "type": "string"
                },
                "issues": {
                  "items": {
                    "properties": {
                      "message": {
                        "type": "string"
                      },
                      "path": {
                        "type": "string"
                      }
                    },
                    "required": [
                      "message",
                      "path"
                    ],
                    "type": "object"
                  },
                  "type": "array"
                },
                "ok": {
                  "const": false
                },
                "reason": {
                  "description": "A stable machine-readable refusal reason, when there is one.",
                  "type": "string"
                },
                "required": {
                  "enum": [
                    "read:bookings",
                    "read:courts",
                    "read:availability",
                    "read:customers",
                    "read:payments",
                    "write:bookings",
                    "read:events",
                    "write:webhooks"
                  ],
                  "type": "string"
                },
                "retryAfterSeconds": {
                  "type": "integer"
                }
              },
              "required": [
                "ok",
                "error"
              ],
              "type": "object"
            }
          }
        },
        "description": "The key lacks the scope this endpoint requires; `required` names it. The call still counts against the key's rate limit — it is an authenticated request we did the work for — and carries the same `x-ratelimit-*` headers a 200 would."
      },
      "InternalError": {
        "content": {
          "application/json": {
            "schema": {
              "properties": {
                "error": {
                  "enum": [
                    "bad_request",
                    "conflict",
                    "internal_error",
                    "invalid_client",
                    "invalid_key",
                    "missing_key",
                    "not_configured",
                    "not_found",
                    "rate_limited",
                    "scope_denied"
                  ],
                  "type": "string"
                },
                "issues": {
                  "items": {
                    "properties": {
                      "message": {
                        "type": "string"
                      },
                      "path": {
                        "type": "string"
                      }
                    },
                    "required": [
                      "message",
                      "path"
                    ],
                    "type": "object"
                  },
                  "type": "array"
                },
                "ok": {
                  "const": false
                },
                "reason": {
                  "description": "A stable machine-readable refusal reason, when there is one.",
                  "type": "string"
                },
                "required": {
                  "enum": [
                    "read:bookings",
                    "read:courts",
                    "read:availability",
                    "read:customers",
                    "read:payments",
                    "write:bookings",
                    "read:events",
                    "write:webhooks"
                  ],
                  "type": "string"
                },
                "retryAfterSeconds": {
                  "type": "integer"
                }
              },
              "required": [
                "ok",
                "error"
              ],
              "type": "object"
            }
          }
        },
        "description": "Something failed on our side. Safe to retry; nothing about the fault is described beyond the code."
      },
      "NotConfigured": {
        "content": {
          "application/json": {
            "schema": {
              "properties": {
                "error": {
                  "enum": [
                    "bad_request",
                    "conflict",
                    "internal_error",
                    "invalid_client",
                    "invalid_key",
                    "missing_key",
                    "not_configured",
                    "not_found",
                    "rate_limited",
                    "scope_denied"
                  ],
                  "type": "string"
                },
                "issues": {
                  "items": {
                    "properties": {
                      "message": {
                        "type": "string"
                      },
                      "path": {
                        "type": "string"
                      }
                    },
                    "required": [
                      "message",
                      "path"
                    ],
                    "type": "object"
                  },
                  "type": "array"
                },
                "ok": {
                  "const": false
                },
                "reason": {
                  "description": "A stable machine-readable refusal reason, when there is one.",
                  "type": "string"
                },
                "required": {
                  "enum": [
                    "read:bookings",
                    "read:courts",
                    "read:availability",
                    "read:customers",
                    "read:payments",
                    "write:bookings",
                    "read:events",
                    "write:webhooks"
                  ],
                  "type": "string"
                },
                "retryAfterSeconds": {
                  "type": "integer"
                }
              },
              "required": [
                "ok",
                "error"
              ],
              "type": "object"
            }
          }
        },
        "description": "This deployment cannot verify keys at all, so no key is valid on it. Not a fault of the request."
      },
      "NotFound": {
        "content": {
          "application/json": {
            "schema": {
              "properties": {
                "error": {
                  "enum": [
                    "bad_request",
                    "conflict",
                    "internal_error",
                    "invalid_client",
                    "invalid_key",
                    "missing_key",
                    "not_configured",
                    "not_found",
                    "rate_limited",
                    "scope_denied"
                  ],
                  "type": "string"
                },
                "issues": {
                  "items": {
                    "properties": {
                      "message": {
                        "type": "string"
                      },
                      "path": {
                        "type": "string"
                      }
                    },
                    "required": [
                      "message",
                      "path"
                    ],
                    "type": "object"
                  },
                  "type": "array"
                },
                "ok": {
                  "const": false
                },
                "reason": {
                  "description": "A stable machine-readable refusal reason, when there is one.",
                  "type": "string"
                },
                "required": {
                  "enum": [
                    "read:bookings",
                    "read:courts",
                    "read:availability",
                    "read:customers",
                    "read:payments",
                    "write:bookings",
                    "read:events",
                    "write:webhooks"
                  ],
                  "type": "string"
                },
                "retryAfterSeconds": {
                  "type": "integer"
                }
              },
              "required": [
                "ok",
                "error"
              ],
              "type": "object"
            }
          }
        },
        "description": "No such court or booking at this club."
      },
      "RateLimited": {
        "content": {
          "application/json": {
            "schema": {
              "properties": {
                "error": {
                  "enum": [
                    "bad_request",
                    "conflict",
                    "internal_error",
                    "invalid_client",
                    "invalid_key",
                    "missing_key",
                    "not_configured",
                    "not_found",
                    "rate_limited",
                    "scope_denied"
                  ],
                  "type": "string"
                },
                "issues": {
                  "items": {
                    "properties": {
                      "message": {
                        "type": "string"
                      },
                      "path": {
                        "type": "string"
                      }
                    },
                    "required": [
                      "message",
                      "path"
                    ],
                    "type": "object"
                  },
                  "type": "array"
                },
                "ok": {
                  "const": false
                },
                "reason": {
                  "description": "A stable machine-readable refusal reason, when there is one.",
                  "type": "string"
                },
                "required": {
                  "enum": [
                    "read:bookings",
                    "read:courts",
                    "read:availability",
                    "read:customers",
                    "read:payments",
                    "write:bookings",
                    "read:events",
                    "write:webhooks"
                  ],
                  "type": "string"
                },
                "retryAfterSeconds": {
                  "type": "integer"
                }
              },
              "required": [
                "ok",
                "error"
              ],
              "type": "object"
            }
          }
        },
        "description": "Per-key ceiling: 600 reads and 60 writes per minute, counted against the key and charged for every authenticated call — including one refused with 403 `scope_denied`, which costs us the same work a 200 does. Rejected credentials are separately limited per calling network, so a run of 401s from one address will also start being answered with this status; the two ceilings return identical bodies and we do not publish which one refused a given call. Always honour `retry-after` rather than retrying immediately.",
        "headers": {
          "retry-after": {
            "schema": {
              "type": "integer"
            }
          }
        }
      },
      "Unauthorized": {
        "content": {
          "application/json": {
            "schema": {
              "properties": {
                "error": {
                  "enum": [
                    "bad_request",
                    "conflict",
                    "internal_error",
                    "invalid_client",
                    "invalid_key",
                    "missing_key",
                    "not_configured",
                    "not_found",
                    "rate_limited",
                    "scope_denied"
                  ],
                  "type": "string"
                },
                "issues": {
                  "items": {
                    "properties": {
                      "message": {
                        "type": "string"
                      },
                      "path": {
                        "type": "string"
                      }
                    },
                    "required": [
                      "message",
                      "path"
                    ],
                    "type": "object"
                  },
                  "type": "array"
                },
                "ok": {
                  "const": false
                },
                "reason": {
                  "description": "A stable machine-readable refusal reason, when there is one.",
                  "type": "string"
                },
                "required": {
                  "enum": [
                    "read:bookings",
                    "read:courts",
                    "read:availability",
                    "read:customers",
                    "read:payments",
                    "write:bookings",
                    "read:events",
                    "write:webhooks"
                  ],
                  "type": "string"
                },
                "retryAfterSeconds": {
                  "type": "integer"
                }
              },
              "required": [
                "ok",
                "error"
              ],
              "type": "object"
            }
          }
        },
        "description": "Missing, unknown or revoked key, or a key for another club. Repeated 401s from one calling network are themselves rate limited — a client that retries a rejected key in a tight loop will be answered 429 instead, so treat a 401 as terminal until the key is replaced."
      }
    },
    "schemas": {
      "Availability": {
        "properties": {
          "blocked": {
            "items": {
              "properties": {
                "blockId": {
                  "format": "uuid",
                  "type": "string"
                },
                "courtId": {
                  "format": "uuid",
                  "type": "string"
                },
                "endsAt": {
                  "format": "date-time",
                  "type": "string"
                },
                "reason": {
                  "enum": [
                    "maintenance",
                    "event",
                    "class",
                    "other"
                  ],
                  "type": "string"
                },
                "startsAt": {
                  "format": "date-time",
                  "type": "string"
                }
              },
              "required": [
                "blockId",
                "courtId",
                "endsAt",
                "reason",
                "startsAt"
              ],
              "type": "object"
            },
            "type": "array"
          },
          "busy": {
            "items": {
              "properties": {
                "bookingId": {
                  "format": "uuid",
                  "type": "string"
                },
                "courtId": {
                  "format": "uuid",
                  "type": "string"
                },
                "endsAt": {
                  "format": "date-time",
                  "type": "string"
                },
                "hasMatch": {
                  "type": "boolean"
                },
                "holdExpiresAt": {
                  "format": "date-time",
                  "type": [
                    "null",
                    "string"
                  ]
                },
                "startsAt": {
                  "format": "date-time",
                  "type": "string"
                },
                "status": {
                  "enum": [
                    "held",
                    "confirmed",
                    "cancelled",
                    "no_show"
                  ],
                  "type": "string"
                }
              },
              "required": [
                "bookingId",
                "courtId",
                "endsAt",
                "hasMatch",
                "holdExpiresAt",
                "startsAt",
                "status"
              ],
              "type": "object"
            },
            "type": "array"
          },
          "closed": {
            "items": {
              "properties": {
                "endsAt": {
                  "format": "date-time",
                  "type": "string"
                },
                "startsAt": {
                  "format": "date-time",
                  "type": "string"
                }
              },
              "required": [
                "endsAt",
                "startsAt"
              ],
              "type": "object"
            },
            "type": "array"
          },
          "courts": {
            "items": {
              "properties": {
                "allowedMinutes": {
                  "items": {
                    "type": "integer"
                  },
                  "type": "array"
                },
                "bufferMinutes": {
                  "type": "integer"
                },
                "capacity": {
                  "type": "integer"
                },
                "id": {
                  "format": "uuid",
                  "type": "string"
                },
                "membersOnly": {
                  "type": "boolean"
                },
                "name": {
                  "type": "string"
                },
                "slotMinutes": {
                  "type": "integer"
                },
                "sport": {
                  "enum": [
                    "padel",
                    "tennis",
                    "pickleball",
                    "squash",
                    "badminton",
                    "padbol",
                    "beach_tennis",
                    "football_7",
                    "basketball"
                  ],
                  "type": "string"
                },
                "startAlignmentMinutes": {
                  "enum": [
                    30,
                    60,
                    null
                  ],
                  "type": [
                    "integer",
                    "null"
                  ]
                },
                "surface": {
                  "enum": [
                    "artificial_grass",
                    "clay",
                    "hard",
                    "carpet",
                    "concrete",
                    "other",
                    null
                  ],
                  "type": [
                    "null",
                    "string"
                  ]
                }
              },
              "required": [
                "allowedMinutes",
                "bufferMinutes",
                "capacity",
                "id",
                "membersOnly",
                "name",
                "slotMinutes",
                "sport",
                "startAlignmentMinutes",
                "surface"
              ],
              "type": "object"
            },
            "type": "array"
          },
          "from": {
            "format": "date-time",
            "type": "string"
          },
          "ok": {
            "const": true
          },
          "openingHours": {
            "enum": [
              "published",
              "unpublished"
            ],
            "type": "string"
          },
          "to": {
            "format": "date-time",
            "type": "string"
          }
        },
        "required": [
          "blocked",
          "busy",
          "closed",
          "courts",
          "from",
          "ok",
          "openingHours",
          "to"
        ],
        "type": "object"
      },
      "Booking": {
        "properties": {
          "accessCode": {
            "pattern": "^[0-9]{6}$",
            "type": [
              "string",
              "null"
            ]
          },
          "accessValidFrom": {
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "accessValidUntil": {
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "courtId": {
            "format": "uuid",
            "type": "string"
          },
          "createdAt": {
            "format": "date-time",
            "type": "string"
          },
          "endsAt": {
            "format": "date-time",
            "type": "string"
          },
          "id": {
            "format": "uuid",
            "type": "string"
          },
          "currency": {
            "type": "string"
          },
          "notes": {
            "type": [
              "string",
              "null"
            ]
          },
          "price": {
            "description": "`priceCents` as a decimal string (`\"350.00\"`) — use this one for money, never a float.",
            "type": "string"
          },
          "priceCents": {
            "type": "integer"
          },
          "startsAt": {
            "format": "date-time",
            "type": "string"
          },
          "status": {
            "enum": [
              "held",
              "confirmed",
              "cancelled",
              "no_show"
            ],
            "type": "string"
          }
        },
        "required": [
          "accessCode",
          "accessValidFrom",
          "accessValidUntil",
          "courtId",
          "createdAt",
          "currency",
          "endsAt",
          "id",
          "notes",
          "price",
          "priceCents",
          "startsAt",
          "status"
        ],
        "type": "object"
      },
      "BookingCancelled": {
        "properties": {
          "id": {
            "format": "uuid",
            "type": "string"
          },
          "matchId": {
            "format": "uuid",
            "type": [
              "null",
              "string"
            ]
          },
          "ok": {
            "const": true
          }
        },
        "required": [
          "id",
          "matchId",
          "ok"
        ],
        "type": "object"
      },
      "BookingCreated": {
        "properties": {
          "id": {
            "format": "uuid",
            "type": "string"
          },
          "ok": {
            "const": true
          }
        },
        "required": [
          "id",
          "ok"
        ],
        "type": "object"
      },
      "EventItem": {
        "properties": {
          "data": {
            "description": "Exactly the `data` a webhook delivery of this event carries — see the event under `webhooks`.",
            "type": "object"
          },
          "event": {
            "enum": [
              "booking.created",
              "booking.confirmed",
              "booking.cancelled",
              "booking.updated",
              "payment.succeeded",
              "match.filled",
              "membership.started",
              "membership.cancelled",
              "program.booked",
              "user.signup",
              "club.player.registered",
              "court.session.starting",
              "court.session.ended",
              "court.door.unlock_requested",
              "checkin.recorded"
            ],
            "type": "string"
          },
          "id": {
            "description": "Stable for one fact (the same across every subscription it was delivered to and across re-deliveries) — de-duplicate on it.",
            "type": "string"
          },
          "occurredAt": {
            "format": "date-time",
            "type": "string"
          }
        },
        "required": [
          "data",
          "event",
          "id",
          "occurredAt"
        ],
        "type": "object"
      },
      "HookRemoved": {
        "properties": {
          "id": {
            "format": "uuid",
            "type": "string"
          },
          "ok": {
            "const": true
          }
        },
        "required": [
          "id",
          "ok"
        ],
        "type": "object"
      },
      "HookSubscribed": {
        "properties": {
          "event": {
            "enum": [
              "booking.created",
              "booking.confirmed",
              "booking.cancelled",
              "booking.updated",
              "payment.succeeded",
              "match.filled",
              "membership.started",
              "membership.cancelled",
              "program.booked",
              "user.signup",
              "club.player.registered",
              "court.session.starting",
              "court.session.ended",
              "court.door.unlock_requested",
              "checkin.recorded"
            ],
            "type": "string"
          },
          "id": {
            "format": "uuid",
            "type": "string"
          },
          "ok": {
            "const": true
          },
          "secret": {
            "description": "The HMAC-SHA256 signing secret of this hook's deliveries (`x-map-signature`). Shown once.",
            "type": "string"
          },
          "targetUrl": {
            "format": "uri",
            "type": "string"
          }
        },
        "required": [
          "event",
          "id",
          "ok",
          "secret",
          "targetUrl"
        ],
        "type": "object"
      },
      "Court": {
        "properties": {
          "active": {
            "type": "boolean"
          },
          "allowedMinutes": {
            "items": {
              "type": "integer"
            },
            "type": "array"
          },
          "capacity": {
            "type": "integer"
          },
          "enclosure": {
            "enum": [
              "indoor",
              "outdoor",
              "covered",
              null
            ],
            "type": [
              "null",
              "string"
            ]
          },
          "hasLighting": {
            "type": "boolean"
          },
          "id": {
            "format": "uuid",
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "slotMinutes": {
            "type": "integer"
          },
          "sport": {
            "enum": [
              "padel",
              "tennis",
              "pickleball",
              "squash",
              "badminton",
              "padbol",
              "beach_tennis",
              "football_7",
              "basketball"
            ],
            "type": "string"
          },
          "surface": {
            "enum": [
              "artificial_grass",
              "clay",
              "hard",
              "carpet",
              "concrete",
              "other",
              null
            ],
            "type": [
              "null",
              "string"
            ]
          }
        },
        "required": [
          "active",
          "allowedMinutes",
          "capacity",
          "enclosure",
          "hasLighting",
          "id",
          "name",
          "slotMinutes",
          "sport",
          "surface"
        ],
        "type": "object"
      },
      "Customer": {
        "properties": {
          "categoryId": {
            "format": "uuid",
            "type": [
              "string",
              "null"
            ]
          },
          "email": {
            "type": [
              "string",
              "null"
            ]
          },
          "firstVisitAt": {
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "id": {
            "format": "uuid",
            "type": "string"
          },
          "lastVisitAt": {
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "name": {
            "type": "string"
          },
          "phone": {
            "type": [
              "string",
              "null"
            ]
          },
          "tags": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "totalSpend": {
            "description": "`totalSpendCents` as a decimal string (`\"1250.00\"`): the same net-of-refunds amount, never a float.",
            "type": "string"
          },
          "totalSpendCents": {
            "description": "What this customer has actually spent at this club, in centavos: bookings and till sales, NET of every refund recorded against their booking payments — settled and still-queued alike, since a refund written down is money the club has already agreed to give back. Floored at zero. A partner must NOT subtract refunds from this again; doing so double-counts.",
            "type": "integer"
          },
          "visitCount": {
            "description": "How many visits this club has recorded for this customer. GROSS, and deliberately so: a cancelled or refunded booking leaves the visit counted, because there is no ledger of visits returned to net against. Do not read it as a count of visits paid for — pair it with totalSpendCents, which is net.",
            "type": "integer"
          }
        },
        "required": [
          "categoryId",
          "email",
          "firstVisitAt",
          "id",
          "lastVisitAt",
          "name",
          "phone",
          "tags",
          "totalSpend",
          "totalSpendCents",
          "visitCount"
        ],
        "type": "object"
      },
      "CustomerDetail": {
        "properties": {
          "categoryId": {
            "format": "uuid",
            "type": [
              "string",
              "null"
            ]
          },
          "email": {
            "type": [
              "string",
              "null"
            ]
          },
          "firstVisitAt": {
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "id": {
            "format": "uuid",
            "type": "string"
          },
          "lastVisitAt": {
            "format": "date-time",
            "type": [
              "string",
              "null"
            ]
          },
          "name": {
            "type": "string"
          },
          "phone": {
            "type": [
              "string",
              "null"
            ]
          },
          "tags": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "totalSpend": {
            "description": "`totalSpendCents` as a decimal string (`\"1250.00\"`): the same net-of-refunds amount, never a float.",
            "type": "string"
          },
          "totalSpendCents": {
            "description": "What this customer has actually spent at this club, in centavos: bookings and till sales, NET of every refund recorded against their booking payments — settled and still-queued alike, since a refund written down is money the club has already agreed to give back. Floored at zero. A partner must NOT subtract refunds from this again; doing so double-counts.",
            "type": "integer"
          },
          "visitCount": {
            "description": "How many visits this club has recorded for this customer. GROSS, and deliberately so: a cancelled or refunded booking leaves the visit counted, because there is no ledger of visits returned to net against. Do not read it as a count of visits paid for — pair it with totalSpendCents, which is net.",
            "type": "integer"
          },
          "marketingConsent": {
            "description": "The player's newest answer to THIS club's marketing prompt: `granted`, `denied`, or `unknown` (never asked, or no account). Treat `unknown` as no.",
            "enum": [
              "denied",
              "granted",
              "unknown"
            ],
            "type": "string"
          }
        },
        "required": [
          "categoryId",
          "email",
          "firstVisitAt",
          "id",
          "lastVisitAt",
          "name",
          "phone",
          "tags",
          "totalSpend",
          "totalSpendCents",
          "visitCount",
          "marketingConsent"
        ],
        "type": "object"
      },
      "Payment": {
        "properties": {
          "amount": {
            "type": "string"
          },
          "amountCents": {
            "type": "integer"
          },
          "bookingId": {
            "format": "uuid",
            "type": [
              "null",
              "string"
            ]
          },
          "createdAt": {
            "format": "date-time",
            "type": "string"
          },
          "currency": {
            "type": "string"
          },
          "depositBalanceDue": {
            "description": "`depositBalanceDueCents` as a decimal string. Present exactly when `depositBalanceDueCents` is.",
            "type": "string"
          },
          "depositBalanceDueCents": {
            "description": "Present only when this charge took the club's booking deposit and the rest of the seat is still to be paid at the club: that balance, in minor units. It was never charged online, so it is not part of `amountCents`, `net` or `fee`. Absent (treat as 0) when the charge is the whole seat, once the club records the balance as collected, and once the charge is voided or refunded in full.",
            "minimum": 1,
            "type": "integer"
          },
          "extras": {
            "type": "string"
          },
          "fee": {
            "type": "string"
          },
          "id": {
            "format": "uuid",
            "type": "string"
          },
          "net": {
            "type": "string"
          },
          "netCents": {
            "type": "integer"
          },
          "paidAt": {
            "format": "date-time",
            "type": [
              "null",
              "string"
            ]
          },
          "processing": {
            "properties": {
              "failureReason": {
                "type": [
                  "null",
                  "string"
                ]
              },
              "holdStatus": {
                "enum": [
                  "none",
                  "authorized",
                  "captured",
                  "released",
                  "failed"
                ],
                "type": "string"
              },
              "method": {
                "enum": [
                  "card",
                  "oxxo",
                  "spei",
                  "cash_at_club",
                  "wallet",
                  "on_account"
                ],
                "type": "string"
              },
              "provider": {
                "type": "string"
              }
            },
            "required": [
              "failureReason",
              "holdStatus",
              "method",
              "provider"
            ],
            "type": "object"
          },
          "refunded": {
            "type": "string"
          },
          "refundedCents": {
            "type": "integer"
          },
          "serviceStartsAt": {
            "format": "date-time",
            "type": [
              "null",
              "string"
            ]
          },
          "status": {
            "enum": [
              "pending",
              "authorized",
              "paid",
              "refunded",
              "partially_refunded",
              "failed",
              "voided",
              "credited"
            ],
            "type": "string"
          }
        },
        "required": [
          "amount",
          "amountCents",
          "bookingId",
          "createdAt",
          "currency",
          "extras",
          "fee",
          "id",
          "net",
          "netCents",
          "paidAt",
          "processing",
          "refunded",
          "refundedCents",
          "serviceStartsAt",
          "status"
        ],
        "type": "object"
      },
      "Me": {
        "description": "The signed-in player. Never an email, a phone number, a date of birth or a gender.",
        "properties": {
          "city": {
            "type": [
              "string",
              "null"
            ]
          },
          "courtSide": {
            "type": [
              "string",
              "null"
            ]
          },
          "displayName": {
            "type": "string"
          },
          "handedness": {
            "type": [
              "string",
              "null"
            ]
          },
          "id": {
            "description": "Stable for the life of the account. Key your own record on it.",
            "type": "string"
          },
          "image": {
            "type": [
              "string",
              "null"
            ]
          },
          "sports": {
            "items": {
              "enum": [
                "padel",
                "tennis",
                "pickleball",
                "squash",
                "badminton",
                "padbol",
                "beach_tennis",
                "football_7",
                "basketball"
              ],
              "type": "string"
            },
            "type": "array"
          }
        },
        "required": [
          "city",
          "courtSide",
          "displayName",
          "handedness",
          "id",
          "image",
          "sports"
        ],
        "type": "object"
      },
      "MyBooking": {
        "description": "A cancha the signed-in player booked. No club note and no door code.",
        "properties": {
          "clubId": {
            "format": "uuid",
            "type": "string"
          },
          "clubName": {
            "type": [
              "string",
              "null"
            ]
          },
          "courtName": {
            "type": [
              "string",
              "null"
            ]
          },
          "currency": {
            "type": "string"
          },
          "endsAt": {
            "format": "date-time",
            "type": "string"
          },
          "id": {
            "format": "uuid",
            "type": "string"
          },
          "price": {
            "description": "Decimal string in `currency`.",
            "type": "string"
          },
          "startsAt": {
            "format": "date-time",
            "type": "string"
          },
          "status": {
            "enum": [
              "held",
              "confirmed",
              "cancelled",
              "no_show"
            ],
            "type": "string"
          }
        },
        "required": [
          "clubId",
          "clubName",
          "courtName",
          "currency",
          "endsAt",
          "id",
          "price",
          "startsAt",
          "status"
        ],
        "type": "object"
      },
      "MyMatch": {
        "description": "A game the signed-in player holds a seat in. Nobody else in the game is named.",
        "properties": {
          "clubId": {
            "format": "uuid",
            "type": [
              "string",
              "null"
            ]
          },
          "clubName": {
            "type": [
              "string",
              "null"
            ]
          },
          "courtName": {
            "type": [
              "string",
              "null"
            ]
          },
          "endsAt": {
            "format": "date-time",
            "type": "string"
          },
          "id": {
            "format": "uuid",
            "type": "string"
          },
          "sport": {
            "enum": [
              "padel",
              "tennis",
              "pickleball",
              "squash",
              "badminton",
              "padbol",
              "beach_tennis",
              "football_7",
              "basketball"
            ],
            "type": "string"
          },
          "startsAt": {
            "format": "date-time",
            "type": "string"
          },
          "status": {
            "enum": [
              "draft",
              "open",
              "full",
              "confirmed",
              "playing",
              "completed",
              "cancelled",
              "expired"
            ],
            "type": "string"
          },
          "team": {
            "enum": [
              "team_a",
              "team_b",
              null
            ],
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "clubId",
          "clubName",
          "courtName",
          "endsAt",
          "id",
          "sport",
          "startsAt",
          "status",
          "team"
        ],
        "type": "object"
      },
      "Token": {
        "properties": {
          "accessToken": {
            "type": "string"
          },
          "expiresAt": {
            "format": "date-time",
            "type": "string"
          },
          "expiresIn": {
            "type": "integer"
          },
          "ok": {
            "const": true
          },
          "scope": {
            "type": "string"
          },
          "tokenType": {
            "const": "Bearer"
          }
        },
        "required": [
          "accessToken",
          "expiresAt",
          "expiresIn",
          "ok",
          "tokenType"
        ],
        "type": "object"
      },
      "Error": {
        "properties": {
          "error": {
            "enum": [
              "bad_request",
              "conflict",
              "internal_error",
              "invalid_client",
              "invalid_key",
              "missing_key",
              "not_configured",
              "not_found",
              "rate_limited",
              "scope_denied"
            ],
            "type": "string"
          },
          "issues": {
            "items": {
              "properties": {
                "message": {
                  "type": "string"
                },
                "path": {
                  "type": "string"
                }
              },
              "required": [
                "message",
                "path"
              ],
              "type": "object"
            },
            "type": "array"
          },
          "ok": {
            "const": false
          },
          "reason": {
            "description": "A stable machine-readable refusal reason, when there is one.",
            "type": "string"
          },
          "required": {
            "enum": [
              "read:bookings",
              "read:courts",
              "read:availability",
              "read:customers",
              "read:payments",
              "write:bookings",
              "read:events",
              "write:webhooks"
            ],
            "type": "string"
          },
          "retryAfterSeconds": {
            "type": "integer"
          }
        },
        "required": [
          "ok",
          "error"
        ],
        "type": "object"
      },
      "WebhookEnvelope": {
        "properties": {
          "data": {
            "type": "object"
          },
          "deliveredAt": {
            "format": "date-time",
            "type": "string"
          },
          "event": {
            "type": "string"
          },
          "id": {
            "format": "uuid",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "event",
          "data",
          "deliveredAt"
        ],
        "type": "object"
      }
    },
    "securitySchemes": {
      "clubApiKey": {
        "bearerFormat": "mp_…",
        "description": "A club API key from the club panel (Developers, or Integrations for a certified partner). Shown once; stored hashed. Scopes: read:bookings, read:courts, read:availability, read:customers, read:payments, write:bookings, read:events, write:webhooks.",
        "scheme": "bearer",
        "type": "http"
      },
      "playerToken": {
        "bearerFormat": "mpu_…",
        "description": "Sign in with MeetAndPlay: the access token of an OAuth application a PLAYER signed in to, from POST /oauth/token. It reads that one player's own data on /me… and nothing else. Scopes: read:me, read:my_matches, read:my_bookings. 120 calls per 60 seconds per sign-in. See docs/partner-api/oauth.md.",
        "scheme": "bearer",
        "type": "http"
      }
    }
  },
  "info": {
    "description": "Read a club's courts, availability, bookings and customers, and create or cancel bookings, with a scoped club API key. Every write goes through the same rules as the club panel.",
    "title": "MeetAndPlay Club API",
    "version": "1.0.0",
    "x-changelog": [
      {
        "changes": [
          "Every webhook delivery carries an `x-map-payload-version` header (currently `1`): the version of the payload contract. It rises only for a breaking change to an existing event's `data`, announced here with the usual notice; new fields and new events keep it."
        ],
        "date": "2026-10-03",
        "version": "1.10.0"
      },
      {
        "changes": [
          "New webhook event checkin.recorded: a booking's party (a bare reservation) or one seated player was marked as arrived, at the desk, by QR scan, at the kiosk or by beacon. Payload `data.checkin` = { bookingId, clubId, matchId, userId, checkedInAt }; `userId` is null for a bare reservation. Ids and a timestamp only. Delivered once per arrival."
        ],
        "date": "2026-10-03",
        "version": "1.9.0"
      },
      {
        "changes": [
          "Zapier / Make: POST /clubs/{clubId}/hooks (`{ target_url, event }`) and DELETE /clubs/{clubId}/hooks/{hookId} under the new scope write:webhooks — REST-hook subscribe and unsubscribe on top of the existing club webhooks, same signed deliveries.",
          "GET /clubs/{clubId}/hooks/samples/{event} and GET /clubs/{clubId}/events/{event}?since=&limit= under the new scope read:events: a static sample per event and a polling read of the club's recent webhook events with stable ids. See docs/ops/zapier.md."
        ],
        "date": "2026-10-03",
        "version": "1.8.0"
      },
      {
        "changes": [
          "Sign in with MeetAndPlay: an OAuth application registered for PLAYERS sends a player to the players app's `/oauth/authorize` and exchanges the code at POST /oauth/token exactly as a club application does. It receives an `mpu_…` access token for that one player (no `club_id` in the answer) with player scopes only: read:me, read:my_matches, read:my_bookings.",
          "New endpoints for that token: GET /me, GET /me/matches, GET /me/bookings. They name no user in the path — the token is the player. A club API key or a club's OAuth token is refused on them, and a player's token is refused on every /clubs/{clubId}/… path."
        ],
        "date": "2026-10-02",
        "version": "1.7.0"
      },
      {
        "changes": [
          "Partner OAuth: POST /oauth/token also accepts `grant_type=authorization_code` (with `code`, `redirect_uri` and the PKCE `code_verifier`) and `grant_type=refresh_token`, authenticated with the application's `client_id` and `client_secret` (HTTP Basic or body), as JSON or `application/x-www-form-urlencoded`. These two grants answer in RFC 6749 form (`access_token`, `refresh_token`, `expires_in`, `scope`, `club_id`; errors as `{ error }`). The `mpo_…` access token is sent as a bearer exactly like a key and stops working the moment the club revokes the install. See docs/partner-api/oauth.md."
        ],
        "date": "2026-10-01",
        "version": "1.6.0"
      },
      {
        "changes": [
          "Every remaining money field also as a decimal string beside its integer `*Cents` twin: `totalSpend` on customers, `depositBalanceDue` on GET /clubs/{clubId}/payments items, and `amount` / `depositBalanceDue` on the payment.succeeded webhook plus `price` on the booking.* webhooks. The `*Cents` integers are unchanged and stay; nothing is removed."
        ],
        "date": "2026-10-01",
        "version": "1.5.0"
      },
      {
        "changes": [
          "Optional `depositBalanceDueCents` on GET /clubs/{clubId}/payments items and on the payment.succeeded webhook's `data.payment`: the part of a seat still to be paid at the club when the charge took only the club's booking deposit. Absent (treat as 0) when there is no deposit."
        ],
        "date": "2026-10-01",
        "version": "1.4.0"
      },
      {
        "changes": [
          "New webhook event club.player.registered: a player account became a customer of the club for the first time. Payload `data.player` = { clubId, customerId, userId, registeredAt, source }; ids only, read the person with GET /clubs/{clubId}/customers/{customerId}."
        ],
        "date": "2026-10-01",
        "version": "1.3.0"
      },
      {
        "changes": [
          "POST /oauth/token accepts an optional `scope` (space-separated, a subset of the client's own scopes) and answers with the granted `scope`; a token narrowed this way carries only those scopes."
        ],
        "date": "2026-10-01",
        "version": "1.2.0"
      },
      {
        "changes": [
          "GET /clubs/{clubId}/payments (scope read:payments): service-date and payment-date filters, processing snapshot.",
          "Cursor pagination (`cursor`, `nextCursor`, `hasMore`) on bookings, customers and payments; money also as decimal strings (`price`, `amount`).",
          "Bookings filters: status, ids, courtId, sport, type, participant.",
          "GET /clubs/{clubId}/customers/{customerId} with `marketingConsent`.",
          "POST /oauth/token: exchange a client id and secret for a one-hour bearer.",
          "New webhook events: booking.updated, membership.started, membership.cancelled, program.booked, user.signup.",
          "New error code `invalid_client`; the error-code catalogue is published as `x-error-codes`."
        ],
        "date": "2026-09-30",
        "version": "1.1.0"
      },
      {
        "changes": [
          "Initial release: courts, availability, bookings, customers."
        ],
        "date": "2026-07-01",
        "version": "1.0.0"
      }
    ],
    "x-deprecation-policy": "A field, parameter, endpoint or error code is never removed or changed in meaning without at least one month's notice. The notice is a `deprecated: true` mark in this document plus a changelog entry; the old behaviour keeps working until the announced date. Additive changes (new fields, endpoints, scopes, error codes) are not breaking: ignore keys you do not recognise.",
    "x-error-codes": [
      {
        "code": "bad_request",
        "description": "The request is malformed: a query value, a cursor or a body field did not parse. `reason` names it and `issues` lists body fields.",
        "status": 400
      },
      {
        "code": "missing_key",
        "description": "No `Authorization: Bearer` credential was sent.",
        "status": 401
      },
      {
        "code": "invalid_key",
        "description": "The credential is unknown, revoked, expired (an exchanged token lives one hour) or belongs to another club.",
        "status": 401
      },
      {
        "code": "invalid_client",
        "description": "`POST /oauth/token` was sent a client id and secret that do not match a live key.",
        "status": 401
      },
      {
        "code": "scope_denied",
        "description": "The key is live but lacks the scope in `required`.",
        "status": 403
      },
      {
        "code": "not_found",
        "description": "The club, booking, court or customer is not visible to this key.",
        "status": 404
      },
      {
        "code": "conflict",
        "description": "The write collides with the club's state: the court is taken or blocked, or the booking is already cancelled.",
        "status": 409
      },
      {
        "code": "rate_limited",
        "description": "The key or the caller's address is over its window; wait `retry-after` seconds.",
        "status": 429
      },
      {
        "code": "internal_error",
        "description": "An unexpected fault on our side. Safe to retry reads.",
        "status": 500
      },
      {
        "code": "not_configured",
        "description": "The deployment cannot authenticate keys (no pepper configured).",
        "status": 503
      }
    ]
  },
  "jsonSchemaDialect": "https://spec.openapis.org/oas/3.1/dialect/base",
  "openapi": "3.1.0",
  "paths": {
    "/clubs/{clubId}/availability": {
      "get": {
        "operationId": "getAvailability",
        "summary": "Busy intervals per court, blocked stretches per court, and when the club is closed, in a window of at most 14 days — the same answer the players' app uses.",
        "security": [
          {
            "clubApiKey": []
          }
        ],
        "x-required-scope": "read:availability",
        "parameters": [
          {
            "$ref": "#/components/parameters/clubId"
          },
          {
            "description": "Start of the window, inclusive. An RFC 3339 instant carrying an explicit UTC offset or `Z` (`2026-07-15T12:00:00-05:00`). A date-only value (`2026-07-15`) or a local date-time with no offset (`2026-07-15T12:00:00`) is refused with a 400 rather than guessed at: neither the server's zone nor the club's is used to resolve it.",
            "in": "query",
            "name": "from",
            "required": true,
            "schema": {
              "format": "date-time",
              "type": "string"
            }
          },
          {
            "description": "End of the window, at most 14 days after `from`. An RFC 3339 instant carrying an explicit UTC offset or `Z` (`2026-07-15T12:00:00-05:00`). A date-only value (`2026-07-15`) or a local date-time with no offset (`2026-07-15T12:00:00`) is refused with a 400 rather than guessed at: neither the server's zone nor the club's is used to resolve it.",
            "in": "query",
            "name": "to",
            "required": true,
            "schema": {
              "format": "date-time",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "courtId",
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "sport",
            "schema": {
              "enum": [
                "padel",
                "tennis",
                "pickleball",
                "squash",
                "badminton",
                "padbol",
                "beach_tennis",
                "football_7",
                "basketball"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Availability"
                }
              }
            },
            "description": "Courts and the three separate reasons a span is not for sale, each in its own array and never merged: `busy` (a booking, per court), `closed` — the club's shut periods inside the window, CLUB-WIDE, as `{startsAt, endsAt}` pairs, so the caller can draw the frame as well as the holes — and `blocked`, one court taken off the market by a `court_block`, as `{blockId, courtId, reason, startsAt, endsAt}`. Offer a slot only where all three are clear; `POST /bookings` refuses each of them with a 409 or a 400. `blocked[].reason` is one of maintenance, event, class, other and says WHY the cancha is off the market, so the stretch can be labelled rather than drawn as an unexplained hole. `blocked[].blockId` is the `court_block` row the span came from: a recurring block is clipped to the requested window, so two adjacent or split stretches sharing a `blockId` are one weekly class and not two blocks. The manager's free-text note on a block is NOT returned here and is not returned under any scope on this endpoint — it is written for the club's own desk and `read:availability` is a key a club hands to a booking widget or a PMS vendor, the same reasoning that keeps customer records behind their own `read:customers` scope. `openingHours` says which of two things an empty `closed` means: `published` — we hold this club's hours, read them, and it does not shut inside the window — or `unpublished`, we have never been told its hours, so there is no frame and `closed` is empty for want of data, not because the club is open around the clock. On `unpublished`, do not offer the small hours: fall back to the club's own stated hours. Additive in 1.0.0: `openingHours`, `blocked[].reason` and `blocked[].blockId` are new keys, and every other field keeps its name, its type and its meaning — `closed` is still always present and still always an array, and a `blocked` entry still carries `courtId`, `startsAt` and `endsAt` unchanged."
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/NotConfigured"
          }
        }
      }
    },
    "/clubs/{clubId}/bookings": {
      "get": {
        "operationId": "listBookings",
        "summary": "Bookings, newest first, optionally windowed on start time.",
        "security": [
          {
            "clubApiKey": []
          }
        ],
        "x-required-scope": "read:bookings",
        "parameters": [
          {
            "$ref": "#/components/parameters/clubId"
          },
          {
            "description": "Page size, 1–100 (clamped). Default 50.",
            "in": "query",
            "name": "limit",
            "schema": {
              "maximum": 100,
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "description": "Only bookings starting at or after this instant. An RFC 3339 instant carrying an explicit UTC offset or `Z` (`2026-07-15T12:00:00-05:00`). A date-only value (`2026-07-15`) or a local date-time with no offset (`2026-07-15T12:00:00`) is refused with a 400 rather than guessed at: neither the server's zone nor the club's is used to resolve it.",
            "in": "query",
            "name": "from",
            "schema": {
              "format": "date-time",
              "type": "string"
            }
          },
          {
            "description": "Only bookings starting strictly BEFORE this instant — the bound is exclusive, so a booking starting exactly on `to` is not in the page. An RFC 3339 instant carrying an explicit UTC offset or `Z` (`2026-07-15T12:00:00-05:00`). A date-only value (`2026-07-15`) or a local date-time with no offset (`2026-07-15T12:00:00`) is refused with a 400 rather than guessed at: neither the server's zone nor the club's is used to resolve it.",
            "in": "query",
            "name": "to",
            "schema": {
              "format": "date-time",
              "type": "string"
            }
          },
          {
            "description": "The `nextCursor` of the previous page, verbatim. A value that is not one of ours is a 400.",
            "in": "query",
            "name": "cursor",
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Comma-separated booking statuses to keep.",
            "in": "query",
            "name": "status",
            "schema": {
              "items": {
                "enum": [
                  "held",
                  "confirmed",
                  "cancelled",
                  "no_show"
                ],
                "type": "string"
              },
              "type": "array"
            }
          },
          {
            "description": "Comma-separated booking ids, at most 100.",
            "in": "query",
            "name": "ids",
            "schema": {
              "items": {
                "format": "uuid",
                "type": "string"
              },
              "type": "array"
            }
          },
          {
            "description": "Only this court's bookings.",
            "in": "query",
            "name": "courtId",
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          },
          {
            "description": "Only bookings on courts of this sport.",
            "in": "query",
            "name": "sport",
            "schema": {
              "enum": [
                "padel",
                "tennis",
                "pickleball",
                "squash",
                "badminton",
                "padbol",
                "beach_tennis",
                "football_7",
                "basketball"
              ],
              "type": "string"
            }
          },
          {
            "description": "`recurring` = part of a weekly series, `single` = one-off.",
            "in": "query",
            "name": "type",
            "schema": {
              "enum": [
                "single",
                "recurring"
              ],
              "type": "string"
            }
          },
          {
            "description": "A club customer id or the booking player's account id.",
            "in": "query",
            "name": "participant",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "hasMore": {
                      "type": "boolean"
                    },
                    "items": {
                      "items": {
                        "$ref": "#/components/schemas/Booking"
                      },
                      "type": "array"
                    },
                    "nextCursor": {
                      "description": "Opaque. Pass it back as `cursor` for the next page; `null` on the last page.",
                      "type": [
                        "null",
                        "string"
                      ]
                    },
                    "ok": {
                      "const": true
                    }
                  },
                  "required": [
                    "hasMore",
                    "items",
                    "nextCursor",
                    "ok"
                  ],
                  "type": "object"
                }
              }
            },
            "description": "Bookings."
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/NotConfigured"
          }
        }
      },
      "post": {
        "operationId": "createBooking",
        "summary": "Create a confirmed booking. Refused with 409 if the court is taken or blocked.",
        "security": [
          {
            "clubApiKey": []
          }
        ],
        "x-required-scope": "write:bookings",
        "parameters": [
          {
            "$ref": "#/components/parameters/clubId"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "courtId": {
                    "format": "uuid",
                    "type": "string"
                  },
                  "endsAt": {
                    "format": "date-time",
                    "type": "string"
                  },
                  "notes": {
                    "maxLength": 1000,
                    "type": "string"
                  },
                  "priceCents": {
                    "minimum": 0,
                    "type": "integer"
                  },
                  "startsAt": {
                    "format": "date-time",
                    "type": "string"
                  }
                },
                "required": [
                  "courtId",
                  "startsAt",
                  "endsAt",
                  "priceCents"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BookingCreated"
                }
              }
            },
            "description": "Created."
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/NotConfigured"
          }
        }
      }
    },
    "/clubs/{clubId}/bookings/{bookingId}/cancel": {
      "post": {
        "operationId": "cancelBooking",
        "summary": "Cancel a booking. Any match on it is cancelled and its players refunded, exactly as from the club panel.",
        "security": [
          {
            "clubApiKey": []
          }
        ],
        "x-required-scope": "write:bookings",
        "parameters": [
          {
            "$ref": "#/components/parameters/clubId"
          },
          {
            "$ref": "#/components/parameters/bookingId"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "reason": {
                    "maxLength": 500,
                    "minLength": 1,
                    "type": "string"
                  }
                },
                "required": [
                  "reason"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BookingCancelled"
                }
              }
            },
            "description": "Cancelled. `matchId` names the match that went with it, or is null when the slot carried none."
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/NotConfigured"
          }
        }
      }
    },
    "/clubs/{clubId}/courts": {
      "get": {
        "operationId": "listCourts",
        "summary": "The club's courts.",
        "security": [
          {
            "clubApiKey": []
          }
        ],
        "x-required-scope": "read:courts",
        "parameters": [
          {
            "$ref": "#/components/parameters/clubId"
          },
          {
            "description": "Page size, 1–100 (clamped). Default 50.",
            "in": "query",
            "name": "limit",
            "schema": {
              "maximum": 100,
              "minimum": 1,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "items": {
                      "items": {
                        "$ref": "#/components/schemas/Court"
                      },
                      "type": "array"
                    },
                    "ok": {
                      "const": true
                    }
                  },
                  "required": [
                    "ok",
                    "items"
                  ],
                  "type": "object"
                }
              }
            },
            "description": "Courts."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/NotConfigured"
          }
        }
      }
    },
    "/clubs/{clubId}/customers": {
      "get": {
        "operationId": "listCustomers",
        "summary": "The club's customer records (personal data — its own scope).",
        "security": [
          {
            "clubApiKey": []
          }
        ],
        "x-required-scope": "read:customers",
        "parameters": [
          {
            "$ref": "#/components/parameters/clubId"
          },
          {
            "description": "Page size, 1–100 (clamped). Default 50.",
            "in": "query",
            "name": "limit",
            "schema": {
              "maximum": 100,
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "description": "The `nextCursor` of the previous page, verbatim. A value that is not one of ours is a 400.",
            "in": "query",
            "name": "cursor",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "hasMore": {
                      "type": "boolean"
                    },
                    "items": {
                      "items": {
                        "$ref": "#/components/schemas/Customer"
                      },
                      "type": "array"
                    },
                    "nextCursor": {
                      "description": "Opaque. Pass it back as `cursor` for the next page; `null` on the last page.",
                      "type": [
                        "null",
                        "string"
                      ]
                    },
                    "ok": {
                      "const": true
                    }
                  },
                  "required": [
                    "hasMore",
                    "items",
                    "nextCursor",
                    "ok"
                  ],
                  "type": "object"
                }
              }
            },
            "description": "Customers."
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/NotConfigured"
          }
        }
      }
    },
    "/clubs/{clubId}/customers/{customerId}": {
      "get": {
        "operationId": "getCustomer",
        "summary": "One customer record, with the club's marketing-consent flag for them.",
        "security": [
          {
            "clubApiKey": []
          }
        ],
        "x-required-scope": "read:customers",
        "parameters": [
          {
            "$ref": "#/components/parameters/clubId"
          },
          {
            "in": "path",
            "name": "customerId",
            "required": true,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "customer": {
                      "$ref": "#/components/schemas/CustomerDetail"
                    },
                    "ok": {
                      "const": true
                    }
                  },
                  "required": [
                    "customer",
                    "ok"
                  ],
                  "type": "object"
                }
              }
            },
            "description": "The customer."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/NotConfigured"
          }
        }
      }
    },
    "/clubs/{clubId}/payments": {
      "get": {
        "operationId": "listPayments",
        "summary": "Payments, newest first, filterable by service date and by payment date, with a processing snapshot (financial data — its own scope).",
        "security": [
          {
            "clubApiKey": []
          }
        ],
        "x-required-scope": "read:payments",
        "parameters": [
          {
            "$ref": "#/components/parameters/clubId"
          },
          {
            "description": "Page size, 1–100 (clamped). Default 50.",
            "in": "query",
            "name": "limit",
            "schema": {
              "maximum": 100,
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "description": "The `nextCursor` of the previous page, verbatim. A value that is not one of ours is a 400.",
            "in": "query",
            "name": "cursor",
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Only payments whose booking starts at or after this instant (the SERVICE date). An RFC 3339 instant carrying an explicit UTC offset or `Z` (`2026-07-15T12:00:00-05:00`). A date-only value (`2026-07-15`) or a local date-time with no offset (`2026-07-15T12:00:00`) is refused with a 400 rather than guessed at: neither the server's zone nor the club's is used to resolve it.",
            "in": "query",
            "name": "serviceFrom",
            "schema": {
              "format": "date-time",
              "type": "string"
            }
          },
          {
            "description": "Only payments whose booking starts strictly before this instant; with `serviceFrom` it must be later than it, or the answer is `400 payments.window`. An RFC 3339 instant carrying an explicit UTC offset or `Z` (`2026-07-15T12:00:00-05:00`). A date-only value (`2026-07-15`) or a local date-time with no offset (`2026-07-15T12:00:00`) is refused with a 400 rather than guessed at: neither the server's zone nor the club's is used to resolve it.",
            "in": "query",
            "name": "serviceTo",
            "schema": {
              "format": "date-time",
              "type": "string"
            }
          },
          {
            "description": "Only payments paid at or after this instant (the PAYMENT date). An RFC 3339 instant carrying an explicit UTC offset or `Z` (`2026-07-15T12:00:00-05:00`). A date-only value (`2026-07-15`) or a local date-time with no offset (`2026-07-15T12:00:00`) is refused with a 400 rather than guessed at: neither the server's zone nor the club's is used to resolve it.",
            "in": "query",
            "name": "paidFrom",
            "schema": {
              "format": "date-time",
              "type": "string"
            }
          },
          {
            "description": "Only payments paid strictly before this instant; with `paidFrom` it must be later than it, or the answer is `400 payments.window`. An RFC 3339 instant carrying an explicit UTC offset or `Z` (`2026-07-15T12:00:00-05:00`). A date-only value (`2026-07-15`) or a local date-time with no offset (`2026-07-15T12:00:00`) is refused with a 400 rather than guessed at: neither the server's zone nor the club's is used to resolve it.",
            "in": "query",
            "name": "paidTo",
            "schema": {
              "format": "date-time",
              "type": "string"
            }
          },
          {
            "description": "Comma-separated payment statuses to keep.",
            "in": "query",
            "name": "status",
            "schema": {
              "items": {
                "enum": [
                  "pending",
                  "authorized",
                  "paid",
                  "refunded",
                  "partially_refunded",
                  "failed",
                  "voided",
                  "credited"
                ],
                "type": "string"
              },
              "type": "array"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "hasMore": {
                      "type": "boolean"
                    },
                    "items": {
                      "items": {
                        "$ref": "#/components/schemas/Payment"
                      },
                      "type": "array"
                    },
                    "nextCursor": {
                      "description": "Opaque. Pass it back as `cursor` for the next page; `null` on the last page.",
                      "type": [
                        "null",
                        "string"
                      ]
                    },
                    "ok": {
                      "const": true
                    }
                  },
                  "required": [
                    "hasMore",
                    "items",
                    "nextCursor",
                    "ok"
                  ],
                  "type": "object"
                }
              }
            },
            "description": "Payments."
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/NotConfigured"
          }
        }
      }
    },
    "/clubs/{clubId}/events/{event}": {
      "get": {
        "operationId": "listEvents",
        "summary": "The club's latest webhook events of one type, newest first, one item per fact with a stable `id` (polling fallback). Only events recorded while the club had an active subscription to that event are visible.",
        "security": [
          {
            "clubApiKey": []
          }
        ],
        "x-required-scope": "read:events",
        "parameters": [
          {
            "$ref": "#/components/parameters/clubId"
          },
          {
            "$ref": "#/components/parameters/event"
          },
          {
            "description": "Only events first recorded after this instant. An RFC 3339 instant carrying an explicit UTC offset or `Z` (`2026-07-15T12:00:00-05:00`). A date-only value (`2026-07-15`) or a local date-time with no offset (`2026-07-15T12:00:00`) is refused with a 400 rather than guessed at: neither the server's zone nor the club's is used to resolve it.",
            "in": "query",
            "name": "since",
            "schema": {
              "format": "date-time",
              "type": "string"
            }
          },
          {
            "description": "Page size, 1–100 (clamped). Default 50.",
            "in": "query",
            "name": "limit",
            "schema": {
              "maximum": 100,
              "minimum": 1,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "items": {
                      "items": {
                        "$ref": "#/components/schemas/EventItem"
                      },
                      "type": "array"
                    },
                    "ok": {
                      "const": true
                    }
                  },
                  "required": [
                    "ok",
                    "items"
                  ],
                  "type": "object"
                }
              }
            },
            "description": "Newest first. Empty when the club had no active subscription to this event while it happened."
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/NotConfigured"
          }
        }
      }
    },
    "/clubs/{clubId}/hooks": {
      "post": {
        "operationId": "subscribeHook",
        "summary": "Zapier / Make REST hook: subscribe a public https `target_url` to one webhook event. Answers the hook id (keep it to unsubscribe) and its signing secret.",
        "security": [
          {
            "clubApiKey": []
          }
        ],
        "x-required-scope": "write:webhooks",
        "parameters": [
          {
            "$ref": "#/components/parameters/clubId"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "event": {
                    "enum": [
                      "booking.created",
                      "booking.confirmed",
                      "booking.cancelled",
                      "booking.updated",
                      "payment.succeeded",
                      "match.filled",
                      "membership.started",
                      "membership.cancelled",
                      "program.booked",
                      "user.signup",
                      "club.player.registered",
                      "court.session.starting",
                      "court.session.ended",
                      "court.door.unlock_requested",
                      "checkin.recorded"
                    ],
                    "type": "string"
                  },
                  "target_url": {
                    "description": "Public https URL. Private, loopback and link-local hosts are refused. `targetUrl` is accepted as a synonym.",
                    "format": "uri",
                    "maxLength": 2000,
                    "type": "string"
                  }
                },
                "required": [
                  "event",
                  "target_url"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HookSubscribed"
                }
              }
            },
            "description": "Subscribed."
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/NotConfigured"
          }
        }
      }
    },
    "/clubs/{clubId}/hooks/{hookId}": {
      "delete": {
        "operationId": "unsubscribeHook",
        "summary": "Zapier / Make REST hook: unsubscribe a hook an API credential subscribed. A webhook the club made in its panel is a 404 here.",
        "security": [
          {
            "clubApiKey": []
          }
        ],
        "x-required-scope": "write:webhooks",
        "parameters": [
          {
            "$ref": "#/components/parameters/clubId"
          },
          {
            "$ref": "#/components/parameters/hookId"
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HookRemoved"
                }
              }
            },
            "description": "Unsubscribed; nothing still queued for it is sent."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/NotConfigured"
          }
        }
      }
    },
    "/clubs/{clubId}/hooks/samples/{event}": {
      "get": {
        "operationId": "getHookSample",
        "summary": "A static sample of one webhook event, built by the same payload builder a live delivery uses — for Zapier's test trigger.",
        "security": [
          {
            "clubApiKey": []
          }
        ],
        "x-required-scope": "read:events",
        "parameters": [
          {
            "$ref": "#/components/parameters/clubId"
          },
          {
            "$ref": "#/components/parameters/event"
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "items": {
                      "items": {
                        "$ref": "#/components/schemas/EventItem"
                      },
                      "type": "array"
                    },
                    "ok": {
                      "const": true
                    }
                  },
                  "required": [
                    "ok",
                    "items"
                  ],
                  "type": "object"
                }
              }
            },
            "description": "One static sample item."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/NotConfigured"
          }
        }
      }
    },
    "/me": {
      "get": {
        "operationId": "getMe",
        "summary": "Sign in with MeetAndPlay: the player who signed in to the application — a stable id, the name they play under, picture, city and sports.",
        "security": [
          {
            "playerToken": []
          }
        ],
        "x-required-scope": "read:me",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "me": {
                      "$ref": "#/components/schemas/Me"
                    },
                    "ok": {
                      "const": true
                    }
                  },
                  "required": [
                    "me",
                    "ok"
                  ],
                  "type": "object"
                }
              }
            },
            "description": "The signed-in player."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/NotConfigured"
          }
        }
      }
    },
    "/me/bookings": {
      "get": {
        "operationId": "listMyBookings",
        "summary": "Sign in with MeetAndPlay: the canchas the signed-in player booked, newest first.",
        "security": [
          {
            "playerToken": []
          }
        ],
        "x-required-scope": "read:my_bookings",
        "parameters": [
          {
            "description": "`upcoming` (starts now or later) or `past` (already started). Absent = both. Anything else is a 400.",
            "in": "query",
            "name": "window",
            "schema": {
              "enum": [
                "upcoming",
                "past"
              ],
              "type": "string"
            }
          },
          {
            "description": "Page size, 1–100 (clamped). Default 50.",
            "in": "query",
            "name": "limit",
            "schema": {
              "maximum": 100,
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "description": "The `nextCursor` of the previous page, verbatim. A value that is not one of ours is a 400.",
            "in": "query",
            "name": "cursor",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "hasMore": {
                      "type": "boolean"
                    },
                    "items": {
                      "items": {
                        "$ref": "#/components/schemas/MyBooking"
                      },
                      "type": "array"
                    },
                    "nextCursor": {
                      "description": "Opaque. Pass it back as `cursor` for the next page; `null` on the last page.",
                      "type": [
                        "null",
                        "string"
                      ]
                    },
                    "ok": {
                      "const": true
                    }
                  },
                  "required": [
                    "hasMore",
                    "items",
                    "nextCursor",
                    "ok"
                  ],
                  "type": "object"
                }
              }
            },
            "description": "The player's own bookings."
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/NotConfigured"
          }
        }
      }
    },
    "/me/matches": {
      "get": {
        "operationId": "listMyMatches",
        "summary": "Sign in with MeetAndPlay: the games the signed-in player holds a seat in, newest first.",
        "security": [
          {
            "playerToken": []
          }
        ],
        "x-required-scope": "read:my_matches",
        "parameters": [
          {
            "description": "`upcoming` (starts now or later) or `past` (already started). Absent = both. Anything else is a 400.",
            "in": "query",
            "name": "window",
            "schema": {
              "enum": [
                "upcoming",
                "past"
              ],
              "type": "string"
            }
          },
          {
            "description": "Page size, 1–100 (clamped). Default 50.",
            "in": "query",
            "name": "limit",
            "schema": {
              "maximum": 100,
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "description": "The `nextCursor` of the previous page, verbatim. A value that is not one of ours is a 400.",
            "in": "query",
            "name": "cursor",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "hasMore": {
                      "type": "boolean"
                    },
                    "items": {
                      "items": {
                        "$ref": "#/components/schemas/MyMatch"
                      },
                      "type": "array"
                    },
                    "nextCursor": {
                      "description": "Opaque. Pass it back as `cursor` for the next page; `null` on the last page.",
                      "type": [
                        "null",
                        "string"
                      ]
                    },
                    "ok": {
                      "const": true
                    }
                  },
                  "required": [
                    "hasMore",
                    "items",
                    "nextCursor",
                    "ok"
                  ],
                  "type": "object"
                }
              }
            },
            "description": "The player's own games."
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/NotConfigured"
          }
        }
      }
    },
    "/oauth/token": {
      "post": {
        "operationId": "exchangeToken",
        "summary": "Exchange a client id and secret (a club API key) for a one-hour bearer token; or, for a partner OAuth application, an authorization code or a refresh token for an access token.",
        "security": [],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "client_id": {
                    "description": "The API key's id (a uuid) for `client_credentials`; the OAuth application's `client_id` for the other two grants.",
                    "type": "string"
                  },
                  "client_secret": {
                    "type": "string"
                  },
                  "code": {
                    "description": "`authorization_code` only: the one-shot code from the consent redirect. Good once, for five minutes.",
                    "type": "string"
                  },
                  "code_verifier": {
                    "description": "`authorization_code` only: the PKCE verifier whose S256 challenge was sent to the consent screen.",
                    "type": "string"
                  },
                  "grant_type": {
                    "default": "client_credentials",
                    "enum": [
                      "client_credentials",
                      "authorization_code",
                      "refresh_token"
                    ]
                  },
                  "redirect_uri": {
                    "description": "`authorization_code` only: byte-identical to the one the code was issued for.",
                    "type": "string"
                  },
                  "refresh_token": {
                    "description": "`refresh_token` only. Rotated on every use: the answer carries the next one.",
                    "type": "string"
                  },
                  "scope": {
                    "description": "Optional, space-separated. Narrows the token to a subset of the client's own scopes; a scope the client does not hold answers 400.",
                    "type": "string"
                  }
                },
                "required": [
                  "client_id",
                  "client_secret"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Token"
                }
              }
            },
            "description": "A bearer token valid for one hour. Send it exactly like a key; revoking the key revokes its tokens."
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/NotConfigured"
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "operationId": "getOpenApiDocument",
        "summary": "This document.",
        "security": [],
        "responses": {
          "200": {
            "description": "This document."
          }
        }
      }
    }
  },
  "servers": [
    {
      "url": "https://api.meetandplay.mx/api/v1"
    }
  ],
  "webhooks": {
    "booking.created": {
      "post": {
        "description": "Delivered to a subscribed club webhook when booking.created happens. Verify `x-map-signature` before trusting the body.",
        "parameters": [
          {
            "$ref": "#/components/parameters/webhookSignature"
          },
          {
            "$ref": "#/components/parameters/webhookEvent"
          },
          {
            "$ref": "#/components/parameters/webhookDelivery"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "410": {
            "description": "Gone — stop delivering this row."
          },
          "2XX": {
            "description": "Accepted. Anything else is retried."
          }
        }
      }
    },
    "booking.confirmed": {
      "post": {
        "description": "Delivered to a subscribed club webhook when booking.confirmed happens. Verify `x-map-signature` before trusting the body.",
        "parameters": [
          {
            "$ref": "#/components/parameters/webhookSignature"
          },
          {
            "$ref": "#/components/parameters/webhookEvent"
          },
          {
            "$ref": "#/components/parameters/webhookDelivery"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "410": {
            "description": "Gone — stop delivering this row."
          },
          "2XX": {
            "description": "Accepted. Anything else is retried."
          }
        }
      }
    },
    "booking.cancelled": {
      "post": {
        "description": "Delivered to a subscribed club webhook when booking.cancelled happens. Verify `x-map-signature` before trusting the body.",
        "parameters": [
          {
            "$ref": "#/components/parameters/webhookSignature"
          },
          {
            "$ref": "#/components/parameters/webhookEvent"
          },
          {
            "$ref": "#/components/parameters/webhookDelivery"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "410": {
            "description": "Gone — stop delivering this row."
          },
          "2XX": {
            "description": "Accepted. Anything else is retried."
          }
        }
      }
    },
    "booking.updated": {
      "post": {
        "description": "Delivered to a subscribed club webhook when booking.updated happens. Verify `x-map-signature` before trusting the body.",
        "parameters": [
          {
            "$ref": "#/components/parameters/webhookSignature"
          },
          {
            "$ref": "#/components/parameters/webhookEvent"
          },
          {
            "$ref": "#/components/parameters/webhookDelivery"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "410": {
            "description": "Gone — stop delivering this row."
          },
          "2XX": {
            "description": "Accepted. Anything else is retried."
          }
        }
      }
    },
    "payment.succeeded": {
      "post": {
        "description": "Delivered when a payment at the club settles. `data.payment.amountCents` is what was charged; when the club asks for a booking deposit it is the deposit only, and `depositBalanceDueCents` carries the balance to be paid at the club. Verify `x-map-signature` before trusting the body.",
        "parameters": [
          {
            "$ref": "#/components/parameters/webhookSignature"
          },
          {
            "$ref": "#/components/parameters/webhookEvent"
          },
          {
            "$ref": "#/components/parameters/webhookDelivery"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEnvelope"
                  },
                  {
                    "properties": {
                      "data": {
                        "properties": {
                          "payment": {
                            "properties": {
                              "amount": {
                                "description": "`amountCents` as a decimal string.",
                                "type": "string"
                              },
                              "amountCents": {
                                "description": "What was charged, in minor units.",
                                "type": "integer"
                              },
                              "currency": {
                                "type": "string"
                              },
                              "depositBalanceDue": {
                                "description": "`depositBalanceDueCents` as a decimal string. Present exactly when `depositBalanceDueCents` is.",
                                "type": "string"
                              },
                              "depositBalanceDueCents": {
                                "description": "Present only when the charge took the club's booking deposit: the rest of the seat, in minor units, still to be paid at the club and not part of `amountCents`. Absent (treat as 0) when there is no deposit.",
                                "minimum": 1,
                                "type": "integer"
                              },
                              "id": {
                                "format": "uuid",
                                "type": "string"
                              },
                              "matchId": {
                                "format": "uuid",
                                "type": [
                                  "null",
                                  "string"
                                ]
                              },
                              "status": {
                                "enum": [
                                  "paid"
                                ],
                                "type": "string"
                              }
                            },
                            "required": [
                              "amount",
                              "amountCents",
                              "currency",
                              "id",
                              "matchId",
                              "status"
                            ],
                            "type": "object"
                          }
                        },
                        "required": [
                          "payment"
                        ],
                        "type": "object"
                      },
                      "event": {
                        "const": "payment.succeeded"
                      }
                    },
                    "type": "object"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "410": {
            "description": "Gone — stop delivering this row."
          },
          "2XX": {
            "description": "Accepted. Anything else is retried."
          }
        }
      }
    },
    "match.filled": {
      "post": {
        "description": "Delivered to a subscribed club webhook when match.filled happens. Verify `x-map-signature` before trusting the body.",
        "parameters": [
          {
            "$ref": "#/components/parameters/webhookSignature"
          },
          {
            "$ref": "#/components/parameters/webhookEvent"
          },
          {
            "$ref": "#/components/parameters/webhookDelivery"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "410": {
            "description": "Gone — stop delivering this row."
          },
          "2XX": {
            "description": "Accepted. Anything else is retried."
          }
        }
      }
    },
    "membership.started": {
      "post": {
        "description": "Delivered to a subscribed club webhook when membership.started happens. Verify `x-map-signature` before trusting the body.",
        "parameters": [
          {
            "$ref": "#/components/parameters/webhookSignature"
          },
          {
            "$ref": "#/components/parameters/webhookEvent"
          },
          {
            "$ref": "#/components/parameters/webhookDelivery"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "410": {
            "description": "Gone — stop delivering this row."
          },
          "2XX": {
            "description": "Accepted. Anything else is retried."
          }
        }
      }
    },
    "membership.cancelled": {
      "post": {
        "description": "Delivered to a subscribed club webhook when membership.cancelled happens. Verify `x-map-signature` before trusting the body.",
        "parameters": [
          {
            "$ref": "#/components/parameters/webhookSignature"
          },
          {
            "$ref": "#/components/parameters/webhookEvent"
          },
          {
            "$ref": "#/components/parameters/webhookDelivery"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "410": {
            "description": "Gone — stop delivering this row."
          },
          "2XX": {
            "description": "Accepted. Anything else is retried."
          }
        }
      }
    },
    "program.booked": {
      "post": {
        "description": "Delivered to a subscribed club webhook when program.booked happens. Verify `x-map-signature` before trusting the body.",
        "parameters": [
          {
            "$ref": "#/components/parameters/webhookSignature"
          },
          {
            "$ref": "#/components/parameters/webhookEvent"
          },
          {
            "$ref": "#/components/parameters/webhookDelivery"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "410": {
            "description": "Gone — stop delivering this row."
          },
          "2XX": {
            "description": "Accepted. Anything else is retried."
          }
        }
      }
    },
    "user.signup": {
      "post": {
        "description": "Delivered to a subscribed club webhook when user.signup happens. Verify `x-map-signature` before trusting the body.",
        "parameters": [
          {
            "$ref": "#/components/parameters/webhookSignature"
          },
          {
            "$ref": "#/components/parameters/webhookEvent"
          },
          {
            "$ref": "#/components/parameters/webhookDelivery"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "410": {
            "description": "Gone — stop delivering this row."
          },
          "2XX": {
            "description": "Accepted. Anything else is retried."
          }
        }
      }
    },
    "club.player.registered": {
      "post": {
        "description": "Delivered once per player per club, the first time a player account becomes a customer of the club (a confirmed booking, a shop order, an approved linked membership, a residency proof or the marketing opt-in). A walk-in or hand-entered customer with no account never fires it. Carries ids only: no name, email or phone. Verify `x-map-signature` before trusting the body.",
        "parameters": [
          {
            "$ref": "#/components/parameters/webhookSignature"
          },
          {
            "$ref": "#/components/parameters/webhookEvent"
          },
          {
            "$ref": "#/components/parameters/webhookDelivery"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEnvelope"
                  },
                  {
                    "properties": {
                      "data": {
                        "properties": {
                          "player": {
                            "properties": {
                              "clubId": {
                                "format": "uuid",
                                "type": "string"
                              },
                              "customerId": {
                                "description": "The club customer id; pass it to GET /clubs/{clubId}/customers/{customerId}.",
                                "format": "uuid",
                                "type": "string"
                              },
                              "registeredAt": {
                                "format": "date-time",
                                "type": "string"
                              },
                              "source": {
                                "description": "What first put the player on the club's customer list.",
                                "enum": [
                                  "booking",
                                  "shop_order",
                                  "membership_link",
                                  "residency_proof",
                                  "marketing_opt_in",
                                  "validation_request",
                                  "match_seat"
                                ],
                                "type": "string"
                              },
                              "userId": {
                                "description": "The player's account id (opaque text).",
                                "type": "string"
                              }
                            },
                            "required": [
                              "clubId",
                              "customerId",
                              "userId",
                              "registeredAt",
                              "source"
                            ],
                            "type": "object"
                          }
                        },
                        "required": [
                          "player"
                        ],
                        "type": "object"
                      },
                      "event": {
                        "const": "club.player.registered"
                      }
                    },
                    "type": "object"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "410": {
            "description": "Gone — stop delivering this row."
          },
          "2XX": {
            "description": "Accepted. Anything else is retried."
          }
        }
      }
    },
    "court.session.starting": {
      "post": {
        "description": "Delivered to a subscribed club webhook when court.session.starting happens. Verify `x-map-signature` before trusting the body.",
        "parameters": [
          {
            "$ref": "#/components/parameters/webhookSignature"
          },
          {
            "$ref": "#/components/parameters/webhookEvent"
          },
          {
            "$ref": "#/components/parameters/webhookDelivery"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "410": {
            "description": "Gone — stop delivering this row."
          },
          "2XX": {
            "description": "Accepted. Anything else is retried."
          }
        }
      }
    },
    "court.session.ended": {
      "post": {
        "description": "Delivered to a subscribed club webhook when court.session.ended happens. Verify `x-map-signature` before trusting the body.",
        "parameters": [
          {
            "$ref": "#/components/parameters/webhookSignature"
          },
          {
            "$ref": "#/components/parameters/webhookEvent"
          },
          {
            "$ref": "#/components/parameters/webhookDelivery"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "410": {
            "description": "Gone — stop delivering this row."
          },
          "2XX": {
            "description": "Accepted. Anything else is retried."
          }
        }
      }
    },
    "court.door.unlock_requested": {
      "post": {
        "description": "Delivered to a subscribed club webhook when court.door.unlock_requested happens. Verify `x-map-signature` before trusting the body.",
        "parameters": [
          {
            "$ref": "#/components/parameters/webhookSignature"
          },
          {
            "$ref": "#/components/parameters/webhookEvent"
          },
          {
            "$ref": "#/components/parameters/webhookDelivery"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "410": {
            "description": "Gone — stop delivering this row."
          },
          "2XX": {
            "description": "Accepted. Anything else is retried."
          }
        }
      }
    },
    "checkin.recorded": {
      "post": {
        "description": "Delivered once per arrival: the desk ticked a party or a player in, a QR was scanned, the kiosk or a beacon checked a player in. Taking an arrival back and ticking it again does not deliver a second time. Carries ids and a timestamp only: no name, email or phone. Verify `x-map-signature` before trusting the body.",
        "parameters": [
          {
            "$ref": "#/components/parameters/webhookSignature"
          },
          {
            "$ref": "#/components/parameters/webhookEvent"
          },
          {
            "$ref": "#/components/parameters/webhookDelivery"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEnvelope"
                  },
                  {
                    "properties": {
                      "data": {
                        "properties": {
                          "checkin": {
                            "properties": {
                              "bookingId": {
                                "format": "uuid",
                                "type": "string"
                              },
                              "checkedInAt": {
                                "format": "date-time",
                                "type": "string"
                              },
                              "clubId": {
                                "format": "uuid",
                                "type": "string"
                              },
                              "matchId": {
                                "format": "uuid",
                                "type": [
                                  "null",
                                  "string"
                                ]
                              },
                              "userId": {
                                "description": "The arrived player's account id (opaque text); null when a whole bare reservation arrived at once.",
                                "type": [
                                  "null",
                                  "string"
                                ]
                              }
                            },
                            "required": [
                              "bookingId",
                              "checkedInAt",
                              "clubId",
                              "matchId",
                              "userId"
                            ],
                            "type": "object"
                          }
                        },
                        "required": [
                          "checkin"
                        ],
                        "type": "object"
                      },
                      "event": {
                        "const": "checkin.recorded"
                      }
                    },
                    "type": "object"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "410": {
            "description": "Gone — stop delivering this row."
          },
          "2XX": {
            "description": "Accepted. Anything else is retried."
          }
        }
      }
    }
  }
}