50 "bookingsAddOnBody sends addOn.taxGroup only when the mapping sets it; the Create Add On body has no other tax reference. The add-on group calls carry only service and add-on ids."
51 ]
52 }
53 },
54 "preferredWrite": {
55 "surface": "Add-Ons",
56 "endpoint": "POST /addons/v1/add-ons",
57 "writerId": "importBookingsAddOn",
58 "verification": "verified-live",
59 "importSafe": true,
60 "bulk": true,
61 "decision": "importSafe is true: an add-on is a catalog object with no customer-facing side effect documented anywhere on the Add On object, Create Add On, or Bulk Create Add Ons pages (read 2026-08-16) - no notification field, no recipient, no delivery prose. It is also invisible to customers until it is placed in an add-on group on a service, which is a second, deliberate call. Note this is the WEAKER of the two importSafe claims in this domain; it rests on absence of documented notification, never on a live probe."
71 "summary": "Create Add On declares durationInMinutes and maxQuantity as a required ONE-OF: every add-on is EITHER duration-based (durationInMinutes 1..1440, i.e. max 24 hours) OR quantity-based (maxQuantity 1..9999), never both and never neither. A source extra that is conceptually both ('add 30 minutes, up to 3 times') cannot be expressed as one add-on and must be split or ledgered. The two kinds also behave differently at booking time: for duration-based add-ons quantity is not applicable - passing quantity 1 is accepted but silently cleared, and any other value returns INVALID_ARGUMENT with the message 'Invalid AddOn details: either duration or quantity must be set correctly'. LIVE 2026-10-27 (throwaway site, muted): a create carrying BOTH durationInMinutes and maxQuantity is ACCEPTED (200) and silently keeps only durationInMinutes. importBookingsAddOn therefore refuses a both-kinds body before sending; the split is a mapping decision."
72 },
73 {
74 "code": "add-on-is-invisible-until-grouped",
75 "severity": "blocker",
76 "summary": "Creating an add-on is only half the import. About Add-On Groups states plainly: 'Add-ons for Bookings services must be included in a group before customers can book them.' The group is the thing that binds an add-on to a service and it lives on a DIFFERENT surface (Services V2: Create Add On Group / Set Add Ons For Group / Update Add On Group / List Add On Groups By Service Id), not on the Add-Ons API. An import that stops after POST /addons/v1/add-ons produces orphan add-ons that no customer can ever select and that no site page shows - a silent, look-successful failure mode."
77 },
78 {
79 "code": "group-limits-cap-the-catalog",
80 "severity": "warning",
81 "summary": "About Add-On Groups: a maximum of 3 add-on groups per service and 7 add-ons per group - 'Exceeding these limits results in validation errors'. That is a hard ceiling of 21 selectable add-ons per service, which the Booking object independently corroborates (bookedAddOns maxItems 21). A source product carrying more optional extras than that cannot be recreated in full; count add-ons per source product BEFORE the import and surface the overflow at the approval gate rather than discovering it as a validation error mid-run. LIVE 2026-10-27 (throwaway site, muted): Set Add Ons For Group with 8 ids 400s \"addOnIds has size 8, expected 7 or less\" (7 accepted). A 4th group on one service was ACCEPTED, so the 3-group limit is not enforced at group create today; the writer keeps one import group per service and never relies on either behaviour. An extra beyond the 7th for a service is created but left ungrouped there (owner-approved 2026-10-27) and reported as finding add-on-group-full."
86 "summary": "'Each add-on group is associated with a single service and requires a service ID when created', while an add-on itself may be reused across groups on different services. The import order is therefore forced and three-phase: services first (bookings/service), then add-ons (which are service-independent and can be deduplicated across the whole source catalog), then one group per service that binds them. This is also why bookings/service records add-on-groups-not-settable-at-create - Create Service explicitly refuses existing add-on groups."
91 "summary": "price is a Money object carrying its OWN currency field (ISO 4217, required format CURRENCY) rather than inheriting the site currency the way pricing-plans/plan.currency does. value is a decimal string with maxScale 2 (gte 0, lte 1000000000000000). LIVE 2026-10-27 (throwaway site, muted): a EUR add-on on a USD site was stored as EUR - Wix neither coerces nor rejects a foreign currency. Send the site currency explicitly and check the readback; do not assume inheritance. Read-back also normalizes value (\"10.50\" -> \"10.5\"), so compare prices as numbers."
96 "summary": "'Deleting an add-on automatically removes it from all groups where it's included.' Rollback of a partial add-on import is therefore not isolated - deleting one add-on mutates the composition of every service group that referenced it, with no error and no notification. Roll back groups and add-ons together, and re-read the affected services' groups afterwards rather than trusting the local plan."
101 "summary": "The Booking object stores bookedAddOns with name, nameTranslated and durationInMinutes captured 'at the time of booking' alongside the id/groupId. Historical bookings therefore carry their own copy of the add-on wording, so importing bookings does NOT require the add-on catalog to be imported first for the names to be right - but bookings/booking still needs real add-on and group GUIDs unless flowControlSettings.skipAddOnValidation is set. Import add-ons before bookings anyway; that keeps the crosswalk honest."
106 "summary": "LIVE 2026-10-27 (throwaway site, muted): Wix accepts a second add-on with the same name, price and kind, and an add-on has no client-assigned key (extendedFields needs an app namespace). importBookingsAddOn reconciles by the crosswalk id first, then Query Add Ons by name filtered to an exact name/price/currency/kind/taxGroup match before every create: one match is adopted (reconciled-existing), more than one returns reconciliation-ambiguous and writes nothing, and after a create of unknown outcome the same lookup decides (unknown-outcome when not exactly one). Mapping must therefore dedupe identical extras into one record, as mappingGuidance already requires."
107 }
108 ],
109 "mappingGuidance": [
110 "Dependencies: bookings/service is required because an add-on must be placed in a service's add-on group; tax/tax-group is required only when the optional taxGroup GUID is mapped for eCommerce checkout. A plan with no tax-group setup step that leaves addOn.taxGroup unmapped records a not-referenced noProducer proof for importBookingsAddOn -> tax/tax-group (the contract marks it conditional, guarded at addOn.taxGroup; the mapping preflight supplies the exact proof as suggestedProof on dependency_proof_expected); the gate then refuses any payload that carries a taxGroup.",
111 "One Wix add-on per distinct source extra, deduplicated across the whole source catalog - add-ons are service-independent objects and the same one may be reused in groups on many services, so never mint one add-on per (product x extra) pair.",
112 "Classify each source extra into the required ONE-OF before writing: an extra that lengthens the appointment becomes durationInMinutes (cap 1440); an extra sold in units becomes maxQuantity (cap 9999). If the source expresses neither, quantity-based with maxQuantity 1 is the closest faithful default - record the choice in the faithfulness ledger.",
113 "name is capped at 100 characters (minLength 1); truncate long source labels and keep the full text in the ledger. There is no description field on an add-on - any source blurb has nowhere native to go.",
114 "price.value is a decimal string with at most 2 decimal places; price.currency must be sent explicitly as the site currency ISO code (see add-on-currency-is-explicit-not-inherited).",
115 "Write add-ons one record at a time through importBookingsAddOn (below): it creates the add-on and places it in the one import group of each service, appending in record order, so run the records in source order to keep the source display order. Bulk Create Add Ons, Create Add On Group and Set Add Ons For Group have no other marked builder, so the import client refuses them outside that writer.",
116 "taxGroup is an eCommerce tax-group GUID and only applies to Wix eCommerce checkout; leave it unset for imports that are not also provisioning tax groups (the field 'doesn't apply to custom checkout/billing implementations').",
117 "Persist both the add-on GUID and its owning group GUID per source extra in the crosswalk - bookings/booking needs bookedAddOns[].id AND bookedAddOns[].groupId, and the group GUID is not derivable from the add-on.",
118 "Writer: importBookingsAddOn(recordClient, { addOn, serviceIds, groupName?, knownAddOnId? }). addOn is the Create Add On body (name, exactly one of durationInMinutes | maxQuantity, price {value, currency = site currency}, optional taxGroup); serviceIds are resolveReference(\"bookings/service\", ...) results for every service the extra belongs to, at payload path serviceIds[i]. The create is a partial write (only addOn is sent); each service then gets the add-on appended to its one import group (created on first use, default name \"Add-ons\"). Run records in source order so the group order is the source order. Outcomes: written | reconciled-existing | partially-written (the add-on exists but placing it in a group failed: addOnId and groupFailures are returned, failed_retryable, a re-run places it) | reconciliation-ambiguous | unknown-outcome | read-back-mismatch | failed | target-read-failed; ungroupedServiceIds lists services whose group was full (cutover item: the merchant picks which extras to show). A crosswalk add-on whose values differ (the source extra changed 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 (read back, never re-send), and ungrouped add-ons become one cutover operator action."
119 ],
120 "setupRequirements": [
121 "Wix Bookings app installed (appId 13d21c63-b5ec-5912-8397-c3a5ddb27a97) - see the bookings-app-required shared pitfall",
122 "SCOPE.DC-BOOKINGS.MANAGE-BOOKINGS (the permission on Create Add On, Bulk Create Add Ons, and the Services V2 add-on-group methods alike)",
123 "bookings/service imported first - an add-on group cannot be created without a service GUID"
129 "note": "object page: id, revision, name, price (Money with its own currency + formattedValue), taxGroup, extendedFields, tags.privateTags/tags; the duration-vs-quantity ONE-OF is on the create/response schema (read 2026-08-16)"
144 "note": "the binding half of this entity, and why it lives on Services V2 not Add-Ons: 'Add-ons for Bookings services must be included in a group before customers can book them'; group requires a service ID; limits 3 groups per service and 7 add-ons per group; deleting an add-on removes it from all groups; addOnIds order is display order (read 2026-08-16)"
149 "note": "consumer side: booking.bookedAddOns[] carries id, groupId, quantity, durationInMinutes, name and nameTranslated 'at the time of booking', maxItems 21 - which independently confirms the 3x7 group ceiling (read 2026-08-16)"
154 "note": "the source profile this domain was authored against; it is the authority for the two add-on-shaped product taxonomies (mwb_booking_service, mwb_booking_cost) that sourceAliases points at, and for the fact that their prices and quantity rules sit in REST-invisible term meta"
159 "note": "sourceAliases added 2026-08-16 after the live probe: WPS Bookings for WooCommerce DOES expose an add-on-shaped surface - the product taxonomies mwb_booking_service ('Additional Services') and mwb_booking_cost ('Additional Costs'), both registered show_in_rest:true and both serving 200 with X-WP-Total 0 on an anonymized verification environment (the taxonomies exist, no terms created). confidence is 'low' because only the term NAMES are reachable: the plugin stores every price and quantity rule as term meta and never calls register_meta, so the fields this entity actually needs are invisible to REST. See the profile's booking-additional-service / booking-additional-cost entities."
164 "note": "LIVE 2026-10-27 (throwaway site, muted): Create Add On (duration and quantity), Get, Query by name, Create Add On Group, Set Add Ons For Group (twice: idempotent), List Add On Groups By Service Id, Delete Add On Group and Delete Add On all 200. Both-kinds body accepted with maxQuantity dropped; 8 ids per group 400 MAX_SIZE 7; a 4th group accepted; EUR on a USD site stored; duplicate name/price accepted; price normalized on read."
165 }
166 ],
167 "notes": "Authored 2026-08-16 to complete the bookings vertical alongside bookings/service and bookings/booking. Docs-verified only, never exercised live - reliability unknown, verification docs. sourceAliases carries ONE low-confidence entry, added 2026-08-16: WPS Bookings for WooCommerce's two REST-exposed product taxonomies (mwb_booking_service, mwb_booking_cost) are a real, live-verified route claim - both returned 200 with X-WP-Total 0 - but only the term names come back, because the plugin's prices and quantity rules live in unregistered term meta. So the source mapping is proven to EXIST and proven to be INCOMPLETE, which is what 'low' records. The entity is genuinely two surfaces stitched together - Add-Ons for the catalog object and Services V2 for the add-on group that makes it bookable - which is why the docs survey claims both surfaces for it. Promoted to verified-live 2026-10-27 with the importBookingsAddOn writer; see the live evidence row."