Setting the file. One moment.
Wix Bookings Checkout · Wix Vibe Headless · wix/skills · Skills Docs
ContentsBack to the top of the page Wix Blog
references/bookings/app/rest/ wix-bookings-checkout.js
JavaScript · 147 lines · 7 KB
14 * paymentStatus {string} — "NOT_PAID" until checkout completes,
15 * bookedEntity.slot {object} — { serviceId, scheduleId, startDate, endDate, timezone, location },
16 * contactDetails {object}, totalParticipants {number}
17 */
18
19 /** Visitor's local IANA time zone — used as default for the booking timezone. */
20 function defaultTimeZone () {
21 try {
22 return Intl. DateTimeFormat (). resolvedOptions ().timeZone || "UTC" ;
23 } catch {
24 return "UTC" ;
25 }
26 }
27
28 /**
29 * Create a booking for a slot. The booking starts as status "CREATED" and is NOT yet on the
30 * calendar — it is confirmed automatically after the buyer completes the hosted checkout (call
31 * checkoutBooking next). selectedPaymentOption is "ONLINE".
32 *
33 * Works for **both** service types — it reads the discriminator off the slot:
34 * - APPOINTMENT slot (from `listAvailableSlots`/`getAvailableSlot`) carries `scheduleId`.
35 * - CLASS/COURSE slot (from `listEventTimeSlots`) carries `eventInfo.eventId` and no `scheduleId`;
36 * Wix derives the session's date/resource/location from that event.
37 * For appointments, call `getAvailableSlot` first to re-validate and get staff (`availableResources`).
38 * Throws on an unbookable slot or missing booking id.
39 * https://dev.wix.com/docs/api-reference/business-solutions/bookings/bookings/bookings-writer-v2/create-booking.md
40 *
41 * @param {object} slot A TimeSlot from listAvailableSlots/getAvailableSlot/listEventTimeSlots.
42 * @param {{ firstName?: string, lastName?: string, email: string, phone?: string }} contactDetails
43 * @param {{ totalParticipants?: number, timeZone?: string, title?: string }} [options]
44 * @returns {Promise<object>} The created booking (status "CREATED").
45 */
46 export async function createBooking ( slot , contactDetails , { totalParticipants = 1 , timeZone , title } = {}) {
47 if ( ! slot || slot.bookable === false ) {
48 throw new Error ( "Cannot book: the selected slot is not bookable. Re-check availability and pick another time." );
49 }
50 // A slot is bound either by scheduleId (appointment) or by eventInfo.eventId (class/course).
51 const eventId = slot.eventInfo?.eventId;
52 if ( ! slot.serviceId || ( ! slot.scheduleId && ! eventId) || ! slot.localStartDate || ! slot.localEndDate) {
53 throw new Error ( "Cannot book: slot is missing serviceId, localStartDate/localEndDate, and a scheduleId (appointment) or eventInfo.eventId (class/course)." );
54 }
55 if ( ! contactDetails?.email) {
56 throw new Error ( "Cannot book: contactDetails.email is required." );
57 }
58
59 const resource = slot.availableResources?.[ 0 ]?.resources?.[ 0 ];
60
61 // The availability slot's location uses the SERVICE location enum (e.g. "BUSINESS"), but the
62 // createBooking endpoint expects the BOOKING location enum [UNDEFINED, OWNER_BUSINESS,
63 // OWNER_CUSTOM, CUSTOM]. Passing slot.location straight through 400s
64 // ("slot.location.locationType enum must be in [...]"). Remap before sending.
65 const bookingLocation = (() => {
66 const loc = slot.location;
67 if ( ! loc) return null ;
68 const valid = [ "UNDEFINED" , "OWNER_BUSINESS" , "OWNER_CUSTOM" , "CUSTOM" ];
69 const map = { BUSINESS: "OWNER_BUSINESS" , CUSTOM: "CUSTOM" , CUSTOMER: "CUSTOM" };
70 const locationType = map[loc.locationType] || (valid. includes (loc.locationType) ? loc.locationType : "OWNER_BUSINESS" );
71 return { ... loc, locationType };
72 })();
73
74 const bookedSlot = {
75 serviceId: slot.serviceId,
76 // Appointment → scheduleId; class/course → eventId. Send exactly one.
77 ... (eventId ? { eventId } : { scheduleId: slot.scheduleId }),
78 startDate: slot.localStartDate,
79 endDate: slot.localEndDate,
80 timezone: timeZone || defaultTimeZone (),
81 ... (bookingLocation ? { location: bookingLocation } : {}),
82 ... (resource ? { resource: { id: resource.id, name: resource.name } } : {}),
83 };
84
85 const res = await wixApiRequest ( "/bookings/v2/bookings" , {
86 method: "POST" ,
87 body: {
88 booking: {
89 bookedEntity: { slot: bookedSlot, title: title || undefined , tags: [ "INDIVIDUAL" ] },
90 contactDetails,
91 additionalFields: [],
92 totalParticipants,
93 selectedPaymentOption: "ONLINE" ,
94 },
95 participantNotification: { notifyParticipants: true },
96 },
97 });
98 const booking = res?.booking;
99 if ( ! booking?.id) throw new Error ( "Failed to create the booking (no booking id returned)." );
100 return booking;
101 }
102
103 /**
104 * Create an eCommerce checkout for a created booking and return the hosted checkout URL.
105 * Redirect the buyer there (window.location.href = ...). On return, the booking is confirmed.
106 * Throws if no redirect URL is produced.
107 * https://dev.wix.com/docs/rest/business-solutions/e-commerce/checkout/create-checkout.md
108 * @param {string} bookingId booking.id from createBooking().
109 * @returns {Promise<string>} The hosted-checkout URL to redirect to.
110 */
111 export async function checkoutBooking ( bookingId ) {
112 if ( ! bookingId) throw new Error ( "checkoutBooking requires a bookingId." );
113
114 const checkoutRes = await wixApiRequest ( "/ecom/v1/checkouts" , {
115 method: "POST" ,
116 body: {
117 channelType: "WEB" ,
118 lineItems: [
119 { quantity: 1 , catalogReference: { appId: BOOKINGS_APP_ID , catalogItemId: bookingId } },
120 ],
121 },
122 });
123 const checkoutId = checkoutRes?.checkout?.id;
124 if ( ! checkoutId) throw new Error ( "Failed to create a checkout for the booking." );
125
126 const redirect = await wixApiRequest ( "/headless/v1/redirect-session" , {
127 method: "POST" ,
128 body: { ecomCheckout: { checkoutId }, callbacks: { postFlowUrl: window.location.href } },
129 });
130 const url = redirect?.redirectSession?.fullUrl;
131 if ( ! url) throw new Error ( "Failed to create the checkout redirect session." );
132 return url;
133 }
134
135 /**
136 * Convenience: create the booking and return the hosted-checkout URL in one call.
137 * Equivalent to createBooking() then checkoutBooking(booking.id). Throws loudly on any failure.
138 * @param {object} slot
139 * @param {{ firstName?: string, lastName?: string, email: string, phone?: string }} contactDetails
140 * @param {{ totalParticipants?: number, timeZone?: string, title?: string }} [options]
141 * @returns {Promise<{ booking: object, checkoutUrl: string }>}
142 */
143 export async function bookAndCheckout ( slot , contactDetails , options = {}) {
144 const booking = await createBooking (slot, contactDetails, options);
145 const checkoutUrl = await checkoutBooking (booking.id);
146 return { booking, checkoutUrl };
147 }