Subchapter 9.23
references/inline-recipes/how-to-code-restaurant-orders.mdMarkdown17 KBView on GitHub
RECIPE: How to Code a Wix Restaurants Ordering Frontend (Online Orders + eCommerce cart)
A contract for the frontend code that lets a visitor order from a Wix Restaurants menu: reading the ordering operation and fulfillment methods, adding a menu item to the eCommerce cart with the restaurant catalogReference, and checking out. This recipe is the how (which modules, which calls, which fields), not the what — which menu/items to sell and how the page looks come from the request you’re fulfilling.
This recipe is for CODING ordering, not seeding it. It assumes the backend already exists — a Restaurants Menus backend (menu → sections → items) and the Restaurants Orders app installed and configured (an
ENABLEDoperation with fulfillment methods, every menu ordering-enabled). Seesetup-restaurants.md+setup-restaurant-orders.mdfor the backend. This recipe says nothing about creating any of it — only how to read and purchase from frontend code.
Reading (displaying) the menu is a SEPARATE recipe — pair this with it.
how-to-code-restaurants.mdcovers listing menus/sections/items, assembling the hierarchy insectionIds/itemIdsorder, readingitem.priceInfo.price, and rendering images. That recipe is display-only (no cart). This recipe adds the ordering layer on top: it consumes the samemenus/sections/itemsreads and needs each rendered item’s_id, its section’s_id, and the menu’s_idto build a cart line. Don’t duplicate the menu-reading here — read it there, order it here.
⚠️ Reading rule — always append
.md?apiView=SDKto every doc link below. The Wix docs render two views of the same page. The bare / REST view showsid; the?apiView=SDKview shows_id— and the SDK is what your frontend calls. Reading the REST view by mistake is the most common source of theentity.id-is-undefinedcart bug. Fetch the.md?apiView=SDKform directly; don’t re-discover these with search.
Restaurants Orders app id (a constant you need for the cart’s catalogReference.appId):
9a5d83fd-8570-482e-81ab-cfa88942ee60
⚠️ This is the ORDERS app id, NOT the Stores app id. A restaurant menu item added to the cart must carry appId: '9a5d83fd-…' (Orders). Using the Stores id (215238eb-…) — muscle memory from how-to-code-a-store.md — makes eCommerce resolve the line against the wrong catalog and the add fails. Menu items are not Stores products.
| Need | Package | Module (namespace) |
|---|---|---|
| Menus / sections / items (display) | @wix/restaurants | menus / sections / items — see how-to-code-restaurants.md |
The ordering operation (for operationId) | @wix/restaurants | operations |
| Fulfillment methods (pickup/delivery to show) | @wix/restaurants | fulfillmentMethods |
| Cart (add / get / checkout) | @wix/ecom | currentCartV2 |
| Redirect to hosted checkout | @wix/redirects | redirects |
Migrating from Cart V1 / Checkout V1? The code below is V2-only — see the migration guide (opens in a new tab) for the before/after.
Auth / client — framework split:
operations / fulfillmentMethods / currentCartV2 directly from server components and backend routes (src/pages/api/*.ts) — no createClient, no OAuthStrategy, no clientId.
import { operations, fulfillmentMethods } from '@wix/restaurants';
import { currentCartV2 } from '@wix/ecom';
const { operations: ops } = await operations.listOperations();import { createClient, OAuthStrategy } from '@wix/sdk';
import { menus, sections, items, operations, fulfillmentMethods } from '@wix/restaurants';
import { currentCartV2 } from '@wix/ecom';
import { redirects } from '@wix/redirects';
const client = createClient({
modules: { menus, sections, items, operations, fulfillmentMethods, currentCartV2, redirects },
auth: OAuthStrategy({ clientId: /* the project's PUBLIC OAuth client id */ }),
});
// then: await client.operations.listOperations(), await client.currentCartV2.addLineItemsToCurrentCart({...})clientId is public, not a secret. A mis-wired public env var inlines as undefined and 400s every call.These are SDK read shapes (?apiView=SDK), so entity ids are _id. Prices/fees are decimal strings. The cart-add body (under Adding a menu item to the cart) is a separate write shape; the _id rule applies to read entities, not to request params (note the redirect session’s checkoutId, which is just the cart’s _id).
// operations.listOperations() → { operations: [...] } (no arguments)
operation = {
_id, // → cart options.operationId (NOT .id → undefined)
name, default, // the auto-created ordering operation has default: true
onlineOrderingStatus, // "ENABLED" when the site is taking orders
fulfillmentIds, // ids of the fulfillment methods attached to this operation
defaultFulfillmentType, // "PICKUP" | "DELIVERY"
}
// fulfillmentMethods.listFulfillmentMethods() → { fulfillmentMethods: [...] }
fulfillmentMethod = {
_id, type, // "PICKUP" | "DELIVERY"
name, enabled, // show only enabled:true to the visitor
fee, minOrderPrice, // decimal STRINGS ("0", "5") — no currency symbol
pickupOptions, // present when type PICKUP
deliveryOptions, // present when type DELIVERY (deliveryTimeInMinutes, deliveryArea, …)
}
// item (from items.listItems — see how-to-code-restaurants.md)
item = { _id, name, priceInfo: { price } } // price is a decimal STRING; _id → cart catalogItemId
// currentCartV2.getCurrentCart() → { cart: { _id, lineItems: [...] } } // NOTE: returns { cart } — destructure it
lineItem = { _id, name: { original }, quantityInfo: { confirmedQuantity }, pricing: { unitPrice: { amount } }, attributes: { image } }
// price → pricing.unitPrice (ConvertedMoney, NO formatted string in V2 — format it yourself; .amount is site currency, .convertedAmount the buyer's display currency); qty → quantityInfo.confirmedQuantity; image → attributes.image (wix:image:// → resolve)
// the cart's _id is the checkout id → pass to the redirect session:
// redirects.createRedirectSession({ ecomCheckout: { checkoutId: cart._id }, callbacks }) → { redirectSession: { fullUrl } }Each subsection is a self-contained feature — implement only what the site uses. The only ordering is within a feature (read the operation before you build a cart line that references it).
Get the operation so you have the operationId every cart line needs. listOperations() takes no arguments and returns a plain array.
const { operations: ops = [] } = await operations.listOperations();
const op = ops.find((o) => o.onlineOrderingStatus === 'ENABLED') ?? ops.find((o) => o.default) ?? ops[0];
const operationId = op._id;⚠️ list* return the array as OPTIONAL — default it (= []) or the strict build fails. listOperations() / listFulfillmentMethods() / listItems() type the returned array as T[] | undefined, so destructuring { operations: ops } and calling ops.find(...) directly errors under strict / astro check ('ops' is possibly 'undefined', TS18048) — a build-breaker on managed Astro. Default it in the destructure ({ operations: ops = [] }) or guard with ?? [].
⚠️ CRITICAL: the entity id is _id, NOT id. operation.id / item.id / section.id are undefined in SDK code. Feeding operation.id into options.operationId (or item.id into catalogItemId) sends an empty string and the add-to-cart fails. Use _id everywhere. (A surprising id field means you’re reading the REST doc view — re-open with ?apiView=SDK.)
Read the operation once and reuse operationId for every add-to-cart on the page — don’t call listOperations() per line item.
List the methods to render the visitor’s pickup/delivery choices (fee, minimum, type). Show only enabled: true.
const { fulfillmentMethods: methods = [] } = await fulfillmentMethods.listFulfillmentMethods();
const offered = methods.filter((m) => m.enabled);Doc: https://dev.wix.com/docs/api-reference/business-solutions/restaurants/online-orders/fulfillment-methods/list-fulfillment-methods.md?apiView=SDK (opens in a new tab) (default the array = [] — see the optional-array note above)
⚠️ listFulfillmentMethods takes an OPTIONS object, not a query builder. It accepts an optional { paging } object and returns a Promise of { fulfillmentMethods } directly — do not chain .find()/.eq() on it (that’s the queryProducts builder pattern from stores; it does not apply here). fee and minOrderPrice are decimal strings — render them with your currency, and Number(...) only if you need arithmetic. Which method the buyer actually uses (and the delivery time slot) is chosen on the hosted checkout; listing here is for display, not a required pre-checkout step.
This is the one shape that differs materially from a Stores cart. Build a line item whose catalogReference names the Orders app and carries operationId + menuId + sectionId in options.
await currentCartV2.addLineItemsToCurrentCart({
catalogItems: [{ // the write shape uses `catalogItems`
quantity,
catalogReference: {
catalogItemId: item._id, // the MENU ITEM's _id
appId: '9a5d83fd-8570-482e-81ab-cfa88942ee60', // the ORDERS app id (not Stores)
options: {
operationId, // from listOperations()
menuId, // the _id of the menu the item is shown in
sectionId, // the _id of the section the item is shown under
},
},
}],
});⚠️ CRITICAL: options MUST carry operationId, menuId, AND sectionId — all three. A restaurant line item is identified by where in the menu it was ordered from, not by a variant. Omitting any of the three makes eCommerce unable to resolve the item against the Restaurants catalog — Cart V2 rejects the add with an explicit error rather than accepting an invalid line. This is the restaurant analog of the store’s mandatory variantId.
⚠️ CRITICAL: there is NO variantId here. options.variantId is a Stores concept — menu items have no variants in this flow. Don’t copy the store recipe’s variantId resolution; a restaurant line uses operationId/menuId/sectionId instead. (Menu item modifiers — “extra cheese” — are a separate concern and out of scope for the basic order flow.)
⚠️ Thread sectionId and menuId from the render, not a lookup. When you assemble the menu (menu → sections → items, per how-to-code-restaurants.md), each item is rendered inside a known section and menu — capture that section._id and menu._id at render time and pass them to the add-to-cart handler alongside item._id. Re-deriving an item’s section afterward is unnecessary and error-prone; you already have it in the render context.
Optional: options.onlineOrderingPageUrl (e.g. "/online-ordering") lets the buyer click the cart line to return to the item — include it only if your site has such a page.
The cart’s _id is the checkout id — pass it into the redirect session’s ecomCheckout.checkoutId. Read the current cart, then hand its id to a redirect session (identical to the storefront flow — restaurant orders ride on the same eCommerce checkout).
Doc: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/cart-v2/get-current-cart.md?apiView=SDK (opens in a new tab)
const { cart } = await currentCartV2.getCurrentCart(); // NOTE: returns { cart } — destructure it
const session = await redirects.createRedirectSession({
ecomCheckout: { checkoutId: cart._id }, // the cart's _id IS the checkout id
callbacks: { postFlowUrl: `${origin}/`, thankYouPageUrl: `${origin}/` },
});
window.location.href = session.redirectSession.fullUrl; // the hosted-checkout URL (fulfillment + time slot chosen here)⚠️ The cart’s _id is the checkout id. Pass cart._id into the redirect session’s ecomCheckout.checkoutId. And getCurrentCart() returns { cart } — destructure it, or cart is undefined and cart._id throws “Cannot read properties of undefined (reading ‘_id’)”.
⚠️ CRITICAL: origin for postFlowUrl/thankYouPageUrl MUST be the https:// published host — derive it from window.location.origin, NEVER new URL(request.url).origin. The Headless redirect allowlist registers the site’s https:// host and treats http://<same host> as a different, unlisted origin. If you build the redirect session in a server route (src/pages/api/*), new URL(request.url).origin resolves to http:// behind Wix’s TLS-terminating proxy → the buyer’s return redirect 403s with “… isn’t listed as an allowed redirect domain.” Pass window.location.origin from the client into the route (or force the scheme to https). Doc: https://dev.wix.com/docs/go-headless/getting-started/setup/manage-urls/add-allowed-redirect-domains (opens in a new tab).
⚠️ LIVE PAID CHECKOUT PRECONDITION. A visitor can add to cart and reach the hosted checkout with just this code, but completing a paid order needs the site to have a premium plan and a configured payment method. That’s site provisioning, not a frontend bug — if checkout can’t collect payment, the setup is incomplete, not the code.
Menu item price is item.priceInfo.price — a decimal string with no currency symbol (see how-to-code-restaurants.md; the SDK PriceInfo type has only price, no formattedPrice). A cart line’s price is lineItem.pricing.unitPrice / pricing.totalPrice — a ConvertedMoney { amount, convertedAmount }; Cart V2 line items carry no preformatted price, so format it yourself. The currency lives on the cart, not the money object — format from cart.customerInfo?.currencyCode ?? cart.businessInfo?.currencyCode (see the Formatting cart prices section of how-to-code-a-store.md for the exact Intl.NumberFormat helper), e.g. new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(Number(pricing.totalPrice.convertedAmount ?? pricing.totalPrice.amount)). Cart line images are wix:image:// identifiers — resolve them with media.getScaledToFillImageUrl(id, w, h, {}) (4 args), never raw (see the store/menu recipes’ image callout).
A correct Restaurants ordering frontend:
operations / fulfillmentMethods from @wix/restaurants (plus menus/sections/items for display) and currentCartV2 / redirects from @wix/ecom / @wix/redirects;_id (never id) for the operation, menu, section, and item;operationId once from listOperations() (the ENABLED/default operation) and reuses it;9a5d83fd-… (never the Stores id) and options carrying operationId + menuId + sectionId (all three) — no variantId;menuId/sectionId from the render context, not a re-lookup;_id (the checkout id) to redirects, with an https:// window.location.origin for the callbacks; the hosted checkout collects fulfillment + payment (which needs a premium plan + payment method).