VTee API

A REST API for building on top of your VTee business — pull your booking calendar into home-automation dashboards, let an AI call agent check availability and book bays, or sync reservations into your own tools. JSON in, JSON out, scoped to your business by an API key.

Base URL  https://vteegolf.com/api/v1

Getting started

Every request is scoped to a single VTee business by its API key — there is no business ID in the URL. To get a key for your integration (an AI call agent, a Home Assistant setup, a partner app), ask the VTee team through your usual contact or the contact page. Keys are issued per integration, shown once at creation, and can be rotated or revoked at any time without affecting your other integrations.

curl https://vteegolf.com/api/v1/business/info \
  -H "Authorization: Bearer vtk_your_api_key"

Authentication

Send your key on every request as a bearer token. Keys start with vtk_.

Authorization: Bearer vtk_...
  • A missing or malformed header, or an invalid or revoked key, returns 401.
  • Treat the key like a password: server-side only, never in a browser, mobile app, or repository. If a key leaks, ask for a rotation — the old key stops working the moment the new one is issued.

Rate limits

Each API key may make 120 requests per minute across all endpoints (a fixed one-minute window). Higher limits can be granted per key — ask when you request the key. When the limit is exceeded, requests return 429 until the window resets:

HTTP/1.1 429 Too Many Requests
Retry-After: 21
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 21

{ "error": "Rate limit exceeded — try again shortly" }
  • Retry-After / X-RateLimit-Reset — seconds until the window resets. Wait that long before retrying.
  • X-RateLimit-Limit / X-RateLimit-Remaining — your per-minute ceiling and what is left of it. Rate-limit headers are also included on successful responses from newer endpoints (such as the bookings calendar), so clients can pace themselves before hitting the wall.
  • Back off exponentially on repeated 429s rather than hammering the reset.

Conventions & errors

  • All requests and responses are JSON. Dates are YYYY-MM-DD strings and times are 24-hour HH:MMstrings, both in the business's local timezone (returned by business info). Durations are integer minutes.
  • Read endpoints that matter to voice platforms have a POST twin that accepts the same arguments in the JSON body — platforms like Retell can only POST LLM-generated arguments to a static URL. The twin also unwraps arguments nested under a top-level args object, so Retell custom functions work without a wrapper.
  • Multi-location businesses can pass locationId on most endpoints; omitting it uses the default location (or all locations for the calendar feed).

Errors always carry an error message:

{ "error": "Missing or invalid date parameter (YYYY-MM-DD)" }
StatusMeaning
400Invalid or missing parameters
401Missing, invalid, or revoked API key
404Resource not found (or belongs to another business)
409Conflict — e.g. the slot was just taken, or the time is appointment-only
429Rate limit exceeded — retry after Retry-After seconds
500Something went wrong on our side

Endpoints

Business info

GET/business/info

Name, address, phone, timezone, weekly hours, today's effective hours (including holiday overrides), bay count, and booking configuration for the business your key belongs to. Call it once at startup to learn the timezone and booking rules.

Query parameters
NameTypeRequiredDescription
locationIdintegeroptionalLocation to describe (multi-location businesses). Defaults to the primary location.
{
  "name": "Iron Tee Golf",
  "address": "123 Fairway Dr, Austin, TX, 78701",
  "phone": "+15125550142",
  "timezone": "America/Chicago",
  "todayHours": { "date": "2026-08-26", "isOpen": true, "openTime": "09:00", "closeTime": "22:00" },
  "hours": [
    { "day": "monday", "isOpen": true, "openTime": "09:00", "closeTime": "22:00", "appointmentOnly": false },
    { "day": "tuesday", "isOpen": false }
  ],
  "bayCount": 6,
  "durationConfig": { "minDuration": 30, "maxDuration": 240, "interval": 30, "advanceBookingDays": 10 }
}

Availability

GET/availability?date=2026-08-30
POST/availability

Open time slots for one day: which bays are free at each slot, which durations fit, and what each duration costs. This is what an agent should call before booking. Dates beyond the business's advance-booking window return 400 with the furthest bookable date.

