Verde for developers

Booking API

New here? Read the technical notes first - which way in fits, keys, conventions, payments and webhooks in practice.

Book a Verde club’s tee times from a website or app of your own. Everything a club’s own booking page offers comes through the same rules, prices and availability - the API cannot show a time or a price the club’s site would not.

Building a web page and just want Verde’s booking in it? The club’s Booking Embed does that with two lines of HTML and no server - ask the club for its embed code.

Keys

The club creates a secret key in Verde under Website → API → Build Your Own → Step 1 and gives it to you. It looks like vsk_live_…, is shown to the club once, and belongs to that one club. A key can read (times, prices) or read and book.

Server to server only. Send it from your server, never from a browser: a request that carries a browser’sOrigin header is refused. No CORS headers are sent.

Authorization: Bearer vsk_live_...

To act for a signed-in member, add their token as well - see Members:

Verde-Member: vmt_...

Base URL and format

https://www.imverde.com/api/public/v1

Use this address exactly, with the www. The bare imverde.com redirects to it, and an HTTP client that follows a redirect to another host drops the Authorization header on the way - so the request arrives without your key and is answered 401 unauthenticated.

JSON in, JSON out. Times are ISO 8601; dates are YYYY-MM-DD in the club’s time zone (returned byGET /club). Money is integer cents, US dollars.

Subscription

The club must have an active Verde Booking Embed & API subscription, and one for the booking type you use (tee times, and so on). If either lapses, requests return 402 with a message saying which - nothing is booked.

Limits

Per key: 120 requests a minute, of which 20 may create or cancel bookings. Over that, 429 withRetry-After.

Errors

{ "error": { "code": "module_inactive", "message": "..." } }
400invalid_date, invalid_start, invalid_players, invalid_name, invalid_email, invalid_duration, invalid_bay, invalid_court, invalid_room, invalid_area, invalid_time, invalid_dates, invalid_enquiry, invalid_type, invalid_booking, invalid_guests, invalid_party_size, cart_unavailable, idempotency_key_required, invalid_code, invalid_paymentSomething in the request is missing or malformed.
401unauthenticated, member_token_invalid, member_requiredNo key, a malformed or revoked key; or a member token that has ended, or none where a member is needed.
402subscription_inactive, module_inactiveThe club is not paying for Booking Embed & API, or for this booking type.
403browser_request, insufficient_scopeCalled from a browser, or the key lacks the scope.
404course_not_found, booking_not_found, bay_not_found, court_not_found, room_not_found, area_not_found, tournament_not_found, reservation_not_found, payment_not_foundAn id that is not this club’s (or a member’s booking).
409enquiries_closed, entry_refused, release_in_progress, payment_refused, payment_incomplete, booking_refused_refunded, not_available, not_enough_spots, past, too_short, too_long, beyond_booking_window, members_only, account_required, closed, price_changed, payment_required, booking_refused, cancel_refusedThe club’s rules say no - the message says why.
405method_not_allowedWrong HTTP method; see the Allow header.
422idempotency_key_reusedThe same Idempotency-Key sent with a different body.
429rate_limitedOver the per-minute limit; wait for Retry-After seconds.
500internalVerde’s side. Safe to retry with the same Idempotency-Key.

Endpoints

GET /club

The club the key belongs to: name, time zone, its courses, and which booking types it offers.

{ "club": { "id": "…", "name": "Treetop Golf Club", "slug": "treetop", "timezone": "America/Chicago", "courses": [{ "id": "…", "name": "Championship", "holes": 18 }], "avenues": { "tee_times": true, "simulators": false, "courts": false, "lodging": false, "dining": true, "tournaments": true, "private_events": false, "packages": true } } }

GET /tee-times?date=YYYY-MM-DD&course_id=…

The tee times a guest can book that day, built exactly as the club’s booking page builds them.course_id is optional (the club’s first course). Prices are per player; a cart, where offered, is extra per player.

{ "date": "2026-10-03", "timezone": "America/Chicago", "course_id": "…", "tee_times": [ { "start": "2026-10-03T13:10:00.000Z", "time": "8:10 AM", "holes": 18, "spots_remaining": 3, "price_cents": 4800, "cart_price_cents": 1800, "rate_name": "Weekend", "twilight": false } ] }

