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.
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-DDstrings and times are 24-hourHH: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
argsobject, so Retell custom functions work without a wrapper. - Multi-location businesses can pass
locationIdon 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)" }| Status | Meaning |
|---|---|
| 400 | Invalid or missing parameters |
| 401 | Missing, invalid, or revoked API key |
| 404 | Resource not found (or belongs to another business) |
| 409 | Conflict — e.g. the slot was just taken, or the time is appointment-only |
| 429 | Rate limit exceeded — retry after Retry-After seconds |
| 500 | Something went wrong on our side |
Endpoints
Business info
/business/infoName, 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.
| Name | Type | Required | Description |
|---|---|---|---|
| locationId | integer | optional | Location 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
/availability?date=2026-08-30/availabilityOpen 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.
| Name | Type | Required | Description |
|---|---|---|---|
| date | string | required | Day to check, YYYY-MM-DD. |
| bookingType | string | optional | SIMULATOR (default), LESSONS, … |
| locationId | integer | optional | Location 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)
/reservations/calendar?startDate=2026-08-01&endDate=2026-08-31/reservations/calendarEvery 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.
| Name | Type | Required | Description |
|---|---|---|---|
| startDate | string | required | First day of the range, YYYY-MM-DD (inclusive). |
| endDate | string | required | Last day of the range, YYYY-MM-DD (inclusive). At most 31 days after startDate. |
| status | string | optional | Comma-separated statuses to include. Default: ACTIVE,HOLD,PENDING_PAYMENT,COMPLETED (everything occupying bay time). Add CANCELED, REFUNDED, or FAILED explicitly if you need them. |
| bookingType | string | optional | Filter to one type: SIMULATOR, LESSONS, MEMBERSHIP, EVENT, PACKAGE, ADMIN_BLOCK. |
| locationId | integer | optional | Filter 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"
}
]
}customerisnullfor admin blocks;isGuestdistinguishes walk-in guest bookings from account holders. Customer emails are never included in the calendar feed.endDatediffers fromdateonly when a booking crosses midnight;endTimeis 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
/reservations?phone=5125550199/reservations/lookupA 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.
| Name | Type | Required | Description |
|---|---|---|---|
| phone | string | required | Customer phone number; punctuation and country code are normalized. |
| locationId | integer | optional | Filter 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
/reservationsBooks 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.
| Name | Type | Required | Description |
|---|---|---|---|
| date | string | required | YYYY-MM-DD. |
| startTime | string | required | HH:MM, 24-hour, business-local. Use a time offered by the availability endpoint. |
| duration | integer | required | Minutes (max 1440). Must be one of the offered duration options. |
| guestPhone | string | required | Customer's phone — used to match an existing account. |
| guestFirstName | string | optional | Required when the phone matches no existing customer. |
| guestLastName | string | optional | Required when the phone matches no existing customer. |
| bayId | integer | optional | Specific bay; omitted = auto-select. |
| bookingType | string | optional | Default SIMULATOR. |
| notes | string | optional | Free-text note shown to staff. |
| locationId | integer | optional | Location 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
/reservations/{id}/reservations/modifyReschedules 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.
| Name | Type | Required | Description |
|---|---|---|---|
| reservationId | integer | required | POST twin only — the reservation to change (PATCH takes it in the URL). |
| date | string | optional | New date, YYYY-MM-DD. |
| startTime | string | optional | New start time, HH:MM. |
| duration | integer | optional | New duration in minutes. |
| bayId | integer | optional | Move to a specific bay. |
| locationId | integer | optional | Location context for the change. |
Returns the updated reservation summary, or 409 when the new slot conflicts.
Cancel a reservation
/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
/customers/lookupChecks 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
/memberships?phone=5125550199/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.