RECIPE: Book an Appointment

When to use this recipe

Any visitor request about a Wix Bookings service catalog, availability, or making an appointment. Examples:

  • "What services do you offer?" / "Do you do couples massage?"
  • "Is there a free slot for a haircut this Friday at 2pm?"
  • "Find a haircut slot next Tuesday morning"
  • "Book me a haircut with Maria tomorrow"
  • "When is the next available massage?"

Inputs you need before STEP 6

InputHow to get it
Visitor tokenSTEP 1 (GenerateVisitorToken, or reuse one already in the conversation)
Business timezoneGetBusinessDetails tool if the MCP has it, otherwise the timeZone field of any STEP 3 response
Service GUID, schedule.id, form.id, type, duration, payment.optionsSTEP 2 (POST /bookings/v2/services/query)
Staff resource GUID (only if the user named a person)STEP 2 with conditionalFields: ["STAFF_MEMBER_DETAILS"]staffMemberDetails.staffMembers[].staffMemberId
A bookable slot (localStartDate, localEndDate, scheduleId, location, availableResources)STEP 3 (POST /_api/service-availability/v2/time-slots)
Fresh confirmation the slot is still openSTEP 4 (POST /_api/service-availability/v2/time-slots/get)
Booking form field keys and the visitor's answersSTEP 5 (GET /form-schema-service/v4/forms/{formId}/summary) + ask the visitor

Decision tree — pick the shortest path

  • "What do you offer?" / "Do you do X?" → STEP 1 + STEP 2. Answer from the catalog; offer to check availability.
  • "Is X free on at → STEP 1 + STEP 2 + STEP 3 with a one-minute window at that time.
  • "Find an X slot <morning/afternoon>" → STEP 1 + STEP 2 + STEP 3 over the day-part window.
  • "When is the next X?" → STEP 1 + STEP 2 + STEP 3 over 14 days with timeSlotsPerDay: 1, then drill into one day.
  • "Book me X [with ] [at → STEP 1 → STEP 2 → STEP 3 (with resourceIds if staff named) → STEP 5 (collect details) → STEP 4 (re-validate) → STEP 6 → STEP 7 if paying online.

This recipe covers appointment services (type: "APPOINTMENT"), the common case. For CLASS use List Event Time Slots and book with bookedEntity.slot.eventId; for COURSE book with bookedEntity.schedule.scheduleId (see References).


Date and time rules

Apply these before STEP 3 on every request.

  • Business timezone is the reference. Resolve "today", "tomorrow", "this Friday" against the current date in the business timezone, never UTC or your own clock. If the visitor states their own zone or city, either convert their time into the business zone, or pass their IANA zone as timeZone in STEP 3 and Wix returns slots in it.
  • Relative dates. "Tomorrow" = today + 1. "This " = the next occurrence on or after today. "Next " = the following week's occurrence when today is that weekday or later, otherwise same as "this". "This weekend" = coming Saturday and Sunday. State the resolved date in your reply so the visitor can correct you.
  • Day parts. Morning 08:00-12:00, midday 11:00-14:00, afternoon 12:00-17:00, evening 17:00-21:00, unspecified 00:00-23:59. If the window is empty, widen to the whole day and say so.
  • Exact times. For "at 2pm" query fromLocalDate = <date>T14:00:00, toLocalDate = <date>T14:01:00 and check the returned localStartDate equals the requested time. Slot granularity is per service (15/30/60 min); if the exact minute is not offered, return the two nearest slots.
  • Formats. Time-slot APIs take local ISO date-times with no offset (2026-09-18T14:00:00) plus an IANA timeZone; any offset in the string is ignored when timeZone is set. Create Booking takes absolute times (2026-09-22T18:00:00.000Z or 2026-09-22T11:00:00.000-07:00). Convert local → absolute per date (DST changes the offset), and keep endDate - startDate equal to the service duration.
  • Past and policy limits. Past times return an empty list. Slots flagged tooEarlyToBook / tooLateToBook are excluded by bookable: true. Never say a time is free without a fresh STEP 3 or STEP 4 call.