An empty list comes with a reason: members_only, account_required (the club books members only online), closed, not_set_up, past or beyond_booking_window.

POST /tee-times/quote

What a round costs, with the club’s own fees and tax, and whether the API can book it now. Nothing is held.cart means every player takes a cart.

{ "start": "2026-10-03T13:10:00.000Z", "players": 2, "cart": true } { "quote": { "start": "…", "time": "8:10 AM", "holes": 18, "players": 2, "cart": true, "greens_fee_cents": 9600, "cart_fee_cents": 3600, "service_fee_cents": 200, "tax_cents": 924, "total_cents": 14324, "payment": { "mode": "pay_at_course", "due_now_cents": 0, "api_bookable": true } } }

api_bookable is false when the club takes a deposit or payment up front, or does not allow paying at the course. The API books pay-at-the-course only for now; send those golfers to the club’s own booking page or embed.

POST /tee-times/bookings

Books it for a golfer, paid at the course. Requires an Idempotency-Key header unique to this booking: a retry with the same key and body returns the first answer (Idempotent-Replayed: true) and never books twice. Send expected_total_cents from your quote to be told price_changed instead of booked at a new price. The quote’s total includes the club’s service fees and tax, and the booking records the same total. The golfer gets the club’s normal confirmation email, with the access code that lets them manage it.

Idempotency-Key: order-8f2c1e { "start": "2026-10-03T13:10:00.000Z", "players": 2, "cart": true, "name": "Pat Lee", "email": "pat@example.com", "phone": "402-555-0100", "player_names": ["Pat Lee", "Sam Ortiz"], "expected_total_cents": 14324 } 201 { "booking": { "id": "…", "access_code": "QRST6789", "status": "confirmed", "start": "…", "holes": 18, "players": 2, "cart": true, "name": "Pat Lee", "email": "pat@example.com", "total_cents": 14324, "payment": "pay_at_course", "manage_url": "https://imverde.com/booking/QRST6789" } }

GET /bookings/{id}

One of the club’s guest bookings: status, start, players, total and its manage link.

POST /bookings/{id}/cancel

Cancels it under the club’s own rules - the same as the golfer canceling from their confirmation - so a cancellation too close to the tee time is refused with the club’s message. Canceling twice is not an error.

GET /simulators?date=YYYY-MM-DD&duration=60

Each active bay and its free start times for a session of that length, in the club’s time zone. The length must fit the club’s minimum, maximum and step.

{ "date": "2026-10-03", "timezone": "America/Chicago", "duration_minutes": 60, "bays": [ { "id": "…", "name": "Bay 1", "max_players": 4, "simulator_type": "Trackman", "starts": ["2026-10-03T14:00:00.000Z", "2026-10-03T16:00:00.000Z"] } ] }

POST /simulators/quote

{ "bay_id": "…", "start": "2026-10-03T14:00:00.000Z", "duration": 60 } { "quote": { "bay_id": "…", "start": "…", "duration_minutes": 60, "subtotal_cents": 6000, "tax_cents": 420, "total_cents": 6420, "payment": "pay_at_venue" } }

POST /simulators/bookings

With an Idempotency-Key, as for tee times. Paid at the venue.

{ "bay_id": "…", "start": "…", "duration": 60, "party_size": 2, "name": "Pat Lee", "email": "pat@example.com", "expected_total_cents": 6420 } 201 { "reservation": { "id": "…", "type": "simulator", "status": "confirmed", "bay_id": "…", "start": "…", "duration_minutes": 60, "party_size": 2, "total_cents": 6420, "payment": "pay_at_venue" } }

POST /simulators/reservations/{id}/cancel

Cancels a guest simulator reservation under the club’s own rules. A late cancellation may carry the club’s fee, reported as fee_cents with late: true. Canceling twice is not an error.

GET /courts?date=YYYY-MM-DD&duration=60

Each available court and its free hourly starts for that length, with peak times flagged.

