All skills
wix avatar

/wix-headless

@8f7fa5d official
by Wix.comwix/skills33 stars
33

Connect Wix business services (Stores, Bookings, CMS, Blog, Events, Forms, and more) to a Wix Headless frontend — infer the needed capabilities, install the apps, seed backend content, and produce an SDK-integration guide. For managed (Wix-hosted) projects it can also build the frontend: scaffold a new site (create) or wire an existing/brought-in design (connect), then build and release. Works across managed, self-managed, and stripe project types. Triggers: set up a Wix Headless backend, add Wix business features to my app, build or host a Wix site, connect/implement this design with Wix.

Use this Skill: https://skilld.dev/gh/wix/skills/wix-headless

This session only. Nothing lands on disk.

referencesinline-recipeshow-to-code-restaurant-reservations.md

≈4.9k tokens on demand. Your agent reads this file only when SKILL.md points to it.

RECIPE: How to Code a Wix Table-Reservations Frontend (Table Reservations)

A contract for the frontend code that lets a visitor book a table: reading the reservation location, fetching available time slots for a date + party size, and creating a reservation. This recipe is the how (which modules, which calls, which fields), not the what — the restaurant, the party size, and the page design come from the request / the visitor's input.

This recipe is for CODING reservations, not seeding them. It assumes the backend already exists — the Table Reservations app installed and a reservation location configured with online reservations enabled (see setup-restaurant-reservations.md). It says nothing about creating or configuring the location — only how visitors read availability and book from frontend code.

⚠️ Reading rule — always append .md?apiView=SDK to every doc link below. The bare / REST view shows id; the ?apiView=SDK view shows _id — and the SDK is what your frontend calls. Reading the REST view is the most common source of the entity.id-is-undefined bug. Fetch the .md?apiView=SDK form directly.

⚠️ ONLINE RESERVATIONS MUST BE ENABLED (premium) for this flow to work. The visitor flow reads/creates against a location whose onlineReservationsEnabled is true — and (per setup-restaurant-reservations.md) that toggle is premium-only (428 PREMIUM_ONLY on a free site). On a non-premium site the location exists and you can read it, but time-slot availability / booking won't function. If bookings fail on a site you know is configured, check the site is premium and the toggle is on — it's a provisioning precondition, not a code bug.


The modules and the client (read this first)

One package: @wix/table-reservations — a distinct package from @wix/restaurants (menus) and from the Orders app. It exposes the namespaces you use:

Need Package Module (namespace)
The reservation location (id, party-size bounds) @wix/table-reservations reservationLocations
Available time slots for a date + party size @wix/table-reservations timeSlots
Create / hold / reserve a reservation @wix/table-reservations reservations
Experiences — special dining occasions guests reserve (only when experiences are in the run) @wix/table-reservations experiences

Experiences ride on this same package and booking mechanism — see the Booking an experience feature below. Skip that section entirely if the run has no experiences.

No cart, no @wix/ecom, no @wix/redirects. A basic table reservation is a hold, not a purchase — there's no checkout in this flow. (Deposit/prepayment reservations exist — status PAYMENT_INFORMATION_PENDING — but they're out of scope for the standard book-a-table flow.)

Auth / client — framework split:

  • Astro (Wix-managed): authentication is ambient. Call reservationLocations / timeSlots / reservations directly from server components and backend routes (src/pages/api/*.ts) — no createClient, no OAuthStrategy, no clientId.
    import { reservationLocations, timeSlots, reservations } from '@wix/table-reservations';
    const { reservationLocations: locs = [] } = await reservationLocations.listReservationLocations();
  • Non-Astro (Vite/React/Vue/static): build one manual visitor client and reuse it:
    import { createClient, OAuthStrategy } from '@wix/sdk';
    import { reservationLocations, timeSlots, reservations } from '@wix/table-reservations';
    
    const client = createClient({
      modules: { reservationLocations, timeSlots, reservations },
      auth: OAuthStrategy({ clientId: /* the project's PUBLIC OAuth client id */ }),
    });
    // then: await client.timeSlots.getTimeSlots(locationId, new Date(date), partySize)
    The clientId is public, not a secret. A mis-wired public env var inlines as undefined and 400s every call.

