54 "decision": "importSafe is true (site mute is trusted). An import path exists: Create Booking accepts an arbitrary bookedEntity.slot.startDate/endDate, status CONFIRMED is settable at create by an identity with Manage Bookings permissions, and flowControlSettings.skipAvailabilityValidation bypasses availability. Customer email/SMS from Bookings (including the sendSmsReminder default) is covered by the site-wide mute verified before any write."
55 },
56 "reliability": {
57 "status": "unknown",
58 "flags": []
59 },
60 "contactFields": [
61 {
62 "kind": "email",
63 "targetPath": "booking.contactDetails.email",
64 "source": "target-schema",
65 "notes": "Customer email on the booking. The Booking object states outright: 'If no contact with this email exist, a new contact is created' - so this field silently writes into CRM as well as Bookings."
66 },
67 {
68 "kind": "phone",
69 "targetPath": "booking.contactDetails.phone",
70 "source": "target-schema",
71 "notes": "The SMS destination. Create Booking: 'the customer receives an SMS 24 hours before the session starts. The phone number is taken from contactDetails.phone' - and sendSmsReminder defaults to true. Covered by site mute: when getSiteMuteState reads muted:true, this is not a reason to block, skip, delay or warn about the entity."
72 }
73 ],
74 "pitfalls": [
75 {
76 "code": "sms-reminder-defaults-to-true",
77 "severity": "info",
78 "summary": "sendSmsReminder has 'Default: true' on both Create Booking and Bulk Create Booking (read 2026-08-16), the inverse of events/rsvp's disableNotifications and this API's own participantNotification.notifyParticipants. Sending sendSmsReminder:false is harmless; it is not required. Covered by site mute: when getSiteMuteState reads muted:true, this is not a reason to block, skip, delay or warn about the entity."
79 },
80 {
81 "code": "notification-suppression-unproven",
82 "severity": "info",
83 "summary": "Docs only (2026-08-16): two request-level controls exist on Create/Bulk Create/Confirm Booking - participantNotification.notifyParticipants (default false) and sendSmsReminder (default true). Neither is documented to govern the site's own Bookings automation emails (confirmation, reminders, staff alert). Covered by site mute: when getSiteMuteState reads muted:true, this is not a reason to block, skip, delay or warn about the entity."
88 "summary": "LIVE 2026-10-27 (throwaway site, muted): past-dated appointment bookings ARE accepted. With flowControlSettings.skipAvailabilityValidation:true, CONFIRMED and CREATED bookings dated 2024 and 2025 were created and read back; the same past slot WITHOUT it 428s SLOT_NOT_AVAILABLE (\"No available slot\"). importBooking always sends skipAvailabilityValidation, skipBusinessConfirmation and skipSelectedPaymentOptionValidation, so booking history imports natively - not as CMS archive rows."
89 },
90 {
91 "code": "payment-status-is-ignored-at-create",
92 "severity": "blocker",
93 "summary": "'All bookings are created with paymentStatus=UNDEFINED, ignoring the payment status you specify.' A historical booking that was paid in the source therefore CANNOT be imported paid in one call - the field is accepted and discarded. The documented way to set it is a second call, Confirm Booking (or Confirm or Decline Booking), which 'also updates the booking's paymentStatus, if you specify a new payment status'. This collides with the next pitfall: Confirm Booking only accepts bookings whose status is PENDING, CREATED or WAITING_LIST, so a booking created directly as CONFIRMED has ALREADY BURNED its one chance to set a payment status through this path. The correct sequence for a paid historical booking is create as CREATED, then Confirm Booking with revision and paymentStatus - never create as CONFIRMED first. LIVE 2026-10-27 (throwaway site, muted): confirmed - paymentStatus PAID sent at create came back UNDEFINED; create as CREATED then Confirm Booking with paymentStatus PAID returned CONFIRMED/PAID, for both a future and a past slot. importBooking does exactly this whenever a paymentStatus is mapped."
98 "summary": "Create Booking defaults to status=CREATED, and 'such bookings aren't visible in the Booking Calendar' - an import that leaves the default produces records the site owner cannot see in the product they actually use, which will read as a failed migration. Setting status=CONFIRMED at create is possible but restricted: 'Only identities with Manage Bookings permissions can set status=CONFIRMED.' Two knock-on facts: for appointment services Wix Bookings only populates bookedEntity.slot.eventId (i.e. creates the calendar session) when the status becomes CONFIRMED, so an unconfirmed import leaves no calendar footprint at all; and confirmation is also the moment most likely to trigger customer-facing automation, which is why this interacts directly with notification-suppression-unproven."
99 },
100 {
101 "code": "booking-creates-a-crm-contact",
102 "severity": "blocker",
103 "summary": "contactDetails.email on the Booking object: 'If no contact with this email exist, a new contact is created.' A booking import therefore writes into Contacts as a side effect - it can duplicate contacts already imported by crm/contact, and booking volume becomes contact volume. This is the same trap that was CORRECTED live for events/rsvp on 2026-08-16, where the KB had wrongly claimed RSVPs do not touch CRM; take the documented statement at face value here rather than repeating that mistake. Sequence bookings after crm/contact, reconcile source customers by email, and pass the crosswalked contactDetails.contactId so Wix matches instead of creating. Count the contact writes in the plan. LIVE 2026-10-27 (throwaway site, muted): with contactDetails.contactId of an existing contact, the site contact count stayed the same across 10 creates. importBooking refuses a booking without contactId."
108 "summary": "The three service types take three different bookedEntity shapes and only one of them is self-sufficient. APPOINTMENT: bookedEntity.slot with startDate/endDate/resource/location - Wix Bookings creates the session itself on confirmation, so the import is complete. CLASS: bookedEntity.slot.eventId, a GUID of an EXISTING calendar event, and 'when you specify eventId, Wix Bookings automatically derives startDate, endDate, timezone, resource, and location based on the event details. Manually specified values are ignored' - so a class booking is impossible until the session exists. COURSE: bookedEntity.schedule.scheduleId. No entity in this domain creates class sessions or course schedules; that is the recorded events-v3 gap in the docs survey. Consequence for bookings/service's own mapping rule - a source booking product with per-slot capacity greater than 1 becomes a CLASS - is that such products can have their SERVICE imported but not their BOOKINGS. Detect the capacity>1 branch during planning and disclose it at the approval gate. importBooking returns class-session-missing (writes nothing) for a slot.eventId booking, and the builder refuses one, because no import entity produces the session id the gate would have to check."
109 },
110 {
111 "code": "bulk-create-caps-at-twelve",
112 "severity": "warning",
113 "summary": "Bulk Create Booking (POST /bookings/v2/bulk/bookings/create) validates createBookingsInfo as minItems 1 / maxItems 12 - 'Max: 12 bookings'. That is an order of magnitude tighter than the sibling bulk endpoints this KB has met (Bulk Create Services 100, Bulk Create Add Ons 100), so a booking history of any size is a long chain of small calls: budget the run time and the rate-limit headroom accordingly, and make every batch crosswalk-guarded so a mid-chain failure resumes instead of duplicating. bulk is recorded as true because the endpoint exists, not because it is efficient."
118 "summary": "The overrides an import actually needs each carry their own permission beyond Manage Bookings: skipBusinessConfirmation requires BOOKINGS.OVERRIDE_AVAILABILITY, skipSelectedPaymentOptionValidation requires BOOKINGS.MANAGE_PAYMENTS, skipAddOnValidation requires BOOKINGS.MANAGE_ADDONS, and skipAvailabilityValidation warns to 'make sure the call's identity has the required permissions'. A run that discovers this at import time fails with 403 UNAUTHORIZED_OPERATION per call. Verify the identity's permissions during setup, and note that the docs' own remedies - a higher app permission, or Velo elevation - are deliberate escalations that belong in the approval-gate disclosure, not in silently generated code."
123 "summary": "The Bookings Validation Service Plugin lets a site (or an installed app) implement Validate Before Create and reject bookings by its own rules. On a target site that has one, an import can be refused for reasons that exist in no Wix doc and in no error the KB can enumerate ahead of time. Check for installed validation extensions during setup, and treat an unexplained systematic create failure as this before assuming the payload is wrong."
124 },
125 {
126 "code": "participants-are-a-required-one-of",
127 "severity": "warning",
128 "summary": "'You must specify either participantsChoices or totalParticipants.' Use totalParticipants (minimum 1) only when the service has fixed pricing and no variants; use participantsChoices when the service has service options and variants, and 'the call fails if the specified participantsChoices aren't among the supported service options and variants'. Since bookings/service maps source products to FIXED pricing with no variants, totalParticipants is the correct branch for this vertical - but it must be sent, as neither is defaulted."
133 "summary": "selectedPaymentOption (UNDEFINED | OFFLINE | ONLINE | MEMBERSHIP | MEMBERSHIP_OFFLINE) is required when the customer pays through a Wix eCommerce checkout, and the Create Booking call fails without it; 'for custom checkouts, you don't have to specify this field'. A historical import is neither - it recreates a settled record, not a purchase. OFFLINE is the honest value for a booking that was paid on the source platform's gateway, since that money never lands in Wix; skipSelectedPaymentOptionValidation exists if the chosen option is not among those the service offers."
138 "summary": "Confirm Booking's only required parameter is revision, and revision 'increments by 1 each time the booking is updated, rescheduled, or canceled'. Any two-phase import (create, then confirm with paymentStatus) must carry the revision returned by the create response into the second call - do not assume 1, and re-read the booking if a retry loses the value. LIVE 2026-10-27 (throwaway site, muted): the revision ALSO moves without any call of ours - a CONFIRMED booking went from the create/confirm response revision to the next one a moment later (Wix attaches the calendar event asynchronously), and a cancel with the response revision 428d INVALID_REVISION. importBooking re-reads the booking (Query Extended Bookings by id) immediately before every confirm and cancel."
143 "summary": "LIVE 2026-10-27 (throwaway site, muted): Cancel Booking on a past booking 428s BOOKING_POLICY_VIOLATION unless flowControlSettings.ignoreCancellationPolicy:true; with it the booking became CANCELED and kept its paymentStatus (PAID stays PAID). A cancelled source booking is replayed as create -> (confirm with paymentStatus) -> cancel; creating with status CANCELED is not used."
144 },
145 {
146 "code": "booked-add-on-needs-a-group-id",
147 "severity": "warning",
148 "summary": "LIVE 2026-10-27 (throwaway site, muted): every bookedAddOns[] entry needs a valid groupId GUID - omitting it 400s \"groupId is not a valid GUID\", even with skipAddOnValidation. An add-on that is not in any group of the service (left ungrouped by the 7-per-group limit) is accepted when sent with the import group id of that service plus skipAddOnValidation:true, and the booking keeps its name snapshot. importBooking fills groupId from List Add On Groups By Service Id; a service with no group at all fails the record with booked-add-on-group-missing."
153 "summary": "LIVE 2026-10-27 (throwaway site, muted): externalUserId is stored, echoed and filterable in Query Extended Bookings ({filter:{externalUserId}}), and NOT unique (a second create with the same value succeeded). importBooking sets it to a GUID derived from the source stable key (bookingExternalKey) and looks it up before every create: one match is adopted, more than one is reconciliation-ambiguous. Get Booking is not a public endpoint (404); reads go through POST /bookings/bookings-reader/v2/extended-bookings/query."
154 }
155 ],
156 "mappingGuidance": [
157 "Order this entity LAST in the domain: it needs a crosswalked service GUID (bookedEntity.slot.serviceId), the staff member's resource GUID, add-on and add-on-group GUIDs for bookedAddOns, and a CRM contact to match against. domain.json's defaultImportOrder service -> service-add-on -> booking is that dependency chain.",
158 "Writer: importBooking(recordClient, { booking, paymentStatus?, canceled?, knownBookingId? }). booking is the Create Booking booking object: bookedEntity.slot {serviceId, scheduleId, startDate, endDate, timezone, resource, location} for an appointment (or schedule {serviceId, scheduleId} for a course), contactDetails.contactId (required), totalParticipants, optional bookedAddOns [{id, quantity}] - groupId is filled by the writer. Do not map status: the writer creates CONFIRMED, or CREATED then Confirm Booking when paymentStatus (NOT_PAID | PAID | PARTIALLY_PAID | REFUNDED | EXEMPT) is mapped, then Cancel Booking (ignoreCancellationPolicy) when canceled is true. The body always carries skipAvailabilityValidation, skipBusinessConfirmation, skipSelectedPaymentOptionValidation (plus skipAddOnValidation with add-ons), notifyParticipants:false and sendSmsReminder:false. Outcomes: written | reconciled-existing | reconciliation-ambiguous | unknown-outcome | partially-written (resume from the crosswalk id) | read-back-mismatch | failed | target-read-failed. Identity is the crosswalk id or the externalUserId key alone: a booking found that way whose service, contact or slot differs (a source reschedule after import) returns target-drift and writes nothing. Record each result with bookingsJournalRow / bookingsOperatorActions from rp-target-wix lib/bookings-outcomes.js: unknown-outcome is succeeded_unverified, partially-written is failed_retryable (a re-run resumes it), drift, ambiguity and read-back mismatches are needs_verification plus one cutover operator action, class-session-missing is failed_terminal plus one operator action.",
159 "Map the source booking window to bookedEntity.slot.startDate/endDate in ISO-8601, and ALWAYS send bookedEntity.slot.timezone as an IANA name - the docs state the slot is permanently redisplayed to the customer in the timezone captured at booking time, so a wrong timezone here is a permanent per-record defect, not a display setting that can be fixed later.",
160 "bookedEntity.slot.resource.id is the staff member's RESOURCE GUID (the same resourceId that bookings/service puts in staffMemberIds), not a staff-member GUID. Specifying it triggers an availability check that fails the call if the resource is busy; omitting it defers resource assignment to confirmation. For a historical import against a single placeholder staff member, expect self-collisions across back-to-back source bookings and plan on flowControlSettings.skipAvailabilityValidation.",
161 "contactDetails: send the crosswalked contactId together with firstName/lastName/email/phone so Wix matches the existing contact instead of minting a new one. Do NOT also send formSubmission unless the booking form data is genuinely being migrated - when formSubmission is present Wix overwrites every contactDetails field from it and ignores what you sent, except contactId.",
162 "Source booking status: confirmed/completed -> canceled:false; cancelled -> canceled:true; the source payment state -> paymentStatus. PENDING, DECLINED and WAITING_LIST are not reproduced; a source booking in such a state is a mapping decision (usually confirmed history or a ledger row).",
163 "externalUserId is owned by the writer (its reconciliation key, derived from the source stable key); never map the source booking id or a user id into it.",
164 "totalParticipants (minimum 1) is the correct participant branch for this vertical; participantsChoices is only for services carrying options and variants, which bookings/service does not create.",
165 "Every booking that reaches Wix also reaches CRM. Reconcile the source customer list against the contact crosswalk before this step runs and count the contact writes in the plan report."
166 ],
167 "setupRequirements": [
168 "Wix Bookings app installed (appId 13d21c63-b5ec-5912-8397-c3a5ddb27a97) - see the bookings-app-required shared pitfall",
169 "SCOPE.DC-BOOKINGS.MANAGE-BOOKINGS - also the permission that gates setting status=CONFIRMED at create",
170 "BOOKINGS.OVERRIDE_AVAILABILITY, BOOKINGS.MANAGE_PAYMENTS and BOOKINGS.MANAGE_ADDONS if the corresponding flowControlSettings overrides are used",
171 "bookings/service and bookings/service-add-on imported, with service, resource, add-on and add-on-group GUIDs in the crosswalk",
172 "crm/contact imported and reconciled by email, because every booking create touches Contacts",
173 "Site-wide notification mute verified active (getSiteMuteState muted:true) before any write",
174 "At least one staff member provisioned, with its resourceId known"
185 "note": "POST https://www.wixapis.com/bookings/v2/bookings (schema header also shows the internal /_api/bookings-service/v2/bookings form); permission SCOPE.DC-BOOKINGS.MANAGE-BOOKINGS; booking required, bookedEntity required, participantsChoices|totalParticipants required ONE-OF; 'Create Booking defaults to status=CREATED. Such bookings aren't visible in the Booking Calendar. Only identities with Manage Bookings permissions can set status=CONFIRMED'; 'All bookings are created with paymentStatus=UNDEFINED, ignoring the payment status you specify'; sendSmsReminder Default: true; participantNotification.notifyParticipants Default: false; flowControlSettings skipAvailabilityValidation / skipBusinessConfirmation / skipSelectedPaymentOptionValidation / skipAddOnValidation with their per-flag permissions; class bookings derive everything from eventId; errors 403 UNAUTHORIZED_OPERATION and 428 VALIDATION_FAILURE, and NO documented past-date restriction (read 2026-08-16)"
190 "note": "POST https://www.wixapis.com/bookings/v2/bulk/bookings/create; createBookingsInfo minItems 1, maxItems 12 ('Max: 12 bookings'); the same sendSmsReminder Default: true and participantNotification defaults repeat per element, so the unsafe default is per-booking not per-request (read 2026-08-16)"
195 "note": "POST https://www.wixapis.com/bookings/v2/bookings/{bookingId}/confirm; required parameter revision; 'You can only confirm bookings with a status of PENDING, CREATED, or WAITING_LIST'; 'Also updates the booking's paymentStatus, if you specify a new payment status'; same two notification controls and no others (read 2026-08-16)"
200 "note": "the former importSafe:false pattern (superseded 2026-10-27: notifications are covered by the site mute), and the two live corrections it warns against repeating: documented silence about a precondition (draft events) turned out to be a hard rejection, and a KB claim that guest records do not touch CRM turned out to be wrong"
205 "note": "the source profile this domain was authored against; the live WP test site carried no booking records, so the source-to-booking FIELD mapping could not be observed - the /wc/v3/orders route claim in sourceAliases rests on the storage model, not on an observed payload"
210 "note": "sourceAliases added 2026-08-16 from the live probe of WPS Bookings for WooCommerce 3.12.0: the plugin registers NO booking post type and NO booking order type (authed /wp/v2/types on an anonymized verification environment carries neither), so a booked appointment on WooCommerce is an ORDER LINE ITEM on /wc/v3/orders, flagged by order meta mwb_order_type='booking' and carrying its window in line-item meta (_wps_single_cal_date_time_from/_to, _wps_single_cal_booking_dates, or legacy _mwb_bfwp_date_time_from/_to). confidence 'medium': the route and the key names are established from the live REST index plus the published 3.12.0 source, but no booking order was available on the test site, so no payload has been observed."
215 "note": "LIVE 2026-10-27 (throwaway site, muted): appointment service + existing contact; Create Booking future CREATED -> Confirm PAID (CONFIRMED/PAID); past CONFIRMED with skipAvailabilityValidation 200; past without it 428 SLOT_NOT_AVAILABLE; past CREATED -> Confirm PAID 200; Cancel past 428 BOOKING_POLICY_VIOLATION, then 200 with ignoreCancellationPolicy; stale revision 428 INVALID_REVISION; bookedAddOns with groupId 200, without 400; ungrouped add-on with another group id + skipAddOnValidation 200; Query Extended Bookings by id, externalUserId and contactDetails.contactId 200; contact count unchanged. Notification behaviour was not probed (site mute trusted, Elad 2026-10-27)."
216 }
217 ],
218 "notes": "Write shape verified live 2026-10-27 on a throwaway site, with the importBooking writer: past-dated appointment bookings are accepted with skipAvailabilityValidation, paid and cancelled history is replayed by confirm and cancel calls, and an existing contactId creates no contact. Class sessions are still not imported (no entity produces them), so class-session bookings return class-session-missing. WPS Bookings for WooCommerce registers no booking post type or custom table, so a booked appointment is represented by WooCommerce order-line metadata; no representative booking payload has been verified and the per-field mapping remains [NEEDS-VERIFICATION]. The writer always sends sendSmsReminder:false and notifyParticipants:false. Notifications are covered by the site mute."