{ "date": "2026-10-03", "timezone": "America/Chicago", "duration_minutes": 60, "courts": [ { "id": "…", "name": "Court 1", "sport": "pickleball", "surface": "hard", "starts": [ { "start": "2026-10-03T21:00:00.000Z", "peak": false }, { "start": "2026-10-03T22:00:00.000Z", "peak": true } ] } ] }

POST /courts/quote

{ "court_id": "…", "start": "…", "duration": 60, "paddles": 4, "balls": true } { "quote": { "court_cents": 3000, "equipment_cents": 1700, "subtotal_cents": 4700, "tax_cents": 0, "total_cents": 4700, "peak": true, "payment": "pay_at_venue", … } }

POST /courts/bookings

With an Idempotency-Key. Paid at the venue.

{ "court_id": "…", "start": "…", "duration": 60, "players": 4, "paddles": 4, "balls": true, "name": "Pat Lee", "email": "pat@example.com", "expected_total_cents": 4700 }

POST /courts/reservations/{id}/cancel

As for simulators.

GET /lodging?check_in=YYYY-MM-DD&check_out=YYYY-MM-DD&guests=2

The club’s rooms free for the whole stay. A guest checking out on a date frees the room for one arriving that date. guests is optional and leaves out rooms too small.

{ "check_in": "2026-10-03", "check_out": "2026-10-05", "nights": 2, "check_in_time": "15:00", "check_out_time": "11:00", "rooms": [ { "id": "…", "name": "Cabin 1", "type": "cabin", "max_guests": 4, "beds": 2, "bed_type": "queen", "amenities": ["wifi"], "image_url": "…", "base_rate_cents": 15000 } ] }

POST /lodging/quote

{ "room_id": "…", "check_in": "2026-10-03", "check_out": "2026-10-05" } { "quote": { "nights": 2, "rate_per_night_cents": 15000, "room_cents": 30000, "fees_cents": 2500, "service_fee_cents": 0, "subtotal_cents": 32500, "tax_cents": 2275, "total_cents": 34775, "payment": { "mode": "pay_at_venue", "api_bookable": true } } }

When the club requires a deposit, api_bookable is false: send the guest to the club’s own booking page or embed until the API takes cards.

POST /lodging/bookings

With an Idempotency-Key. Paid at the venue.

{ "room_id": "…", "check_in": "2026-10-03", "check_out": "2026-10-05", "guests": 2, "name": "Pat Lee", "email": "pat@example.com", "requests": "Late arrival", "expected_total_cents": 34775 }

POST /lodging/reservations/{id}/cancel

Cancels a room booking under the club’s rules: free until the club’s cutoff before arrival; after that, the club’s late fee (a percentage of the stay) is recorded and returned as fee_cents. From arrival day on, only the club can cancel (cancel_refused). A guest booking made at this club, or - with Verde-Member - the member’s own. paid_cents is what had been paid; the refund, less any fee, is the club’s to make.

200 { "reservation": { "id": "…", "type": "lodging", "status": "cancelled" }, "fee_cents": 15000, "late": true, "paid_cents": 30000 }

GET /dining

The club’s dining areas that take reservations, with their hours and rules.

{ "timezone": "America/Chicago", "areas": [ { "id": "…", "name": "The Grill", "open_time": "17:00", "close_time": "21:00", "slot_interval_minutes": 30, "duration_minutes": 90, "max_party_size": 6, "advance_booking_days": 30, "auto_confirm": true, "deposit_required": false } ] }

GET /dining/availability?area_id=…&date=YYYY-MM-DD&party_size=2

Each slot and how many tables are free for the whole stay - the same rule the reservation itself is held to, so a slot shown free is one that will be accepted (unless someone takes it first).

{ "area_id": "…", "date": "2026-10-03", "duration_minutes": 90, "slots": [ { "time": "18:00", "tables_free": 3, "status": "available" }, { "time": "18:30", "tables_free": 0, "status": "full" } ] }

POST /dining/reservations

With an Idempotency-Key. time is the club’s local time. The reservation comes backconfirmed where the area auto-confirms, or waitlist where the club confirms reservations itself. An area that takes a deposit returns payment_required for now.