The shapes you read (field cheat-sheet)

SDK read shapes (?apiView=SDK), so entity ids are _id.

// reservationLocations.listReservationLocations()  →  { reservationLocations: [...] }   (no arguments)
reservationLocation = {
  _id,                         // → the reservationLocationId every call below needs   (NOT .id)
  default,                     // the auto-provisioned location has default: true
  configuration: { onlineReservations: {
    partySize: { min, max },   // bound the party-size input to this range
    onlineReservationsEnabled, // must be true for the flow to work (premium)
    approval: { mode },        // "AUTOMATIC" → reserve confirms immediately; manual → status REQUESTED
  } },
}

// timeSlots.getTimeSlots(reservationLocationId, date, partySize, options?)  →  { timeSlots: [...] }
//   NOTE: the SDK types `date` as a Date (NOT the ISO string the REST doc shows) — pass new Date(...)
timeSlot = {
  startDate,                   // a Date — pass THIS into createHeldReservation
  duration,                    // minutes
  status,                      // "AVAILABLE" | "UNAVAILABLE" | "NON_WORKING_HOURS" — book only AVAILABLE
  manualApproval,              // whether this slot needs staff approval
}

// reservations.createHeldReservation({ reservationLocationId, startDate, partySize })  →  { reservation: {...} }
// reservations.reserveReservation(reservationId, reservee, revision)  →  { reservation: {...} }
reservation = {
  _id, revision,               // keep BOTH from the held reservation — reserve needs the revision
  status,                      // "HELD" after create → "RESERVED" (auto) or "REQUESTED" (manual approval) after reserve
}

The features (build the ones the site needs)

Each subsection is a self-contained feature. The only ordering is the booking chain itself (location → slots → hold → reserve).

Reading the reservation location (and the _id rule)

Get the location so you have its _id and the party-size bounds for your form. listReservationLocations() takes no arguments.

const { reservationLocations: locs = [] } = await reservationLocations.listReservationLocations();
const loc = locs.find((l) => l.default) ?? locs[0];
const reservationLocationId = loc._id;
const { min, max } = loc.configuration?.onlineReservations?.partySize ?? { min: 1, max: 20 };

Doc: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/reservations/reservation-locations/list-reservation-locations.md?apiView=SDK