STEP 1: Get a visitor token

If you do not already have a visitor token in the conversation context, call the GenerateVisitorToken tool to mint one. Reuse the same token for every step — the booking, the checkout and the checkout URL must all be created by the same visitor identity.


STEP 2: Find the service (and its staff)

POST https://www.wixapis.com/bookings/v2/services/query

WARNING — $contains is not a supported filter operator on this endpoint. Sending "filter": { "name": { "$contains": "massage" } } returns:

Copy

$startsWith works but is case-sensitive. For anything fuzzy ("do you do massage?", "haircut", "the couples one") fetch all non-hidden services and match name, tagLine, description and category.name client-side, case-insensitive, with synonyms.

List the catalog with staff (use this by default):

Copy

If pagingMetadata.total > 100, repeat with paging.offset.

What you need from the response

Copy
  • staffMemberDetails.staffMembers[].staffMemberId is the resource GUID used by availability (resourceIds) and booking (resource.id). No separate staff query needed. If a service lacks staff details, POST /bookings/v1/staff-members/query with {"query":{}} works for visitors — use its resourceId, not id.
  • If the user named a person who is not on the requested service, say who does provide it instead of forcing the booking.
  • Answer "what do you offer" with name, one-line description, duration, price and staff for each non-hidden, online-bookable service.

STEP 3: List available slots

POST https://www.wixapis.com/_api/service-availability/v2/time-slots

WARNING — availableResources comes back empty unless you send includeResourceTypeIds. Without it every slot has "availableResources": [] and you cannot tell who is free. Always include the staff resource type constant 1cd44cf8-756f-41c3-bd90-3e2ffcaf1155.

Slots in a window (day part or whole day):

Copy

Variants:

  • Exact time ("Friday at 2pm"): fromLocalDate = the time, toLocalDate = one minute later.
  • Specific staff: add "resourceIds": ["<STAFF_RESOURCE_GUID>"].
  • Several business locations: add "locations": [{ "id": "<LOCATION_GUID>", "locationType": "BUSINESS" }].
  • "Next available" overview: 14-day range plus "timeSlotsPerDay": 1.
  • More pages: pass cursorPagingMetadata.cursors.next as cursorPaging.cursor.

What you need from the response

Copy

Empty timeSlots means nothing is open in that window (closed day, fully booked, or past). Widen the window and offer alternatives instead of answering a bare "no".


STEP 4: Re-validate the chosen slot

POST https://www.wixapis.com/_api/service-availability/v2/time-slots/get

Call this immediately before STEP 6, after the visitor has picked a time and given their details. It returns the full resource list for that one slot.

Copy

What you need from the response

Copy

If bookable is false or the requested person is missing, go back to STEP 3 and offer alternatives.


STEP 5: Get the booking form fields and collect the visitor's details

GET https://www.wixapis.com/form-schema-service/v4/forms/<FORM_GUID>/summary

Copy

What you need from the response

Copy
  • fields[].target are the exact JSON keys for formSubmission in STEP 6. They are snake_case (first_name), not camelCase.
  • The default form requires first name, last name, email and phone. Custom forms can add or require more; ask the visitor for every required field and never invent contact data.
  • phone must be a valid E.164 number from a country the site allows. Reserved fake numbers (+1555010xxxx) are rejected.

STEP 6: Create the booking

POST https://www.wixapis.com/bookings/v2/bookings

WARNING — camelCase form keys return 400. This body fragment fails:

Copy
Copy

Use the target keys from STEP 5 and a real phone number.

Copy

Field mapping:

FieldSource
slot.serviceIdSTEP 2 services[].id
slot.scheduleIdSTEP 3 timeSlots[].scheduleId (= service schedule.id)
slot.startDate / endDateSTEP 3 localStartDate / localEndDate converted to absolute time in the business zone
slot.timezonebusiness timezone
slot.resourceone entry from STEP 4 availableResources[].resources[]; omit the whole resource object when the visitor has no preference and Wix will assign staff
slot.locationSTEP 3 location.id + name, with locationType OWNER_BUSINESS (slots say BUSINESS; CUSTOMER and CUSTOM map unchanged)
selectedPaymentOptionONLINE if payment.options.online; OFFLINE if only inPerson; MEMBERSHIP for pricing-plan members. Ask when both online and offline are allowed
formSubmissionSTEP 5 target keys → visitor answers. Do not also send booking.contactDetails

What you need from the response

Copy

A CREATED booking does not hold the slot for the visitor until it is paid or confirmed, so run STEP 7 right away. If the call fails because the slot was just taken, return to STEP 3. If selectedPaymentOption was OFFLINE, stop here and tell the visitor the booking is submitted and (if the business approves manually) awaiting confirmation.


POST https://www.wixapis.com/ecom/v1/checkouts

WARNING — catalogItemId is the booking GUID, not the service GUID, and appId is the Wix Bookings catalog app ID, not the site's app or the service's appId field value you might see elsewhere.

Copy

What you need from the response

Copy

Then:

Copy
Copy

Reply with the summary (service, local date and time, staff, location, total) and the checkout URL. Wix confirms the booking automatically after payment (CONFIRMED, or PENDING when the business approves manually). Unpaid CREATED bookings expire on their own.


Common pitfalls (from real conversations)

These are the ways this flow has failed in practice. The steps warn about them inline; this section is the consolidated reference.

1. $contains on Query Services returns 400

Copy

Resolution: drop the filter (or use hidden: false only), fetch up to 100 services, match client-side and case-insensitively across name, tagLine, description, category.name.

2. List Availability Time Slots returns availableResources: []

You omitted includeResourceTypeIds. Without it the slots are still correct but anonymous, so you cannot honour "with Maria" or name the stylist. Resolution: always send "includeResourceTypeIds": ["1cd44cf8-756f-41c3-bd90-3e2ffcaf1155"]; to restrict to one person also send resourceIds.

3. camelCase or made-up form keys fail Create Booking

Copy

Resolution: keys are the target values from Get Form Summary (first_name, last_name, email, phone, …).

4. Phone rejected: "country code must correspond to one from allowed countries"

Placeholder numbers such as +15555550123 fail validation. Resolution: collect the visitor's real number in E.164 form; do not fabricate one to get past the step.

5. Wrong locationType on the booking

Slots report "locationType": "BUSINESS", but Create Booking expects OWNER_BUSINESS for a business location. Copy id and name from the slot and translate the type.

6. Treating a CREATED booking as done

status: "CREATED" is unpaid and unconfirmed; it neither blocks the slot nor appears in the business calendar. Always continue to STEP 7 for online payment, and say "pay here to confirm" rather than "you're booked".

7. Checkout with the service GUID

catalogReference.catalogItemId must be the booking GUID from STEP 6. The service GUID produces an unusable checkout. appId is always 13d21c63-b5ec-5912-8397-c3a5ddb27a97.

8. Trying to read, cancel or reschedule as a visitor

GET /bookings/v2/bookings/{id}404, POST /bookings/v2/bookings/{id}/cancel403 with a visitor token. Visitors cannot manage bookings through the API. Resolution: point them to the confirmation email link, the site's members area, or the business contact details from GetBusinessDetails.


Constants

ConstantValue
Wix Bookings catalog appId (checkout)13d21c63-b5ec-5912-8397-c3a5ddb27a97
Staff member resource type ID1cd44cf8-756f-41c3-bd90-3e2ffcaf1155
Default booking form ID00000000-0000-0000-0000-000000000000

Everything else — service, schedule, staff, location and form IDs, timezone, prices, durations — is per site. Discover it in STEP 2 and STEP 3 during the conversation; never reuse IDs from memory or from another site.

All endpoints in this recipe accept visitor or member authentication. Route every call through GenerateVisitorToken + CallWixSiteAPI / ExecuteWixAPI with the same token.


References

Last updated: 15 September 2026

Did this help?