Query parameters (GET) or JSON body (POST)
NameTypeRequiredDescription
datestringrequiredDay to check, YYYY-MM-DD.
bookingTypestringoptionalSIMULATOR (default), LESSONS, …
locationIdintegeroptionalLocation to check.
{
  "isOpen": true,
  "timeSlots": [
    {
      "time": "18:00",
      "timeDisplay": "6:00 PM",
      "isPeak": true,
      "isAppointmentOnly": false,
      "availableBays": [{ "bayId": 3, "name": "Bay 3", "type": "GOLF_SIM" }],
      "durationOptions": [
        { "minutes": 60, "label": "1 hour", "price": 45 },
        { "minutes": 90, "label": "1.5 hours", "price": 67.5 }
      ]
    }
  ]
}

A closed day returns { "isOpen": false, "timeSlots": [] } — with "appointmentOnly": true when the day is bookable only by contacting the business.

Bookings calendar (date range)

GET/reservations/calendar?startDate=2026-08-01&endDate=2026-08-31
POST/reservations/calendar

Every booking in a date range — the feed for calendar views, dashboards, and home-automation panels. Returns each reservation with its bay, times, status, and customer. Ranges are capped at 31 days per call; page by month for longer horizons. Cross-midnight bookings that spill into the range are included.

Query parameters (GET) or JSON body (POST)
NameTypeRequiredDescription
startDatestringrequiredFirst day of the range, YYYY-MM-DD (inclusive).
endDatestringrequiredLast day of the range, YYYY-MM-DD (inclusive). At most 31 days after startDate.
statusstringoptionalComma-separated statuses to include. Default: ACTIVE,HOLD,PENDING_PAYMENT,COMPLETED (everything occupying bay time). Add CANCELED, REFUNDED, or FAILED explicitly if you need them.
bookingTypestringoptionalFilter to one type: SIMULATOR, LESSONS, MEMBERSHIP, EVENT, PACKAGE, ADMIN_BLOCK.
locationIdintegeroptionalFilter to one location. Omit for all locations.
curl "https://vteegolf.com/api/v1/reservations/calendar?startDate=2026-08-01&endDate=2026-08-31" \
  -H "Authorization: Bearer vtk_your_api_key"
{
  "startDate": "2026-08-01",
  "endDate": "2026-08-31",
  "count": 2,
  "reservations": [
    {
      "id": 18412,
      "date": "2026-08-14",
      "endDate": "2026-08-14",
      "startTime": "18:00",
      "endTime": "19:30",
      "durationMinutes": 90,
      "status": "ACTIVE",
      "type": "SIMULATOR",
      "price": 67.5,
      "bay": { "id": 3, "name": "Bay 3", "type": "GOLF_SIM" },
      "locationId": null,
      "guestCount": 0,
      "customer": { "name": "Jordan Smith", "phone": "5125550199", "isGuest": false },
      "product": { "id": 12, "name": "Sim Rental" },
      "groupId": null,
      "eventId": null,
      "createdAt": "2026-08-02T16:21:09.000Z"
    },
    {
      "id": 18475,
      "date": "2026-08-20",
      "endDate": "2026-08-20",
      "startTime": "09:00",
      "endTime": "12:00",
      "durationMinutes": 180,
      "status": "ACTIVE",
      "type": "ADMIN_BLOCK",
      "price": 0,
      "bay": { "id": 1, "name": "Bay 1", "type": "GOLF_SIM" },
      "locationId": null,
      "guestCount": 0,
      "customer": null,
      "product": { "id": 12, "name": "Sim Rental" },
      "groupId": null,
      "eventId": 91,
      "createdAt": "2026-08-10T11:00:00.000Z"
    }
  ]
}
  • customer is null for admin blocks; isGuest distinguishes walk-in guest bookings from account holders. Customer emails are never included in the calendar feed.
  • endDate differs from date only when a booking crosses midnight; endTime is wall-clock and wraps accordingly.
  • Multi-bay group bookings share a groupId.
  • Responses over 5,000 rows set "truncated": true — narrow the range if you ever see it.

Look up reservations by phone

GET/reservations?phone=5125550199
POST/reservations/lookup

A customer's reservations, matched by phone number (with or without country code). This is how a call agent answers "when is my booking again?". The POST twin takes { "phone": "..." } in the body.

Parameters
NameTypeRequiredDescription
phonestringrequiredCustomer phone number; punctuation and country code are normalized.
locationIdintegeroptionalFilter to one location.
{
  "reservations": [
    {
      "id": 18412,
      "date": "2026-08-14",
      "startTime": "18:00",
      "duration": 90,
      "bayName": "Bay 3",
      "price": 67.5,
      "status": "ACTIVE",
      "user": { "id": 512, "firstName": "Jordan", "lastName": "Smith" }
    }
  ]
}