⚠️ CRITICAL: the entity id is _id, NOT id. location.id / reservation.id are undefined in SDK code — use _id. (A surprising id field means you're on the REST doc view — re-open with ?apiView=SDK.)

⚠️ Default the array (= []). listReservationLocations() types the returned array as optional, so destructuring { reservationLocations: locs } and calling locs.find(...) directly errors under strict / astro check ('locs' is possibly 'undefined'). Default it, exactly as with the restaurants list* methods.

Getting available time slots

Call getTimeSlots with positional args: the location id, a Date object (new Date(selectedDate) — NOT the ISO string the doc shows; see the CRITICAL note below), the party size, and optional { slotsBefore, slotsAfter, duration }. Then keep the AVAILABLE slots.

const { timeSlots: slots = [] } = await timeSlots.getTimeSlots(
  reservationLocationId,
  new Date(selectedDate),                 // a Date — the SDK types this param as Date (see callout)
  partySize,
  { slotsBefore: 4, slotsAfter: 4 },
);
const available = slots.filter((s) => s.status === 'AVAILABLE');

Doc: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/reservations/time-slots/get-time-slots.md?apiView=SDK

⚠️ CRITICAL: getTimeSlots takes POSITIONAL args, not a single options object. The signature is getTimeSlots(reservationLocationId, date, partySize, options?) — passing one object (getTimeSlots({ reservationLocationId, date, partySize })) does not type-check. Only slotsBefore/slotsAfter/duration go in the 4th options arg.

⚠️ CRITICAL: the date param is a Date, NOT the ISO-8601 string the doc shows (doc/SDK drift). The REST/SDK doc describes date as an ISO-8601 string, but the shipped @wix/table-reservations type is Date — passing new Date(...).toISOString() fails to compile (TS2345: 'string' is not assignable to 'Date'). Pass a Date object (new Date(selectedDate)). Both the date arg you send and each slot.startDate you read back are Dates — so pass the chosen slot's startDate straight into createHeldReservation, no conversion.

⚠️ Book only status === 'AVAILABLE'. UNAVAILABLE (can't seat that party then) and NON_WORKING_HOURS (closed) are also returned — filter them out before presenting slots, or the hold will fail.

Making a reservation (hold → reserve)

The two-step flow from the sample: hold the slot (10-minute temporary reservation) while the visitor enters details, then reserve it with their name + phone.

// 1 · hold the slot — startDate is the Date from the chosen AVAILABLE time slot
const held = await reservations.createHeldReservation({
  reservationLocationId,
  startDate: chosenSlot.startDate,
  partySize,
});
// ⚠️ `held.reservation` is OPTIONAL and its `_id`/`revision` are `string | null` in the SDK type —
// narrow before use or the strict/`astro check` build fails (TS18048 / TS2345).
if (!held.reservation?._id || !held.reservation.revision) throw new Error('hold failed');
const reservationId = held.reservation._id;
const revision = held.reservation.revision;   // reserve needs this

// 2 · reserve it — reservee needs firstName + phone at minimum
const done = await reservations.reserveReservation(
  reservationId,
  { firstName, lastName, email, phone },
  revision,
);
// done.reservation.status === 'RESERVED'  (auto-approval)  |  'REQUESTED'  (manual approval on)

Docs: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/reservations/reservations/create-held-reservation.md?apiView=SDK · https://dev.wix.com/docs/api-reference/business-solutions/restaurants/reservations/reservations/reserve-reservation.md?apiView=SDK

⚠️ CRITICAL: reserveReservation(reservationId, reservee, revision) takes THREE positional args — the reservation id, the reservee object, and the revision from the held reservation. Omitting the revision (or wrapping the three in one object) does not type-check. Keep held.reservation.revision from step 1.

⚠️ CRITICAL: reservee.firstName and reservee.phone are required. A reservation from any source other than WALK_IN (i.e. every online booking) requires at least firstName and phone — omitting them returns an error. Collect them in the details form before calling reserve.

⚠️ A HELD reservation expires in 10 MINUTES. If the visitor takes longer than 10 min to confirm, reserveReservation returns an error — start a fresh hold. And do not call updateReservation to move a reservation off HELD (it errors); the only way forward from HELD is reserveReservation.

Result status: with the seeded default (approval.mode: "AUTOMATIC", manual approval off) the reservation becomes RESERVED immediately — confirm to the visitor. If the location requires manual approval it becomes REQUESTED (staff must approve) — tell the visitor it's pending.

Single-shot alternative: if you collect all details up front (no "hold while they type" step), call reservations.createReservation({ details: { reservationLocationId, startDate, partySize }, reservee }) instead — same firstName/phone requirement; it returns RESERVED/REQUESTED directly, skipping the HELD phase. ⚠️ The location/time/party go under a nested details object (NOT top-level), and this method returns the Reservation DIRECTLY — not wrapped in { reservation } (unlike createHeldReservation/reserveReservation). Top-level fields or reading .reservation off the result fails to type-check. Doc: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/reservations/reservations/create-reservation.md?apiView=SDK

Booking an experience (only when experiences are in the run)

An experience is a reservation that is a curated dining occasion (chef's table, tasting menu). It uses the same booking mechanism as a table, with two differences: you list experiences to show and choose from, and you offer bookable times projected from the experience's own schedule (there is no experience-scoped slot API — see Gotcha C) rather than from getScheduledTimeSlots, then stamp the chosen experienceId onto the reservation. Read the shapes from the docs (.md?apiView=SDK); the earned drift gotchas below are what the docs won't tell you.

Flow (mirrors the docs' sample flow — https://dev.wix.com/docs/api-reference/business-solutions/restaurants/reservations/experiences/sample-flows.md):

import { experiences, reservations } from '@wix/table-reservations';

// 1 · list experiences to display (name, price, schedule) — see gotcha A
const { items: exps = [] } = await experiences.queryExperiences().find();
const exp = exps.find((e) => e._id === chosenId);

// 2 · offer bookable times from the EXPERIENCE'S OWN schedule — do NOT call
//     getScheduledTimeSlots for an experience (it can't scope to one — Gotcha C).
//     queryExperiences() already returned the schedule; project the next occurrences yourself.
const sched = exp.configuration?.onlineReservations?.businessSchedule; // { durationInMinutes, entries[] }
// entries[].weeklyOptions.startDaysAndTimes[{ day, time }]  →  next N dates matching each weekday+time
// (ONE_TIME entries carry oneTimeOptions.{startDate,startTime}). Build the option list from these.

// 3 · book it — experienceId rides in details (see gotcha B); firstName + phone required
const done = await reservations.createReservation({
  details: { reservationLocationId, experienceId, startDate: chosenOccurrence, partySize },
  reservee: { firstName, lastName, email, phone },
});

Docs: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/reservations/experiences/query-experiences.md?apiView=SDK · https://dev.wix.com/docs/api-reference/business-solutions/restaurants/reservations/reservations/create-reservation.md?apiView=SDK

⚠️ Gotcha A — queryExperiences() is a QUERY BUILDER, not an object-arg method. Call experiences.queryExperiences().find() (chain .eq()/.limit() etc. before .find() if needed) and read the array from the result's .items. Passing a query object — queryExperiences({}) — does not type-check and returns no data; await-ing the un-.find()ed builder gives you the builder, not the results.

⚠️ Gotcha B — details.experienceId is a valid REST field but the SDK Details type may OMIT it (version drift). The REST API accepts experienceId inside details (the Experiences intro and the reservation's own meta confirm details.experienceId), but an installed @wix/table-reservations older than the Experiences feature ships a Details type without it — so TypeScript rejects the property. Build the reservation object and pass it through an any cast (or // @ts-expect-error) so experienceId reaches the request body; it works at runtime. Don't drop the field to satisfy the compiler — a reservation without it books a plain table, not the experience.

⚠️ Gotcha C — there is NO experience-scoped slot fetch; project times from the experience's own businessSchedule instead. The upstream get-scheduled-time-slots doc prose says "pass the GUID of that experience to this method," but no such parameter exists at any layer: the SDK (@wix/auto_sdk_table-reservations_time-slots ≤ 1.0.47, pulled by @wix/table-reservations) field-picks only { reservationLocationId, partySize, timeRange }, and the REST body is those same three fields — so an experienceId option is silently dropped (an as any cast is a no-op here, unlike Gotcha B). Calling getScheduledTimeSlots for an experience returns the location's default slots (wrong day/time/duration), presented to guests as the experience's — and it fails silently: a populated-but-wrong list looks healthy, and the premium gate (below) stops live verification before you'd notice. Instead, derive upcoming occurrences from the experience's own configuration.onlineReservations.businessSchedule — entries[].weeklyOptions.startDaysAndTimes[{ day, time }] (or oneTimeOptions) + durationInMinutes, already returned by queryExperiences(). Treat that as a display projection; real capacity/conflict enforcement happens server-side when createReservation runs.

⚠️ Same premium precondition as online reservations: creating experiences is free, but booking one needs a premium site with online reservations enabled (428 PREMIUM_ONLY otherwise — setup-restaurant-experiences.md). Build the flow; treat a booking failure on a non-premium site as a provisioning precondition, not a code bug.


Conclusion

A correct Table-Reservations frontend:

  • imports reservationLocations / timeSlots / reservations from @wix/table-reservations — one package, three namespaces; no cart / ecom / redirects (a reservation is a hold, not a purchase);
  • uses _id (never id) for the location and reservation, and defaults the list* array (= []) so strict/astro check passes;
  • calls getTimeSlots(locationId, new Date(date), partySize, options?) with positional args — the date param is a Date (not the ISO string the doc shows) — and books only status === 'AVAILABLE' slots (passing the slot's Date startDate onward);
  • books via createHeldReservation({...}) → reserveReservation(id, reservee, revision) (three positional args; firstName + phone required; 10-minute hold; reserveReservation is the only exit from HELD);
  • treats onlineReservationsEnabled (premium) as a precondition — the read/book flow only works once it's on;
  • for experiences (when in the run): lists via queryExperiences().find() (builder → .items); offers times projected from the experience's own businessSchedule — there is no experience-scoped getScheduledTimeSlots (Gotcha C: the experienceId option is silently dropped, so that call returns the location's slots); and books via createReservation with details.experienceId (an any cast past the SDK Details type — Gotcha B) — same premium precondition.</content>

Source: SKILL.md on GitHub

1 warning1d3 checks · Risk SAFE
  • Gen Agent Trust Hub1d

    The wix-headless skill is a robust tool for integrating Wix services into headless frontends. It incorporates secure practices for credential management, ensuring tokens remain outside the AI context. While the skill performs remote code execution and has data ingestion surfaces, these actions are carried out through official vendor channels and structured workflows.

  • Socket1d

    1 alert: gptAnomaly

  • Snyk1d

    Risk: LOW · No issues

Signed by skilld at 8f7fa5d. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub yesterday.

Activeupdated 2 days ago
What it can do
Runs commands Reads files Edits files
All 16 allowed tools
Bash(curl *)Bash(npx @wix/cli@latest *)Bash(npx @wix/cli *)Bash(npm create @wix/new@latest *)Bash(npm install *)Bash(npm run *)Bash(node *)Bash(cd *)Bash(ls *)Bash(mkdir *)Bash(cp *)Bash(mv *)Bash(uuidgen)ReadWriteEdit
  • wix
  • headless
  • ecommerce
  • astro
  • vite
  • jsx
  • hosting
  • cms
  • bookings

README badge

README badge for wix/skills/wix-headless

Scaffolds a new Wix Headless site from scratch or connects an existing HTML/JSX/Vite project to Wix Headless for hosting and business features (ecommerce, bookings, forms, CMS). Handles discovery, design, feature wiring via the Wix SDK, and deployment; routes on operation type (create vs. connect) and frontend framework (Astro by default, or your own build).

Generated from the current SKILL.md.

Does this skill create a new site or connect an existing one?
Both. Path A creates a new site from scratch based on your description (e.g. 'build me an online store'). Path B connects an existing project (HTML/JSX/Vite app, Claude Design output, etc.) to Wix Headless for hosting and feature wiring.
What happens when I connect an existing project to Wix Headless?
The skill analyzes your project, installs the necessary Wix Business Solutions apps, and wires the Wix SDK directly into your existing source files so each installed app powers its corresponding feature.
What frameworks does this skill support?
For new sites, it defaults to Astro but supports any framework you explicitly name (React, Vue, Svelte, Vite, etc.). For existing projects, it works with HTML, JSX, TSX, Vue, and other web frontends.
Does this skill handle ecommerce, bookings, and content sites?
Yes. It routes to different vertical packs depending on your needs: stores and ecommerce, bookings and appointments, CMS and blogs, forms, and gift cards. All verticals include CMS by default.
How does the skill authenticate with Wix?
It uses the Wix CLI (`npx @wix/cli@latest token`) to generate bearer tokens for API calls. The login flow is safe for non-interactive agents and includes a recovery ladder if needed.

Generated from the current SKILL.md. These answers refresh after source changes.