Subchapter 1.59
references/service-plugin/BOOKINGS-VALIDATION.mdMarkdown8 KBView on GitHub
The Bookings Validation SPI lets you implement custom validation logic for booking operations. When a customer creates, cancels, or reschedules a booking (single-service or multi-service), Wix calls the handler matching that operation before it executes. Return valid: true to allow the operation or valid: false to block it with a customer-facing message.
bookingsValidation.provideHandlers requires all six handlers in the object literal — TypeScript rejects a partial object even though you may only care about one or two targets. For the ones you don’t have real logic for, provide a no-op that returns an empty results/singleServiceBookingResults array (an empty array is a no-op: “Omitting an item’s result treats it as valid,” and an empty results array omits every item’s result). Confirmed live: wix generate itself scaffolds all six handlers by default — that’s not incidental boilerplate you can trim.
| Handler | Validation target | On error/timeout |
|---|---|---|
validateBeforeCreate | CREATE | Blocked (fail-closed) |
validateBeforeCancel | CANCEL | Blocked (fail-closed) |
validateBeforeReschedule | RESCHEDULE | Continues (fail-open) |
validateBeforeCreateMultiService | CREATE_MULTI_SERVICE | Blocked (fail-closed) |
validateBeforeCancelMultiService | CANCEL_MULTI_SERVICE | Blocked (fail-closed) |
validateBeforeRescheduleMultiService | RESCHEDULE_MULTI_SERVICE | Continues (fail-open) |
Before implementing, call ReadFullDocsMethodSchema on each docs URL to get the full request/response types.
Correlation differs by handler — this is easy to get wrong, and doing so fails tsc, not just at runtime:
| Handler(s) | Item shape | Max items | Result correlates by |
|---|---|---|---|
validateBeforeCreate, validateBeforeCreateMultiService | { itemIndex, booking } | 40 | itemIndex |
validateBeforeCancel, validateBeforeReschedule, and their multi-service equivalents | { booking } — no itemIndex field at all | 8 | bookingId (from booking._id) |
Only the create family carries itemIndex; cancel and reschedule items don’t have one, so their results key on the booking’s own ID instead. The single-service response field is results; the multi-service response field is singleServiceBookingResults — same per-item shape either way. Omitting an item’s result treats it as valid. contactDetails and resource name/email fields are redacted before reaching your handler.
This example enforces a pricing-plan booking limit on create, and blocks late cancellations.
import { bookingsValidation } from "@wix/bookings/service-plugins";
import { auth } from "@wix/essentials";
import { orders } from "@wix/pricing-plans";
bookingsValidation.provideHandlers({
validateBeforeCreate: async (payload) => {
const { request } = payload;
const results = await Promise.all(
(request.items ?? []).map(async (item) => {
const memberId = item.booking?.contactDetails?.contactId;
if (!memberId) {
return { itemIndex: item.itemIndex, result: { valid: true } };
}
// Not orders.listOrders — that method doesn't exist on @wix/pricing-plans.
// managementListOrders takes flat buyerIds/orderStatuses, not a nested filter.
const elevatedListOrders = auth.elevate(orders.managementListOrders);
const { orders: activeOrders } = await elevatedListOrders({
buyerIds: [memberId],
orderStatuses: ["ACTIVE"],
});
if ((activeOrders ?? []).length === 0) {
return {
itemIndex: item.itemIndex,
result: {
valid: false,
invalidReason: {
message: "An active membership is required to book this service.",
fieldViolations: [
{ field: "booking.contactDetails", description: "No active membership found.", code: "PLAN_EXPIRED" },
],
},
},
};
}
return { itemIndex: item.itemIndex, result: { valid: true } };
})
);
return { results };
},
validateBeforeCancel: async (payload) => {
const { request } = payload;
// Cancel items carry no itemIndex — correlate results by the booking's own _id.
const results = (request.items ?? []).map((item) => {
const bookingId = item.booking?._id ?? undefined;
const startDate = item.booking?.bookedEntity?.slot?.startDate;
const hoursUntilStart = startDate
? (new Date(startDate).getTime() - Date.now()) / (1000 * 60 * 60)
: Infinity;
if (hoursUntilStart < 24) {
return {
bookingId,
result: {
valid: false,
invalidReason: { message: "Cancellations within 24 hours of the appointment aren't allowed." },
},
};
}
return { bookingId, result: { valid: true } };
});
return { results };
},
// Unimplemented targets still need a handler — see the note above the example.
validateBeforeReschedule: async () => ({ results: [] }),
validateBeforeCreateMultiService: async () => ({ singleServiceBookingResults: [] }),
validateBeforeCancelMultiService: async () => ({ singleServiceBookingResults: [] }),
validateBeforeRescheduleMultiService: async () => ({ singleServiceBookingResults: [] }),
});None — confirmed live via the real Cancel Booking API: a cancellation under 24h was correctly blocked with a structured 428 VALIDATION_FAILED, and one over 24h correctly succeeded. Wix calls this plugin automatically on the relevant booking operations once the app is installed and released.
itemIndex on create, by bookingId on cancel/reschedule — see the correlation table above); a missing entry is treated as valid, not rejected.validateBeforeCreate, validateBeforeCancel, and their multi-service equivalents fail-closed (block the operation) on error or timeout; validateBeforeReschedule and its multi-service equivalent fail-open (allow it).invalidReason.message and fieldViolations[].description are shown to the customer; keep them clear and free of internal jargon. fieldViolations[].code is for your own programmatic handling, not display.auth.elevate when querying Wix APIs (e.g., pricing plan orders) from the handler.