Create a reservation

POST/reservations

Books a bay. If the phone number matches an existing customer the booking lands on their account (names optional); unknown callers must include first and last name and are booked as guests. Omit bayId to let VTee pick the optimal bay. The customer gets a confirmation SMS with a link to pay or manage the booking; payment is otherwise collected in store.

JSON body
NameTypeRequiredDescription
datestringrequiredYYYY-MM-DD.
startTimestringrequiredHH:MM, 24-hour, business-local. Use a time offered by the availability endpoint.
durationintegerrequiredMinutes (max 1440). Must be one of the offered duration options.
guestPhonestringrequiredCustomer's phone — used to match an existing account.
guestFirstNamestringoptionalRequired when the phone matches no existing customer.
guestLastNamestringoptionalRequired when the phone matches no existing customer.
bayIdintegeroptionalSpecific bay; omitted = auto-select.
bookingTypestringoptionalDefault SIMULATOR.
notesstringoptionalFree-text note shown to staff.
locationIdintegeroptionalLocation to book at.
curl -X POST https://vteegolf.com/api/v1/reservations \
  -H "Authorization: Bearer vtk_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "date": "2026-08-30",
    "startTime": "18:00",
    "duration": 90,
    "guestPhone": "512-555-0199",
    "guestFirstName": "Jordan",
    "guestLastName": "Smith"
  }'
HTTP/1.1 201 Created

{
  "reservation": {
    "id": 18412,
    "date": "2026-08-30",
    "startTime": "18:00",
    "duration": 90,
    "bayName": "Bay 3",
    "price": 67.5,
    "status": "ACTIVE"
  }
}

Conflicts (the slot was just taken, no bay fits, the time is appointment-only) return 409 with a human-readable error an agent can relay verbatim. Dates past the advance-booking window return 400 with the furthest bookable date.

Modify a reservation

PATCH/reservations/{id}
POST/reservations/modify

Reschedules a reservation — date, start time, duration, and/or bay. Only the fields you send change; price recalculates automatically when the duration changes. The POST twin takes reservationId in the body instead of the URL.

JSON body
NameTypeRequiredDescription
reservationIdintegerrequiredPOST twin only — the reservation to change (PATCH takes it in the URL).
datestringoptionalNew date, YYYY-MM-DD.
startTimestringoptionalNew start time, HH:MM.
durationintegeroptionalNew duration in minutes.
bayIdintegeroptionalMove to a specific bay.
locationIdintegeroptionalLocation context for the change.

Returns the updated reservation summary, or 409 when the new slot conflicts.

Cancel a reservation

DELETE/reservations/{id}

Cancels a reservation belonging to your business. Canceling an already-canceled reservation is a no-op that returns { "success": true, "alreadyCanceled": true }.

curl -X DELETE https://vteegolf.com/api/v1/reservations/18412 \
  -H "Authorization: Bearer vtk_your_api_key"

{ "success": true }

Customer lookup

POST/customers/lookup

Checks whether a phone number belongs to a known customer — lets an agent greet a regular by name without re-asking for details. Body: { "phone": "512-555-0199" }.

{ "found": true, "firstName": "Jordan", "lastName": "Smith" }

// or
{ "found": false }

Memberships

GET/memberships?phone=5125550199
GET/memberships/plans

/memberships?phone=returns a customer's memberships with plan, status, and minute usage — { "customer": null, "memberships": [] } when the phone matches nobody. /memberships/planslists the plans the business offers (name, pricing cadence, signup URL where self-serve signup is enabled), for answering "what memberships do you have?".

{
  "customer": { "id": 512, "firstName": "Jordan", "lastName": "Smith", "email": "jordan@example.com", "phone": "5125550199" },
  "memberships": [
    {
      "id": 88,
      "status": "ACTIVE",
      "plan": { "id": 4, "name": "Gold", "type": "UNLIMITED", "monthlyPrice": 199 },
      "minutesUsed": 340,
      "minutesAllowed": 1200,
      "startDate": "2026-01-05",
      "billingDate": 5
    }
  ]
}

Need an endpoint that isn't here, a higher rate limit, or a key for a new integration? Get in touch — the API grows with what integrators need.