{ "area_id": "…", "date": "2026-10-03", "time": "18:00", "party_size": 4, "name": "Pat Lee", "email": "pat@example.com", "occasion": "Birthday", "requests": "Patio" }

POST /dining/reservations/{id}/cancel

Cancels it if it has not started. No fee.

GET /tournaments

The club’s upcoming public tournaments, outings and competitions, with entry fee and spots left.

{ "tournaments": [ { "id": "…", "title": "Fall Scramble", "kind": "golf", "format": "scramble", "starts_at": "2026-10-10T13:00:00.000Z", "entry_fee_cents": 7500, "capacity": 72, "entrants": 40, "spots_left": 32, "registration_closes_at": "…", "guests_can_enter": true, "url": "https://imverde.com/club-events/treetop/fall-scramble" } ] }

Where guests_can_enter is false - a paid event, or one the club keeps to members - link tourl, the club’s own sign-up page.

POST /tournaments/{id}/entries

Enters a golfer as a guest, with an Idempotency-Key. A full event puts them on the waitlist (status: "waitlist"). The same email entered twice returns the first entry withalready_entered: true. For an event with an entry fee (entry_requires_payment in the list) this answerspayment_required: take the fee with POST /payments, type: "event_entry", and the entry is made when the payment completes. A paid entry is never waitlisted - if the event filled while they paid, the payment is refunded.

{ "name": "Pat Lee", "email": "pat@example.com", "phone": "402-555-0100" } 201 { "entry": { "id": "…", "tournament_id": "…", "status": "confirmed", "name": "Pat Lee", "email": "pat@example.com", "already_entered": false } }

Packages

Stay-and-play and other bundles, sold as one price and booked as everything in them at once: the room is assigned, the tee times the golfer chose are booked, and any food and beverage or pro shop credit is set up. Each package has its own payment rule (in full, or a deposit with the rest on arrival) and cancellation rule.

GET /packages

The club’s live packages: what’s included, the price rule, arrival days, season, and the payment and cancellation rules.

{ "packages": [{ "id": "…", "slug": "fall-stay-and-play", "name": "Fall Stay and Play", "includes": { "nights": 2, "rounds": 2, "holes": 18, "cart": true, "food_and_beverage_credit": { "cents": 2500, "per_guest": true }, "pro_shop_credit": null }, "price": { "base_cents": 39900, "covers_guests": 2, "per_extra_guest_cents": 10000, "min_guests": 2, "max_guests": 4 }, "arrival_days": [4, 5], "payment": { "mode": "deposit", "deposit_percent": 25 }, "cancellation": { "free_until_days_before": 7, "late_fee_percent": 20 } }] }

GET /packages/{id}/availability

arrival (YYYY-MM-DD) and guests. The price and what’s due now, whether a room is free, and each day’s tee times with room for the party. A closed arrival day, a party out of range or a date out of season isnot_available with the reason.

{ "quote": { "total_cents": 49900, "due_now_cents": 12475, "deposit": true, "departure": "2026-10-10", "nights": 2 }, "rooms_free": 3, "rounds": 2, "course_id": "…", "bookable": true, "days": [{ "date": "2026-10-08", "tee_times": [{ "start": "2026-10-08T13:00:00.000Z", "time": "8:00 AM" }] }, …] }

Buying one

POST /payments with type: "package", package_id, arrival, guests, tee_times(one start per round, from availability) and the golfer’s name and email - then/payments/{id}/complete. The card pays what’s due now; the stay is booked when the payment completes, and if a room or tee time was taken meanwhile the payment is refunded in full. The completed payment returns theconfirmation number.

GET /packages/bookings/{id}

A package booking made at this club (or, with Verde-Member, the member’s own): the room, the tee times, the credits, and the money.

POST /packages/bookings/{id}/cancel

Cancels the whole stay under the package’s rules: free until its cutoff before arrival, then its late fee, returned as fee_cents. From arrival day only the club can cancel (cancel_refused). paid_cents is what was paid; the refund, less any fee, is the club’s to make.

GET /private-events

Whether the club takes event enquiries, its minimum guests and notice, and the occasions it takes.

