← Files WixARCHIVED FILE
skills/wix-app/references/service-plugin/BOOKINGS-VALIDATION.md
7.94 KB · Oct 8, 2026 · 12:02 UTC
# Bookings Validation Service Plugin Reference
## Overview
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.
## Handlers
| 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) |
## Request and Response Schema
Before implementing, call `ReadFullDocsMethodSchema` on each docs URL to get the full request/response types.
| Handler | Docs URL |
| --- | --- |
| `validateBeforeCreate` | https://dev.wix.com/docs/api-reference/business-solutions/bookings/bookings/bookings-validation-service-plugin/validate-before-create?apiView=SDK |
| `validateBeforeCancel` | https://dev.wix.com/docs/api-reference/business-solutions/bookings/bookings/bookings-validation-service-plugin/validate-before-cancel?apiView=SDK |
| `validateBeforeReschedule` | https://dev.wix.com/docs/api-reference/business-solutions/bookings/bookings/bookings-validation-service-plugin/validate-before-reschedule?apiView=SDK |
| `validateBeforeCreateMultiService` | https://dev.wix.com/docs/api-reference/business-solutions/bookings/bookings/bookings-validation-service-plugin/validate-before-create-multi-service?apiView=SDK |
| `validateBeforeCancelMultiService` | https://dev.wix.com/docs/api-reference/business-solutions/bookings/bookings/bookings-validation-service-plugin/validate-before-cancel-multi-service?apiView=SDK |
| `validateBeforeRescheduleMultiService` | https://dev.wix.com/docs/api-reference/business-solutions/bookings/bookings/bookings-validation-service-plugin/validate-before-reschedule-multi-service?apiView=SDK |
**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.
## Example: Membership Limit and Cancellation Fee
This example enforces a pricing-plan booking limit on create, and blocks late cancellations.
```typescript
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: [] }),
});
```
## Manual Setup Required
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.
## Key Implementation Notes
1. **Bulk requests, per-item results** — return a result for every item you received (keyed by `itemIndex` on create, by `bookingId` on cancel/reschedule — see the correlation table above); a missing entry is treated as valid, not rejected.
2. **Respond fast** — `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).
3. **Multiple providers run concurrently** — if other apps also register validators for the same target, any single rejection blocks the operation.
4. **Write customer-facing messages** — `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.
5. **Elevate permissions** — use `auth.elevate` when querying Wix APIs (e.g., pricing plan orders) from the handler.
SHA-256: 06d55eb89742869875308f2d861afea27f9c021c2a5503bcbd4441b7b13f7a21