{
  "openapi": "3.0.3",
  "info": {
    "title": "P@ssID Club API — court reservations",
    "version": "1.0.0",
    "description": "Lets a club's own system book its courts for players by phone, exactly as the club's reception would. Each reservation gives the players access to the court: they scan the QR on the court, log in with their WhatsApp (one-time code), start recording, and receive every set clip and replay by WhatsApp. If the club configured its webhook in P@ssID, every clip is also POSTed there, signed with HMAC-SHA256.\n\n**Every request** sends `Authorization: Bearer cbk_…` and its own `X-Request-Id`. Successful responses are **200** and carry your `requestId` and our `reservationId`. Repeating a POST/DELETE with the same `X-Request-Id` returns the original response without executing again (`X-Idempotent-Replay: true`). Keys belong to ONE club, expire (90 days by default) and can be revoked at any time by the club owner or P@ssID.",
    "contact": {
      "name": "P@ssID",
      "url": "https://passid.com.mx"
    }
  },
  "servers": [
    {
      "url": "https://gameback.passid.com.mx/api/club/v1",
      "description": "Production"
    }
  ],
  "security": [
    {
      "bearer": []
    }
  ],
  "tags": [
    {
      "name": "Club"
    },
    {
      "name": "Reservations"
    },
    {
      "name": "Tracking"
    }
  ],
  "paths": {
    "/me": {
      "get": {
        "tags": [
          "Club"
        ],
        "summary": "Check the key: club, key expiry and courts",
        "operationId": "me",
        "parameters": [
          {
            "$ref": "#/components/parameters/RequestId"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "requestId": {
                      "type": "string"
                    },
                    "club": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        }
                      }
                    },
                    "key": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "expiresAt": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    },
                    "courts": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Court"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/courts": {
      "get": {
        "tags": [
          "Club"
        ],
        "summary": "Courts with video of this club",
        "operationId": "listCourts",
        "parameters": [
          {
            "$ref": "#/components/parameters/RequestId"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "requestId": {
                      "type": "string"
                    },
                    "courts": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Court"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/courts/{court}/availability": {
      "get": {
        "tags": [
          "Club"
        ],
        "summary": "Free start times of a day",
        "operationId": "availability",
        "parameters": [
          {
            "$ref": "#/components/parameters/RequestId"
          },
          {
            "name": "court",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Court id or slug"
          },
          {
            "name": "date",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2026-11-02"
            },
            "description": "Day in the court's timezone"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "requestId": {
                      "type": "string"
                    },
                    "court": {
                      "$ref": "#/components/schemas/Court"
                    },
                    "date": {
                      "type": "string"
                    },
                    "slots": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "startUtc": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "endUtc": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "maxMinutes": {
                            "type": "integer",
                            "description": "How many minutes can be booked starting here"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/reservations": {
      "post": {
        "tags": [
          "Reservations"
        ],
        "summary": "Book a court for one or more players",
        "operationId": "createReservation",
        "parameters": [
          {
            "$ref": "#/components/parameters/RequestId"
          }
        ],
        "description": "Creates a club reservation (it collides with members, reception and online bookings like any other). Idempotent by X-Request-Id and by externalId.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateReservation"
              },
              "example": {
                "court": "carbono-cancha-01",
                "startUtc": "2026-11-02T18:00:00-06:00",
                "minutes": 60,
                "phone": "3312345678",
                "name": "Pepe",
                "players": [
                  {
                    "phone": "3398765432",
                    "name": "Toño"
                  }
                ],
                "externalId": "CARB-000123",
                "note": "Paid at front desk"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReservationResponse"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "The court is already booked at that time",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "get": {
        "tags": [
          "Reservations"
        ],
        "summary": "Reservations of the club in a range",
        "operationId": "listReservations",
        "parameters": [
          {
            "$ref": "#/components/parameters/RequestId"
          },
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Default: today 00:00 UTC"
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Default: from + 7 days (max 62 days)"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "requestId": {
                      "type": "string"
                    },
                    "from": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "to": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "reservations": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Reservation"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/reservations/{id}": {
      "get": {
        "tags": [
          "Reservations"
        ],
        "summary": "One reservation",
        "operationId": "getReservation",
        "parameters": [
          {
            "$ref": "#/components/parameters/RequestId"
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Our reservationId"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReservationResponse"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "delete": {
        "tags": [
          "Reservations"
        ],
        "summary": "Cancel and free the court",
        "operationId": "cancelReservation",
        "parameters": [
          {
            "$ref": "#/components/parameters/RequestId"
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Cancelled (also when it already was)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReservationResponse"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/requests/{requestId}": {
      "get": {
        "tags": [
          "Tracking"
        ],
        "summary": "What happened with one of your requests",
        "operationId": "requestStatus",
        "parameters": [
          {
            "$ref": "#/components/parameters/RequestId"
          },
          {
            "name": "requestId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The X-Request-Id you sent before"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "requestId": {
                      "type": "string"
                    },
                    "lookedUp": {
                      "type": "string"
                    },
                    "at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "method": {
                      "type": "string"
                    },
                    "path": {
                      "type": "string"
                    },
                    "status": {
                      "type": "integer"
                    },
                    "seconds": {
                      "type": "number"
                    },
                    "reservationId": {
                      "type": "string",
                      "nullable": true
                    },
                    "response": {
                      "type": "string",
                      "description": "Our JSON response (up to 4 KB)"
                    },
                    "attempts": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/logs": {
      "get": {
        "tags": [
          "Tracking"
        ],
        "summary": "Call log: response, status code and seconds of every call",
        "operationId": "logs",
        "description": "Without from/to returns today's calls (Mexico City time). Max range 31 days, newest first, up to 1000. Kept 30 days.",
        "parameters": [
          {
            "$ref": "#/components/parameters/RequestId"
          },
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "requestId": {
                      "type": "string"
                    },
                    "from": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "to": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "calls": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "requestId": {
                            "type": "string"
                          },
                          "at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "key": {
                            "type": "string"
                          },
                          "method": {
                            "type": "string"
                          },
                          "path": {
                            "type": "string"
                          },
                          "query": {
                            "type": "string",
                            "nullable": true
                          },
                          "status": {
                            "type": "integer"
                          },
                          "seconds": {
                            "type": "number"
                          },
                          "reservationId": {
                            "type": "string",
                            "nullable": true
                          },
                          "ip": {
                            "type": "string"
                          },
                          "request": {
                            "type": "string",
                            "nullable": true
                          },
                          "response": {
                            "type": "string",
                            "nullable": true
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearer": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "cbk_…",
        "description": "Club API key. X-Api-Key: cbk_… also works."
      }
    },
    "parameters": {
      "RequestId": {
        "name": "X-Request-Id",
        "in": "header",
        "required": true,
        "schema": {
          "type": "string",
          "pattern": "^[A-Za-z0-9._:-]{1,100}$",
          "example": "carb-7f3c9e21"
        },
        "description": "Your unique id for this request. Echoed in the response; repeating it on POST/DELETE replays the original response."
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Missing X-Request-Id or invalid data",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Invalid, expired or revoked key",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "Not found in this club",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "More than 120 calls per minute with this key",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "requestId": {
            "type": "string"
          },
          "error": {
            "type": "string"
          }
        },
        "required": [
          "error"
        ]
      },
      "Court": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Our court id"
          },
          "slug": {
            "type": "string",
            "example": "carbono-cancha-01"
          },
          "name": {
            "type": "string",
            "example": "Cancha 1"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Court screen (what the QR on the court opens)"
          },
          "slotMinutes": {
            "type": "integer",
            "example": 60
          },
          "timezone": {
            "type": "string",
            "example": "America/Mexico_City"
          }
        }
      },
      "Player": {
        "type": "object",
        "properties": {
          "phone": {
            "type": "string",
            "example": "5213312345678",
            "description": "WhatsApp with country code; 10-digit Mexican numbers are accepted and normalized"
          },
          "name": {
            "type": "string",
            "example": "Pepe"
          }
        },
        "required": [
          "phone"
        ]
      },
      "Reservation": {
        "type": "object",
        "properties": {
          "reservationId": {
            "type": "string",
            "description": "OUR reservation id — store it next to yours"
          },
          "externalId": {
            "type": "string",
            "nullable": true,
            "description": "YOUR reservation id, if you sent one"
          },
          "court": {
            "$ref": "#/components/schemas/Court"
          },
          "startUtc": {
            "type": "string",
            "format": "date-time"
          },
          "endUtc": {
            "type": "string",
            "format": "date-time"
          },
          "players": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Player"
            }
          },
          "status": {
            "type": "string",
            "enum": [
              "upcoming",
              "live",
              "done",
              "cancelled"
            ]
          },
          "source": {
            "type": "string",
            "enum": [
              "api",
              "reception",
              "online"
            ]
          },
          "createdBy": {
            "type": "string"
          },
          "note": {
            "type": "string",
            "nullable": true
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ReservationResponse": {
        "type": "object",
        "properties": {
          "requestId": {
            "type": "string",
            "description": "Echo of your X-Request-Id"
          },
          "reservationId": {
            "type": "string"
          },
          "externalId": {
            "type": "string",
            "nullable": true
          },
          "existing": {
            "type": "boolean",
            "description": "true when your externalId already existed and we returned that reservation"
          },
          "reservation": {
            "$ref": "#/components/schemas/Reservation"
          }
        },
        "required": [
          "requestId",
          "reservationId",
          "reservation"
        ]
      },
      "CreateReservation": {
        "type": "object",
        "required": [
          "court",
          "startUtc"
        ],
        "properties": {
          "court": {
            "type": "string",
            "description": "Court id or slug"
          },
          "startUtc": {
            "type": "string",
            "format": "date-time",
            "description": "ISO 8601 with offset or Z"
          },
          "minutes": {
            "type": "integer",
            "minimum": 15,
            "maximum": 240,
            "default": 60
          },
          "phone": {
            "type": "string",
            "description": "Main player's WhatsApp (or use players)"
          },
          "name": {
            "type": "string"
          },
          "players": {
            "type": "array",
            "maxItems": 8,
            "items": {
              "$ref": "#/components/schemas/Player"
            }
          },
          "externalId": {
            "type": "string",
            "maxLength": 100,
            "description": "Your reservation id; sending it again returns the same reservation"
          },
          "note": {
            "type": "string",
            "maxLength": 200
          }
        }
      }
    }
  }
}