POST /private-events/enquiries

With an Idempotency-Key. An enquiry is a request, not a booking: it arrives in the club’s Private Events as a new enquiry, the club is emailed, and the club replies with a quote. An email or a phone number is required.

{ "name": "Pat Lee", "email": "pat@example.com", "occasion": "wedding", "guests": 120, "preferred_date": "2027-06-12", "message": "Ceremony on the 18th green?" } 201 { "enquiry": { "id": "…", "status": "new", "type": "private_event" } }

occasion is one of wedding, corporate, banquet, celebration, golf_outing,meeting, other. A club not taking enquiries returns enquiries_closed.

Card payments

When a club takes payment up front, the booking endpoints return payment_required. Take the card instead with these two calls and Stripe.js on your own web page - card details go straight from the golfer’s browser to Stripe, never through your server or ours.

POST /payments

With an Idempotency-Key. type is tee_time, simulator, court, lodging orevent_entry (event_id, name, email - the member fee applies with Verde-Member when the event has one), with the same fields as that type’s booking endpoint. The club’s price is worked out on our side; nothing is booked yet. Where the club takes a deposit (a tee time or room quote with payment.mode: "deposit"),amount_cents is the deposit: the booking is made at the full price, marked deposit paid, and the rest is collected at the course or at check-in.

{ "type": "simulator", "bay_id": "…", "start": "…", "duration": 60, "party_size": 2, "name": "Pat Lee", "email": "pat@example.com" } 201 { "payment": { "id": "pi_…", "type": "simulator", "amount_cents": 6420, "client_secret": "pi_…_secret_…", "stripe_account": null, "publishable_key": "pk_live_…", "status": "requires_payment_method" } }

On your page, load Stripe.js with publishable_key - and, when stripe_account is not null, pass it asstripeAccount - then confirm the card with client_secret (Stripe’s Payment Element orconfirmCardPayment). Show amount_cents as the price.

POST /payments/{id}/complete

Once Stripe says the card went through. We check with Stripe, make the booking at the price paid, and return it. If the time was taken while the golfer was paying, the payment is refunded in full and you getbooking_refused_refunded with a message to show them. Safe to call more than once - and if you never call it, our Stripe webhook makes the booking anyway.

200 { "booking": { "id": "…", "total_cents": 6420, "payment": "paid", "already_completed": false } }

For a deposit, payment is "deposit_paid" and total_cents is the whole price; the rest is collected at the course or at check-in.

Members: Sign in with Verde

The API books as a guest unless a member signs in. Where a club books something for members only (or a private club), let members sign in with their Verde account - they approve on Verde, so their password never reaches your site:

  1. The club adds your return address to your key (Member sign-in on the key in Verde). GET /members/client returns your client_id and the registered return_urls, so there’s nothing else to copy.
  2. Send the member to https://www.imverde.com/connect?client_id=…&redirect_uri=…&state=… (a random state you check on return).
  3. They come back to your return address with ?code=vmc_…&state=… (or error=access_denied).
  4. From your server, POST /members/token with { code, redirect_uri } returns a member_token (30 days) and the member’s name, email and phone. Codes last five minutes and work once.
  5. Send Verde-Member: vmt_… with later requests. Tee time, simulator, court and room availability, quotes, bookings and card payments then act as the member; add "payment": "member_account" to charge their member account. POST /members/sign-out ends it.

A member token only works with the key it was issued to. Keep it on your server (an httpOnly cookie is typical), never in the browser’s JavaScript.

GET /members/client

Your key’s sign-in settings: the client_id for the sign-in link, and the return addresses the club registered. Check yours is listed before sending anyone to sign in.

{ "client_id": "eb2fe778-…", "return_urls": ["https://yourclub.com/api/auth/callback"], "sign_in_ready": true, "connect_url": "https://www.imverde.com/connect" }

POST /members/token

Swap the one-time code for a member token. From your server; the redirect_uri must be the same one the sign-in used.

{ "code": "vmc_…", "redirect_uri": "https://yourclub.com/api/auth/callback" } 200 { "member_token": "vmt_…", "expires_at": "2026-10-26T17:00:00.000Z", "member": { "id": "…", "name": "Pat Lee", "email": "pat@example.com", "phone": "+14025550100" } }

