{
  "openapi": "3.1.0",
  "info": {
    "title": "OpenTeeSheet API",
    "version": "1.0.0",
    "summary": "Find tee times, book, cancel, and (venue owners) read today's tee sheet \u2014 for your own scripts and AI agents.",
    "description": "Every endpoint authenticates with a per-customer API key (`Authorization: Bearer ots_sk_...`) created on the account page at https://openteesheet.com/account#agent-access. A key acts AS the customer: a golfer's key can book/cancel/list only their own bookings; a venue owner's key can additionally read their own venue's today's tee sheet. Keys have a `read` or `write` scope. Rate limit: 300 calls per key per hour. The same jobs are available as MCP tools at https://openteesheet.com/mcp and from the `openteesheet` CLI (`npx openteesheet --help`). Setup guide: https://openteesheet.com/docs/agents. Zero commission: prices are always the venue's real rate.",
    "termsOfService": "https://openteesheet.com/terms",
    "contact": {
      "name": "OpenTeeSheet support",
      "url": "https://openteesheet.com"
    }
  },
  "servers": [
    {
      "url": "https://openteesheet.com"
    }
  ],
  "security": [
    {
      "apiKey": []
    }
  ],
  "tags": [
    {
      "name": "account"
    },
    {
      "name": "tee-times",
      "description": "Search venues and open bays."
    },
    {
      "name": "bookings",
      "description": "Book, cancel and list your own bookings."
    },
    {
      "name": "venue",
      "description": "Venue owner jobs."
    },
    {
      "name": "webhooks",
      "description": "Outbound webhooks and the Zapier REST-hook trigger: subscribe a URL, and we POST booking events to it signed with that subscription's own secret (X-OTS-Signature, Stripe's scheme). Needs a venue owner's key."
    }
  ],
  "paths": {
    "/api/v1/me": {
      "get": {
        "tags": [
          "account"
        ],
        "operationId": "getMe",
        "summary": "Who this key belongs to",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Me"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/tee-times/search": {
      "get": {
        "tags": [
          "tee-times"
        ],
        "operationId": "findTeeTimes",
        "summary": "Find venues and (with a date) their open bays",
        "parameters": [
          {
            "name": "city",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "date",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TeeTimesSearch"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/venues/{slug}": {
      "get": {
        "tags": [
          "tee-times"
        ],
        "operationId": "getVenue",
        "summary": "One venue's status, hours and active bays",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VenueInfo"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/bookings": {
      "get": {
        "tags": [
          "bookings"
        ],
        "operationId": "listMyBookings",
        "summary": "The caller's own upcoming and past bookings",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MyBookings"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "tags": [
          "bookings"
        ],
        "operationId": "bookTeeTime",
        "summary": "Book a tee time / simulator bay",
        "description": "SPENDS MONEY (or holds a pay-at-venue slot) and is not reversible by this endpoint once confirmed; use DELETE /api/v1/bookings/{id} to cancel. Without `confirm: true` this returns a PREVIEW (venue, bay, price) and books nothing. Requires a write-scope key. Refuses (422) venues that require card payment at booking time -- an agent cannot complete Stripe Checkout.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BookRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Booked, or a preview when confirm was not true",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BookResult"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Scope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Slot no longer available / venue closed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "This venue requires prepay; an agent cannot complete it",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/bookings/{id}": {
      "get": {
        "tags": [
          "bookings"
        ],
        "operationId": "getMyBooking",
        "summary": "One of the caller's own bookings",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Booking"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "delete": {
        "tags": [
          "bookings"
        ],
        "operationId": "cancelBooking",
        "summary": "Cancel one of the caller's own bookings",
        "description": "DESTRUCTIVE: revokes the door PIN and, if prepaid, issues a refund. Not reversible. Only allowed 30+ minutes before the session. Without `?confirm=true` this returns what WOULD happen and cancels nothing. Requires a write-scope key.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "confirm",
            "in": "query",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Cancelled, or a preview when confirm was not true",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CancelResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Scope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Already cancelled or completed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Too close to session start",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/venue/today": {
      "get": {
        "tags": [
          "venue"
        ],
        "operationId": "getTodaysSheet",
        "summary": "Today's tee sheet for the account's own venue (venue owners only)",
        "description": "403 if this account does not own a venue -- an agent can never read another venue's sheet.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TodaysSheet"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Scope"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/hooks": {
      "get": {
        "tags": [
          "webhooks"
        ],
        "operationId": "listHooks",
        "summary": "This venue's webhook subscriptions and the events available",
        "responses": {
          "200": {
            "description": "The venue's subscriptions. Secrets are not included here.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not a venue owner's key"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "tags": [
          "webhooks"
        ],
        "operationId": "subscribeHook",
        "summary": "Subscribe a URL to booking events (Zapier's \"subscribe\")",
        "description": "Idempotent per (venue, target_url): re-posting the same URL updates that subscription instead of creating a second one. target_url must be https and a public host. events defaults to every event. Needs the write scope. The response includes the signing secret.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "target_url"
                ],
                "properties": {
                  "target_url": {
                    "type": "string",
                    "format": "uri",
                    "description": "https URL we POST to. hookUrl and url are accepted as aliases."
                  },
                  "events": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "booking.created",
                        "booking.confirmed",
                        "booking.cancelled",
                        "booking.no_show"
                      ]
                    }
                  },
                  "label": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The subscription, including its signing secret.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "target_url is not an https public URL"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not a venue owner's key, or the key has no write scope"
          },
          "409": {
            "description": "Too many subscriptions on this venue"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/hooks/{id}": {
      "delete": {
        "tags": [
          "webhooks"
        ],
        "operationId": "unsubscribeHook",
        "summary": "Remove a subscription (Zapier's \"unsubscribe\")",
        "description": "Drops anything still queued for it, so a Zap that is turned off does not receive the backlog when it comes back on. Needs the write scope. A target that answers 410 Gone unsubscribes itself without this call.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Removed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not a venue owner's key, or the key has no write scope"
          },
          "404": {
            "description": "No such webhook on this venue"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/hooks/sample": {
      "get": {
        "tags": [
          "webhooks"
        ],
        "operationId": "sampleHookEvent",
        "summary": "One recent event in the delivered shape (Zapier's \"perform list\")",
        "description": "Answers the venue's most recent real booking as the chosen event would be delivered, or a clearly-marked sample (\"sample\": true) when the venue has no bookings yet -- never an empty list, which is what stalls a Zap's setup screen.",
        "parameters": [
          {
            "name": "event",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "booking.created",
                "booking.confirmed",
                "booking.cancelled",
                "booking.no_show"
              ],
              "default": "booking.created"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "An array of exactly one event payload.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Unknown event"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Not a venue owner's key"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "ots_sk_...",
        "description": "Create a key at https://openteesheet.com/account#agent-access"
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing or invalid API key",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "BadRequest": {
        "description": "Bad request",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "Not found",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Scope": {
        "description": "Key lacks the required scope, or the account does not own a venue",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Rate limit reached (300/key/hour)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          }
        }
      },
      "Me": {
        "type": "object",
        "required": [
          "email",
          "owns_venue",
          "key"
        ],
        "properties": {
          "email": {
            "type": "string"
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "role": {
            "type": "string"
          },
          "owns_venue": {
            "type": "boolean"
          },
          "key": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "scopes": {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "read",
                    "write"
                  ]
                }
              }
            }
          }
        }
      },
      "Venue": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "city": {
            "type": "string"
          },
          "state": {
            "type": "string"
          },
          "min_rate_cents": {
            "type": [
              "integer",
              "null"
            ]
          }
        }
      },
      "Bay": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "venue_id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "simulator": {
            "type": [
              "string",
              "null"
            ]
          },
          "rate_cents": {
            "type": "integer"
          },
          "max_players": {
            "type": "integer"
          }
        }
      },
      "TeeTimesSearch": {
        "type": "object",
        "required": [
          "venues"
        ],
        "properties": {
          "venues": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Venue"
            }
          },
          "bays": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Bay"
            }
          },
          "note": {
            "type": "string"
          }
        }
      },
      "VenueInfo": {
        "type": "object",
        "required": [
          "venue",
          "bays"
        ],
        "properties": {
          "venue": {
            "type": "object"
          },
          "bays": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Bay"
            }
          }
        }
      },
      "Booking": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "venue_name": {
            "type": "string"
          },
          "bay_name": {
            "type": "string"
          },
          "start_time": {
            "type": "string",
            "format": "date-time"
          },
          "end_time": {
            "type": "string",
            "format": "date-time"
          },
          "status": {
            "type": "string"
          },
          "amount_cents": {
            "type": "integer"
          },
          "payment_status": {
            "type": "string"
          }
        }
      },
      "MyBookings": {
        "type": "object",
        "required": [
          "upcoming",
          "past"
        ],
        "properties": {
          "upcoming": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Booking"
            }
          },
          "past": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Booking"
            }
          }
        }
      },
      "BookRequest": {
        "type": "object",
        "required": [
          "venue_id",
          "bay_id",
          "start_time",
          "end_time"
        ],
        "properties": {
          "venue_id": {
            "type": "string"
          },
          "bay_id": {
            "type": "string"
          },
          "start_time": {
            "type": "string",
            "format": "date-time"
          },
          "end_time": {
            "type": "string",
            "format": "date-time"
          },
          "duration_minutes": {
            "type": "integer",
            "minimum": 15,
            "default": 60
          },
          "players": {
            "type": "integer",
            "minimum": 1,
            "default": 1
          },
          "customer_name": {
            "type": "string"
          },
          "customer_email": {
            "type": "string",
            "format": "email"
          },
          "customer_phone": {
            "type": "string"
          },
          "confirm": {
            "type": "boolean",
            "default": false,
            "description": "Must be true to actually book."
          }
        }
      },
      "BookResult": {
        "type": "object",
        "properties": {
          "needsConfirmation": {
            "type": "boolean"
          },
          "preview": {
            "type": "object"
          },
          "booking_id": {
            "type": "string"
          },
          "venue_name": {
            "type": "string"
          },
          "bay_name": {
            "type": "string"
          },
          "amount_cents": {
            "type": "integer"
          },
          "pin_code": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "CancelResult": {
        "type": "object",
        "properties": {
          "needsConfirmation": {
            "type": "boolean"
          },
          "wouldCancel": {
            "type": "object"
          },
          "id": {
            "type": "string"
          },
          "refunded": {
            "type": "boolean"
          },
          "message": {
            "type": "string"
          }
        }
      },
      "TodaysSheet": {
        "type": "object",
        "required": [
          "date",
          "bookings",
          "sessions",
          "revenue_cents"
        ],
        "properties": {
          "date": {
            "type": "string",
            "format": "date"
          },
          "sessions": {
            "type": "integer"
          },
          "revenue_cents": {
            "type": "integer"
          },
          "bookings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Booking"
            }
          }
        }
      }
    }
  }
}