An expired, reused or foreign code is invalid_code - send the member through sign-in again.

GET /members/me

With Verde-Member: who is signed in, whether they belong to the club, and their member charge accounts there.

{ "member": { "id": "…", "name": "Pat Lee", "email": "pat@example.com", "phone": "…", "belongs_to_club": true }, "charge_accounts": [{ "id": "…", "account_number": "1042" }] }

GET /members/bookings

With Verde-Member: everything the member has at the club, soonest first - so your site can show “Your bookings” without anyone typing a confirmation number. Tee times they booked (role: host) or joined (role: player, with who’s playing), simulators, courts, rooms and dining, each with its time, status, total, code and a manage_url where Verde has one; and tee time invitations waiting for an answer, with their share. Add ?past=1 for the last 90 days too.

{ "invitations": [{ "player_id": "…", "start": "2026-10-03T13:20:00.000Z", "host": "Sam Ortiz", "share_cents": 7500, "held_until": "…", "manage_url": "https://www.imverde.com/invites?club=…" }], "bookings": [ { "type": "tee_time", "id": "…", "role": "host", "start": "2026-10-01T14:00:00.000Z", "holes": 18, "players": [{ "name": "Jordan Park", "role": "host", "is_me": true }, { "name": "Pat Lee", "role": "guest" }], "total_cents": 7500, "access_code": "K7Q2M9TX", "manage_url": "https://www.imverde.com/booking/K7Q2M9TX" }, { "type": "simulator", "id": "…", "start": "…", "end": "…", "total_cents": 4000 } ] }

POST /members/invitations/{player_id}

With Verde-Member: answer a tee time invitation from GET /members/bookings. Accepting pays the member’s share the club’s way - one of the invitation’s payment_methods. By card, POST /payments with { "type": "tee_invite", "player_id": "…" } accepts it when the payment completes. The host is told either way.

{ "action": "accept", "payment": "member_account" } { "action": "decline" }

POST /bookings/{id}/change

With Verde-Member, for a tee time the member booked: change the time, the players or carts, as in the club’s app. action: "quote" prices the changed round and returns the difference and how it’s settled (settle: the way the round was paid); members who’ve accepted stay. action: "apply" with expected_delta_cents makes it - a member account charged or credited, a card refunded, or settled at the course. A difference owed by card is payment_required: POST /payments with type: "tee_adjust", the booking_id and the same fields, and the change is made when it completes.

{ "action": "quote", "start": "2026-10-03T14:00:00.000Z", "guests": ["Pat Lee"], "members": ["…"], "cart": true } 200 { "quote": { "total_cents": 15000, "current_total_cents": 7500, "delta_cents": 7500, "settle": "account", "accepted": [{ "member_id": "…", "name": "Sam Ortiz" }], "players": [ … ] } }

GET /members/search?q=

With Verde-Member: other members of the club, by name, to invite to a tee time. Only members can search, and only names and photos come back.

{ "members": [{ "id": "…", "name": "Sam Ortiz", "avatar_url": "https://…" }] }

A member’s tee time: guests, invites and the club’s rules

With Verde-Member, a tee time books exactly as it does in the club’s own app and on its website: each player at their own rate (Member, Guest or public), guests by name, other members invited (they accept, and pay their own share, in the app or at /invites; their place is held until the club’s hold runs out), a cart per player, and the club’s rules - its fair-booking limits and how its members may pay. POST /tee-times/quote returns the priced group as group.

{ "start": "2026-10-03T13:20:00.000Z", "guests": ["Pat Lee"], "members": ["…from GET /members/search…"], "carts": [true, true, false], "discount_code": "FALL10", "gift_cards": ["GC-8K2M"], "points": 500, "payment": "member_account", "expected_total_cents": 11800 } 201 { "booking": { "id": "…", "access_code": "K7Q2M9TX", "players": [ { "kind": "host", "name": "Jordan Park", "rate": "Member", "price_cents": 0, "cart": true }, { "kind": "member", "member_id": "…", "name": "Sam Ortiz", "rate": "Member", "invited": true }, { "kind": "guest", "name": "Pat Lee", "rate": "Guest", "price_cents": 7500, "cart": false } ], "total_cents": 11800, "remaining_cents": 0, "payment": "member_account", … } }

carts lists the member first, then invited members, then guests; cart: true still means carts for everyone. The older players and player_names still work - every other player is a guest. A booking the club’s rules don’t allow is booking_refused, with the club’s reason (for example, the member already has the most upcoming tee times the club allows). By card, POST /payments with type: tee_time takes what’s due now - in full, or the club’s deposit.

Booking as the member

With Verde-Member, the tee time, simulator, court and room endpoints answer for the member: times where guests get account_required or members_only, and bookings in their name (name and email are optional - the member’s own are used). To charge their member account, add:

{ …the booking…, "payment": "member_account" } 201 { "reservation": { "id": "…", "member_id": "…", "total_cents": 6420, "payment": "member_account", … } }

POST /payments with Verde-Member books the same way by card. GET /bookings/{id} and every cancel endpoint (tee times, simulators, courts, rooms) work on the member’s own bookings, cancelled as them - the club’s cut-off and late fee apply as in the app, and fee_cents reports any fee. Dining, tournament entries and private event enquiries don’t change for members; send their name and email as usual.

POST /members/sign-out

Ends the member token at once: { "member_token": "vmt_…" }.

Tee time releases

Some clubs open a season of tee times at one moment, through a line: golfers join a waiting room, get a turn, and book on it. While a release covers a date, those tee times can only be booked by a signed-in golfer whose turn it is - which an API booking, made for a guest, never is. So for a covered date:

  • GET /tee-times returns no times, reason: "release_in_progress", and a release object with its url.
  • Quoting, booking and POST /payments answer 409 release_in_progress.
{ "date": "2027-04-03", "tee_times": [], "reason": "release_in_progress", "release": { "id": "…", "name": "2027 season", "mode": "lottery", "release_at": "2027-01-15T13:00:00.000Z", "closes_at": "2027-01-15T19:00:00.000Z", "first_date": "2027-04-01", "last_date": "2027-10-31", "url": "https://imverde.com/release/…" } }

Send the golfer to url. When the release ends (closes_at), any tee times left are bookable through the API as usual.

Webhooks

Instead of asking us what changed, have us tell you. The club adds your https address under Website → API → Build Your Own → Step 6 and copies you the signing secret. From then on we POST to it whenever one of the club’s bookings is made, paid for or canceled - however it was made: your site, the club’s site, the counter, the member app.

tee_timecreated, paid, canceled
simulatorcreated, paid, canceled
courtcreated, paid, canceled
lodgingcreated, paid, canceled
diningcreated, canceled
tournament_entrycreated, withdrawn
private_eventcreated, updated (its status changed)
pingthe club pressed "Send a test"
POST https://your-system.example/verde-webhooks Verde-Signature: t=1791000000,v1=5f2c… Verde-Event-Id: 6a1f… Verde-Event-Type: tee_time.created { "id": "6a1f…", "type": "tee_time.created", "created_at": "2026-10-01T15:04:05Z", "data": { "object": "tee_time", "id": "…", "status": "confirmed", "payment_status": "pending", "players": "2", "name": "Pat Lee", "email": "pat@example.com", "total_cents": "9600", "source": "booking_api" } }

Check it came from us. Recompute the signature from the raw body and compare, and reject a timestamp more than five minutes old so a copied request can’t be replayed:

const crypto = require('crypto'); function isFromVerde(rawBody, header, secret) { const parts = Object.fromEntries(header.split(',').map((p) => p.split('='))); if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false; const expected = crypto.createHmac('sha256', secret).update(parts.t + '.' + rawBody).digest('hex'); return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 || '')); }

Answer with any 2xx within 10 seconds. Anything else is retried after 1, 5 and 30 minutes, then 2, 6 and 12 hours - eight tries across about a day. The same event can arrive twice, so use Verde-Event-Id to ignore a repeat. An address that fails 50 times in a row is switched off, and the club sees why.